Highly ambitious ATProtocol AppView service and sdks

waitlists!

+94 -4
api/scripts/generate_typescript.ts
··· 19 19 interface LexiconDefinition { 20 20 type: string; 21 21 record?: LexiconRecord; 22 + properties?: Record<string, LexiconProperty>; 22 23 } 23 24 24 25 interface Lexicon { ··· 529 530 // Convert lexicon type to TypeScript type 530 531 function convertLexiconTypeToTypeScript( 531 532 def: any, 532 - currentLexicon: string 533 + currentLexicon: string, 534 + propertyName?: string 533 535 ): string { 534 536 const type = def.type; 535 537 switch (type) { 536 538 case "string": 539 + // For knownValues, return the type alias name 540 + if (def.knownValues && Array.isArray(def.knownValues) && def.knownValues.length > 0 && propertyName) { 541 + // Reference the generated type alias with namespace 542 + const namespace = nsidToNamespace(currentLexicon); 543 + return `${namespace}${defNameToPascalCase(propertyName)}`; 544 + } 537 545 return "string"; 538 546 case "integer": 539 547 return "number"; ··· 736 744 737 745 if (defValue.properties) { 738 746 for (const [propName, propDef] of Object.entries(defValue.properties)) { 739 - const tsType = convertLexiconTypeToTypeScript(propDef as any, lexicon.id); 747 + const tsType = convertLexiconTypeToTypeScript(propDef as any, lexicon.id, propName); 740 748 const required = 741 749 defValue.required && defValue.required.includes(propName); 742 750 ··· 775 783 776 784 if (recordDef.properties) { 777 785 for (const [propName, propDef] of Object.entries(recordDef.properties)) { 778 - const tsType = convertLexiconTypeToTypeScript(propDef as any, lexicon.id); 786 + const tsType = convertLexiconTypeToTypeScript(propDef as any, lexicon.id, propName); 779 787 const required = isPropertyRequired(recordDef, propName); 780 788 781 789 properties.push({ ··· 866 874 }); 867 875 } 868 876 877 + // Generate type aliases for string fields with knownValues 878 + function generateKnownValuesTypes(): void { 879 + for (const lexicon of lexicons) { 880 + if (lexicon.definitions && typeof lexicon.definitions === "object") { 881 + for (const [defKey, defValue] of Object.entries(lexicon.definitions)) { 882 + if (defValue.type === "record" && defValue.record?.properties) { 883 + for (const [propName, propDef] of Object.entries(defValue.record.properties)) { 884 + const prop = propDef as any; 885 + if (prop.type === "string" && prop.knownValues && Array.isArray(prop.knownValues) && prop.knownValues.length > 0) { 886 + // Generate a type alias for this property, namespaced by lexicon 887 + const namespace = nsidToNamespace(lexicon.id); 888 + const pascalPropName = defNameToPascalCase(propName); 889 + const typeName = `${namespace}${pascalPropName}`; 890 + 891 + const knownValueTypes = prop.knownValues.map((value: string) => `'${value}'`).join('\n | '); 892 + const typeDefinition = `${knownValueTypes}\n | (string & {})`; 893 + 894 + sourceFile.addTypeAlias({ 895 + name: typeName, 896 + isExported: true, 897 + type: typeDefinition, 898 + leadingTrivia: "\n", 899 + }); 900 + } 901 + } 902 + } else if (defValue.type === "object" && defValue.properties) { 903 + for (const [propName, propDef] of Object.entries(defValue.properties)) { 904 + const prop = propDef as any; 905 + if (prop.type === "string" && prop.knownValues && Array.isArray(prop.knownValues) && prop.knownValues.length > 0) { 906 + // Generate a type alias for this property, namespaced by lexicon 907 + const namespace = nsidToNamespace(lexicon.id); 908 + const pascalPropName = defNameToPascalCase(propName); 909 + const typeName = `${namespace}${pascalPropName}`; 910 + 911 + const knownValueTypes = prop.knownValues.map((value: string) => `'${value}'`).join('\n | '); 912 + const typeDefinition = `${knownValueTypes}\n | (string & {})`; 913 + 914 + sourceFile.addTypeAlias({ 915 + name: typeName, 916 + isExported: true, 917 + type: typeDefinition, 918 + leadingTrivia: "\n", 919 + }); 920 + } 921 + } 922 + } else if (defValue.type === "string") { 923 + // Handle standalone string definitions with knownValues (like labelValue) 924 + const stringDef = defValue as any; 925 + if (stringDef.knownValues && Array.isArray(stringDef.knownValues) && stringDef.knownValues.length > 0) { 926 + // Generate a type alias for this definition, namespaced by lexicon 927 + const namespace = nsidToNamespace(lexicon.id); 928 + const typeName = `${namespace}${defNameToPascalCase(defKey)}`; 929 + 930 + const knownValueTypes = stringDef.knownValues.map((value: string) => `'${value}'`).join('\n | '); 931 + const typeDefinition = `${knownValueTypes}\n | (string & {})`; 932 + 933 + sourceFile.addTypeAlias({ 934 + name: typeName, 935 + isExported: true, 936 + type: typeDefinition, 937 + leadingTrivia: "\n", 938 + }); 939 + } 940 + } 941 + } 942 + } 943 + } 944 + } 945 + 869 946 // Add lexicon-specific interfaces and types 870 947 function addLexiconInterfaces(): void { 871 948 // First pass: Generate all individual definition interfaces/types ··· 932 1009 isReadonly: true, 933 1010 }); 934 1011 break; 1012 + case "string": 1013 + // Check if this is a string type with knownValues 1014 + const stringDef = defValue as any; 1015 + if (stringDef.knownValues && Array.isArray(stringDef.knownValues) && stringDef.knownValues.length > 0) { 1016 + // This generates a type alias, reference it in the namespace with full name 1017 + namespaceProperties.push({ 1018 + name: defName, 1019 + type: `${namespace}${defNameToPascalCase(defKey)}`, 1020 + isReadonly: true, 1021 + }); 1022 + } 1023 + break; 935 1024 case "union": 936 1025 case "array": 937 1026 case "token": ··· 1144 1233 { 1145 1234 name: "client", 1146 1235 type: "SlicesClient", 1147 - scope: "private", 1236 + scope: "private" as any, 1148 1237 isReadonly: true, 1149 1238 }, 1150 1239 ]), ··· 1441 1530 1442 1531 // Generate the TypeScript 1443 1532 addBaseInterfaces(); 1533 + generateKnownValuesTypes(); 1444 1534 addLexiconInterfaces(); 1445 1535 addClientClass(); 1446 1536
+2 -6
api/src/handler_xrpc_dynamic.rs
··· 575 575 576 576 match LexiconValidator::for_slice(&state.database, validation_slice_uri).await { 577 577 Ok(validator) => { 578 - // Debug: Get lexicons from the system slice to see what's there 579 - if collection == "network.slices.lexicon" {} 580 578 581 579 if let Err(e) = validator.validate_record(&collection, &record_data) { 582 580 return Err(( ··· 588 586 )); 589 587 } 590 588 } 591 - Err(e) => { 589 + Err(_e) => { 592 590 // If no lexicon found, continue without validation (backwards compatibility) 593 - eprintln!("Could not load lexicon validator: {:?}", e); 594 591 } 595 592 } 596 593 ··· 689 686 )); 690 687 } 691 688 } 692 - Err(e) => { 689 + Err(_e) => { 693 690 // If no lexicon found, continue without validation (backwards compatibility) 694 - eprintln!("Could not load lexicon validator: {:?}", e); 695 691 } 696 692 } 697 693
+1 -1
docker-compose.yml
··· 35 35 HTTP_PORT: "8081" 36 36 DATABASE_URL: "sqlite:///data/aip.db" 37 37 HTTP_CLIENT_TIMEOUT: "30" 38 - OAUTH_SUPPORTED_SCOPES: "openid email profile atproto transition:generic account:email blob:image/* repo:network.slices.slice repo:network.slices.lexicon repo:network.slices.actor.profile repo:network.slices.waiting" 38 + OAUTH_SUPPORTED_SCOPES: "openid email profile atproto transition:generic account:email blob:image/* repo:network.slices.slice repo:network.slices.lexicon repo:network.slices.actor.profile repo:network.slices.waitlist.request" 39 39 ENABLE_CLIENT_API: "true" 40 40 ADMIN_DIDS: "did:plc:bcgltzqazw5tb6k2g3ttenbj" 41 41 DPOP_NONCE_SEED: "local-dev-nonce-seed"
+5 -3
docs/sdk-usage.md
··· 118 118 where: { 119 119 releaseDate: { 120 120 gte: "1990-01-01", 121 - lte: "1999-12-31" 121 + lte: "1999-12-31", 122 122 }, 123 123 }, 124 124 }); ··· 368 368 // Filter by exact handle 369 369 const exactHandle = await client.network.slices.slice.getActors({ 370 370 where: { 371 - handle: { eq: "alice.bsky.social" }, 371 + handle: { eq: "user.bsky.social" }, 372 372 }, 373 373 }); 374 374 ··· 492 492 // Limit to specific collections 493 493 const specificSearch = await client.network.slices.slice.getSliceRecords({ 494 494 where: { 495 - collection: { in: ["com.recordcollector.album", "com.recordcollector.review"] }, 495 + collection: { 496 + in: ["com.recordcollector.album", "com.recordcollector.review"], 497 + }, 496 498 json: { contains: "grunge" }, 497 499 }, 498 500 });
+1 -1
frontend/deno.json
··· 8 8 "jsxImportSource": "preact" 9 9 }, 10 10 "imports": { 11 - "@slices/client": "jsr:@slices/client@^0.1.0-alpha.2", 11 + "@slices/client": "jsr:@slices/client@^0.1.0-alpha.3", 12 12 "@slices/oauth": "jsr:@slices/oauth@^0.4.1", 13 13 "@slices/session": "jsr:@slices/session@^0.2.1", 14 14 "@std/assert": "jsr:@std/assert@^1.0.14",
+4 -7
frontend/deno.lock
··· 2 2 "version": "5", 3 3 "specifiers": { 4 4 "jsr:@shikijs/shiki@*": "3.7.0", 5 - "jsr:@slices/client@~0.1.0-alpha.2": "0.1.0-alpha.2", 5 + "jsr:@slices/client@~0.1.0-alpha.3": "0.1.0-alpha.3", 6 6 "jsr:@slices/oauth@~0.4.1": "0.4.1", 7 7 "jsr:@slices/session@~0.2.1": "0.2.1", 8 8 "jsr:@std/assert@^1.0.14": "1.0.14", ··· 43 43 "npm:shiki" 44 44 ] 45 45 }, 46 - "@slices/client@0.1.0-alpha.2": { 47 - "integrity": "d3c591e89ab5b7ed7988faf9428bb7b3539484c6b90005a7c66f2188cc60fe19", 48 - "dependencies": [ 49 - "jsr:@slices/oauth" 50 - ] 46 + "@slices/client@0.1.0-alpha.3": { 47 + "integrity": "c18d6ad2dbe1043bbeb7da7c5a11724fa0fa388c3e6e96089bb033f518c4b23c" 51 48 }, 52 49 "@slices/oauth@0.4.1": { 53 50 "integrity": "15f20df2ba81e9d1764291c8b4f6e3eb38cfc953750eeb3815872b7e22475492" ··· 672 669 }, 673 670 "workspace": { 674 671 "dependencies": [ 675 - "jsr:@slices/client@~0.1.0-alpha.2", 672 + "jsr:@slices/client@~0.1.0-alpha.3", 676 673 "jsr:@slices/oauth@~0.4.1", 677 674 "jsr:@slices/session@~0.2.1", 678 675 "jsr:@std/assert@^1.0.14",
+2 -2
frontend/scripts/register-oauth-client.sh
··· 45 45 { 46 46 "client_name": "$CLIENT_NAME", 47 47 "redirect_uris": ["$REDIRECT_URI"], 48 - "scope": "openid email profile atproto transition:generic account:email blob:image/* repo:network.slices.slice repo:network.slices.lexicon repo:network.slices.actor.profile repo:network.slices.waiting", 48 + "scope": "openid email profile atproto transition:generic account:email blob:image/* repo:network.slices.slice repo:network.slices.lexicon repo:network.slices.actor.profile repo:network.slices.waitlist.request", 49 49 "grant_types": ["authorization_code", "refresh_token"], 50 50 "response_types": ["code"], 51 51 "token_endpoint_auth_method": "client_secret_basic" ··· 106 106 echo " - Client ID: $CLIENT_ID" 107 107 echo " - Client Name: $CLIENT_NAME" 108 108 echo " - Redirect URI: $REDIRECT_URI" 109 - echo " - Scopes: openid email profile atproto transition:generic account:email blob:image/* repo:network.slices.slice repo:network.slices.lexicon repo:network.slices.actor.profile repo:network.slices.waiting" 109 + echo " - Scopes: openid email profile atproto transition:generic account:email blob:image/* repo:network.slices.slice repo:network.slices.lexicon repo:network.slices.actor.profile repo:network.slices.waitlist.request" 110 110 echo " - Config saved to: $CONFIG_FILE" 111 111 echo 112 112 echo "🔧 Environment variables saved to $CONFIG_FILE:"
+1484 -99
frontend/src/client.ts
··· 1 1 // Generated TypeScript client for AT Protocol records 2 - // Generated at: 2025-09-16 21:02:30 UTC 3 - // Lexicons: 9 2 + // Generated at: 2025-09-17 17:42:43 UTC 3 + // Lexicons: 25 4 4 5 5 /** 6 6 * @example Usage ··· 12 12 * 'at://did:plc:bcgltzqazw5tb6k2g3ttenbj/network.slices.slice/3lymhd4jhrd2z' 13 13 * ); 14 14 * 15 - * // Get records from the app.bsky.actor.profile collection 16 - * const records = await client.app.bsky.actor.profile.getRecords(); 15 + * // Get records from the app.bsky.graph.follow collection 16 + * const records = await client.app.bsky.graph.follow.getRecords(); 17 17 * 18 18 * // Get a specific record 19 - * const record = await client.app.bsky.actor.profile.getRecord({ 20 - * uri: 'at://did:plc:example/app.bsky.actor.profile/3abc123' 19 + * const record = await client.app.bsky.graph.follow.getRecord({ 20 + * uri: 'at://did:plc:example/app.bsky.graph.follow/3abc123' 21 21 * }); 22 22 * 23 23 * // Get records with filtering and search 24 - * const filteredRecords = await client.app.bsky.actor.profile.getRecords({ 24 + * const filteredRecords = await client.app.bsky.graph.follow.getRecords({ 25 25 * where: { 26 26 * text: { contains: "example search term" } 27 27 * } 28 28 * }); 29 29 * 30 30 * // Use slice-level methods for cross-collection queries with type safety 31 - * const sliceRecords = await client.network.slices.slice.getSliceRecords<AppBskyActorProfile>({ 31 + * const sliceRecords = await client.network.slices.slice.getSliceRecords<AppBskyGraphFollow>({ 32 32 * where: { 33 - * collection: { eq: 'app.bsky.actor.profile' } 33 + * collection: { eq: 'app.bsky.graph.follow' } 34 34 * } 35 35 * }); 36 36 * 37 37 * // Search across multiple collections using union types 38 - * const multiCollectionRecords = await client.network.slices.slice.getSliceRecords<AppBskyActorProfile | AppBskyActorProfile>({ 38 + * const multiCollectionRecords = await client.network.slices.slice.getSliceRecords<AppBskyGraphFollow | AppBskyActorProfile>({ 39 39 * where: { 40 - * collection: { in: ['app.bsky.actor.profile', 'app.bsky.actor.profile'] }, 40 + * collection: { in: ['app.bsky.graph.follow', 'app.bsky.actor.profile'] }, 41 41 * text: { contains: 'example search term' }, 42 42 * did: { in: ['did:plc:user1', 'did:plc:user2'] } 43 43 * }, ··· 263 263 message: string; 264 264 } 265 265 266 + export type AppBskyGraphDefsListPurpose = 267 + | "app.bsky.graph.defs#modlist" 268 + | "app.bsky.graph.defs#curatelist" 269 + | "app.bsky.graph.defs#referencelist" 270 + | (string & {}); 271 + 272 + export type AppBskyFeedDefsEvent = 273 + | "app.bsky.feed.defs#requestLess" 274 + | "app.bsky.feed.defs#requestMore" 275 + | "app.bsky.feed.defs#clickthroughItem" 276 + | "app.bsky.feed.defs#clickthroughAuthor" 277 + | "app.bsky.feed.defs#clickthroughReposter" 278 + | "app.bsky.feed.defs#clickthroughEmbed" 279 + | "app.bsky.feed.defs#interactionSeen" 280 + | "app.bsky.feed.defs#interactionLike" 281 + | "app.bsky.feed.defs#interactionRepost" 282 + | "app.bsky.feed.defs#interactionReply" 283 + | "app.bsky.feed.defs#interactionQuote" 284 + | "app.bsky.feed.defs#interactionShare" 285 + | (string & {}); 286 + 287 + export type AppBskyFeedDefsContentMode = 288 + | "app.bsky.feed.defs#contentModeUnspecified" 289 + | "app.bsky.feed.defs#contentModeVideo" 290 + | (string & {}); 291 + 292 + export type AppBskyActorDefsActorTarget = 293 + | "all" 294 + | "exclude-following" 295 + | (string & {}); 296 + 297 + export type AppBskyActorDefsType = "feed" | "list" | "timeline" | (string & {}); 298 + 299 + export type AppBskyActorDefsSort = 300 + | "oldest" 301 + | "newest" 302 + | "most-likes" 303 + | "random" 304 + | "hotness" 305 + | (string & {}); 306 + 307 + export type AppBskyActorDefsMutedWordTarget = "content" | "tag" | (string & {}); 308 + 309 + export type AppBskyActorDefsVisibility = 310 + | "ignore" 311 + | "show" 312 + | "warn" 313 + | "hide" 314 + | (string & {}); 315 + 316 + export type AppBskyActorDefsAllowIncoming = 317 + | "all" 318 + | "none" 319 + | "following" 320 + | (string & {}); 321 + 322 + export type ComAtprotoLabelDefsLabelValue = 323 + | "!hide" 324 + | "!no-promote" 325 + | "!warn" 326 + | "!no-unauthenticated" 327 + | "dmca-violation" 328 + | "doxxing" 329 + | "porn" 330 + | "sexual" 331 + | "nudity" 332 + | "nsfl" 333 + | "gore" 334 + | (string & {}); 335 + 336 + export type ComAtprotoLabelDefsBlurs = 337 + | "content" 338 + | "media" 339 + | "none" 340 + | (string & {}); 341 + 342 + export type ComAtprotoLabelDefsSeverity = 343 + | "inform" 344 + | "alert" 345 + | "none" 346 + | (string & {}); 347 + 348 + export type ComAtprotoLabelDefsDefaultSetting = 349 + | "ignore" 350 + | "warn" 351 + | "hide" 352 + | (string & {}); 353 + 354 + export interface AppBskyEmbedDefsAspectRatio { 355 + width: number; 356 + height: number; 357 + } 358 + 359 + export interface AppBskyEmbedRecordMain { 360 + record: ComAtprotoRepoStrongRef; 361 + } 362 + 363 + export interface AppBskyEmbedRecordView { 364 + record: 365 + | AppBskyEmbedRecord["ViewRecord"] 366 + | AppBskyEmbedRecord["ViewNotFound"] 367 + | AppBskyEmbedRecord["ViewBlocked"] 368 + | AppBskyEmbedRecord["ViewDetached"] 369 + | AppBskyFeedDefs["GeneratorView"] 370 + | AppBskyGraphDefs["ListView"] 371 + | AppBskyLabelerDefs["LabelerView"] 372 + | AppBskyGraphDefs["StarterPackViewBasic"] 373 + | { $type: string; [key: string]: unknown }; 374 + } 375 + 376 + export interface AppBskyEmbedRecordViewRecord { 377 + cid: string; 378 + uri: string; 379 + /** The record data itself. */ 380 + value: unknown; 381 + author: AppBskyActorDefs["ProfileViewBasic"]; 382 + embeds?: 383 + | AppBskyEmbedImages["View"] 384 + | AppBskyEmbedVideo["View"] 385 + | AppBskyEmbedExternal["View"] 386 + | AppBskyEmbedRecord["View"] 387 + | AppBskyEmbedRecordWithMedia["View"] 388 + | { $type: string; [key: string]: unknown }[]; 389 + labels?: ComAtprotoLabelDefs["Label"][]; 390 + indexedAt: string; 391 + likeCount?: number; 392 + quoteCount?: number; 393 + replyCount?: number; 394 + repostCount?: number; 395 + } 396 + 397 + export interface AppBskyEmbedRecordViewBlocked { 398 + uri: string; 399 + author: AppBskyFeedDefs["BlockedAuthor"]; 400 + blocked: boolean; 401 + } 402 + 403 + export interface AppBskyEmbedRecordViewDetached { 404 + uri: string; 405 + detached: boolean; 406 + } 407 + 408 + export interface AppBskyEmbedRecordViewNotFound { 409 + uri: string; 410 + notFound: boolean; 411 + } 412 + 413 + export interface AppBskyEmbedImagesMain { 414 + images: AppBskyEmbedImages["Image"][]; 415 + } 416 + 417 + export interface AppBskyEmbedImagesView { 418 + images: AppBskyEmbedImages["ViewImage"][]; 419 + } 420 + 421 + export interface AppBskyEmbedImagesImage { 422 + /** Alt text description of the image, for accessibility. */ 423 + alt: string; 424 + image: BlobRef; 425 + aspectRatio?: AppBskyEmbedDefs["AspectRatio"]; 426 + } 427 + 428 + export interface AppBskyEmbedImagesViewImage { 429 + /** Alt text description of the image, for accessibility. */ 430 + alt: string; 431 + /** Fully-qualified URL where a thumbnail of the image can be fetched. For example, CDN location provided by the App View. */ 432 + thumb: string; 433 + /** Fully-qualified URL where a large version of the image can be fetched. May or may not be the exact original blob. For example, CDN location provided by the App View. */ 434 + fullsize: string; 435 + aspectRatio?: AppBskyEmbedDefs["AspectRatio"]; 436 + } 437 + 438 + export interface AppBskyEmbedRecordWithMediaMain { 439 + media: 440 + | AppBskyEmbedImages["Main"] 441 + | AppBskyEmbedVideo["Main"] 442 + | AppBskyEmbedExternal["Main"] 443 + | { $type: string; [key: string]: unknown }; 444 + record: AppBskyEmbedRecord["Main"]; 445 + } 446 + 447 + export interface AppBskyEmbedRecordWithMediaView { 448 + media: 449 + | AppBskyEmbedImages["View"] 450 + | AppBskyEmbedVideo["View"] 451 + | AppBskyEmbedExternal["View"] 452 + | { $type: string; [key: string]: unknown }; 453 + record: AppBskyEmbedRecord["View"]; 454 + } 455 + 456 + export interface AppBskyEmbedVideoMain { 457 + /** Alt text description of the video, for accessibility. */ 458 + alt?: string; 459 + video: BlobRef; 460 + captions?: AppBskyEmbedVideo["Caption"][]; 461 + aspectRatio?: AppBskyEmbedDefs["AspectRatio"]; 462 + } 463 + 464 + export interface AppBskyEmbedVideoView { 465 + alt?: string; 466 + cid: string; 467 + playlist: string; 468 + thumbnail?: string; 469 + aspectRatio?: AppBskyEmbedDefs["AspectRatio"]; 470 + } 471 + 472 + export interface AppBskyEmbedVideoCaption { 473 + file: BlobRef; 474 + lang: string; 475 + } 476 + 477 + export interface AppBskyEmbedExternalMain { 478 + external: AppBskyEmbedExternal["External"]; 479 + } 480 + 481 + export interface AppBskyEmbedExternalView { 482 + external: AppBskyEmbedExternal["ViewExternal"]; 483 + } 484 + 485 + export interface AppBskyEmbedExternalExternal { 486 + uri: string; 487 + thumb?: BlobRef; 488 + title: string; 489 + description: string; 490 + } 491 + 492 + export interface AppBskyEmbedExternalViewExternal { 493 + uri: string; 494 + thumb?: string; 495 + title: string; 496 + description: string; 497 + } 498 + 499 + export interface AppBskyGraphFollow { 500 + subject: string; 501 + createdAt: string; 502 + } 503 + 504 + export type AppBskyGraphFollowSortFields = "subject" | "createdAt"; 505 + export type AppBskyGraphDefsModlist = "app.bsky.graph.defs#modlist"; 506 + 507 + export interface AppBskyGraphDefsListView { 508 + cid: string; 509 + uri: string; 510 + name: string; 511 + avatar?: string; 512 + labels?: ComAtprotoLabelDefs["Label"][]; 513 + viewer?: AppBskyGraphDefs["ListViewerState"]; 514 + creator: AppBskyActorDefs["ProfileView"]; 515 + purpose: AppBskyGraphDefs["ListPurpose"]; 516 + indexedAt: string; 517 + description?: string; 518 + listItemCount?: number; 519 + descriptionFacets?: AppBskyRichtextFacet["Main"][]; 520 + } 521 + 522 + export type AppBskyGraphDefsCuratelist = "app.bsky.graph.defs#curatelist"; 523 + 524 + export interface AppBskyGraphDefsListItemView { 525 + uri: string; 526 + subject: AppBskyActorDefs["ProfileView"]; 527 + } 528 + 529 + export interface AppBskyGraphDefsRelationship { 530 + did: string; 531 + /** if the actor follows this DID, this is the AT-URI of the follow record */ 532 + following?: string; 533 + /** if the actor is followed by this DID, contains the AT-URI of the follow record */ 534 + followedBy?: string; 535 + } 536 + 537 + export interface AppBskyGraphDefsListViewBasic { 538 + cid: string; 539 + uri: string; 540 + name: string; 541 + avatar?: string; 542 + labels?: ComAtprotoLabelDefs["Label"][]; 543 + viewer?: AppBskyGraphDefs["ListViewerState"]; 544 + purpose: AppBskyGraphDefs["ListPurpose"]; 545 + indexedAt?: string; 546 + listItemCount?: number; 547 + } 548 + 549 + export interface AppBskyGraphDefsNotFoundActor { 550 + actor: string; 551 + notFound: boolean; 552 + } 553 + 554 + export type AppBskyGraphDefsReferencelist = "app.bsky.graph.defs#referencelist"; 555 + 556 + export interface AppBskyGraphDefsListViewerState { 557 + muted?: boolean; 558 + blocked?: string; 559 + } 560 + 561 + export interface AppBskyGraphDefsStarterPackView { 562 + cid: string; 563 + uri: string; 564 + list?: AppBskyGraphDefs["ListViewBasic"]; 565 + feeds?: AppBskyFeedDefs["GeneratorView"][]; 566 + labels?: ComAtprotoLabelDefs["Label"][]; 567 + record: unknown; 568 + creator: AppBskyActorDefs["ProfileViewBasic"]; 569 + indexedAt: string; 570 + joinedWeekCount?: number; 571 + listItemsSample?: AppBskyGraphDefs["ListItemView"][]; 572 + joinedAllTimeCount?: number; 573 + } 574 + 575 + export interface AppBskyGraphDefsStarterPackViewBasic { 576 + cid: string; 577 + uri: string; 578 + labels?: ComAtprotoLabelDefs["Label"][]; 579 + record: unknown; 580 + creator: AppBskyActorDefs["ProfileViewBasic"]; 581 + indexedAt: string; 582 + listItemCount?: number; 583 + joinedWeekCount?: number; 584 + joinedAllTimeCount?: number; 585 + } 586 + 587 + export interface AppBskyFeedDefsPostView { 588 + cid: string; 589 + uri: string; 590 + embed?: 591 + | AppBskyEmbedImages["View"] 592 + | AppBskyEmbedVideo["View"] 593 + | AppBskyEmbedExternal["View"] 594 + | AppBskyEmbedRecord["View"] 595 + | AppBskyEmbedRecordWithMedia["View"] 596 + | { $type: string; [key: string]: unknown }; 597 + author: AppBskyActorDefs["ProfileViewBasic"]; 598 + labels?: ComAtprotoLabelDefs["Label"][]; 599 + record: unknown; 600 + viewer?: AppBskyFeedDefs["ViewerState"]; 601 + indexedAt: string; 602 + likeCount?: number; 603 + quoteCount?: number; 604 + replyCount?: number; 605 + threadgate?: AppBskyFeedDefs["ThreadgateView"]; 606 + repostCount?: number; 607 + } 608 + 609 + export interface AppBskyFeedDefsReplyRef { 610 + root: 611 + | AppBskyFeedDefs["PostView"] 612 + | AppBskyFeedDefs["NotFoundPost"] 613 + | AppBskyFeedDefs["BlockedPost"] 614 + | { $type: string; [key: string]: unknown }; 615 + parent: 616 + | AppBskyFeedDefs["PostView"] 617 + | AppBskyFeedDefs["NotFoundPost"] 618 + | AppBskyFeedDefs["BlockedPost"] 619 + | { $type: string; [key: string]: unknown }; 620 + /** When parent is a reply to another post, this is the author of that post. */ 621 + grandparentAuthor?: AppBskyActorDefs["ProfileViewBasic"]; 622 + } 623 + 624 + export interface AppBskyFeedDefsReasonPin {} 625 + 626 + export interface AppBskyFeedDefsBlockedPost { 627 + uri: string; 628 + author: AppBskyFeedDefs["BlockedAuthor"]; 629 + blocked: boolean; 630 + } 631 + 632 + export interface AppBskyFeedDefsInteraction { 633 + item?: string; 634 + event?: AppBskyFeedDefsEvent; 635 + /** Context on a feed item that was originally supplied by the feed generator on getFeedSkeleton. */ 636 + feedContext?: string; 637 + } 638 + 639 + export type AppBskyFeedDefsRequestLess = "app.bsky.feed.defs#requestLess"; 640 + export type AppBskyFeedDefsRequestMore = "app.bsky.feed.defs#requestMore"; 641 + 642 + export interface AppBskyFeedDefsViewerState { 643 + like?: string; 644 + pinned?: boolean; 645 + repost?: string; 646 + threadMuted?: boolean; 647 + replyDisabled?: boolean; 648 + embeddingDisabled?: boolean; 649 + } 650 + 651 + export interface AppBskyFeedDefsFeedViewPost { 652 + post: AppBskyFeedDefs["PostView"]; 653 + reply?: AppBskyFeedDefs["ReplyRef"]; 654 + reason?: 655 + | AppBskyFeedDefs["ReasonRepost"] 656 + | AppBskyFeedDefs["ReasonPin"] 657 + | { 658 + $type: string; 659 + [key: string]: unknown; 660 + }; 661 + /** Context provided by feed generator that may be passed back alongside interactions. */ 662 + feedContext?: string; 663 + } 664 + 665 + export interface AppBskyFeedDefsNotFoundPost { 666 + uri: string; 667 + notFound: boolean; 668 + } 669 + 670 + export interface AppBskyFeedDefsReasonRepost { 671 + by: AppBskyActorDefs["ProfileViewBasic"]; 672 + indexedAt: string; 673 + } 674 + 675 + export interface AppBskyFeedDefsBlockedAuthor { 676 + did: string; 677 + viewer?: AppBskyActorDefs["ViewerState"]; 678 + } 679 + 680 + export interface AppBskyFeedDefsGeneratorView { 681 + cid: string; 682 + did: string; 683 + uri: string; 684 + avatar?: string; 685 + labels?: ComAtprotoLabelDefs["Label"][]; 686 + viewer?: AppBskyFeedDefs["GeneratorViewerState"]; 687 + creator: AppBskyActorDefs["ProfileView"]; 688 + indexedAt: string; 689 + likeCount?: number; 690 + contentMode?: AppBskyFeedDefsContentMode; 691 + description?: string; 692 + displayName: string; 693 + descriptionFacets?: AppBskyRichtextFacet["Main"][]; 694 + acceptsInteractions?: boolean; 695 + } 696 + 697 + export interface AppBskyFeedDefsThreadContext { 698 + rootAuthorLike?: string; 699 + } 700 + 701 + export interface AppBskyFeedDefsThreadViewPost { 702 + post: AppBskyFeedDefs["PostView"]; 703 + parent?: 704 + | AppBskyFeedDefs["ThreadViewPost"] 705 + | AppBskyFeedDefs["NotFoundPost"] 706 + | AppBskyFeedDefs["BlockedPost"] 707 + | { $type: string; [key: string]: unknown }; 708 + replies?: 709 + | AppBskyFeedDefs["ThreadViewPost"] 710 + | AppBskyFeedDefs["NotFoundPost"] 711 + | AppBskyFeedDefs["BlockedPost"] 712 + | { $type: string; [key: string]: unknown }[]; 713 + threadContext?: AppBskyFeedDefs["ThreadContext"]; 714 + } 715 + 716 + export interface AppBskyFeedDefsThreadgateView { 717 + cid?: string; 718 + uri?: string; 719 + lists?: AppBskyGraphDefs["ListViewBasic"][]; 720 + record?: unknown; 721 + } 722 + 723 + export type AppBskyFeedDefsInteractionLike = 724 + "app.bsky.feed.defs#interactionLike"; 725 + export type AppBskyFeedDefsInteractionSeen = 726 + "app.bsky.feed.defs#interactionSeen"; 727 + export type AppBskyFeedDefsClickthroughItem = 728 + "app.bsky.feed.defs#clickthroughItem"; 729 + export type AppBskyFeedDefsContentModeVideo = 730 + "app.bsky.feed.defs#contentModeVideo"; 731 + export type AppBskyFeedDefsInteractionQuote = 732 + "app.bsky.feed.defs#interactionQuote"; 733 + export type AppBskyFeedDefsInteractionReply = 734 + "app.bsky.feed.defs#interactionReply"; 735 + export type AppBskyFeedDefsInteractionShare = 736 + "app.bsky.feed.defs#interactionShare"; 737 + 738 + export interface AppBskyFeedDefsSkeletonFeedPost { 739 + post: string; 740 + reason?: 741 + | AppBskyFeedDefs["SkeletonReasonRepost"] 742 + | AppBskyFeedDefs["SkeletonReasonPin"] 743 + | { $type: string; [key: string]: unknown }; 744 + /** Context that will be passed through to client and may be passed to feed generator back alongside interactions. */ 745 + feedContext?: string; 746 + } 747 + 748 + export type AppBskyFeedDefsClickthroughEmbed = 749 + "app.bsky.feed.defs#clickthroughEmbed"; 750 + export type AppBskyFeedDefsInteractionRepost = 751 + "app.bsky.feed.defs#interactionRepost"; 752 + 753 + export interface AppBskyFeedDefsSkeletonReasonPin {} 754 + 755 + export type AppBskyFeedDefsClickthroughAuthor = 756 + "app.bsky.feed.defs#clickthroughAuthor"; 757 + export type AppBskyFeedDefsClickthroughReposter = 758 + "app.bsky.feed.defs#clickthroughReposter"; 759 + 760 + export interface AppBskyFeedDefsGeneratorViewerState { 761 + like?: string; 762 + } 763 + 764 + export interface AppBskyFeedDefsSkeletonReasonRepost { 765 + repost: string; 766 + } 767 + 768 + export type AppBskyFeedDefsContentModeUnspecified = 769 + "app.bsky.feed.defs#contentModeUnspecified"; 770 + 771 + export interface AppBskyFeedPostgate { 772 + /** Reference (AT-URI) to the post record. */ 773 + post: string; 774 + createdAt: string; 775 + /** List of rules defining who can embed this post. If value is an empty array or is undefined, no particular rules apply and anyone can embed. */ 776 + embeddingRules?: 777 + | AppBskyFeedPostgate["DisableRule"] 778 + | { 779 + $type: string; 780 + [key: string]: unknown; 781 + }[]; 782 + /** List of AT-URIs embedding this post that the author has detached from. */ 783 + detachedEmbeddingUris?: string[]; 784 + } 785 + 786 + export type AppBskyFeedPostgateSortFields = "post" | "createdAt"; 787 + 788 + export interface AppBskyFeedPostgateDisableRule {} 789 + 790 + export interface AppBskyFeedThreadgate { 791 + /** Reference (AT-URI) to the post record. */ 792 + post: string; 793 + /** List of rules defining who can reply to this post. If value is an empty array, no one can reply. If value is undefined, anyone can reply. */ 794 + allow?: 795 + | AppBskyFeedThreadgate["MentionRule"] 796 + | AppBskyFeedThreadgate["FollowerRule"] 797 + | AppBskyFeedThreadgate["FollowingRule"] 798 + | AppBskyFeedThreadgate["ListRule"] 799 + | { $type: string; [key: string]: unknown }[]; 800 + createdAt: string; 801 + /** List of hidden reply URIs. */ 802 + hiddenReplies?: string[]; 803 + } 804 + 805 + export type AppBskyFeedThreadgateSortFields = "post" | "createdAt"; 806 + 807 + export interface AppBskyFeedThreadgateListRule { 808 + list: string; 809 + } 810 + 811 + export interface AppBskyFeedThreadgateMentionRule {} 812 + 813 + export interface AppBskyFeedThreadgateFollowerRule {} 814 + 815 + export interface AppBskyFeedThreadgateFollowingRule {} 816 + 817 + export interface AppBskyRichtextFacetTag { 818 + tag: string; 819 + } 820 + 821 + export interface AppBskyRichtextFacetLink { 822 + uri: string; 823 + } 824 + 825 + export interface AppBskyRichtextFacetMain { 826 + index: AppBskyRichtextFacet["ByteSlice"]; 827 + features: 828 + | AppBskyRichtextFacet["Mention"] 829 + | AppBskyRichtextFacet["Link"] 830 + | AppBskyRichtextFacet["Tag"] 831 + | { $type: string; [key: string]: unknown }[]; 832 + } 833 + 834 + export interface AppBskyRichtextFacetMention { 835 + did: string; 836 + } 837 + 838 + export interface AppBskyRichtextFacetByteSlice { 839 + byteEnd: number; 840 + byteStart: number; 841 + } 842 + 843 + export interface AppBskyActorDefsNux { 844 + id: string; 845 + /** Arbitrary data for the NUX. The structure is defined by the NUX itself. Limited to 300 characters. */ 846 + data?: string; 847 + completed: boolean; 848 + /** The date and time at which the NUX will expire and should be considered completed. */ 849 + expiresAt?: string; 850 + } 851 + 852 + export interface AppBskyActorDefsMutedWord { 853 + id?: string; 854 + /** The muted word itself. */ 855 + value: string; 856 + /** The intended targets of the muted word. */ 857 + targets: AppBskyActorDefs["MutedWordTarget"][]; 858 + /** The date and time at which the muted word will expire and no longer be applied. */ 859 + expiresAt?: string; 860 + /** Groups of users to apply the muted word to. If undefined, applies to all users. */ 861 + actorTarget?: AppBskyActorDefsActorTarget; 862 + } 863 + 864 + export interface AppBskyActorDefsSavedFeed { 865 + id: string; 866 + type: AppBskyActorDefsType; 867 + value: string; 868 + pinned: boolean; 869 + } 870 + 871 + export type AppBskyActorDefsPreferences = 872 + | AppBskyActorDefs["AdultContentPref"] 873 + | AppBskyActorDefs["ContentLabelPref"] 874 + | AppBskyActorDefs["SavedFeedsPref"] 875 + | AppBskyActorDefs["SavedFeedsPrefV2"] 876 + | AppBskyActorDefs["PersonalDetailsPref"] 877 + | AppBskyActorDefs["FeedViewPref"] 878 + | AppBskyActorDefs["ThreadViewPref"] 879 + | AppBskyActorDefs["InterestsPref"] 880 + | AppBskyActorDefs["MutedWordsPref"] 881 + | AppBskyActorDefs["HiddenPostsPref"] 882 + | AppBskyActorDefs["BskyAppStatePref"] 883 + | AppBskyActorDefs["LabelersPref"] 884 + | AppBskyActorDefs["PostInteractionSettingsPref"] 885 + | { $type: string; [key: string]: unknown }[]; 886 + 887 + export interface AppBskyActorDefsProfileView { 888 + did: string; 889 + avatar?: string; 890 + handle: string; 891 + labels?: ComAtprotoLabelDefs["Label"][]; 892 + viewer?: AppBskyActorDefs["ViewerState"]; 893 + createdAt?: string; 894 + indexedAt?: string; 895 + associated?: AppBskyActorDefs["ProfileAssociated"]; 896 + description?: string; 897 + displayName?: string; 898 + } 899 + 900 + export interface AppBskyActorDefsViewerState { 901 + muted?: boolean; 902 + blocking?: string; 903 + blockedBy?: boolean; 904 + following?: string; 905 + followedBy?: string; 906 + mutedByList?: AppBskyGraphDefs["ListViewBasic"]; 907 + blockingByList?: AppBskyGraphDefs["ListViewBasic"]; 908 + knownFollowers?: AppBskyActorDefs["KnownFollowers"]; 909 + } 910 + 911 + export interface AppBskyActorDefsFeedViewPref { 912 + /** The URI of the feed, or an identifier which describes the feed. */ 913 + feed: string; 914 + /** Hide replies in the feed. */ 915 + hideReplies?: boolean; 916 + /** Hide reposts in the feed. */ 917 + hideReposts?: boolean; 918 + /** Hide quote posts in the feed. */ 919 + hideQuotePosts?: boolean; 920 + /** Hide replies in the feed if they do not have this number of likes. */ 921 + hideRepliesByLikeCount?: number; 922 + /** Hide replies in the feed if they are not by followed users. */ 923 + hideRepliesByUnfollowed?: boolean; 924 + } 925 + 926 + export interface AppBskyActorDefsLabelersPref { 927 + labelers: AppBskyActorDefs["LabelerPrefItem"][]; 928 + } 929 + 930 + export interface AppBskyActorDefsInterestsPref { 931 + /** A list of tags which describe the account owner's interests gathered during onboarding. */ 932 + tags: string[]; 933 + } 934 + 935 + export interface AppBskyActorDefsKnownFollowers { 936 + count: number; 937 + followers: AppBskyActorDefs["ProfileViewBasic"][]; 938 + } 939 + 940 + export interface AppBskyActorDefsMutedWordsPref { 941 + /** A list of words the account owner has muted. */ 942 + items: AppBskyActorDefs["MutedWord"][]; 943 + } 944 + 945 + export interface AppBskyActorDefsSavedFeedsPref { 946 + saved: string[]; 947 + pinned: string[]; 948 + timelineIndex?: number; 949 + } 950 + 951 + export interface AppBskyActorDefsThreadViewPref { 952 + /** Sorting mode for threads. */ 953 + sort?: AppBskyActorDefsSort; 954 + /** Show followed users at the top of all replies. */ 955 + prioritizeFollowedUsers?: boolean; 956 + } 957 + 958 + export interface AppBskyActorDefsHiddenPostsPref { 959 + /** A list of URIs of posts the account owner has hidden. */ 960 + items: string[]; 961 + } 962 + 963 + export interface AppBskyActorDefsLabelerPrefItem { 964 + did: string; 965 + } 966 + 967 + export interface AppBskyActorDefsAdultContentPref { 968 + enabled: boolean; 969 + } 970 + 971 + export interface AppBskyActorDefsBskyAppStatePref { 972 + /** Storage for NUXs the user has encountered. */ 973 + nuxs?: AppBskyActorDefs["Nux"][]; 974 + /** An array of tokens which identify nudges (modals, popups, tours, highlight dots) that should be shown to the user. */ 975 + queuedNudges?: string[]; 976 + activeProgressGuide?: AppBskyActorDefs["BskyAppProgressGuide"]; 977 + } 978 + 979 + export interface AppBskyActorDefsContentLabelPref { 980 + label: string; 981 + /** Which labeler does this preference apply to? If undefined, applies globally. */ 982 + labelerDid?: string; 983 + visibility: AppBskyActorDefsVisibility; 984 + } 985 + 986 + export interface AppBskyActorDefsProfileViewBasic { 987 + did: string; 988 + avatar?: string; 989 + handle: string; 990 + labels?: ComAtprotoLabelDefs["Label"][]; 991 + viewer?: AppBskyActorDefs["ViewerState"]; 992 + createdAt?: string; 993 + associated?: AppBskyActorDefs["ProfileAssociated"]; 994 + displayName?: string; 995 + } 996 + 997 + export interface AppBskyActorDefsSavedFeedsPrefV2 { 998 + items: AppBskyActorDefs["SavedFeed"][]; 999 + } 1000 + 1001 + export interface AppBskyActorDefsProfileAssociated { 1002 + chat?: AppBskyActorDefs["ProfileAssociatedChat"]; 1003 + lists?: number; 1004 + labeler?: boolean; 1005 + feedgens?: number; 1006 + starterPacks?: number; 1007 + } 1008 + 1009 + export interface AppBskyActorDefsPersonalDetailsPref { 1010 + /** The birth date of account owner. */ 1011 + birthDate?: string; 1012 + } 1013 + 1014 + export interface AppBskyActorDefsProfileViewDetailed { 1015 + did: string; 1016 + avatar?: string; 1017 + banner?: string; 1018 + handle: string; 1019 + labels?: ComAtprotoLabelDefs["Label"][]; 1020 + viewer?: AppBskyActorDefs["ViewerState"]; 1021 + createdAt?: string; 1022 + indexedAt?: string; 1023 + associated?: AppBskyActorDefs["ProfileAssociated"]; 1024 + pinnedPost?: ComAtprotoRepoStrongRef; 1025 + postsCount?: number; 1026 + description?: string; 1027 + displayName?: string; 1028 + followsCount?: number; 1029 + followersCount?: number; 1030 + joinedViaStarterPack?: AppBskyGraphDefs["StarterPackViewBasic"]; 1031 + } 1032 + 1033 + export interface AppBskyActorDefsBskyAppProgressGuide { 1034 + guide: string; 1035 + } 1036 + 1037 + export interface AppBskyActorDefsProfileAssociatedChat { 1038 + allowIncoming: AppBskyActorDefsAllowIncoming; 1039 + } 1040 + 1041 + export interface AppBskyActorDefsPostInteractionSettingsPref { 1042 + /** Matches threadgate record. List of rules defining who can reply to this users posts. If value is an empty array, no one can reply. If value is undefined, anyone can reply. */ 1043 + threadgateAllowRules?: 1044 + | AppBskyFeedThreadgate["MentionRule"] 1045 + | AppBskyFeedThreadgate["FollowerRule"] 1046 + | AppBskyFeedThreadgate["FollowingRule"] 1047 + | AppBskyFeedThreadgate["ListRule"] 1048 + | { $type: string; [key: string]: unknown }[]; 1049 + /** Matches postgate record. List of rules defining who can embed this users posts. If value is an empty array or is undefined, no particular rules apply and anyone can embed. */ 1050 + postgateEmbeddingRules?: 1051 + | AppBskyFeedPostgate["DisableRule"] 1052 + | { 1053 + $type: string; 1054 + [key: string]: unknown; 1055 + }[]; 1056 + } 1057 + 266 1058 export interface AppBskyActorProfile { 267 1059 /** Small image to be displayed next to posts from account. AKA, 'profile picture' */ 268 1060 avatar?: BlobRef; ··· 288 1080 | "description" 289 1081 | "displayName"; 290 1082 1083 + export interface AppBskyLabelerDefsLabelerView { 1084 + cid: string; 1085 + uri: string; 1086 + labels?: ComAtprotoLabelDefs["Label"][]; 1087 + viewer?: AppBskyLabelerDefs["LabelerViewerState"]; 1088 + creator: AppBskyActorDefs["ProfileView"]; 1089 + indexedAt: string; 1090 + likeCount?: number; 1091 + } 1092 + 1093 + export interface AppBskyLabelerDefsLabelerPolicies { 1094 + /** The label values which this labeler publishes. May include global or custom labels. */ 1095 + labelValues: ComAtprotoLabelDefs["LabelValue"][]; 1096 + /** Label values created by this labeler and scoped exclusively to it. Labels defined here will override global label definitions for this labeler. */ 1097 + labelValueDefinitions?: ComAtprotoLabelDefs["LabelValueDefinition"][]; 1098 + } 1099 + 1100 + export interface AppBskyLabelerDefsLabelerViewerState { 1101 + like?: string; 1102 + } 1103 + 1104 + export interface AppBskyLabelerDefsLabelerViewDetailed { 1105 + cid: string; 1106 + uri: string; 1107 + labels?: ComAtprotoLabelDefs["Label"][]; 1108 + viewer?: AppBskyLabelerDefs["LabelerViewerState"]; 1109 + creator: AppBskyActorDefs["ProfileView"]; 1110 + policies: AppBskyLabelerDefs["LabelerPolicies"]; 1111 + indexedAt: string; 1112 + likeCount?: number; 1113 + } 1114 + 1115 + export interface NetworkSlicesWaitlistRequest { 1116 + /** The AT URI of the slice being requested access to */ 1117 + slice: string; 1118 + /** When the user joined the waitlist */ 1119 + createdAt: string; 1120 + } 1121 + 1122 + export type NetworkSlicesWaitlistRequestSortFields = "slice" | "createdAt"; 1123 + 1124 + export interface NetworkSlicesWaitlistDefsRequestView { 1125 + /** The AT URI of the slice being requested access to */ 1126 + slice: string; 1127 + /** When the user joined the waitlist */ 1128 + createdAt: string; 1129 + /** Profile of the requester */ 1130 + profile?: AppBskyActorDefs["ProfileViewBasic"]; 1131 + } 1132 + 1133 + export interface NetworkSlicesWaitlistDefsInviteView { 1134 + /** The DID being invited */ 1135 + did: string; 1136 + /** The AT URI of the slice this invite is for */ 1137 + slice: string; 1138 + /** When this invitation was created */ 1139 + createdAt: string; 1140 + /** Optional expiration date for this invitation */ 1141 + expiresAt?: string; 1142 + /** The AT URI of this invite record */ 1143 + uri?: string; 1144 + /** Profile of the invitee */ 1145 + profile?: AppBskyActorDefs["ProfileViewBasic"]; 1146 + } 1147 + 1148 + export interface NetworkSlicesWaitlistInvite { 1149 + /** The DID being invited */ 1150 + did: string; 1151 + /** The AT URI of the slice this invite is for */ 1152 + slice: string; 1153 + /** When this invitation was created */ 1154 + createdAt: string; 1155 + /** Optional expiration date for this invitation */ 1156 + expiresAt?: string; 1157 + } 1158 + 1159 + export type NetworkSlicesWaitlistInviteSortFields = 1160 + | "did" 1161 + | "slice" 1162 + | "createdAt" 1163 + | "expiresAt"; 1164 + 291 1165 export interface NetworkSlicesSliceDefsSliceView { 292 1166 uri: string; 293 1167 cid: string; ··· 323 1197 } 324 1198 325 1199 export type NetworkSlicesSliceSortFields = "name" | "domain" | "createdAt"; 326 - 327 - export interface NetworkSlicesWaiting { 328 - /** When the user joined the waitlist */ 329 - createdAt: string; 330 - } 331 - 332 - export type NetworkSlicesWaitingSortFields = "createdAt"; 333 1200 334 1201 export interface NetworkSlicesLexicon { 335 1202 /** Namespaced identifier for the lexicon */ ··· 406 1273 407 1274 export interface ComAtprotoLabelDefsLabelValueDefinition { 408 1275 /** 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. */ 409 - blurs: string; 1276 + blurs: ComAtprotoLabelDefsBlurs; 410 1277 locales: ComAtprotoLabelDefs["LabelValueDefinitionStrings"][]; 411 1278 /** How should a client visually convey this label? 'inform' means neutral and informational; 'alert' means negative and warning; 'none' means show nothing. */ 412 - severity: string; 1279 + severity: ComAtprotoLabelDefsSeverity; 413 1280 /** Does the user need to have adult content enabled in order to configure this label? */ 414 1281 adultOnly?: boolean; 415 1282 /** The value of the label being defined. Must only include lowercase ascii and the '-' character ([a-z-]+). */ 416 1283 identifier: string; 417 1284 /** The default setting for this label. */ 418 - defaultSetting?: string; 1285 + defaultSetting?: ComAtprotoLabelDefsDefaultSetting; 419 1286 } 420 1287 421 1288 export interface ComAtprotoLabelDefsLabelValueDefinitionStrings { ··· 432 1299 uri: string; 433 1300 } 434 1301 1302 + export interface AppBskyEmbedDefs { 1303 + readonly AspectRatio: AppBskyEmbedDefsAspectRatio; 1304 + } 1305 + 1306 + export interface AppBskyEmbedRecord { 1307 + readonly Main: AppBskyEmbedRecordMain; 1308 + readonly View: AppBskyEmbedRecordView; 1309 + readonly ViewRecord: AppBskyEmbedRecordViewRecord; 1310 + readonly ViewBlocked: AppBskyEmbedRecordViewBlocked; 1311 + readonly ViewDetached: AppBskyEmbedRecordViewDetached; 1312 + readonly ViewNotFound: AppBskyEmbedRecordViewNotFound; 1313 + } 1314 + 1315 + export interface AppBskyEmbedImages { 1316 + readonly Main: AppBskyEmbedImagesMain; 1317 + readonly View: AppBskyEmbedImagesView; 1318 + readonly Image: AppBskyEmbedImagesImage; 1319 + readonly ViewImage: AppBskyEmbedImagesViewImage; 1320 + } 1321 + 1322 + export interface AppBskyEmbedRecordWithMedia { 1323 + readonly Main: AppBskyEmbedRecordWithMediaMain; 1324 + readonly View: AppBskyEmbedRecordWithMediaView; 1325 + } 1326 + 1327 + export interface AppBskyEmbedVideo { 1328 + readonly Main: AppBskyEmbedVideoMain; 1329 + readonly View: AppBskyEmbedVideoView; 1330 + readonly Caption: AppBskyEmbedVideoCaption; 1331 + } 1332 + 1333 + export interface AppBskyEmbedExternal { 1334 + readonly Main: AppBskyEmbedExternalMain; 1335 + readonly View: AppBskyEmbedExternalView; 1336 + readonly External: AppBskyEmbedExternalExternal; 1337 + readonly ViewExternal: AppBskyEmbedExternalViewExternal; 1338 + } 1339 + 1340 + export interface AppBskyGraphDefs { 1341 + readonly Modlist: AppBskyGraphDefsModlist; 1342 + readonly ListView: AppBskyGraphDefsListView; 1343 + readonly Curatelist: AppBskyGraphDefsCuratelist; 1344 + readonly ListPurpose: AppBskyGraphDefsListPurpose; 1345 + readonly ListItemView: AppBskyGraphDefsListItemView; 1346 + readonly Relationship: AppBskyGraphDefsRelationship; 1347 + readonly ListViewBasic: AppBskyGraphDefsListViewBasic; 1348 + readonly NotFoundActor: AppBskyGraphDefsNotFoundActor; 1349 + readonly Referencelist: AppBskyGraphDefsReferencelist; 1350 + readonly ListViewerState: AppBskyGraphDefsListViewerState; 1351 + readonly StarterPackView: AppBskyGraphDefsStarterPackView; 1352 + readonly StarterPackViewBasic: AppBskyGraphDefsStarterPackViewBasic; 1353 + } 1354 + 1355 + export interface AppBskyFeedDefs { 1356 + readonly PostView: AppBskyFeedDefsPostView; 1357 + readonly ReplyRef: AppBskyFeedDefsReplyRef; 1358 + readonly ReasonPin: AppBskyFeedDefsReasonPin; 1359 + readonly BlockedPost: AppBskyFeedDefsBlockedPost; 1360 + readonly Interaction: AppBskyFeedDefsInteraction; 1361 + readonly RequestLess: AppBskyFeedDefsRequestLess; 1362 + readonly RequestMore: AppBskyFeedDefsRequestMore; 1363 + readonly ViewerState: AppBskyFeedDefsViewerState; 1364 + readonly FeedViewPost: AppBskyFeedDefsFeedViewPost; 1365 + readonly NotFoundPost: AppBskyFeedDefsNotFoundPost; 1366 + readonly ReasonRepost: AppBskyFeedDefsReasonRepost; 1367 + readonly BlockedAuthor: AppBskyFeedDefsBlockedAuthor; 1368 + readonly GeneratorView: AppBskyFeedDefsGeneratorView; 1369 + readonly ThreadContext: AppBskyFeedDefsThreadContext; 1370 + readonly ThreadViewPost: AppBskyFeedDefsThreadViewPost; 1371 + readonly ThreadgateView: AppBskyFeedDefsThreadgateView; 1372 + readonly InteractionLike: AppBskyFeedDefsInteractionLike; 1373 + readonly InteractionSeen: AppBskyFeedDefsInteractionSeen; 1374 + readonly ClickthroughItem: AppBskyFeedDefsClickthroughItem; 1375 + readonly ContentModeVideo: AppBskyFeedDefsContentModeVideo; 1376 + readonly InteractionQuote: AppBskyFeedDefsInteractionQuote; 1377 + readonly InteractionReply: AppBskyFeedDefsInteractionReply; 1378 + readonly InteractionShare: AppBskyFeedDefsInteractionShare; 1379 + readonly SkeletonFeedPost: AppBskyFeedDefsSkeletonFeedPost; 1380 + readonly ClickthroughEmbed: AppBskyFeedDefsClickthroughEmbed; 1381 + readonly InteractionRepost: AppBskyFeedDefsInteractionRepost; 1382 + readonly SkeletonReasonPin: AppBskyFeedDefsSkeletonReasonPin; 1383 + readonly ClickthroughAuthor: AppBskyFeedDefsClickthroughAuthor; 1384 + readonly ClickthroughReposter: AppBskyFeedDefsClickthroughReposter; 1385 + readonly GeneratorViewerState: AppBskyFeedDefsGeneratorViewerState; 1386 + readonly SkeletonReasonRepost: AppBskyFeedDefsSkeletonReasonRepost; 1387 + readonly ContentModeUnspecified: AppBskyFeedDefsContentModeUnspecified; 1388 + } 1389 + 1390 + export interface AppBskyFeedPostgate { 1391 + readonly Main: AppBskyFeedPostgate; 1392 + readonly DisableRule: AppBskyFeedPostgateDisableRule; 1393 + } 1394 + 1395 + export interface AppBskyFeedThreadgate { 1396 + readonly Main: AppBskyFeedThreadgate; 1397 + readonly ListRule: AppBskyFeedThreadgateListRule; 1398 + readonly MentionRule: AppBskyFeedThreadgateMentionRule; 1399 + readonly FollowerRule: AppBskyFeedThreadgateFollowerRule; 1400 + readonly FollowingRule: AppBskyFeedThreadgateFollowingRule; 1401 + } 1402 + 1403 + export interface AppBskyRichtextFacet { 1404 + readonly Tag: AppBskyRichtextFacetTag; 1405 + readonly Link: AppBskyRichtextFacetLink; 1406 + readonly Main: AppBskyRichtextFacetMain; 1407 + readonly Mention: AppBskyRichtextFacetMention; 1408 + readonly ByteSlice: AppBskyRichtextFacetByteSlice; 1409 + } 1410 + 1411 + export interface AppBskyActorDefs { 1412 + readonly Nux: AppBskyActorDefsNux; 1413 + readonly MutedWord: AppBskyActorDefsMutedWord; 1414 + readonly SavedFeed: AppBskyActorDefsSavedFeed; 1415 + readonly Preferences: AppBskyActorDefsPreferences; 1416 + readonly ProfileView: AppBskyActorDefsProfileView; 1417 + readonly ViewerState: AppBskyActorDefsViewerState; 1418 + readonly FeedViewPref: AppBskyActorDefsFeedViewPref; 1419 + readonly LabelersPref: AppBskyActorDefsLabelersPref; 1420 + readonly InterestsPref: AppBskyActorDefsInterestsPref; 1421 + readonly KnownFollowers: AppBskyActorDefsKnownFollowers; 1422 + readonly MutedWordsPref: AppBskyActorDefsMutedWordsPref; 1423 + readonly SavedFeedsPref: AppBskyActorDefsSavedFeedsPref; 1424 + readonly ThreadViewPref: AppBskyActorDefsThreadViewPref; 1425 + readonly HiddenPostsPref: AppBskyActorDefsHiddenPostsPref; 1426 + readonly LabelerPrefItem: AppBskyActorDefsLabelerPrefItem; 1427 + readonly MutedWordTarget: AppBskyActorDefsMutedWordTarget; 1428 + readonly AdultContentPref: AppBskyActorDefsAdultContentPref; 1429 + readonly BskyAppStatePref: AppBskyActorDefsBskyAppStatePref; 1430 + readonly ContentLabelPref: AppBskyActorDefsContentLabelPref; 1431 + readonly ProfileViewBasic: AppBskyActorDefsProfileViewBasic; 1432 + readonly SavedFeedsPrefV2: AppBskyActorDefsSavedFeedsPrefV2; 1433 + readonly ProfileAssociated: AppBskyActorDefsProfileAssociated; 1434 + readonly PersonalDetailsPref: AppBskyActorDefsPersonalDetailsPref; 1435 + readonly ProfileViewDetailed: AppBskyActorDefsProfileViewDetailed; 1436 + readonly BskyAppProgressGuide: AppBskyActorDefsBskyAppProgressGuide; 1437 + readonly ProfileAssociatedChat: AppBskyActorDefsProfileAssociatedChat; 1438 + readonly PostInteractionSettingsPref: AppBskyActorDefsPostInteractionSettingsPref; 1439 + } 1440 + 1441 + export interface AppBskyLabelerDefs { 1442 + readonly LabelerView: AppBskyLabelerDefsLabelerView; 1443 + readonly LabelerPolicies: AppBskyLabelerDefsLabelerPolicies; 1444 + readonly LabelerViewerState: AppBskyLabelerDefsLabelerViewerState; 1445 + readonly LabelerViewDetailed: AppBskyLabelerDefsLabelerViewDetailed; 1446 + } 1447 + 1448 + export interface NetworkSlicesWaitlistDefs { 1449 + readonly RequestView: NetworkSlicesWaitlistDefsRequestView; 1450 + readonly InviteView: NetworkSlicesWaitlistDefsInviteView; 1451 + } 1452 + 435 1453 export interface NetworkSlicesSliceDefs { 436 1454 readonly SliceView: NetworkSlicesSliceDefsSliceView; 437 1455 readonly SparklinePoint: NetworkSlicesSliceDefsSparklinePoint; ··· 444 1462 export interface ComAtprotoLabelDefs { 445 1463 readonly Label: ComAtprotoLabelDefsLabel; 446 1464 readonly SelfLabel: ComAtprotoLabelDefsSelfLabel; 1465 + readonly LabelValue: ComAtprotoLabelDefsLabelValue; 447 1466 readonly SelfLabels: ComAtprotoLabelDefsSelfLabels; 448 1467 readonly LabelValueDefinition: ComAtprotoLabelDefsLabelValueDefinition; 449 1468 readonly LabelValueDefinitionStrings: ComAtprotoLabelDefsLabelValueDefinitionStrings; 450 1469 } 451 1470 1471 + class FollowGraphBskyAppClient { 1472 + private readonly client: SlicesClient; 1473 + 1474 + constructor(client: SlicesClient) { 1475 + this.client = client; 1476 + } 1477 + 1478 + async getRecords(params?: { 1479 + limit?: number; 1480 + cursor?: string; 1481 + where?: { 1482 + [K in 1483 + | AppBskyGraphFollowSortFields 1484 + | IndexedRecordFields]?: WhereCondition; 1485 + }; 1486 + orWhere?: { 1487 + [K in 1488 + | AppBskyGraphFollowSortFields 1489 + | IndexedRecordFields]?: WhereCondition; 1490 + }; 1491 + sortBy?: SortField<AppBskyGraphFollowSortFields>[]; 1492 + }): Promise<GetRecordsResponse<AppBskyGraphFollow>> { 1493 + return await this.client.getRecords("app.bsky.graph.follow", params); 1494 + } 1495 + 1496 + async getRecord( 1497 + params: GetRecordParams 1498 + ): Promise<RecordResponse<AppBskyGraphFollow>> { 1499 + return await this.client.getRecord("app.bsky.graph.follow", params); 1500 + } 1501 + 1502 + async countRecords(params?: { 1503 + limit?: number; 1504 + cursor?: string; 1505 + where?: { 1506 + [K in 1507 + | AppBskyGraphFollowSortFields 1508 + | IndexedRecordFields]?: WhereCondition; 1509 + }; 1510 + orWhere?: { 1511 + [K in 1512 + | AppBskyGraphFollowSortFields 1513 + | IndexedRecordFields]?: WhereCondition; 1514 + }; 1515 + sortBy?: SortField<AppBskyGraphFollowSortFields>[]; 1516 + }): Promise<CountRecordsResponse> { 1517 + return await this.client.countRecords("app.bsky.graph.follow", params); 1518 + } 1519 + 1520 + async createRecord( 1521 + record: AppBskyGraphFollow, 1522 + useSelfRkey?: boolean 1523 + ): Promise<{ uri: string; cid: string }> { 1524 + return await this.client.createRecord( 1525 + "app.bsky.graph.follow", 1526 + record, 1527 + useSelfRkey 1528 + ); 1529 + } 1530 + 1531 + async updateRecord( 1532 + rkey: string, 1533 + record: AppBskyGraphFollow 1534 + ): Promise<{ uri: string; cid: string }> { 1535 + return await this.client.updateRecord( 1536 + "app.bsky.graph.follow", 1537 + rkey, 1538 + record 1539 + ); 1540 + } 1541 + 1542 + async deleteRecord(rkey: string): Promise<void> { 1543 + return await this.client.deleteRecord("app.bsky.graph.follow", rkey); 1544 + } 1545 + } 1546 + 1547 + class GraphBskyAppClient { 1548 + readonly follow: FollowGraphBskyAppClient; 1549 + private readonly client: SlicesClient; 1550 + 1551 + constructor(client: SlicesClient) { 1552 + this.client = client; 1553 + this.follow = new FollowGraphBskyAppClient(client); 1554 + } 1555 + } 1556 + 1557 + class PostgateFeedBskyAppClient { 1558 + private readonly client: SlicesClient; 1559 + 1560 + constructor(client: SlicesClient) { 1561 + this.client = client; 1562 + } 1563 + 1564 + async getRecords(params?: { 1565 + limit?: number; 1566 + cursor?: string; 1567 + where?: { 1568 + [K in 1569 + | AppBskyFeedPostgateSortFields 1570 + | IndexedRecordFields]?: WhereCondition; 1571 + }; 1572 + orWhere?: { 1573 + [K in 1574 + | AppBskyFeedPostgateSortFields 1575 + | IndexedRecordFields]?: WhereCondition; 1576 + }; 1577 + sortBy?: SortField<AppBskyFeedPostgateSortFields>[]; 1578 + }): Promise<GetRecordsResponse<AppBskyFeedPostgate>> { 1579 + return await this.client.getRecords("app.bsky.feed.postgate", params); 1580 + } 1581 + 1582 + async getRecord( 1583 + params: GetRecordParams 1584 + ): Promise<RecordResponse<AppBskyFeedPostgate>> { 1585 + return await this.client.getRecord("app.bsky.feed.postgate", params); 1586 + } 1587 + 1588 + async countRecords(params?: { 1589 + limit?: number; 1590 + cursor?: string; 1591 + where?: { 1592 + [K in 1593 + | AppBskyFeedPostgateSortFields 1594 + | IndexedRecordFields]?: WhereCondition; 1595 + }; 1596 + orWhere?: { 1597 + [K in 1598 + | AppBskyFeedPostgateSortFields 1599 + | IndexedRecordFields]?: WhereCondition; 1600 + }; 1601 + sortBy?: SortField<AppBskyFeedPostgateSortFields>[]; 1602 + }): Promise<CountRecordsResponse> { 1603 + return await this.client.countRecords("app.bsky.feed.postgate", params); 1604 + } 1605 + 1606 + async createRecord( 1607 + record: AppBskyFeedPostgate, 1608 + useSelfRkey?: boolean 1609 + ): Promise<{ uri: string; cid: string }> { 1610 + return await this.client.createRecord( 1611 + "app.bsky.feed.postgate", 1612 + record, 1613 + useSelfRkey 1614 + ); 1615 + } 1616 + 1617 + async updateRecord( 1618 + rkey: string, 1619 + record: AppBskyFeedPostgate 1620 + ): Promise<{ uri: string; cid: string }> { 1621 + return await this.client.updateRecord( 1622 + "app.bsky.feed.postgate", 1623 + rkey, 1624 + record 1625 + ); 1626 + } 1627 + 1628 + async deleteRecord(rkey: string): Promise<void> { 1629 + return await this.client.deleteRecord("app.bsky.feed.postgate", rkey); 1630 + } 1631 + } 1632 + 1633 + class ThreadgateFeedBskyAppClient { 1634 + private readonly client: SlicesClient; 1635 + 1636 + constructor(client: SlicesClient) { 1637 + this.client = client; 1638 + } 1639 + 1640 + async getRecords(params?: { 1641 + limit?: number; 1642 + cursor?: string; 1643 + where?: { 1644 + [K in 1645 + | AppBskyFeedThreadgateSortFields 1646 + | IndexedRecordFields]?: WhereCondition; 1647 + }; 1648 + orWhere?: { 1649 + [K in 1650 + | AppBskyFeedThreadgateSortFields 1651 + | IndexedRecordFields]?: WhereCondition; 1652 + }; 1653 + sortBy?: SortField<AppBskyFeedThreadgateSortFields>[]; 1654 + }): Promise<GetRecordsResponse<AppBskyFeedThreadgate>> { 1655 + return await this.client.getRecords("app.bsky.feed.threadgate", params); 1656 + } 1657 + 1658 + async getRecord( 1659 + params: GetRecordParams 1660 + ): Promise<RecordResponse<AppBskyFeedThreadgate>> { 1661 + return await this.client.getRecord("app.bsky.feed.threadgate", params); 1662 + } 1663 + 1664 + async countRecords(params?: { 1665 + limit?: number; 1666 + cursor?: string; 1667 + where?: { 1668 + [K in 1669 + | AppBskyFeedThreadgateSortFields 1670 + | IndexedRecordFields]?: WhereCondition; 1671 + }; 1672 + orWhere?: { 1673 + [K in 1674 + | AppBskyFeedThreadgateSortFields 1675 + | IndexedRecordFields]?: WhereCondition; 1676 + }; 1677 + sortBy?: SortField<AppBskyFeedThreadgateSortFields>[]; 1678 + }): Promise<CountRecordsResponse> { 1679 + return await this.client.countRecords("app.bsky.feed.threadgate", params); 1680 + } 1681 + 1682 + async createRecord( 1683 + record: AppBskyFeedThreadgate, 1684 + useSelfRkey?: boolean 1685 + ): Promise<{ uri: string; cid: string }> { 1686 + return await this.client.createRecord( 1687 + "app.bsky.feed.threadgate", 1688 + record, 1689 + useSelfRkey 1690 + ); 1691 + } 1692 + 1693 + async updateRecord( 1694 + rkey: string, 1695 + record: AppBskyFeedThreadgate 1696 + ): Promise<{ uri: string; cid: string }> { 1697 + return await this.client.updateRecord( 1698 + "app.bsky.feed.threadgate", 1699 + rkey, 1700 + record 1701 + ); 1702 + } 1703 + 1704 + async deleteRecord(rkey: string): Promise<void> { 1705 + return await this.client.deleteRecord("app.bsky.feed.threadgate", rkey); 1706 + } 1707 + } 1708 + 1709 + class FeedBskyAppClient { 1710 + readonly postgate: PostgateFeedBskyAppClient; 1711 + readonly threadgate: ThreadgateFeedBskyAppClient; 1712 + private readonly client: SlicesClient; 1713 + 1714 + constructor(client: SlicesClient) { 1715 + this.client = client; 1716 + this.postgate = new PostgateFeedBskyAppClient(client); 1717 + this.threadgate = new ThreadgateFeedBskyAppClient(client); 1718 + } 1719 + } 1720 + 452 1721 class ProfileActorBskyAppClient { 453 1722 private readonly client: SlicesClient; 454 1723 ··· 536 1805 } 537 1806 538 1807 class BskyAppClient { 1808 + readonly graph: GraphBskyAppClient; 1809 + readonly feed: FeedBskyAppClient; 539 1810 readonly actor: ActorBskyAppClient; 540 1811 private readonly client: SlicesClient; 541 1812 542 1813 constructor(client: SlicesClient) { 543 1814 this.client = client; 1815 + this.graph = new GraphBskyAppClient(client); 1816 + this.feed = new FeedBskyAppClient(client); 544 1817 this.actor = new ActorBskyAppClient(client); 545 1818 } 546 1819 } ··· 555 1828 } 556 1829 } 557 1830 1831 + class RequestWaitlistSlicesNetworkClient { 1832 + private readonly client: SlicesClient; 1833 + 1834 + constructor(client: SlicesClient) { 1835 + this.client = client; 1836 + } 1837 + 1838 + async getRecords(params?: { 1839 + limit?: number; 1840 + cursor?: string; 1841 + where?: { 1842 + [K in 1843 + | NetworkSlicesWaitlistRequestSortFields 1844 + | IndexedRecordFields]?: WhereCondition; 1845 + }; 1846 + orWhere?: { 1847 + [K in 1848 + | NetworkSlicesWaitlistRequestSortFields 1849 + | IndexedRecordFields]?: WhereCondition; 1850 + }; 1851 + sortBy?: SortField<NetworkSlicesWaitlistRequestSortFields>[]; 1852 + }): Promise<GetRecordsResponse<NetworkSlicesWaitlistRequest>> { 1853 + return await this.client.getRecords( 1854 + "network.slices.waitlist.request", 1855 + params 1856 + ); 1857 + } 1858 + 1859 + async getRecord( 1860 + params: GetRecordParams 1861 + ): Promise<RecordResponse<NetworkSlicesWaitlistRequest>> { 1862 + return await this.client.getRecord( 1863 + "network.slices.waitlist.request", 1864 + params 1865 + ); 1866 + } 1867 + 1868 + async countRecords(params?: { 1869 + limit?: number; 1870 + cursor?: string; 1871 + where?: { 1872 + [K in 1873 + | NetworkSlicesWaitlistRequestSortFields 1874 + | IndexedRecordFields]?: WhereCondition; 1875 + }; 1876 + orWhere?: { 1877 + [K in 1878 + | NetworkSlicesWaitlistRequestSortFields 1879 + | IndexedRecordFields]?: WhereCondition; 1880 + }; 1881 + sortBy?: SortField<NetworkSlicesWaitlistRequestSortFields>[]; 1882 + }): Promise<CountRecordsResponse> { 1883 + return await this.client.countRecords( 1884 + "network.slices.waitlist.request", 1885 + params 1886 + ); 1887 + } 1888 + 1889 + async createRecord( 1890 + record: NetworkSlicesWaitlistRequest, 1891 + useSelfRkey?: boolean 1892 + ): Promise<{ uri: string; cid: string }> { 1893 + return await this.client.createRecord( 1894 + "network.slices.waitlist.request", 1895 + record, 1896 + useSelfRkey 1897 + ); 1898 + } 1899 + 1900 + async updateRecord( 1901 + rkey: string, 1902 + record: NetworkSlicesWaitlistRequest 1903 + ): Promise<{ uri: string; cid: string }> { 1904 + return await this.client.updateRecord( 1905 + "network.slices.waitlist.request", 1906 + rkey, 1907 + record 1908 + ); 1909 + } 1910 + 1911 + async deleteRecord(rkey: string): Promise<void> { 1912 + return await this.client.deleteRecord( 1913 + "network.slices.waitlist.request", 1914 + rkey 1915 + ); 1916 + } 1917 + } 1918 + 1919 + class InviteWaitlistSlicesNetworkClient { 1920 + private readonly client: SlicesClient; 1921 + 1922 + constructor(client: SlicesClient) { 1923 + this.client = client; 1924 + } 1925 + 1926 + async getRecords(params?: { 1927 + limit?: number; 1928 + cursor?: string; 1929 + where?: { 1930 + [K in 1931 + | NetworkSlicesWaitlistInviteSortFields 1932 + | IndexedRecordFields]?: WhereCondition; 1933 + }; 1934 + orWhere?: { 1935 + [K in 1936 + | NetworkSlicesWaitlistInviteSortFields 1937 + | IndexedRecordFields]?: WhereCondition; 1938 + }; 1939 + sortBy?: SortField<NetworkSlicesWaitlistInviteSortFields>[]; 1940 + }): Promise<GetRecordsResponse<NetworkSlicesWaitlistInvite>> { 1941 + return await this.client.getRecords( 1942 + "network.slices.waitlist.invite", 1943 + params 1944 + ); 1945 + } 1946 + 1947 + async getRecord( 1948 + params: GetRecordParams 1949 + ): Promise<RecordResponse<NetworkSlicesWaitlistInvite>> { 1950 + return await this.client.getRecord( 1951 + "network.slices.waitlist.invite", 1952 + params 1953 + ); 1954 + } 1955 + 1956 + async countRecords(params?: { 1957 + limit?: number; 1958 + cursor?: string; 1959 + where?: { 1960 + [K in 1961 + | NetworkSlicesWaitlistInviteSortFields 1962 + | IndexedRecordFields]?: WhereCondition; 1963 + }; 1964 + orWhere?: { 1965 + [K in 1966 + | NetworkSlicesWaitlistInviteSortFields 1967 + | IndexedRecordFields]?: WhereCondition; 1968 + }; 1969 + sortBy?: SortField<NetworkSlicesWaitlistInviteSortFields>[]; 1970 + }): Promise<CountRecordsResponse> { 1971 + return await this.client.countRecords( 1972 + "network.slices.waitlist.invite", 1973 + params 1974 + ); 1975 + } 1976 + 1977 + async createRecord( 1978 + record: NetworkSlicesWaitlistInvite, 1979 + useSelfRkey?: boolean 1980 + ): Promise<{ uri: string; cid: string }> { 1981 + return await this.client.createRecord( 1982 + "network.slices.waitlist.invite", 1983 + record, 1984 + useSelfRkey 1985 + ); 1986 + } 1987 + 1988 + async updateRecord( 1989 + rkey: string, 1990 + record: NetworkSlicesWaitlistInvite 1991 + ): Promise<{ uri: string; cid: string }> { 1992 + return await this.client.updateRecord( 1993 + "network.slices.waitlist.invite", 1994 + rkey, 1995 + record 1996 + ); 1997 + } 1998 + 1999 + async deleteRecord(rkey: string): Promise<void> { 2000 + return await this.client.deleteRecord( 2001 + "network.slices.waitlist.invite", 2002 + rkey 2003 + ); 2004 + } 2005 + } 2006 + 2007 + class WaitlistSlicesNetworkClient { 2008 + readonly request: RequestWaitlistSlicesNetworkClient; 2009 + readonly invite: InviteWaitlistSlicesNetworkClient; 2010 + private readonly client: SlicesClient; 2011 + 2012 + constructor(client: SlicesClient) { 2013 + this.client = client; 2014 + this.request = new RequestWaitlistSlicesNetworkClient(client); 2015 + this.invite = new InviteWaitlistSlicesNetworkClient(client); 2016 + } 2017 + } 2018 + 558 2019 class SliceSlicesNetworkClient { 559 2020 private readonly client: SlicesClient; 560 2021 ··· 784 2245 } 785 2246 } 786 2247 787 - class WaitingSlicesNetworkClient { 788 - private readonly client: SlicesClient; 789 - 790 - constructor(client: SlicesClient) { 791 - this.client = client; 792 - } 793 - 794 - async getRecords(params?: { 795 - limit?: number; 796 - cursor?: string; 797 - where?: { 798 - [K in 799 - | NetworkSlicesWaitingSortFields 800 - | IndexedRecordFields]?: WhereCondition; 801 - }; 802 - orWhere?: { 803 - [K in 804 - | NetworkSlicesWaitingSortFields 805 - | IndexedRecordFields]?: WhereCondition; 806 - }; 807 - sortBy?: SortField<NetworkSlicesWaitingSortFields>[]; 808 - }): Promise<GetRecordsResponse<NetworkSlicesWaiting>> { 809 - return await this.client.getRecords("network.slices.waiting", params); 810 - } 811 - 812 - async getRecord( 813 - params: GetRecordParams 814 - ): Promise<RecordResponse<NetworkSlicesWaiting>> { 815 - return await this.client.getRecord("network.slices.waiting", params); 816 - } 817 - 818 - async countRecords(params?: { 819 - limit?: number; 820 - cursor?: string; 821 - where?: { 822 - [K in 823 - | NetworkSlicesWaitingSortFields 824 - | IndexedRecordFields]?: WhereCondition; 825 - }; 826 - orWhere?: { 827 - [K in 828 - | NetworkSlicesWaitingSortFields 829 - | IndexedRecordFields]?: WhereCondition; 830 - }; 831 - sortBy?: SortField<NetworkSlicesWaitingSortFields>[]; 832 - }): Promise<CountRecordsResponse> { 833 - return await this.client.countRecords("network.slices.waiting", params); 834 - } 835 - 836 - async createRecord( 837 - record: NetworkSlicesWaiting, 838 - useSelfRkey?: boolean 839 - ): Promise<{ uri: string; cid: string }> { 840 - return await this.client.createRecord( 841 - "network.slices.waiting", 842 - record, 843 - useSelfRkey 844 - ); 845 - } 846 - 847 - async updateRecord( 848 - rkey: string, 849 - record: NetworkSlicesWaiting 850 - ): Promise<{ uri: string; cid: string }> { 851 - return await this.client.updateRecord( 852 - "network.slices.waiting", 853 - rkey, 854 - record 855 - ); 856 - } 857 - 858 - async deleteRecord(rkey: string): Promise<void> { 859 - return await this.client.deleteRecord("network.slices.waiting", rkey); 860 - } 861 - } 862 - 863 2248 class LexiconSlicesNetworkClient { 864 2249 private readonly client: SlicesClient; 865 2250 ··· 1026 2411 } 1027 2412 1028 2413 class SlicesNetworkClient { 2414 + readonly waitlist: WaitlistSlicesNetworkClient; 1029 2415 readonly slice: SliceSlicesNetworkClient; 1030 - readonly waiting: WaitingSlicesNetworkClient; 1031 2416 readonly lexicon: LexiconSlicesNetworkClient; 1032 2417 readonly actor: ActorSlicesNetworkClient; 1033 2418 private readonly client: SlicesClient; 1034 2419 1035 2420 constructor(client: SlicesClient) { 1036 2421 this.client = client; 2422 + this.waitlist = new WaitlistSlicesNetworkClient(client); 1037 2423 this.slice = new SliceSlicesNetworkClient(client); 1038 - this.waiting = new WaitingSlicesNetworkClient(client); 1039 2424 this.lexicon = new LexiconSlicesNetworkClient(client); 1040 2425 this.actor = new ActorSlicesNetworkClient(client); 1041 2426 }
+3 -3
frontend/src/config.ts
··· 7 7 const OAUTH_REDIRECT_URI = Deno.env.get("OAUTH_REDIRECT_URI"); 8 8 const OAUTH_AIP_BASE_URL = Deno.env.get("OAUTH_AIP_BASE_URL"); 9 9 const API_URL = Deno.env.get("API_URL"); 10 - const SLICE_URI = Deno.env.get("SLICE_URI"); 10 + export const SLICE_URI = Deno.env.get("SLICE_URI"); 11 11 12 12 if ( 13 13 !OAUTH_CLIENT_ID || ··· 19 19 ) { 20 20 throw new Error( 21 21 "Missing OAuth configuration. Please ensure .env file contains:\n" + 22 - "OAUTH_CLIENT_ID, OAUTH_CLIENT_SECRET, OAUTH_REDIRECT_URI, OAUTH_AIP_BASE_URL, API_URL, SLICE_URI", 22 + "OAUTH_CLIENT_ID, OAUTH_CLIENT_SECRET, OAUTH_REDIRECT_URI, OAUTH_AIP_BASE_URL, API_URL, SLICE_URI" 23 23 ); 24 24 } 25 25 ··· 47 47 // "repo:network.slices.waiting", 48 48 ], 49 49 }, 50 - oauthStorage, 50 + oauthStorage 51 51 ); 52 52 53 53 // Session setup (shared database)
+178 -57
frontend/src/features/auth/handlers.tsx
··· 3 3 import { atprotoClient, oauthSessions, sessionStore } from "../../config.ts"; 4 4 import { renderHTML } from "../../utils/render.tsx"; 5 5 import { LoginPage } from "./templates/LoginPage.tsx"; 6 + import { SLICE_URI } from "../../config.ts"; 7 + import type { NetworkSlicesActorProfile } from "../../client.ts"; 8 + 9 + // ============================================================================ 10 + // WAITLIST GATING UTILITIES 11 + // ============================================================================ 12 + 13 + async function checkUserAccess( 14 + userDid: string 15 + ): Promise<{ hasAccess: boolean; isOnWaitlist: boolean }> { 16 + try { 17 + // Build the slice URI to query invites for 18 + const sliceUri = SLICE_URI!; 19 + 20 + // Query for invites for this DID - using json field to query the record content 21 + const invitesResult = 22 + await atprotoClient.network.slices.waitlist.invite.getRecords({ 23 + where: { 24 + slice: { eq: sliceUri }, 25 + json: { contains: userDid }, 26 + }, 27 + limit: 1, 28 + }); 29 + 30 + // Check if user has a valid invite 31 + if (invitesResult.records && invitesResult.records.length > 0) { 32 + const invite = invitesResult.records[0]; 33 + 34 + // Check if invite has expired 35 + if (invite.value.expiresAt) { 36 + const expiresAt = new Date(invite.value.expiresAt); 37 + const now = new Date(); 38 + if (expiresAt < now) { 39 + return { hasAccess: false, isOnWaitlist: false }; // Invite has expired 40 + } 41 + } 42 + 43 + return { hasAccess: true, isOnWaitlist: false }; // Valid invite found 44 + } 45 + 46 + // Check if user is already on the waitlist - requests are created by the user so record.did is correct 47 + const requestsResult = 48 + await atprotoClient.network.slices.waitlist.request.getRecords({ 49 + where: { 50 + slice: { eq: sliceUri }, 51 + json: { eq: userDid }, 52 + }, 53 + limit: 1, 54 + }); 55 + 56 + const isOnWaitlist = 57 + requestsResult.records && requestsResult.records.length > 0; 58 + 59 + return { hasAccess: false, isOnWaitlist }; 60 + } catch (error) { 61 + console.error("Error checking user access:", error); 62 + return { hasAccess: false, isOnWaitlist: false }; // Default to blocking access on error 63 + } 64 + } 6 65 7 66 // ============================================================================ 8 67 // LOGIN PAGE HANDLER ··· 14 73 15 74 const error = url.searchParams.get("error"); 16 75 return renderHTML( 17 - <LoginPage error={error || undefined} currentUser={context.currentUser} />, 76 + <LoginPage error={error || undefined} currentUser={context.currentUser} /> 18 77 ); 19 78 } 20 79 ··· 49 108 return Response.redirect( 50 109 new URL( 51 110 "/login?error=" + encodeURIComponent("OAuth initialization failed"), 52 - req.url, 111 + req.url 53 112 ), 54 - 302, 113 + 302 55 114 ); 56 115 } 57 116 } ··· 82 141 return Response.redirect( 83 142 new URL( 84 143 "/login?error=" + encodeURIComponent("Invalid OAuth callback"), 85 - req.url, 144 + req.url 86 145 ), 87 - 302, 146 + 302 88 147 ); 89 148 } 90 149 ··· 92 151 return Response.redirect( 93 152 new URL( 94 153 "/login?error=" + encodeURIComponent("OAuth client not configured"), 95 - req.url, 154 + req.url 96 155 ), 97 - 302, 156 + 302 98 157 ); 99 158 } 100 159 ··· 111 170 return Response.redirect( 112 171 new URL( 113 172 "/login?error=" + encodeURIComponent("Failed to create session"), 114 - req.url, 173 + req.url 115 174 ), 116 - 302, 175 + 302 117 176 ); 118 177 } 119 178 ··· 125 184 try { 126 185 userInfo = await atprotoClient.oauth?.getUserInfo(); 127 186 } catch (error) { 128 - console.log("Failed to get user info:", error); 187 + console.error("Failed to get user info:", error); 188 + } 189 + 190 + // Check waitlist access if user info is available 191 + if (userInfo?.sub) { 192 + const { hasAccess, isOnWaitlist } = await checkUserAccess(userInfo.sub); 193 + if (!hasAccess) { 194 + // Clear OAuth session and redirect to waitlist page 195 + await atprotoClient.oauth?.logout(); 196 + 197 + const errorCode = isOnWaitlist 198 + ? "already_on_waitlist" 199 + : "invite_required"; 200 + return Response.redirect( 201 + new URL(`/waitlist?error=${errorCode}`, req.url), 202 + 302 203 + ); 204 + } 129 205 } 130 206 131 - // Sync external collections if user doesn't have them yet 207 + // Sync external collections first to ensure actor records are populated 132 208 try { 133 - if (!userInfo?.sub) { 134 - console.log( 135 - "No user DID available, skipping external collections sync", 136 - ); 137 - } else { 138 - // Check if user already has bsky profile synced 139 - try { 140 - const profileCheck = await atprotoClient.app.bsky.actor.profile 141 - .getRecords({ 142 - where: { 143 - did: { eq: userInfo.sub }, 144 - }, 145 - limit: 1, 146 - }); 209 + if (userInfo?.sub) { 210 + await atprotoClient.network.slices.slice.syncUserCollections(); 211 + } 212 + } catch (error) { 213 + console.error("Error during external collections sync:", error); 214 + } 215 + 216 + // Create network.slices.actor.profile record for first-time users 217 + if (userInfo?.sub && userInfo?.name) { 218 + try { 219 + // Check if user already has a profile record in our slice 220 + const existingProfile = 221 + await atprotoClient.network.slices.actor.profile.getRecords({ 222 + where: { 223 + did: { eq: userInfo.sub }, 224 + }, 225 + limit: 1, 226 + }); 227 + 228 + // If no profile exists, create one 229 + if (!existingProfile.records || existingProfile.records.length === 0) { 230 + // Fetch their bsky profile to copy avatar and other data 231 + const profileData: NetworkSlicesActorProfile = { 232 + displayName: userInfo.name || userInfo.sub, 233 + createdAt: new Date().toISOString(), 234 + }; 235 + 236 + try { 237 + // Get their bsky profile data 238 + const bskyProfile = 239 + await atprotoClient.app.bsky.actor.profile.getRecords({ 240 + where: { 241 + did: { eq: userInfo.sub }, 242 + }, 243 + limit: 1, 244 + }); 245 + 246 + if (bskyProfile.records && bskyProfile.records.length > 0) { 247 + const bskyData = bskyProfile.records[0].value; 147 248 148 - // If we can't find existing records, sync them 149 - if (!profileCheck.records || profileCheck.records.length === 0) { 150 - console.log("No existing external collections found, syncing..."); 151 - await atprotoClient.network.slices.slice.syncUserCollections(); 152 - } else { 153 - console.log("External collections already synced, skipping sync"); 249 + // Copy over relevant fields from bsky profile 250 + if (bskyData.displayName) { 251 + profileData.displayName = bskyData.displayName; 252 + } 253 + if (bskyData.description) { 254 + profileData.description = bskyData.description; 255 + } 256 + if (bskyData.avatar) { 257 + profileData.avatar = bskyData.avatar; 258 + } 259 + } 260 + } catch (_bskyError) { 261 + // Could not fetch bsky profile, using basic data 154 262 } 155 - } catch (_profileError) { 156 - // If we can't check existing records, skip sync to be safe 157 - console.log( 158 - "Could not check existing external collections, skipping sync", 263 + 264 + // Create the profile record with the data using "self" as the rkey 265 + await atprotoClient.network.slices.actor.profile.createRecord( 266 + profileData, 267 + true 159 268 ); 160 269 } 270 + } catch (error) { 271 + console.error( 272 + "Error creating network.slices.actor.profile record:", 273 + error 274 + ); 275 + // Don't fail the login process if profile creation fails 161 276 } 162 - } catch (error) { 163 - console.log( 164 - "Error during sync check, skipping external collections sync:", 165 - error, 166 - ); 167 277 } 168 278 169 279 // Redirect to user's profile page if handle is available ··· 181 291 return Response.redirect( 182 292 new URL( 183 293 "/login?error=" + encodeURIComponent("Authentication failed"), 184 - req.url, 294 + req.url 185 295 ), 186 - 302, 296 + 302 187 297 ); 188 298 } 189 299 } ··· 235 345 isWaitlistFlow: true, 236 346 handle, 237 347 redirectUri: "/auth/waitlist/callback", 238 - }), 348 + }) 239 349 ); 240 350 241 351 // Initiate OAuth with minimal scope for waitlist, passing state directly 242 352 const authResult = await atprotoClient.oauth.authorize({ 243 353 loginHint: handle, 244 - scope: "atproto repo:network.slices.waiting", 354 + scope: "atproto repo:network.slices.waitlist.request", 245 355 state: waitlistState, 246 356 }); 247 357 ··· 263 373 if (!code || !state) { 264 374 return Response.redirect( 265 375 new URL("/waitlist?error=invalid_callback", req.url), 266 - 302, 376 + 302 267 377 ); 268 378 } 269 379 ··· 282 392 if (!atprotoClient.oauth) { 283 393 return Response.redirect( 284 394 new URL("/waitlist?error=oauth_not_configured", req.url), 285 - 302, 395 + 302 286 396 ); 287 397 } 288 398 ··· 293 403 const userInfo = await atprotoClient.oauth.getUserInfo(); 294 404 295 405 if (!userInfo) { 296 - return Response.redirect(new URL("/waitlist?error=no_user_info", req.url), 302); 406 + return Response.redirect( 407 + new URL("/waitlist?error=no_user_info", req.url), 408 + 302 409 + ); 297 410 } 298 411 299 412 // Create waitlist record 300 413 try { 301 - // For now, just log the waitlist join 302 - console.log("User joined waitlist:", { 303 - did: userInfo.sub, 304 - handle: userInfo.name || waitlistData.handle || "unknown", 305 - joinedAt: new Date().toISOString(), 306 - }); 307 - 308 - await atprotoClient.network.slices.waiting.createRecord( 414 + await atprotoClient.network.slices.waitlist.request.createRecord( 309 415 { 416 + slice: SLICE_URI!, 310 417 createdAt: new Date().toISOString(), 311 418 }, 312 - true, 419 + true 313 420 ); 421 + 422 + // Sync user collections to populate their Bluesky profile data 423 + try { 424 + await atprotoClient.network.slices.slice.syncUserCollections(); 425 + } catch (syncError) { 426 + console.error( 427 + "Failed to sync user collections for waitlist user:", 428 + syncError 429 + ); 430 + // Don't fail the waitlist process if sync fails 431 + } 314 432 } catch (error) { 315 433 console.error("Failed to create waitlist record:", error); 316 434 } ··· 326 444 return Response.redirect(redirectUrl.toString(), 302); 327 445 } catch (error) { 328 446 console.error("Waitlist callback error:", error); 329 - return Response.redirect(new URL("/waitlist?error=waitlist_failed", req.url), 302); 447 + return Response.redirect( 448 + new URL("/waitlist?error=waitlist_failed", req.url), 449 + 302 450 + ); 330 451 } 331 452 } 332 453
+7 -7
frontend/src/features/dashboard/handlers.tsx
··· 10 10 11 11 async function handleProfilePage( 12 12 req: Request, 13 - params?: URLPatternResult, 13 + params?: URLPatternResult 14 14 ): Promise<Response> { 15 15 const context = await withAuth(req); 16 16 ··· 48 48 slices={slices} 49 49 currentUser={context.currentUser} 50 50 profile={profile} 51 - />, 51 + /> 52 52 ); 53 53 } 54 54 ··· 60 60 const authInfo = await atprotoClient.oauth?.getAuthenticationInfo(); 61 61 if (!authInfo?.isAuthenticated) { 62 62 return renderHTML( 63 - <CreateSliceDialog error="Session expired. Please log in again." />, 63 + <CreateSliceDialog error="Session expired. Please log in again." /> 64 64 ); 65 65 } 66 66 ··· 75 75 error="Slice name is required" 76 76 name={name} 77 77 domain={domain} 78 - />, 78 + /> 79 79 ); 80 80 } 81 81 ··· 85 85 error="Primary domain is required" 86 86 name={name} 87 87 domain={domain} 88 - />, 88 + /> 89 89 ); 90 90 } 91 91 ··· 97 97 }; 98 98 99 99 const result = await atprotoClient.network.slices.slice.createRecord( 100 - recordData, 100 + recordData 101 101 ); 102 102 103 103 const uriParts = result.uri.split("/"); ··· 117 117 error="Failed to create slice record. Please try again." 118 118 name={name} 119 119 domain={domain} 120 - />, 120 + /> 121 121 ); 122 122 } 123 123 } catch (_error) {
+1 -1
frontend/src/features/docs/templates/DocsIndexPage.tsx
··· 35 35 href={`/docs/${doc.slug}`} 36 36 className="block" 37 37 > 38 - <Card variant="hover"> 38 + <Card padding="md" variant="hover"> 39 39 <Text as="h2" size="xl" className="font-semibold mb-2"> 40 40 {doc.title} 41 41 </Text>
+2 -6
frontend/src/features/landing/templates/fragments/WaitlistFormModal.tsx
··· 8 8 <h2 class="font-mono text-xl font-bold text-gray-800 mb-4"> 9 9 Join the Waitlist 10 10 </h2> 11 - <form 12 - action="/auth/waitlist/initiate" 13 - method="POST" 14 - class="space-y-4" 15 - > 11 + <form action="/auth/waitlist/initiate" method="POST" class="space-y-4"> 16 12 <div> 17 13 <label 18 14 for="handle-input" ··· 24 20 type="text" 25 21 id="handle-input" 26 22 name="handle" 27 - placeholder="alice.bsky.social" 23 + placeholder="user.bsky.social" 28 24 class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-gray-800 font-mono text-sm" 29 25 required 30 26 />
+1 -1
frontend/src/features/settings/templates/fragments/SettingsForm.tsx
··· 16 16 17 17 export function SettingsForm({ profile }: SettingsFormProps) { 18 18 return ( 19 - <Card> 19 + <Card padding="md"> 20 20 <Text as="h2" size="xl" className="font-semibold mb-4"> 21 21 Profile Settings 22 22 </Text>
+2 -2
frontend/src/features/slices/codegen/templates/SliceCodegenPage.tsx
··· 44 44 hasSliceAccess={hasSliceAccess} 45 45 title={`${slice.name} - Code Generation`} 46 46 > 47 - <Card padding="none"> 47 + <Card> 48 48 <Card.Header 49 49 title="TypeScript Client" 50 50 action={ ··· 64 64 <Card.Content> 65 65 {error ? ( 66 66 <div className="p-6"> 67 - <Card variant="danger"> 67 + <Card padding="md" variant="danger"> 68 68 <Text as="h3" size="lg" className="font-semibold mb-2"> 69 69 ❌ Generation Failed 70 70 </Text>
+1 -1
frontend/src/features/slices/lexicon/templates/LexiconDetailPage.tsx
··· 79 79 </Button> 80 80 </div> 81 81 82 - <Card padding="none"> 82 + <Card> 83 83 <Card.Header title="Lexicon Definitions" /> 84 84 <Card.Content className="text-sm overflow-x-auto [&_pre]:p-4 [&_pre]:m-0"> 85 85 <div dangerouslySetInnerHTML={{ __html: highlightedCode }} />
+1 -1
frontend/src/features/slices/lexicon/templates/SliceLexiconPage.tsx
··· 48 48 </Button> 49 49 </div> 50 50 )} 51 - <Card padding="none"> 51 + <Card> 52 52 <Card.Header 53 53 title={`${lexicons.length} ${ 54 54 lexicons.length === 1 ? "Lexicon" : "Lexicons"
+3
frontend/src/features/slices/mod.ts
··· 9 9 import { syncRoutes } from "./sync/handlers.tsx"; 10 10 import { syncLogsRoutes } from "./sync-logs/handlers.tsx"; 11 11 import { jetstreamRoutes } from "./jetstream/handlers.tsx"; 12 + import { waitlistRoutes } from "./waitlist/handlers.tsx"; 12 13 13 14 // Export individual route groups 14 15 export { ··· 22 23 settingsRoutes, 23 24 syncLogsRoutes, 24 25 syncRoutes, 26 + waitlistRoutes, 25 27 }; 26 28 27 29 // Export consolidated routes array for easy import ··· 36 38 ...syncRoutes, 37 39 ...syncLogsRoutes, 38 40 ...jetstreamRoutes, 41 + ...waitlistRoutes, 39 42 ];
+1 -1
frontend/src/features/slices/oauth/templates/SliceOAuthPage.tsx
··· 64 64 </Button> 65 65 </div> 66 66 67 - <Card padding="none"> 67 + <Card> 68 68 <Card.Header title="OAuth Clients" /> 69 69 <Card.Content> 70 70 {clients.length === 0
+7 -7
frontend/src/features/slices/overview/templates/SliceOverview.tsx
··· 73 73 </div> 74 74 75 75 {(slice.indexedRecordCount ?? 0) > 0 && ( 76 - <Card className="mb-8"> 76 + <Card padding="md" className="mb-8"> 77 77 <Text as="h2" size="xl" className="font-semibold mb-4"> 78 78 📊 Database Status 79 79 </Text> ··· 107 107 )} 108 108 109 109 <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6"> 110 - <Card> 110 + <Card padding="md"> 111 111 <Text as="h2" size="xl" className="font-semibold mb-4 block"> 112 112 📚 Lexicon Definitions 113 113 </Text> ··· 122 122 </Button> 123 123 </Card> 124 124 125 - <Card> 125 + <Card padding="md"> 126 126 <Text as="h2" size="xl" className="font-semibold mb-4 block"> 127 127 📝 View Records 128 128 </Text> ··· 143 143 )} 144 144 </Card> 145 145 146 - <Card> 146 + <Card padding="md"> 147 147 <Text as="h2" size="xl" className="font-semibold mb-4 block"> 148 148 ⚡ Code Generation 149 149 </Text> ··· 158 158 </Button> 159 159 </Card> 160 160 161 - <Card> 161 + <Card padding="md"> 162 162 <Text as="h2" size="xl" className="font-semibold mb-4 block"> 163 163 📖 API Documentation 164 164 </Text> ··· 174 174 </Card> 175 175 176 176 {hasSliceAccess && ( 177 - <Card> 177 + <Card padding="md"> 178 178 <Text as="h2" size="xl" className="font-semibold mb-4 block"> 179 179 🔄 Sync 180 180 </Text> ··· 191 191 )} 192 192 193 193 {collections.length > 0 && ( 194 - <Card> 194 + <Card padding="md"> 195 195 <Text as="h2" size="xl" className="font-semibold mb-4 block"> 196 196 📊 Synced Collections 197 197 </Text>
+43 -12
frontend/src/features/slices/records/templates/fragments/RecordsList.tsx
··· 1 - import type { IndexedRecord } from "../../../../../client.ts"; 1 + import { IndexedRecord } from "@slices/client"; 2 2 import { Card } from "../../../../../shared/fragments/Card.tsx"; 3 3 import { Text } from "../../../../../shared/fragments/Text.tsx"; 4 4 ··· 12 12 13 13 export function RecordsList({ records }: RecordsListProps) { 14 14 return ( 15 - <Card padding="none"> 16 - <Card.Header 17 - title={`Records (${records.length})`} 18 - /> 15 + <Card> 16 + <Card.Header title={`Records (${records.length})`} /> 19 17 <Card.Content className="divide-y divide-zinc-200 dark:divide-zinc-700"> 20 18 {records.map((record) => ( 21 19 <div key={record.uri} className="p-6"> ··· 26 24 </Text> 27 25 <dl className="grid grid-cols-1 gap-x-4 gap-y-2 text-sm"> 28 26 <div className="grid grid-cols-3 gap-4"> 29 - <Text as="dt" size="sm" variant="muted" className="font-medium">URI:</Text> 27 + <Text 28 + as="dt" 29 + size="sm" 30 + variant="muted" 31 + className="font-medium" 32 + > 33 + URI: 34 + </Text> 30 35 <Text as="dd" size="sm" className="col-span-2 break-all"> 31 36 {record.uri} 32 37 </Text> 33 38 </div> 34 39 <div className="grid grid-cols-3 gap-4"> 35 - <Text as="dt" size="sm" variant="muted" className="font-medium"> 40 + <Text 41 + as="dt" 42 + size="sm" 43 + variant="muted" 44 + className="font-medium" 45 + > 36 46 Collection: 37 47 </Text> 38 48 <Text as="dd" size="sm" className="col-span-2"> ··· 40 50 </Text> 41 51 </div> 42 52 <div className="grid grid-cols-3 gap-4"> 43 - <Text as="dt" size="sm" variant="muted" className="font-medium">DID:</Text> 53 + <Text 54 + as="dt" 55 + size="sm" 56 + variant="muted" 57 + className="font-medium" 58 + > 59 + DID: 60 + </Text> 44 61 <Text as="dd" size="sm" className="col-span-2 break-all"> 45 62 {record.did} 46 63 </Text> 47 64 </div> 48 65 <div className="grid grid-cols-3 gap-4"> 49 - <Text as="dt" size="sm" variant="muted" className="font-medium">CID:</Text> 66 + <Text 67 + as="dt" 68 + size="sm" 69 + variant="muted" 70 + className="font-medium" 71 + > 72 + CID: 73 + </Text> 50 74 <Text as="dd" size="sm" className="col-span-2 break-all"> 51 75 {record.cid} 52 76 </Text> 53 77 </div> 54 78 <div className="grid grid-cols-3 gap-4"> 55 - <Text as="dt" size="sm" variant="muted" className="font-medium"> 79 + <Text 80 + as="dt" 81 + size="sm" 82 + variant="muted" 83 + className="font-medium" 84 + > 56 85 Indexed: 57 86 </Text> 58 87 <Text as="dd" size="sm" className="col-span-2"> ··· 66 95 Record Data 67 96 </Text> 68 97 <pre className="bg-zinc-50 dark:bg-zinc-800 border border-zinc-200 dark:border-zinc-700 p-3 text-xs overflow-auto max-h-64"> 69 - <Text as="span" size="xs">{record.pretty_value || 70 - JSON.stringify(record.value, null, 2)}</Text> 98 + <Text as="span" size="xs"> 99 + {record.pretty_value || 100 + JSON.stringify(record.value, null, 2)} 101 + </Text> 71 102 </pre> 72 103 </div> 73 104 </div>
+2 -2
frontend/src/features/slices/settings/templates/SliceSettings.tsx
··· 56 56 {/* Settings Content */} 57 57 <div className="space-y-8"> 58 58 {/* Edit Slice Settings */} 59 - <Card> 59 + <Card padding="md"> 60 60 <Text as="h2" size="xl" className="font-semibold mb-4"> 61 61 Edit Slice Settings 62 62 </Text> ··· 104 104 </Card> 105 105 106 106 {/* Danger Zone */} 107 - <Card className="border-l-4 border-l-red-500"> 107 + <Card padding="md" className="border-l-4 border-l-red-500"> 108 108 <Text 109 109 as="h2" 110 110 size="xl"
+6 -1
frontend/src/features/slices/shared/fragments/SliceTabs.tsx
··· 48 48 } 49 49 ); 50 50 51 - // Add oauth and settings tabs only if user owns the slice 51 + // Add oauth, waitlist and settings tabs only if user owns the slice 52 52 if (hasSliceAccess) { 53 53 tabs.push( 54 54 { 55 55 id: "oauth", 56 56 name: "OAuth Clients", 57 57 href: buildSliceUrlFromView(slice, sliceId, "oauth"), 58 + }, 59 + { 60 + id: "waitlist", 61 + name: "Waitlist", 62 + href: buildSliceUrlFromView(slice, sliceId, "waitlist"), 58 63 }, 59 64 { 60 65 id: "settings",
+1 -1
frontend/src/features/slices/sync/templates/SliceSyncPage.tsx
··· 38 38 <span className="flex items-center gap-2">Start Sync</span> 39 39 </Button> 40 40 </div> 41 - <Card padding="none"> 41 + <Card> 42 42 <Card.Header title="Recent Sync History" /> 43 43 <Card.Content 44 44 hx-get={`/api/slices/${sliceId}/job-history?handle=${slice.creator?.handle}`}
+234
frontend/src/features/slices/waitlist/api.ts
··· 1 + import type { 2 + NetworkSlicesWaitlistDefsRequestView, 3 + NetworkSlicesWaitlistDefsInviteView, 4 + NetworkSlicesWaitlistRequest, 5 + NetworkSlicesWaitlistInvite, 6 + AppBskyActorDefsProfileViewBasic, 7 + AppBskyActorProfile, 8 + AtProtoClient, 9 + } from "../../../client.ts"; 10 + import type { RecordResponse } from "@slices/client"; 11 + import { recordBlobToCdnUrl } from "@slices/client"; 12 + 13 + /** 14 + * Converts a profile record to ProfileViewBasic format 15 + */ 16 + function profileToView( 17 + record: RecordResponse<AppBskyActorProfile>, 18 + did: string, 19 + handle?: string 20 + ): AppBskyActorDefsProfileViewBasic { 21 + return { 22 + did, 23 + handle: handle || did, // Use provided handle or fall back to DID 24 + displayName: record.value.displayName, 25 + avatar: record.value.avatar 26 + ? recordBlobToCdnUrl(record, record.value.avatar, "avatar") 27 + : undefined, 28 + }; 29 + } 30 + 31 + /** 32 + * Converts a waitlist request record to RequestView format 33 + */ 34 + function requestToView( 35 + record: RecordResponse<NetworkSlicesWaitlistRequest>, 36 + profile?: AppBskyActorDefsProfileViewBasic 37 + ): NetworkSlicesWaitlistDefsRequestView { 38 + return { 39 + slice: record.value.slice, 40 + createdAt: record.value.createdAt, 41 + profile, 42 + }; 43 + } 44 + 45 + /** 46 + * Converts a waitlist invite record to InviteView format 47 + */ 48 + function inviteToView( 49 + record: RecordResponse<NetworkSlicesWaitlistInvite>, 50 + profile?: AppBskyActorDefsProfileViewBasic 51 + ): NetworkSlicesWaitlistDefsInviteView { 52 + return { 53 + did: record.value.did, 54 + slice: record.value.slice, 55 + createdAt: record.value.createdAt, 56 + expiresAt: record.value.expiresAt, 57 + uri: record.uri, 58 + profile, 59 + }; 60 + } 61 + 62 + export async function getHydratedWaitlistRequests( 63 + client: AtProtoClient, 64 + sliceUri: string 65 + ): Promise<NetworkSlicesWaitlistDefsRequestView[]> { 66 + // Fetch waitlist requests 67 + const requestsResponse = 68 + await client.network.slices.waitlist.request.getRecords({ 69 + where: { 70 + slice: { eq: sliceUri }, 71 + }, 72 + sortBy: [{ field: "createdAt", direction: "desc" }], 73 + }); 74 + 75 + if (!requestsResponse.records || requestsResponse.records.length === 0) { 76 + return []; 77 + } 78 + 79 + // Get unique DIDs from requests 80 + const dids = [...new Set(requestsResponse.records.map((r) => r.did))]; 81 + 82 + // Fetch profiles and actors for all DIDs 83 + const profilesMap = new Map<string, AppBskyActorDefsProfileViewBasic>(); 84 + 85 + try { 86 + // Fetch actors to get handles 87 + const actorsResponse = await client.network.slices.slice.getActors({ 88 + where: { 89 + did: { in: dids } 90 + } 91 + }); 92 + 93 + // Create a map of DIDs to handles 94 + const handleMap = new Map<string, string>(); 95 + actorsResponse.actors?.forEach((actor) => { 96 + if (actor.handle) { 97 + handleMap.set(actor.did, actor.handle); 98 + } 99 + }); 100 + 101 + // Fetch Bluesky profiles 102 + const profileResponses = await Promise.all( 103 + dids.map((did) => 104 + client.app.bsky.actor.profile 105 + .getRecords({ 106 + where: { did: { eq: did } }, 107 + limit: 1, 108 + }) 109 + .catch(() => null) 110 + ) 111 + ); 112 + 113 + profileResponses.forEach((response, index) => { 114 + if (response?.records?.[0]) { 115 + const record = response.records[0]; 116 + const did = dids[index]; 117 + const handle = handleMap.get(did); 118 + profilesMap.set(did, profileToView(record, did, handle)); 119 + } 120 + }); 121 + } catch (error) { 122 + console.error("Error fetching profiles:", error); 123 + } 124 + 125 + // Transform to RequestView format with profiles 126 + return requestsResponse.records.map((record) => 127 + requestToView(record, profilesMap.get(record.did)) 128 + ); 129 + } 130 + 131 + export async function getHydratedWaitlistInvites( 132 + client: AtProtoClient, 133 + sliceUri: string 134 + ): Promise<NetworkSlicesWaitlistDefsInviteView[]> { 135 + // Fetch waitlist invites 136 + const invitesResponse = 137 + await client.network.slices.waitlist.invite.getRecords({ 138 + where: { 139 + slice: { eq: sliceUri }, 140 + }, 141 + sortBy: [{ field: "createdAt", direction: "desc" }], 142 + }); 143 + 144 + if (!invitesResponse.records || invitesResponse.records.length === 0) { 145 + return []; 146 + } 147 + 148 + // Get unique DIDs from invites 149 + const dids = [...new Set(invitesResponse.records.map((r) => r.value.did))]; 150 + 151 + // Fetch profiles and actors for all DIDs 152 + const profilesMap = new Map<string, AppBskyActorDefsProfileViewBasic>(); 153 + 154 + try { 155 + // Fetch actors to get handles 156 + const actorsResponse = await client.network.slices.slice.getActors({ 157 + where: { 158 + did: { in: dids } 159 + } 160 + }); 161 + 162 + // Create a map of DIDs to handles 163 + const handleMap = new Map<string, string>(); 164 + actorsResponse.actors?.forEach((actor) => { 165 + if (actor.handle) { 166 + handleMap.set(actor.did, actor.handle); 167 + } 168 + }); 169 + 170 + // Fetch Bluesky profiles 171 + const profileResponses = await Promise.all( 172 + dids.map((did) => 173 + client.app.bsky.actor.profile 174 + .getRecords({ 175 + where: { did: { eq: did } }, 176 + limit: 1, 177 + }) 178 + .catch(() => null) 179 + ) 180 + ); 181 + 182 + profileResponses.forEach((response, index) => { 183 + if (response?.records?.[0]) { 184 + const record = response.records[0]; 185 + const did = dids[index]; 186 + const handle = handleMap.get(did); 187 + profilesMap.set(did, profileToView(record, did, handle)); 188 + } 189 + }); 190 + } catch (error) { 191 + console.error("Error fetching profiles:", error); 192 + } 193 + 194 + // Transform to InviteView format with profiles 195 + return invitesResponse.records.map((record) => 196 + inviteToView(record, profilesMap.get(record.value.did)) 197 + ); 198 + } 199 + 200 + export async function createInviteFromRequest( 201 + client: AtProtoClient, 202 + requestUri: string, 203 + requestDid: string, 204 + sliceUri: string 205 + ): Promise<{ uri: string; cid: string }> { 206 + // Create the invite record 207 + const result = await client.network.slices.waitlist.invite.createRecord({ 208 + did: requestDid, 209 + slice: sliceUri, 210 + createdAt: new Date().toISOString(), 211 + }); 212 + 213 + // Delete the request record 214 + try { 215 + const rkey = requestUri.split("/").pop(); 216 + if (rkey) { 217 + await client.network.slices.waitlist.request.deleteRecord(rkey); 218 + } 219 + } catch (error) { 220 + console.error("Failed to delete request after creating invite:", error); 221 + } 222 + 223 + return result; 224 + } 225 + 226 + export async function removeInvite( 227 + client: AtProtoClient, 228 + inviteUri: string 229 + ): Promise<void> { 230 + const rkey = inviteUri.split("/").pop(); 231 + if (rkey) { 232 + await client.network.slices.waitlist.invite.deleteRecord(rkey); 233 + } 234 + }
+347
frontend/src/features/slices/waitlist/handlers.tsx
··· 1 + import type { Route } from "@std/http/unstable-route"; 2 + import { withAuth } from "../../../routes/middleware.ts"; 3 + import { withSliceAccess } from "../../../routes/slice-middleware.ts"; 4 + import { extractSliceParams, buildSliceUrlFromView } from "../../../utils/slice-params.ts"; 5 + import { getSliceClient } from "../../../utils/client.ts"; 6 + import { getRkeyFromUri, buildSliceUri } from "../../../utils/at-uri.ts"; 7 + import { renderHTML } from "../../../utils/render.tsx"; 8 + import { hxRedirect } from "../../../utils/htmx.ts"; 9 + import { SliceWaitlistPage } from "./templates/SliceWaitlistPage.tsx"; 10 + import { CreateInviteModal } from "./templates/fragments/CreateInviteModal.tsx"; 11 + import type { 12 + NetworkSlicesWaitlistDefsRequestView, 13 + NetworkSlicesWaitlistDefsInviteView, 14 + NetworkSlicesWaitlistInvite, 15 + } from "../../../client.ts"; 16 + import { 17 + getHydratedWaitlistRequests, 18 + getHydratedWaitlistInvites, 19 + } from "./api.ts"; 20 + 21 + async function handleSliceWaitlistPage( 22 + req: Request, 23 + params?: URLPatternResult 24 + ): Promise<Response> { 25 + const authContext = await withAuth(req); 26 + const sliceParams = extractSliceParams(params); 27 + 28 + if (!sliceParams) { 29 + return Response.redirect(new URL("/", req.url), 302); 30 + } 31 + 32 + const context = await withSliceAccess( 33 + authContext, 34 + sliceParams.handle, 35 + sliceParams.sliceId 36 + ); 37 + 38 + // Check if slice exists and user has access 39 + if (!context.sliceContext?.slice || !context.sliceContext?.hasAccess) { 40 + return new Response("Slice not found or access denied", { status: 404 }); 41 + } 42 + 43 + // Get the active tab from query params 44 + const url = new URL(req.url); 45 + const activeTab = url.searchParams.get("tab") || "requests"; 46 + 47 + // Fetch waitlist requests and invites from the API 48 + const sliceClient = getSliceClient( 49 + authContext, 50 + sliceParams.sliceId, 51 + context.sliceContext.profileDid 52 + ); 53 + 54 + // Build the slice URI to filter by 55 + const sliceUri = buildSliceUri( 56 + context.sliceContext.profileDid, 57 + sliceParams.sliceId 58 + ); 59 + 60 + let requests: NetworkSlicesWaitlistDefsRequestView[] = []; 61 + let invites: NetworkSlicesWaitlistDefsInviteView[] = []; 62 + 63 + try { 64 + // Fetch hydrated requests with profile information 65 + requests = await getHydratedWaitlistRequests(sliceClient, sliceUri); 66 + 67 + // Fetch hydrated invites with profile information 68 + invites = await getHydratedWaitlistInvites(sliceClient, sliceUri); 69 + } catch (error) { 70 + console.error("Error fetching waitlist data:", error); 71 + // Continue with empty arrays if fetch fails 72 + } 73 + 74 + return renderHTML( 75 + <SliceWaitlistPage 76 + slice={context.sliceContext!.slice!} 77 + sliceId={sliceParams.sliceId} 78 + requests={requests} 79 + invites={invites} 80 + currentUser={authContext.currentUser} 81 + hasSliceAccess={context.sliceContext?.hasAccess} 82 + activeTab={activeTab} 83 + /> 84 + ); 85 + } 86 + 87 + async function handleCreateInviteModal( 88 + req: Request, 89 + params?: URLPatternResult 90 + ): Promise<Response> { 91 + const authContext = await withAuth(req); 92 + const sliceParams = extractSliceParams(params); 93 + 94 + if (!sliceParams) { 95 + return new Response("Invalid slice parameters", { status: 400 }); 96 + } 97 + 98 + const context = await withSliceAccess( 99 + authContext, 100 + sliceParams.handle, 101 + sliceParams.sliceId 102 + ); 103 + 104 + if (!context.sliceContext?.slice || !context.sliceContext?.hasAccess) { 105 + return new Response("Slice not found or access denied", { status: 404 }); 106 + } 107 + 108 + return renderHTML( 109 + <CreateInviteModal 110 + slice={context.sliceContext!.slice!} 111 + sliceId={sliceParams.sliceId} 112 + /> 113 + ); 114 + } 115 + 116 + async function handleCreateInvite( 117 + req: Request, 118 + params?: URLPatternResult 119 + ): Promise<Response> { 120 + const authContext = await withAuth(req); 121 + const sliceParams = extractSliceParams(params); 122 + 123 + if (!sliceParams) { 124 + return new Response("Invalid slice parameters", { status: 400 }); 125 + } 126 + 127 + const context = await withSliceAccess( 128 + authContext, 129 + sliceParams.handle, 130 + sliceParams.sliceId 131 + ); 132 + 133 + if (!context.sliceContext?.slice || !context.sliceContext?.hasAccess) { 134 + return new Response("Slice not found or access denied", { status: 404 }); 135 + } 136 + 137 + const formData = await req.formData(); 138 + const did = formData.get("did")?.toString()?.trim(); 139 + const expiresAt = formData.get("expiresAt")?.toString(); 140 + 141 + if (!did) { 142 + return new Response("DID is required", { status: 400 }); 143 + } 144 + 145 + try { 146 + const sliceClient = getSliceClient( 147 + authContext, 148 + sliceParams.sliceId, 149 + context.sliceContext.profileDid 150 + ); 151 + 152 + const sliceUri = buildSliceUri( 153 + context.sliceContext.profileDid, 154 + sliceParams.sliceId 155 + ); 156 + 157 + const inviteData: NetworkSlicesWaitlistInvite = { 158 + did, 159 + slice: sliceUri, 160 + createdAt: new Date().toISOString(), 161 + }; 162 + 163 + // Convert datetime-local input to ISO 8601 string 164 + if (expiresAt) { 165 + // datetime-local gives us "2024-12-25T14:30" format 166 + // We need to convert it to a proper ISO string with timezone 167 + const expiresDate = new Date(expiresAt); 168 + if (!isNaN(expiresDate.getTime())) { 169 + inviteData.expiresAt = expiresDate.toISOString(); 170 + } 171 + } 172 + 173 + await sliceClient.network.slices.waitlist.invite.createRecord(inviteData); 174 + 175 + // Redirect back to the waitlist page with invites tab 176 + const redirectUrl = buildSliceUrlFromView( 177 + context.sliceContext!.slice!, 178 + sliceParams.sliceId, 179 + "waitlist?tab=invites" 180 + ); 181 + return hxRedirect(redirectUrl); 182 + } catch (error) { 183 + // Use the raw error message from the client 184 + const userErrorMessage = 185 + error instanceof Error ? error.message : "Failed to create invite"; 186 + 187 + // Return the error modal with the specific error message 188 + return renderHTML( 189 + <CreateInviteModal 190 + slice={context.sliceContext!.slice!} 191 + sliceId={sliceParams.sliceId} 192 + error={userErrorMessage} 193 + /> 194 + ); 195 + } 196 + } 197 + 198 + async function handleRevokeInvite( 199 + req: Request, 200 + params?: URLPatternResult 201 + ): Promise<Response> { 202 + const authContext = await withAuth(req); 203 + const sliceParams = extractSliceParams(params); 204 + 205 + if (!sliceParams) { 206 + return new Response("Invalid slice parameters", { status: 400 }); 207 + } 208 + 209 + const context = await withSliceAccess( 210 + authContext, 211 + sliceParams.handle, 212 + sliceParams.sliceId 213 + ); 214 + 215 + if (!context.sliceContext?.slice || !context.sliceContext?.hasAccess) { 216 + return new Response("Slice not found or access denied", { status: 404 }); 217 + } 218 + 219 + const formData = await req.formData(); 220 + const uri = formData.get("uri")?.toString(); 221 + 222 + if (!uri) { 223 + return new Response("URI is required", { status: 400 }); 224 + } 225 + 226 + try { 227 + const sliceClient = getSliceClient( 228 + authContext, 229 + sliceParams.sliceId, 230 + context.sliceContext.profileDid 231 + ); 232 + 233 + const rkey = getRkeyFromUri(uri); 234 + await sliceClient.network.slices.waitlist.invite.deleteRecord(rkey); 235 + 236 + // Return success with HX-Refresh to reload the page 237 + return new Response("", { 238 + status: 200, 239 + headers: { 240 + "HX-Refresh": "true", 241 + }, 242 + }); 243 + } catch (error) { 244 + console.error("Error revoking invite:", error); 245 + return new Response("Failed to revoke invite", { status: 500 }); 246 + } 247 + } 248 + 249 + async function handleCreateInviteFromRequest( 250 + req: Request, 251 + params?: URLPatternResult 252 + ): Promise<Response> { 253 + const authContext = await withAuth(req); 254 + const sliceParams = extractSliceParams(params); 255 + 256 + if (!sliceParams) { 257 + return new Response("Invalid slice parameters", { status: 400 }); 258 + } 259 + 260 + const context = await withSliceAccess( 261 + authContext, 262 + sliceParams.handle, 263 + sliceParams.sliceId 264 + ); 265 + 266 + if (!context.sliceContext?.slice || !context.sliceContext?.hasAccess) { 267 + return new Response("Slice not found or access denied", { status: 404 }); 268 + } 269 + 270 + try { 271 + const sliceClient = getSliceClient( 272 + authContext, 273 + sliceParams.sliceId, 274 + context.sliceContext.profileDid 275 + ); 276 + 277 + const formData = await req.formData(); 278 + const did = formData.get("did")?.toString()?.trim(); 279 + 280 + console.log("Creating invite for DID:", did); 281 + 282 + if (!did) { 283 + return new Response("DID is required", { status: 400 }); 284 + } 285 + 286 + const sliceUri = buildSliceUri(context.sliceContext.profileDid, sliceParams.sliceId); 287 + 288 + const inviteData: NetworkSlicesWaitlistInvite = { 289 + did, 290 + slice: sliceUri, 291 + createdAt: new Date().toISOString(), 292 + }; 293 + 294 + await sliceClient.network.slices.waitlist.invite.createRecord(inviteData); 295 + 296 + // Return success with HX-Refresh to reload the page 297 + return new Response("", { 298 + status: 200, 299 + headers: { 300 + "HX-Refresh": "true", 301 + }, 302 + }); 303 + } catch (error) { 304 + console.error("Error creating invite:", error); 305 + const userErrorMessage = 306 + error instanceof Error ? error.message : "Failed to create invite"; 307 + return new Response(userErrorMessage, { status: 500 }); 308 + } 309 + } 310 + 311 + export const waitlistRoutes: Route[] = [ 312 + { 313 + method: "GET", 314 + pattern: new URLPattern({ 315 + pathname: "/profile/:handle/slice/:rkey/waitlist", 316 + }), 317 + handler: handleSliceWaitlistPage, 318 + }, 319 + { 320 + method: "GET", 321 + pattern: new URLPattern({ 322 + pathname: "/profile/:handle/slice/:rkey/waitlist/invite/new", 323 + }), 324 + handler: handleCreateInviteModal, 325 + }, 326 + { 327 + method: "POST", 328 + pattern: new URLPattern({ 329 + pathname: "/profile/:handle/slice/:rkey/waitlist/invite", 330 + }), 331 + handler: handleCreateInvite, 332 + }, 333 + { 334 + method: "POST", 335 + pattern: new URLPattern({ 336 + pathname: "/profile/:handle/slice/:rkey/waitlist/invite/from-request", 337 + }), 338 + handler: handleCreateInviteFromRequest, 339 + }, 340 + { 341 + method: "DELETE", 342 + pattern: new URLPattern({ 343 + pathname: "/profile/:handle/slice/:rkey/waitlist/invite", 344 + }), 345 + handler: handleRevokeInvite, 346 + }, 347 + ];
+94
frontend/src/features/slices/waitlist/templates/SliceWaitlistPage.tsx
··· 1 + import { SlicePage } from "../../shared/fragments/SlicePage.tsx"; 2 + import { WaitlistRequestsList } from "./fragments/WaitlistRequestsList.tsx"; 3 + import { WaitlistInvitesList } from "./fragments/WaitlistInvitesList.tsx"; 4 + import { Button } from "../../../../shared/fragments/Button.tsx"; 5 + import { Tabs } from "../../../../shared/fragments/Tabs.tsx"; 6 + import { buildSliceUrlFromView } from "../../../../utils/slice-params.ts"; 7 + import type { AuthenticatedUser } from "../../../../routes/middleware.ts"; 8 + import type { 9 + NetworkSlicesSliceDefsSliceView, 10 + NetworkSlicesWaitlistDefsRequestView, 11 + NetworkSlicesWaitlistDefsInviteView 12 + } from "../../../../client.ts"; 13 + 14 + interface SliceWaitlistPageProps { 15 + slice: NetworkSlicesSliceDefsSliceView; 16 + sliceId: string; 17 + requests?: NetworkSlicesWaitlistDefsRequestView[]; 18 + invites?: NetworkSlicesWaitlistDefsInviteView[]; 19 + currentUser?: AuthenticatedUser; 20 + hasSliceAccess?: boolean; 21 + activeTab?: string; 22 + } 23 + 24 + export function SliceWaitlistPage({ 25 + slice, 26 + sliceId, 27 + requests = [], 28 + invites = [], 29 + currentUser, 30 + hasSliceAccess, 31 + activeTab = "requests", 32 + }: SliceWaitlistPageProps) { 33 + return ( 34 + <SlicePage 35 + slice={slice} 36 + sliceId={sliceId} 37 + currentTab="waitlist" 38 + currentUser={currentUser} 39 + hasSliceAccess={hasSliceAccess} 40 + title={`${slice.name} - Waitlist`} 41 + > 42 + <div className="space-y-6"> 43 + <div className="flex justify-end"> 44 + <Button 45 + variant="primary" 46 + size="md" 47 + hx-get={buildSliceUrlFromView(slice, sliceId, "waitlist/invite/new")} 48 + hx-target="#modal-container" 49 + > 50 + Create Invite 51 + </Button> 52 + </div> 53 + 54 + <Tabs variant="bordered"> 55 + <Tabs.List> 56 + <Tabs.Tab 57 + active={activeTab === "requests"} 58 + count={requests.length} 59 + hxGet={buildSliceUrlFromView(slice, sliceId, "waitlist?tab=requests")} 60 + hxTarget="body" 61 + > 62 + Requests 63 + </Tabs.Tab> 64 + <Tabs.Tab 65 + active={activeTab === "invites"} 66 + count={invites.length} 67 + hxGet={buildSliceUrlFromView(slice, sliceId, "waitlist?tab=invites")} 68 + hxTarget="body" 69 + > 70 + Invites 71 + </Tabs.Tab> 72 + </Tabs.List> 73 + 74 + <Tabs.Content active={activeTab === "requests"}> 75 + <WaitlistRequestsList 76 + requests={requests} 77 + invites={invites} 78 + slice={slice} 79 + sliceId={sliceId} 80 + /> 81 + </Tabs.Content> 82 + 83 + <Tabs.Content active={activeTab === "invites"}> 84 + <WaitlistInvitesList 85 + invites={invites} 86 + slice={slice} 87 + sliceId={sliceId} 88 + /> 89 + </Tabs.Content> 90 + </Tabs> 91 + </div> 92 + </SlicePage> 93 + ); 94 + }
+73
frontend/src/features/slices/waitlist/templates/fragments/CreateInviteModal.tsx
··· 1 + import { Modal } from "../../../../../shared/fragments/Modal.tsx"; 2 + import { Input } from "../../../../../shared/fragments/Input.tsx"; 3 + import { Button } from "../../../../../shared/fragments/Button.tsx"; 4 + import { Text } from "../../../../../shared/fragments/Text.tsx"; 5 + import { buildSliceUrlFromView } from "../../../../../utils/slice-params.ts"; 6 + import type { NetworkSlicesSliceDefsSliceView } from "../../../../../client.ts"; 7 + 8 + interface CreateInviteModalProps { 9 + slice: NetworkSlicesSliceDefsSliceView; 10 + sliceId: string; 11 + error?: string; 12 + } 13 + 14 + export function CreateInviteModal({ slice, sliceId, error }: CreateInviteModalProps) { 15 + return ( 16 + <Modal 17 + title="Create Invite" 18 + description="Grant a specific DID access to your slice" 19 + size="md" 20 + > 21 + {error && ( 22 + <div className="mb-4 p-3 bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800 rounded-md"> 23 + <Text as="p" variant="error" size="sm"> 24 + {error} 25 + </Text> 26 + </div> 27 + )} 28 + <form 29 + hx-post={buildSliceUrlFromView(slice, sliceId, "waitlist/invite")} 30 + className="space-y-4" 31 + > 32 + <div> 33 + <Text as="label" variant="label" className="block mb-2"> 34 + DID 35 + </Text> 36 + <Input 37 + type="text" 38 + name="did" 39 + placeholder="did:plc:example123..." 40 + required 41 + className="w-full" 42 + /> 43 + <Text as="p" variant="muted" size="sm" className="mt-1"> 44 + The AT Protocol DID to grant access 45 + </Text> 46 + </div> 47 + 48 + <div> 49 + <Text as="label" variant="label" className="block mb-2"> 50 + Expires At (Optional) 51 + </Text> 52 + <Input type="datetime-local" name="expiresAt" className="w-full" /> 53 + <Text as="p" variant="muted" size="sm" className="mt-1"> 54 + Leave empty for no expiration 55 + </Text> 56 + </div> 57 + 58 + <div className="flex justify-end gap-3 pt-4"> 59 + <Button 60 + type="button" 61 + variant="outline" 62 + _="on click set #modal-container's innerHTML to ''" 63 + > 64 + Cancel 65 + </Button> 66 + <Button type="submit" variant="success"> 67 + Create Invite 68 + </Button> 69 + </div> 70 + </form> 71 + </Modal> 72 + ); 73 + }
+93
frontend/src/features/slices/waitlist/templates/fragments/WaitlistInvitesList.tsx
··· 1 + import { ListItem } from "../../../../../shared/fragments/ListItem.tsx"; 2 + import { Button } from "../../../../../shared/fragments/Button.tsx"; 3 + import { Text } from "../../../../../shared/fragments/Text.tsx"; 4 + import { EmptyState } from "../../../../../shared/fragments/EmptyState.tsx"; 5 + import { ActorAvatar } from "../../../../../shared/fragments/ActorAvatar.tsx"; 6 + import { buildSliceUrlFromView } from "../../../../../utils/slice-params.ts"; 7 + import { timeAgo } from "../../../../../utils/time.ts"; 8 + import { UserCheck } from "lucide-preact"; 9 + import type { 10 + NetworkSlicesWaitlistDefsInviteView, 11 + NetworkSlicesSliceDefsSliceView 12 + } from "../../../../../client.ts"; 13 + 14 + interface WaitlistInvitesListProps { 15 + invites: NetworkSlicesWaitlistDefsInviteView[]; 16 + slice: NetworkSlicesSliceDefsSliceView; 17 + sliceId: string; 18 + } 19 + 20 + export function WaitlistInvitesList({ 21 + invites, 22 + slice, 23 + sliceId, 24 + }: WaitlistInvitesListProps) { 25 + const isExpired = (invite: NetworkSlicesWaitlistDefsInviteView) => { 26 + if (!invite.expiresAt) return false; 27 + return new Date(invite.expiresAt) < new Date(); 28 + }; 29 + 30 + if (invites.length === 0) { 31 + return ( 32 + <EmptyState 33 + icon={<UserCheck size={48} strokeWidth={1} />} 34 + title="No invites created" 35 + description="Create invites to grant specific DIDs access to your slice." 36 + withPadding 37 + /> 38 + ); 39 + } 40 + 41 + return ( 42 + <div className="bg-white dark:bg-zinc-900 border border-zinc-200 dark:border-zinc-700 rounded-sm"> 43 + {invites.map((invite, index) => ( 44 + <ListItem key={`invite-${index}`}> 45 + <div className="flex items-center justify-between w-full px-6 py-4"> 46 + <div className="flex items-center gap-3 flex-1 min-w-0"> 47 + <ActorAvatar 48 + profile={invite.profile || { handle: invite.did }} 49 + size={40} 50 + /> 51 + <div className="flex-1 min-w-0"> 52 + <Text as="div" size="sm" className="font-medium truncate"> 53 + {invite.profile?.displayName || invite.profile?.handle || invite.did} 54 + </Text> 55 + <div className="flex items-center gap-4 mt-0.5"> 56 + <Text as="div" size="xs" variant="muted"> 57 + {invite.profile?.handle && invite.profile.handle !== invite.did && `@${invite.profile.handle} • `}Created {timeAgo(invite.createdAt)} 58 + </Text> 59 + {invite.expiresAt && ( 60 + <Text 61 + as="div" 62 + size="xs" 63 + variant={isExpired(invite) ? "error" : "muted"} 64 + > 65 + {isExpired(invite) ? "Expired" : "Expires"} {timeAgo(invite.expiresAt)} 66 + </Text> 67 + )} 68 + </div> 69 + </div> 70 + </div> 71 + <div className="ml-4"> 72 + <form 73 + hx-delete={buildSliceUrlFromView(slice, sliceId, "waitlist/invite")} 74 + hx-trigger="submit" 75 + hx-confirm="Revoke this invite?" 76 + style="display: inline;" 77 + > 78 + <input type="hidden" name="uri" value={invite.uri} /> 79 + <Button 80 + type="submit" 81 + variant="outline" 82 + size="sm" 83 + > 84 + Revoke 85 + </Button> 86 + </form> 87 + </div> 88 + </div> 89 + </ListItem> 90 + ))} 91 + </div> 92 + ); 93 + }
+92
frontend/src/features/slices/waitlist/templates/fragments/WaitlistRequestsList.tsx
··· 1 + import { ListItem } from "../../../../../shared/fragments/ListItem.tsx"; 2 + import { Button } from "../../../../../shared/fragments/Button.tsx"; 3 + import { Text } from "../../../../../shared/fragments/Text.tsx"; 4 + import { EmptyState } from "../../../../../shared/fragments/EmptyState.tsx"; 5 + import { ActorAvatar } from "../../../../../shared/fragments/ActorAvatar.tsx"; 6 + import { Users } from "lucide-preact"; 7 + import type { 8 + NetworkSlicesWaitlistDefsRequestView, 9 + NetworkSlicesWaitlistDefsInviteView, 10 + NetworkSlicesSliceDefsSliceView 11 + } from "../../../../../client.ts"; 12 + import { buildSliceUrlFromView } from "../../../../../utils/slice-params.ts"; 13 + import { timeAgo } from "../../../../../utils/time.ts"; 14 + 15 + interface WaitlistRequestsListProps { 16 + requests: NetworkSlicesWaitlistDefsRequestView[]; 17 + invites: NetworkSlicesWaitlistDefsInviteView[]; 18 + slice: NetworkSlicesSliceDefsSliceView; 19 + sliceId: string; 20 + } 21 + 22 + export function WaitlistRequestsList({ 23 + requests, 24 + invites, 25 + slice, 26 + sliceId, 27 + }: WaitlistRequestsListProps) { 28 + if (requests.length === 0) { 29 + return ( 30 + <EmptyState 31 + icon={<Users size={48} strokeWidth={1} />} 32 + title="No requests yet" 33 + description="Waitlist requests will appear here when users request access." 34 + withPadding 35 + /> 36 + ); 37 + } 38 + 39 + return ( 40 + <div className="bg-white dark:bg-zinc-900 border border-zinc-200 dark:border-zinc-700 rounded-sm"> 41 + {requests.map((request, index) => { 42 + // Check if this DID already has an invite 43 + const requestDid = request.profile?.did || "unknown"; 44 + const hasInvite = invites.some(invite => invite.did === requestDid); 45 + 46 + return ( 47 + <ListItem key={`request-${index}`}> 48 + <div className="flex items-center justify-between w-full px-6 py-4"> 49 + <div className="flex items-center gap-3 flex-1 min-w-0"> 50 + <ActorAvatar 51 + profile={request.profile || { handle: requestDid }} 52 + size={40} 53 + /> 54 + <div className="flex-1 min-w-0"> 55 + <Text 56 + as="div" 57 + size="sm" 58 + className="font-medium truncate" 59 + > 60 + {request.profile?.displayName || request.profile?.handle || requestDid} 61 + </Text> 62 + <Text as="div" size="xs" variant="muted" className="mt-0.5"> 63 + {request.profile?.handle && request.profile.handle !== requestDid && `@${request.profile.handle} • `}Requested {timeAgo(request.createdAt)} 64 + </Text> 65 + </div> 66 + </div> 67 + <div className="flex items-center gap-2 ml-4"> 68 + {!hasInvite && ( 69 + <Button 70 + variant="success" 71 + size="sm" 72 + hx-post={buildSliceUrlFromView(slice, sliceId, "waitlist/invite/from-request")} 73 + hx-vals={JSON.stringify({ did: requestDid })} 74 + hx-target="closest div[class*='bg-white']" 75 + hx-swap="outerHTML" 76 + > 77 + Invite 78 + </Button> 79 + )} 80 + {hasInvite && ( 81 + <Text size="sm" variant="muted"> 82 + Already invited 83 + </Text> 84 + )} 85 + </div> 86 + </div> 87 + </ListItem> 88 + ); 89 + })} 90 + </div> 91 + ); 92 + }
+16
frontend/src/features/waitlist/handlers.tsx
··· 2 2 import { withAuth } from "../../routes/middleware.ts"; 3 3 import { renderHTML } from "../../utils/render.tsx"; 4 4 import { WaitlistPage } from "./templates/WaitlistPage.tsx"; 5 + import { publicClient, SLICE_URI } from "../../config.ts"; 6 + import { getHydratedWaitlistRequests } from "../slices/waitlist/api.ts"; 5 7 6 8 async function handleWaitlistPage(req: Request): Promise<Response> { 7 9 const context = await withAuth(req); ··· 12 14 const handle = url.searchParams.get("handle"); 13 15 const error = url.searchParams.get("error"); 14 16 17 + // Fetch recent waitlist requests to show avatars for social proof 18 + let recentRequests; 19 + if (SLICE_URI) { 20 + try { 21 + recentRequests = await getHydratedWaitlistRequests(publicClient, SLICE_URI); 22 + // Limit to most recent 50 and reverse to show newest first 23 + recentRequests = recentRequests.slice(0, 50); 24 + } catch (error) { 25 + console.error("Failed to fetch recent waitlist requests:", error); 26 + // Continue without recent requests if fetch fails 27 + } 28 + } 29 + 15 30 return renderHTML( 16 31 <WaitlistPage 17 32 success={success} 18 33 handle={handle || undefined} 19 34 error={error || undefined} 20 35 currentUser={context.currentUser} 36 + recentRequests={recentRequests} 21 37 /> 22 38 ); 23 39 }
+6 -3
frontend/src/features/waitlist/templates/WaitlistPage.tsx
··· 3 3 import { WaitlistSuccess } from "./fragments/WaitlistSuccess.tsx"; 4 4 import { Text } from "../../../shared/fragments/Text.tsx"; 5 5 import type { AuthenticatedUser } from "../../../routes/middleware.ts"; 6 + import type { NetworkSlicesWaitlistDefsRequestView } from "../../../client.ts"; 6 7 7 8 interface WaitlistPageProps { 8 9 success?: boolean; 9 10 handle?: string; 10 11 error?: string; 11 12 currentUser?: AuthenticatedUser; 13 + recentRequests?: NetworkSlicesWaitlistDefsRequestView[]; 12 14 } 13 15 14 16 export function WaitlistPage({ ··· 16 18 handle, 17 19 error, 18 20 currentUser, 21 + recentRequests, 19 22 }: WaitlistPageProps) { 20 23 return ( 21 24 <Layout title="Join the Waitlist - Slices" currentUser={currentUser}> 22 - <div className="min-h-screen bg-white dark:bg-zinc-900 flex items-center justify-center px-4 py-16"> 25 + <div className="min-h-[calc(100vh-3.5rem)] bg-white dark:bg-zinc-900 flex items-center justify-center px-4 py-16"> 23 26 <div className="w-full max-w-md"> 24 27 {success ? ( 25 - <WaitlistSuccess handle={handle} /> 28 + <WaitlistSuccess handle={handle} recentRequests={recentRequests} /> 26 29 ) : ( 27 30 <> 28 31 <div className="text-center mb-8"> ··· 33 36 Be among the first to experience the future of AT Protocol ecosystem tools. 34 37 </Text> 35 38 </div> 36 - <WaitlistForm error={error} /> 39 + <WaitlistForm error={error} recentRequests={recentRequests} /> 37 40 </> 38 41 )} 39 42 </div>
+44 -6
frontend/src/features/waitlist/templates/fragments/WaitlistForm.tsx
··· 3 3 import { Card } from "../../../../shared/fragments/Card.tsx"; 4 4 import { Text } from "../../../../shared/fragments/Text.tsx"; 5 5 import { FlashMessage } from "../../../../shared/fragments/FlashMessage.tsx"; 6 + import { ActorAvatar } from "../../../../shared/fragments/ActorAvatar.tsx"; 7 + import type { NetworkSlicesWaitlistDefsRequestView } from "../../../../client.ts"; 6 8 7 9 interface WaitlistFormProps { 8 10 error?: string; 11 + recentRequests?: NetworkSlicesWaitlistDefsRequestView[]; 9 12 } 10 13 11 - export function WaitlistForm({ error }: WaitlistFormProps) { 14 + export function WaitlistForm({ error, recentRequests }: WaitlistFormProps) { 12 15 const getErrorMessage = (error: string) => { 13 16 switch (error) { 14 17 case "oauth_not_configured": ··· 19 22 return "Could not retrieve user information."; 20 23 case "waitlist_failed": 21 24 return "Failed to join waitlist. Please try again."; 25 + case "invite_required": 26 + return "You need an invite to access this service. Join the waitlist to request access."; 27 + case "already_on_waitlist": 28 + return "You're already on the waitlist! We'll notify you when your invite is ready."; 22 29 default: 23 30 return "An error occurred. Please try again."; 24 31 } 25 32 }; 26 33 27 34 return ( 28 - <Card> 35 + <Card padding="md"> 36 + {recentRequests && recentRequests.length > 0 && ( 37 + <div className="mb-6 text-center"> 38 + <Text as="p" size="sm" variant="muted" className="mb-3"> 39 + Join {recentRequests.length} others who are waiting 40 + </Text> 41 + <div className="flex flex-wrap justify-center gap-1"> 42 + {recentRequests.slice(0, 20).map((request, index) => ( 43 + <div key={index} className="relative"> 44 + <ActorAvatar 45 + profile={request.profile || { handle: "user" }} 46 + size={24} 47 + className="border border-white dark:border-zinc-800" 48 + /> 49 + </div> 50 + ))} 51 + {recentRequests.length > 20 && ( 52 + <div className="w-6 h-6 bg-zinc-100 dark:bg-zinc-800 border border-white dark:border-zinc-800 rounded-full flex items-center justify-center"> 53 + <Text size="xs" variant="muted"> 54 + +{recentRequests.length - 20} 55 + </Text> 56 + </div> 57 + )} 58 + </div> 59 + </div> 60 + )} 61 + 29 62 <form action="/auth/waitlist/initiate" method="POST"> 30 63 <div className="space-y-6"> 31 64 {error && ( ··· 36 69 <Input 37 70 label="Your handle" 38 71 name="handle" 39 - placeholder="alice.bsky.social" 72 + placeholder="user.bsky.social" 40 73 required 41 74 /> 42 75 <Text as="p" size="xs" variant="muted"> ··· 45 78 </div> 46 79 47 80 <div className="space-y-4"> 48 - <Button type="submit" variant="primary" className="w-full justify-center"> 81 + <Button 82 + type="submit" 83 + variant="primary" 84 + className="w-full justify-center" 85 + > 49 86 Join Waitlist 50 87 </Button> 51 88 52 89 <Text as="p" size="xs" variant="muted" className="text-center"> 53 - By joining the waitlist, you'll be notified when Slices is ready for you. 90 + By joining the waitlist, you'll be notified when Slices is ready 91 + for you. 54 92 </Text> 55 93 </div> 56 94 </div> 57 95 </form> 58 96 </Card> 59 97 ); 60 - } 98 + }
+47 -4
frontend/src/features/waitlist/templates/fragments/WaitlistSuccess.tsx
··· 2 2 import { Card } from "../../../../shared/fragments/Card.tsx"; 3 3 import { Text } from "../../../../shared/fragments/Text.tsx"; 4 4 import { Link } from "../../../../shared/fragments/Link.tsx"; 5 + import { ActorAvatar } from "../../../../shared/fragments/ActorAvatar.tsx"; 5 6 import { Check } from "lucide-preact"; 7 + import type { NetworkSlicesWaitlistDefsRequestView } from "../../../../client.ts"; 6 8 7 9 interface WaitlistSuccessProps { 8 10 handle?: string; 11 + recentRequests?: NetworkSlicesWaitlistDefsRequestView[]; 9 12 } 10 13 11 - export function WaitlistSuccess({ handle }: WaitlistSuccessProps) { 14 + export function WaitlistSuccess({ 15 + handle, 16 + recentRequests, 17 + }: WaitlistSuccessProps) { 12 18 return ( 13 - <Card className="text-center"> 19 + <Card padding="md" className="text-center"> 14 20 <div className="flex justify-center mb-6"> 15 21 <div className="w-16 h-16 bg-green-100 dark:bg-green-900/30 rounded-full flex items-center justify-center"> 16 22 <Check size={32} className="text-green-600 dark:text-green-400" /> ··· 22 28 </Text> 23 29 24 30 <Text as="p" variant="secondary" className="mb-6"> 25 - Thanks for joining the waitlist{handle ? <>, <Text as="span" className="font-bold">{handle}</Text></> : ""}! We'll notify you as soon as Slices is ready for you. 31 + Thanks for joining the waitlist 32 + {handle ? ( 33 + <> 34 + ,{" "} 35 + <Text as="span" className="font-bold"> 36 + {handle} 37 + </Text> 38 + </> 39 + ) : ( 40 + "" 41 + )} 42 + ! We'll notify you as soon as Slices is ready for you. 26 43 </Text> 27 44 45 + {recentRequests && recentRequests.length > 0 && ( 46 + <div className="mb-6"> 47 + <Text as="p" size="sm" variant="muted" className="mb-3"> 48 + You've joined {recentRequests.length} others 49 + </Text> 50 + <div className="flex flex-wrap justify-center gap-1"> 51 + {recentRequests.slice(0, 30).map((request, index) => ( 52 + <div key={index} className="relative"> 53 + <ActorAvatar 54 + profile={request.profile || { handle: "user" }} 55 + size={32} 56 + className="border border-white dark:border-zinc-800" 57 + /> 58 + </div> 59 + ))} 60 + {recentRequests.length > 30 && ( 61 + <div className="w-8 h-8 bg-zinc-100 dark:bg-zinc-800 border border-white dark:border-zinc-800 rounded-full flex items-center justify-center"> 62 + <Text size="xs" variant="muted"> 63 + +{recentRequests.length - 30} 64 + </Text> 65 + </div> 66 + )} 67 + </div> 68 + </div> 69 + )} 70 + 28 71 <div className="space-y-4"> 29 72 <Button href="/" variant="primary" className="w-full justify-center"> 30 73 Back to Home ··· 44 87 </div> 45 88 </Card> 46 89 ); 47 - } 90 + }
+9 -3
frontend/src/lib/api.ts
··· 176 176 if (creator) { 177 177 const sparklineData = sparklinesMap[sliceRecord.uri]; 178 178 const statsData = statsMap[sliceRecord.uri]; 179 - sliceViews.push(sliceToView(sliceRecord, creator, sparklineData, statsData)); 179 + sliceViews.push( 180 + sliceToView(sliceRecord, creator, sparklineData, statsData) 181 + ); 180 182 } 181 183 } 182 184 ··· 215 217 if (creator) { 216 218 const sparklineData = sparklinesMap[sliceRecord.uri]; 217 219 const statsData = statsMap[sliceRecord.uri]; 218 - sliceViews.push(sliceToView(sliceRecord, creator, sparklineData, statsData)); 220 + sliceViews.push( 221 + sliceToView(sliceRecord, creator, sparklineData, statsData) 222 + ); 219 223 } 220 224 } 221 225 ··· 251 255 if (creator) { 252 256 const sparklineData = sparklinesMap[sliceRecord.uri]; 253 257 const statsData = statsMap[sliceRecord.uri]; 254 - sliceViews.push(sliceToView(sliceRecord, creator, sparklineData, statsData)); 258 + sliceViews.push( 259 + sliceToView(sliceRecord, creator, sparklineData, statsData) 260 + ); 255 261 } 256 262 } 257 263
+5 -3
frontend/src/routes/mod.ts
··· 1 1 import type { Route } from "@std/http/unstable-route"; 2 2 import { landingRoutes } from "../features/landing/handlers.tsx"; 3 3 import { authRoutes } from "../features/auth/handlers.tsx"; 4 - import { waitlistRoutes } from "../features/waitlist/handlers.tsx"; 5 4 import { dashboardRoutes } from "../features/dashboard/handlers.tsx"; 5 + import { waitlistRoutes as globalWaitlistRoutes } from "../features/waitlist/handlers.tsx"; 6 6 import { 7 7 apiDocsRoutes, 8 8 codegenRoutes, ··· 14 14 settingsRoutes as sliceSettingsRoutes, 15 15 syncLogsRoutes, 16 16 syncRoutes, 17 + waitlistRoutes as sliceWaitlistRoutes, 17 18 } from "../features/slices/mod.ts"; 18 19 import { settingsRoutes } from "../features/settings/handlers.tsx"; 19 20 import { docsRoutes } from "../features/docs/handlers.tsx"; ··· 25 26 // Auth routes (login, oauth, logout) 26 27 ...authRoutes, 27 28 28 - // Waitlist page 29 - ...waitlistRoutes, 29 + // Global waitlist page 30 + ...globalWaitlistRoutes, 30 31 31 32 // Documentation routes 32 33 ...docsRoutes, ··· 45 46 ...syncRoutes, 46 47 ...syncLogsRoutes, 47 48 ...jetstreamRoutes, 49 + ...sliceWaitlistRoutes, 48 50 49 51 // Dashboard routes (home page, create slice) 50 52 ...dashboardRoutes,
+2 -2
frontend/src/shared/fragments/Card.tsx
··· 8 8 variant?: CardVariant; 9 9 padding?: "none" | "sm" | "md" | "lg"; 10 10 className?: string; 11 - children: JSX.Element | JSX.Element[]; 11 + children: ComponentChildren; 12 12 } 13 13 14 14 interface CardHeaderProps { ··· 37 37 38 38 export function Card({ 39 39 variant = "default", 40 - padding = "md", 40 + padding = "none", 41 41 className, 42 42 children, 43 43 ...props
+8 -6
frontend/src/shared/fragments/FlashMessage.tsx
··· 14 14 if (type === "success") { 15 15 return ( 16 16 <Card padding="sm" className={`mb-4 bg-green-50 dark:bg-green-900/20 border-green-200 dark:border-green-800 ${className}`}> 17 - <div className="flex items-center gap-2"> 17 + <div className="flex items-center gap-3"> 18 18 <CheckCircle2 19 - size={16} 19 + size={20} 20 + className="flex-shrink-0" 20 21 style={{ fill: '#16a34a', stroke: 'white', strokeWidth: 1 }} 21 22 /> 22 - <Text variant="success"> 23 + <Text variant="success" className="flex-1"> 23 24 {message} 24 25 </Text> 25 26 </div> ··· 29 30 30 31 return ( 31 32 <Card padding="sm" className={`mb-4 bg-red-50 dark:bg-red-900/20 border-red-200 dark:border-red-800 ${className}`}> 32 - <div className="flex items-center gap-2"> 33 + <div className="flex items-center gap-3"> 33 34 <XCircle 34 - size={16} 35 + size={20} 36 + className="flex-shrink-0" 35 37 style={{ fill: '#dc2626', stroke: 'white', strokeWidth: 1 }} 36 38 /> 37 - <Text variant="error"> 39 + <Text variant="error" className="flex-1"> 38 40 {message} 39 41 </Text> 40 42 </div>
+29 -13
frontend/src/shared/fragments/Layout.tsx
··· 31 31 <meta name="description" content={description} /> 32 32 33 33 {/* Favicon - Letter S */} 34 - <link rel="icon" href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'><rect width='100' height='100' fill='black'/><text x='50' y='70' font-size='60' font-family='system-ui' fill='white' text-anchor='middle' font-weight='bold'>S</text></svg>" /> 34 + <link 35 + rel="icon" 36 + href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'><rect width='100' height='100' fill='black'/><text x='50' y='70' font-size='60' font-family='system-ui' fill='white' text-anchor='middle' font-weight='bold'>S</text></svg>" 37 + /> 35 38 36 39 {/* Open Graph / Facebook */} 37 40 <meta property="og:type" content="website" /> ··· 78 81 {showNavigation && <Navigation currentUser={currentUser} />} 79 82 <div 80 83 className={`min-h-screen flex flex-col ${ 81 - fullWidth ? "" : "max-w-5xl mx-auto sm:border-x border-zinc-200 dark:border-zinc-800" 84 + fullWidth 85 + ? "" 86 + : "max-w-5xl mx-auto sm:border-x border-zinc-200 dark:border-zinc-800" 82 87 }`} 83 88 style={backgroundStyle} 84 89 > 85 - {showNavigation 86 - ? ( 87 - <main className={cn("flex-1 sm:pt-14", !backgroundStyle && "bg-white dark:bg-zinc-900")}> 88 - {children} 89 - </main> 90 - ) 91 - : ( 92 - <main className={cn("flex-1", !backgroundStyle && "bg-white dark:bg-zinc-900")}> 93 - {children} 94 - </main> 95 - )} 90 + {showNavigation ? ( 91 + <main 92 + className={cn( 93 + "flex-1 sm:pt-14", 94 + !backgroundStyle && "bg-white dark:bg-zinc-900" 95 + )} 96 + > 97 + {children} 98 + </main> 99 + ) : ( 100 + <main 101 + className={cn( 102 + "flex-1", 103 + !backgroundStyle && "bg-white dark:bg-zinc-900" 104 + )} 105 + > 106 + {children} 107 + </main> 108 + )} 96 109 </div> 110 + 111 + {/* Modal container for HTMX modals */} 112 + <div id="modal-container"></div> 97 113 </body> 98 114 </html> 99 115 );
+1 -1
frontend/src/shared/fragments/LogViewer.tsx
··· 27 27 const infoCount = logs.filter((l) => l.level === "info").length; 28 28 29 29 return ( 30 - <Card padding="none"> 30 + <Card> 31 31 <div className="bg-zinc-50 dark:bg-zinc-800 px-6 py-3 border-b border-zinc-200 dark:border-zinc-700 rounded-t-sm"> 32 32 <div className="flex items-center gap-4"> 33 33 <Text as="span" size="sm">
+8 -6
frontend/src/shared/fragments/Navigation.tsx
··· 16 16 > 17 17 Slices 18 18 </a> 19 - <a 20 - href="/docs" 21 - className="px-3 py-1.5 text-sm text-zinc-600 dark:text-zinc-400 hover:text-zinc-900 dark:hover:text-white hover:bg-zinc-100 dark:hover:bg-zinc-800 rounded-md transition-colors" 22 - > 23 - Docs 24 - </a> 19 + {currentUser?.isAuthenticated && ( 20 + <a 21 + href="/docs" 22 + className="px-3 py-1.5 text-sm text-zinc-600 dark:text-zinc-400 hover:text-zinc-900 dark:hover:text-white hover:bg-zinc-100 dark:hover:bg-zinc-800 rounded-md transition-colors" 23 + > 24 + Docs 25 + </a> 26 + )} 25 27 </div> 26 28 <div className="flex items-center space-x-2"> 27 29 {currentUser?.isAuthenticated
+131
frontend/src/shared/fragments/Tabs.tsx
··· 1 + import type { JSX, ComponentChildren } from "preact"; 2 + import { cn } from "../../utils/cn.ts"; 3 + import { passPropsToChildren } from "../../utils/preact.ts"; 4 + 5 + type TabVariant = "underline" | "bordered"; 6 + 7 + interface TabsProps { 8 + variant?: TabVariant; 9 + className?: string; 10 + children: ComponentChildren; 11 + } 12 + 13 + interface TabsListProps { 14 + variant?: TabVariant; 15 + className?: string; 16 + children: ComponentChildren; 17 + } 18 + 19 + interface TabProps { 20 + variant?: TabVariant; 21 + active?: boolean; 22 + className?: string; 23 + children: ComponentChildren; 24 + count?: number; 25 + hxGet?: string; 26 + hxTarget?: string; 27 + } 28 + 29 + interface TabsContentProps { 30 + active?: boolean; 31 + className?: string; 32 + children: ComponentChildren; 33 + } 34 + 35 + export function Tabs({ 36 + variant = "underline", 37 + className, 38 + children, 39 + }: TabsProps): JSX.Element { 40 + const childrenWithVariant = passPropsToChildren(children, { variant }); 41 + 42 + return <div className={cn("w-full", className)}>{childrenWithVariant}</div>; 43 + } 44 + 45 + Tabs.List = function TabsList({ 46 + variant = "underline", 47 + className, 48 + children, 49 + }: TabsListProps): JSX.Element { 50 + const listClasses = 51 + variant === "bordered" 52 + ? "bg-zinc-50 dark:bg-zinc-800/50 relative after:absolute after:bottom-0 after:left-0 after:right-0 after:h-px after:bg-zinc-200 after:dark:bg-zinc-700" 53 + : "border-b border-zinc-200 dark:border-zinc-700"; 54 + 55 + const childrenWithVariant = passPropsToChildren(children, { variant }); 56 + 57 + return ( 58 + <nav className={cn(listClasses, className)}> 59 + <div 60 + className={cn( 61 + "flex overflow-x-auto scrollbar-hide", 62 + variant === "bordered" ? "space-x-0" : "space-x-1" 63 + )} 64 + > 65 + {childrenWithVariant} 66 + </div> 67 + </nav> 68 + ); 69 + }; 70 + 71 + Tabs.Tab = function Tab({ 72 + variant = "underline", 73 + active = false, 74 + className, 75 + children, 76 + count, 77 + hxGet, 78 + hxTarget, 79 + }: TabProps): JSX.Element { 80 + const getTabClasses = () => { 81 + if (variant === "bordered") { 82 + return cn( 83 + "py-2 px-4 font-medium text-sm whitespace-nowrap flex-shrink-0 transition-colors relative", 84 + active 85 + ? "bg-white dark:bg-zinc-900 text-zinc-900 dark:text-zinc-100 border border-zinc-200 dark:border-zinc-700 border-b-white dark:border-b-zinc-900 rounded-t-md z-20 relative" 86 + : "bg-transparent text-zinc-500 dark:text-zinc-400" 87 + ); 88 + } 89 + 90 + return cn( 91 + "py-2 px-4 border-b-2 font-medium text-sm whitespace-nowrap flex-shrink-0 transition-colors", 92 + active 93 + ? "border-blue-500 dark:border-blue-400 text-blue-600 dark:text-blue-400" 94 + : "border-transparent text-zinc-500 dark:text-zinc-400 hover:text-zinc-700 dark:hover:text-zinc-300 hover:border-zinc-300 dark:hover:border-zinc-600" 95 + ); 96 + }; 97 + 98 + return ( 99 + <button 100 + type="button" 101 + className={cn(getTabClasses(), className)} 102 + hx-get={hxGet} 103 + hx-target={hxTarget} 104 + hx-push-url="true" 105 + > 106 + {children} 107 + {count !== undefined && ( 108 + <span 109 + className={cn( 110 + "ml-2 px-2 py-0.5 text-xs rounded-full", 111 + active 112 + ? "bg-blue-100 dark:bg-blue-900 text-blue-800 dark:text-blue-200" 113 + : "bg-zinc-100 dark:bg-zinc-700 text-zinc-600 dark:text-zinc-300" 114 + )} 115 + > 116 + {count} 117 + </span> 118 + )} 119 + </button> 120 + ); 121 + }; 122 + 123 + Tabs.Content = function TabsContent({ 124 + active = false, 125 + className, 126 + children, 127 + }: TabsContentProps): JSX.Element | null { 128 + if (!active) return null; 129 + 130 + return <div className={cn("py-6", className)}>{children}</div>; 131 + };
+3 -1
frontend/src/shared/fragments/Text.tsx
··· 24 24 | "h3" 25 25 | "h4" 26 26 | "h5" 27 - | "h6"; 27 + | "h6" 28 + | "dt" 29 + | "dd"; 28 30 29 31 interface TextProps { 30 32 variant?: TextVariant;
+25
frontend/src/utils/preact.ts
··· 1 + import type { ComponentChildren } from "preact"; 2 + import { cloneElement } from "preact"; 3 + 4 + /** 5 + * Passes props down to all children that accept them 6 + */ 7 + export function passPropsToChildren( 8 + children: ComponentChildren, 9 + props: Record<string, unknown> 10 + ): ComponentChildren { 11 + if (Array.isArray(children)) { 12 + return children.map(child => { 13 + if (child && typeof child === 'object' && 'type' in child) { 14 + return cloneElement(child as any, props); 15 + } 16 + return child; 17 + }); 18 + } 19 + 20 + if (children && typeof children === 'object' && 'type' in children) { 21 + return cloneElement(children as any, props); 22 + } 23 + 24 + return children; 25 + }
+695
lexicons/app/bsky/actor/defs.json
··· 1 + { 2 + "lexicon": 1, 3 + "id": "app.bsky.actor.defs", 4 + "defs": { 5 + "nux": { 6 + "type": "object", 7 + "required": [ 8 + "id", 9 + "completed" 10 + ], 11 + "properties": { 12 + "id": { 13 + "type": "string", 14 + "maxLength": 100 15 + }, 16 + "data": { 17 + "type": "string", 18 + "maxLength": 3000, 19 + "description": "Arbitrary data for the NUX. The structure is defined by the NUX itself. Limited to 300 characters.", 20 + "maxGraphemes": 300 21 + }, 22 + "completed": { 23 + "type": "boolean", 24 + "default": false 25 + }, 26 + "expiresAt": { 27 + "type": "string", 28 + "format": "datetime", 29 + "description": "The date and time at which the NUX will expire and should be considered completed." 30 + } 31 + }, 32 + "description": "A new user experiences (NUX) storage object" 33 + }, 34 + "mutedWord": { 35 + "type": "object", 36 + "required": [ 37 + "value", 38 + "targets" 39 + ], 40 + "properties": { 41 + "id": { 42 + "type": "string" 43 + }, 44 + "value": { 45 + "type": "string", 46 + "maxLength": 10000, 47 + "description": "The muted word itself.", 48 + "maxGraphemes": 1000 49 + }, 50 + "targets": { 51 + "type": "array", 52 + "items": { 53 + "ref": "app.bsky.actor.defs#mutedWordTarget", 54 + "type": "ref" 55 + }, 56 + "description": "The intended targets of the muted word." 57 + }, 58 + "expiresAt": { 59 + "type": "string", 60 + "format": "datetime", 61 + "description": "The date and time at which the muted word will expire and no longer be applied." 62 + }, 63 + "actorTarget": { 64 + "type": "string", 65 + "default": "all", 66 + "description": "Groups of users to apply the muted word to. If undefined, applies to all users.", 67 + "knownValues": [ 68 + "all", 69 + "exclude-following" 70 + ] 71 + } 72 + }, 73 + "description": "A word that the account owner has muted." 74 + }, 75 + "savedFeed": { 76 + "type": "object", 77 + "required": [ 78 + "id", 79 + "type", 80 + "value", 81 + "pinned" 82 + ], 83 + "properties": { 84 + "id": { 85 + "type": "string" 86 + }, 87 + "type": { 88 + "type": "string", 89 + "knownValues": [ 90 + "feed", 91 + "list", 92 + "timeline" 93 + ] 94 + }, 95 + "value": { 96 + "type": "string" 97 + }, 98 + "pinned": { 99 + "type": "boolean" 100 + } 101 + } 102 + }, 103 + "preferences": { 104 + "type": "array", 105 + "items": { 106 + "refs": [ 107 + "#adultContentPref", 108 + "#contentLabelPref", 109 + "#savedFeedsPref", 110 + "#savedFeedsPrefV2", 111 + "#personalDetailsPref", 112 + "#feedViewPref", 113 + "#threadViewPref", 114 + "#interestsPref", 115 + "#mutedWordsPref", 116 + "#hiddenPostsPref", 117 + "#bskyAppStatePref", 118 + "#labelersPref", 119 + "#postInteractionSettingsPref" 120 + ], 121 + "type": "union" 122 + } 123 + }, 124 + "profileView": { 125 + "type": "object", 126 + "required": [ 127 + "did", 128 + "handle" 129 + ], 130 + "properties": { 131 + "did": { 132 + "type": "string", 133 + "format": "did" 134 + }, 135 + "avatar": { 136 + "type": "string", 137 + "format": "uri" 138 + }, 139 + "handle": { 140 + "type": "string", 141 + "format": "handle" 142 + }, 143 + "labels": { 144 + "type": "array", 145 + "items": { 146 + "ref": "com.atproto.label.defs#label", 147 + "type": "ref" 148 + } 149 + }, 150 + "viewer": { 151 + "ref": "#viewerState", 152 + "type": "ref" 153 + }, 154 + "createdAt": { 155 + "type": "string", 156 + "format": "datetime" 157 + }, 158 + "indexedAt": { 159 + "type": "string", 160 + "format": "datetime" 161 + }, 162 + "associated": { 163 + "ref": "#profileAssociated", 164 + "type": "ref" 165 + }, 166 + "description": { 167 + "type": "string", 168 + "maxLength": 2560, 169 + "maxGraphemes": 256 170 + }, 171 + "displayName": { 172 + "type": "string", 173 + "maxLength": 640, 174 + "maxGraphemes": 64 175 + } 176 + } 177 + }, 178 + "viewerState": { 179 + "type": "object", 180 + "properties": { 181 + "muted": { 182 + "type": "boolean" 183 + }, 184 + "blocking": { 185 + "type": "string", 186 + "format": "at-uri" 187 + }, 188 + "blockedBy": { 189 + "type": "boolean" 190 + }, 191 + "following": { 192 + "type": "string", 193 + "format": "at-uri" 194 + }, 195 + "followedBy": { 196 + "type": "string", 197 + "format": "at-uri" 198 + }, 199 + "mutedByList": { 200 + "ref": "app.bsky.graph.defs#listViewBasic", 201 + "type": "ref" 202 + }, 203 + "blockingByList": { 204 + "ref": "app.bsky.graph.defs#listViewBasic", 205 + "type": "ref" 206 + }, 207 + "knownFollowers": { 208 + "ref": "#knownFollowers", 209 + "type": "ref" 210 + } 211 + }, 212 + "description": "Metadata about the requesting account's relationship with the subject account. Only has meaningful content for authed requests." 213 + }, 214 + "feedViewPref": { 215 + "type": "object", 216 + "required": [ 217 + "feed" 218 + ], 219 + "properties": { 220 + "feed": { 221 + "type": "string", 222 + "description": "The URI of the feed, or an identifier which describes the feed." 223 + }, 224 + "hideReplies": { 225 + "type": "boolean", 226 + "description": "Hide replies in the feed." 227 + }, 228 + "hideReposts": { 229 + "type": "boolean", 230 + "description": "Hide reposts in the feed." 231 + }, 232 + "hideQuotePosts": { 233 + "type": "boolean", 234 + "description": "Hide quote posts in the feed." 235 + }, 236 + "hideRepliesByLikeCount": { 237 + "type": "integer", 238 + "description": "Hide replies in the feed if they do not have this number of likes." 239 + }, 240 + "hideRepliesByUnfollowed": { 241 + "type": "boolean", 242 + "default": true, 243 + "description": "Hide replies in the feed if they are not by followed users." 244 + } 245 + } 246 + }, 247 + "labelersPref": { 248 + "type": "object", 249 + "required": [ 250 + "labelers" 251 + ], 252 + "properties": { 253 + "labelers": { 254 + "type": "array", 255 + "items": { 256 + "ref": "#labelerPrefItem", 257 + "type": "ref" 258 + } 259 + } 260 + } 261 + }, 262 + "interestsPref": { 263 + "type": "object", 264 + "required": [ 265 + "tags" 266 + ], 267 + "properties": { 268 + "tags": { 269 + "type": "array", 270 + "items": { 271 + "type": "string", 272 + "maxLength": 640, 273 + "maxGraphemes": 64 274 + }, 275 + "maxLength": 100, 276 + "description": "A list of tags which describe the account owner's interests gathered during onboarding." 277 + } 278 + } 279 + }, 280 + "knownFollowers": { 281 + "type": "object", 282 + "required": [ 283 + "count", 284 + "followers" 285 + ], 286 + "properties": { 287 + "count": { 288 + "type": "integer" 289 + }, 290 + "followers": { 291 + "type": "array", 292 + "items": { 293 + "ref": "#profileViewBasic", 294 + "type": "ref" 295 + }, 296 + "maxLength": 5, 297 + "minLength": 0 298 + } 299 + }, 300 + "description": "The subject's followers whom you also follow" 301 + }, 302 + "mutedWordsPref": { 303 + "type": "object", 304 + "required": [ 305 + "items" 306 + ], 307 + "properties": { 308 + "items": { 309 + "type": "array", 310 + "items": { 311 + "ref": "app.bsky.actor.defs#mutedWord", 312 + "type": "ref" 313 + }, 314 + "description": "A list of words the account owner has muted." 315 + } 316 + } 317 + }, 318 + "savedFeedsPref": { 319 + "type": "object", 320 + "required": [ 321 + "pinned", 322 + "saved" 323 + ], 324 + "properties": { 325 + "saved": { 326 + "type": "array", 327 + "items": { 328 + "type": "string", 329 + "format": "at-uri" 330 + } 331 + }, 332 + "pinned": { 333 + "type": "array", 334 + "items": { 335 + "type": "string", 336 + "format": "at-uri" 337 + } 338 + }, 339 + "timelineIndex": { 340 + "type": "integer" 341 + } 342 + } 343 + }, 344 + "threadViewPref": { 345 + "type": "object", 346 + "properties": { 347 + "sort": { 348 + "type": "string", 349 + "description": "Sorting mode for threads.", 350 + "knownValues": [ 351 + "oldest", 352 + "newest", 353 + "most-likes", 354 + "random", 355 + "hotness" 356 + ] 357 + }, 358 + "prioritizeFollowedUsers": { 359 + "type": "boolean", 360 + "description": "Show followed users at the top of all replies." 361 + } 362 + } 363 + }, 364 + "hiddenPostsPref": { 365 + "type": "object", 366 + "required": [ 367 + "items" 368 + ], 369 + "properties": { 370 + "items": { 371 + "type": "array", 372 + "items": { 373 + "type": "string", 374 + "format": "at-uri" 375 + }, 376 + "description": "A list of URIs of posts the account owner has hidden." 377 + } 378 + } 379 + }, 380 + "labelerPrefItem": { 381 + "type": "object", 382 + "required": [ 383 + "did" 384 + ], 385 + "properties": { 386 + "did": { 387 + "type": "string", 388 + "format": "did" 389 + } 390 + } 391 + }, 392 + "mutedWordTarget": { 393 + "type": "string", 394 + "maxLength": 640, 395 + "knownValues": [ 396 + "content", 397 + "tag" 398 + ], 399 + "maxGraphemes": 64 400 + }, 401 + "adultContentPref": { 402 + "type": "object", 403 + "required": [ 404 + "enabled" 405 + ], 406 + "properties": { 407 + "enabled": { 408 + "type": "boolean", 409 + "default": false 410 + } 411 + } 412 + }, 413 + "bskyAppStatePref": { 414 + "type": "object", 415 + "properties": { 416 + "nuxs": { 417 + "type": "array", 418 + "items": { 419 + "ref": "app.bsky.actor.defs#nux", 420 + "type": "ref" 421 + }, 422 + "maxLength": 100, 423 + "description": "Storage for NUXs the user has encountered." 424 + }, 425 + "queuedNudges": { 426 + "type": "array", 427 + "items": { 428 + "type": "string", 429 + "maxLength": 100 430 + }, 431 + "maxLength": 1000, 432 + "description": "An array of tokens which identify nudges (modals, popups, tours, highlight dots) that should be shown to the user." 433 + }, 434 + "activeProgressGuide": { 435 + "ref": "#bskyAppProgressGuide", 436 + "type": "ref" 437 + } 438 + }, 439 + "description": "A grab bag of state that's specific to the bsky.app program. Third-party apps shouldn't use this." 440 + }, 441 + "contentLabelPref": { 442 + "type": "object", 443 + "required": [ 444 + "label", 445 + "visibility" 446 + ], 447 + "properties": { 448 + "label": { 449 + "type": "string" 450 + }, 451 + "labelerDid": { 452 + "type": "string", 453 + "format": "did", 454 + "description": "Which labeler does this preference apply to? If undefined, applies globally." 455 + }, 456 + "visibility": { 457 + "type": "string", 458 + "knownValues": [ 459 + "ignore", 460 + "show", 461 + "warn", 462 + "hide" 463 + ] 464 + } 465 + } 466 + }, 467 + "profileViewBasic": { 468 + "type": "object", 469 + "required": [ 470 + "did", 471 + "handle" 472 + ], 473 + "properties": { 474 + "did": { 475 + "type": "string", 476 + "format": "did" 477 + }, 478 + "avatar": { 479 + "type": "string", 480 + "format": "uri" 481 + }, 482 + "handle": { 483 + "type": "string", 484 + "format": "handle" 485 + }, 486 + "labels": { 487 + "type": "array", 488 + "items": { 489 + "ref": "com.atproto.label.defs#label", 490 + "type": "ref" 491 + } 492 + }, 493 + "viewer": { 494 + "ref": "#viewerState", 495 + "type": "ref" 496 + }, 497 + "createdAt": { 498 + "type": "string", 499 + "format": "datetime" 500 + }, 501 + "associated": { 502 + "ref": "#profileAssociated", 503 + "type": "ref" 504 + }, 505 + "displayName": { 506 + "type": "string", 507 + "maxLength": 640, 508 + "maxGraphemes": 64 509 + } 510 + } 511 + }, 512 + "savedFeedsPrefV2": { 513 + "type": "object", 514 + "required": [ 515 + "items" 516 + ], 517 + "properties": { 518 + "items": { 519 + "type": "array", 520 + "items": { 521 + "ref": "app.bsky.actor.defs#savedFeed", 522 + "type": "ref" 523 + } 524 + } 525 + } 526 + }, 527 + "profileAssociated": { 528 + "type": "object", 529 + "properties": { 530 + "chat": { 531 + "ref": "#profileAssociatedChat", 532 + "type": "ref" 533 + }, 534 + "lists": { 535 + "type": "integer" 536 + }, 537 + "labeler": { 538 + "type": "boolean" 539 + }, 540 + "feedgens": { 541 + "type": "integer" 542 + }, 543 + "starterPacks": { 544 + "type": "integer" 545 + } 546 + } 547 + }, 548 + "personalDetailsPref": { 549 + "type": "object", 550 + "properties": { 551 + "birthDate": { 552 + "type": "string", 553 + "format": "datetime", 554 + "description": "The birth date of account owner." 555 + } 556 + } 557 + }, 558 + "profileViewDetailed": { 559 + "type": "object", 560 + "required": [ 561 + "did", 562 + "handle" 563 + ], 564 + "properties": { 565 + "did": { 566 + "type": "string", 567 + "format": "did" 568 + }, 569 + "avatar": { 570 + "type": "string", 571 + "format": "uri" 572 + }, 573 + "banner": { 574 + "type": "string", 575 + "format": "uri" 576 + }, 577 + "handle": { 578 + "type": "string", 579 + "format": "handle" 580 + }, 581 + "labels": { 582 + "type": "array", 583 + "items": { 584 + "ref": "com.atproto.label.defs#label", 585 + "type": "ref" 586 + } 587 + }, 588 + "viewer": { 589 + "ref": "#viewerState", 590 + "type": "ref" 591 + }, 592 + "createdAt": { 593 + "type": "string", 594 + "format": "datetime" 595 + }, 596 + "indexedAt": { 597 + "type": "string", 598 + "format": "datetime" 599 + }, 600 + "associated": { 601 + "ref": "#profileAssociated", 602 + "type": "ref" 603 + }, 604 + "pinnedPost": { 605 + "ref": "com.atproto.repo.strongRef", 606 + "type": "ref" 607 + }, 608 + "postsCount": { 609 + "type": "integer" 610 + }, 611 + "description": { 612 + "type": "string", 613 + "maxLength": 2560, 614 + "maxGraphemes": 256 615 + }, 616 + "displayName": { 617 + "type": "string", 618 + "maxLength": 640, 619 + "maxGraphemes": 64 620 + }, 621 + "followsCount": { 622 + "type": "integer" 623 + }, 624 + "followersCount": { 625 + "type": "integer" 626 + }, 627 + "joinedViaStarterPack": { 628 + "ref": "app.bsky.graph.defs#starterPackViewBasic", 629 + "type": "ref" 630 + } 631 + } 632 + }, 633 + "bskyAppProgressGuide": { 634 + "type": "object", 635 + "required": [ 636 + "guide" 637 + ], 638 + "properties": { 639 + "guide": { 640 + "type": "string", 641 + "maxLength": 100 642 + } 643 + }, 644 + "description": "If set, an active progress guide. Once completed, can be set to undefined. Should have unspecced fields tracking progress." 645 + }, 646 + "profileAssociatedChat": { 647 + "type": "object", 648 + "required": [ 649 + "allowIncoming" 650 + ], 651 + "properties": { 652 + "allowIncoming": { 653 + "type": "string", 654 + "knownValues": [ 655 + "all", 656 + "none", 657 + "following" 658 + ] 659 + } 660 + } 661 + }, 662 + "postInteractionSettingsPref": { 663 + "type": "object", 664 + "required": [], 665 + "properties": { 666 + "threadgateAllowRules": { 667 + "type": "array", 668 + "items": { 669 + "refs": [ 670 + "app.bsky.feed.threadgate#mentionRule", 671 + "app.bsky.feed.threadgate#followerRule", 672 + "app.bsky.feed.threadgate#followingRule", 673 + "app.bsky.feed.threadgate#listRule" 674 + ], 675 + "type": "union" 676 + }, 677 + "maxLength": 5, 678 + "description": "Matches threadgate record. List of rules defining who can reply to this users posts. If value is an empty array, no one can reply. If value is undefined, anyone can reply." 679 + }, 680 + "postgateEmbeddingRules": { 681 + "type": "array", 682 + "items": { 683 + "refs": [ 684 + "app.bsky.feed.postgate#disableRule" 685 + ], 686 + "type": "union" 687 + }, 688 + "maxLength": 5, 689 + "description": "Matches postgate record. List of rules defining who can embed this users posts. If value is an empty array or is undefined, no particular rules apply and anyone can embed." 690 + } 691 + }, 692 + "description": "Default post interaction settings for the account. These values should be applied as default values when creating new posts. These refs should mirror the threadgate and postgate records exactly." 693 + } 694 + } 695 + }
+12 -4
lexicons/app/bsky/actor/profile.json
··· 10 10 "properties": { 11 11 "avatar": { 12 12 "type": "blob", 13 - "accept": ["image/png", "image/jpeg"], 13 + "accept": [ 14 + "image/png", 15 + "image/jpeg" 16 + ], 14 17 "maxSize": 1000000, 15 18 "description": "Small image to be displayed next to posts from account. AKA, 'profile picture'" 16 19 }, 17 20 "banner": { 18 21 "type": "blob", 19 - "accept": ["image/png", "image/jpeg"], 22 + "accept": [ 23 + "image/png", 24 + "image/jpeg" 25 + ], 20 26 "maxSize": 1000000, 21 27 "description": "Larger horizontal image to display behind profile view." 22 28 }, 23 29 "labels": { 24 - "refs": ["com.atproto.label.defs#selfLabels"], 30 + "refs": [ 31 + "com.atproto.label.defs#selfLabels" 32 + ], 25 33 "type": "union", 26 34 "description": "Self-label values, specific to the Bluesky application, on the overall account." 27 35 }, ··· 53 61 "description": "A declaration of a Bluesky account profile." 54 62 } 55 63 } 56 - } 64 + }
+24
lexicons/app/bsky/embed/defs.json
··· 1 + { 2 + "lexicon": 1, 3 + "id": "app.bsky.embed.defs", 4 + "defs": { 5 + "aspectRatio": { 6 + "type": "object", 7 + "required": [ 8 + "width", 9 + "height" 10 + ], 11 + "properties": { 12 + "width": { 13 + "type": "integer", 14 + "minimum": 1 15 + }, 16 + "height": { 17 + "type": "integer", 18 + "minimum": 1 19 + } 20 + }, 21 + "description": "width:height represents an aspect ratio. It may be approximate, and may not correspond to absolute dimensions in any given unit." 22 + } 23 + } 24 + }
+82
lexicons/app/bsky/embed/external.json
··· 1 + { 2 + "lexicon": 1, 3 + "id": "app.bsky.embed.external", 4 + "defs": { 5 + "main": { 6 + "type": "object", 7 + "required": [ 8 + "external" 9 + ], 10 + "properties": { 11 + "external": { 12 + "ref": "#external", 13 + "type": "ref" 14 + } 15 + }, 16 + "description": "A representation of some externally linked content (eg, a URL and 'card'), embedded in a Bluesky record (eg, a post)." 17 + }, 18 + "view": { 19 + "type": "object", 20 + "required": [ 21 + "external" 22 + ], 23 + "properties": { 24 + "external": { 25 + "ref": "#viewExternal", 26 + "type": "ref" 27 + } 28 + } 29 + }, 30 + "external": { 31 + "type": "object", 32 + "required": [ 33 + "uri", 34 + "title", 35 + "description" 36 + ], 37 + "properties": { 38 + "uri": { 39 + "type": "string", 40 + "format": "uri" 41 + }, 42 + "thumb": { 43 + "type": "blob", 44 + "accept": [ 45 + "image/*" 46 + ], 47 + "maxSize": 1000000 48 + }, 49 + "title": { 50 + "type": "string" 51 + }, 52 + "description": { 53 + "type": "string" 54 + } 55 + } 56 + }, 57 + "viewExternal": { 58 + "type": "object", 59 + "required": [ 60 + "uri", 61 + "title", 62 + "description" 63 + ], 64 + "properties": { 65 + "uri": { 66 + "type": "string", 67 + "format": "uri" 68 + }, 69 + "thumb": { 70 + "type": "string", 71 + "format": "uri" 72 + }, 73 + "title": { 74 + "type": "string" 75 + }, 76 + "description": { 77 + "type": "string" 78 + } 79 + } 80 + } 81 + } 82 + }
+91
lexicons/app/bsky/embed/images.json
··· 1 + { 2 + "lexicon": 1, 3 + "id": "app.bsky.embed.images", 4 + "description": "A set of images embedded in a Bluesky record (eg, a post).", 5 + "defs": { 6 + "main": { 7 + "type": "object", 8 + "required": [ 9 + "images" 10 + ], 11 + "properties": { 12 + "images": { 13 + "type": "array", 14 + "items": { 15 + "ref": "#image", 16 + "type": "ref" 17 + }, 18 + "maxLength": 4 19 + } 20 + } 21 + }, 22 + "view": { 23 + "type": "object", 24 + "required": [ 25 + "images" 26 + ], 27 + "properties": { 28 + "images": { 29 + "type": "array", 30 + "items": { 31 + "ref": "#viewImage", 32 + "type": "ref" 33 + }, 34 + "maxLength": 4 35 + } 36 + } 37 + }, 38 + "image": { 39 + "type": "object", 40 + "required": [ 41 + "image", 42 + "alt" 43 + ], 44 + "properties": { 45 + "alt": { 46 + "type": "string", 47 + "description": "Alt text description of the image, for accessibility." 48 + }, 49 + "image": { 50 + "type": "blob", 51 + "accept": [ 52 + "image/*" 53 + ], 54 + "maxSize": 1000000 55 + }, 56 + "aspectRatio": { 57 + "ref": "app.bsky.embed.defs#aspectRatio", 58 + "type": "ref" 59 + } 60 + } 61 + }, 62 + "viewImage": { 63 + "type": "object", 64 + "required": [ 65 + "thumb", 66 + "fullsize", 67 + "alt" 68 + ], 69 + "properties": { 70 + "alt": { 71 + "type": "string", 72 + "description": "Alt text description of the image, for accessibility." 73 + }, 74 + "thumb": { 75 + "type": "string", 76 + "format": "uri", 77 + "description": "Fully-qualified URL where a thumbnail of the image can be fetched. For example, CDN location provided by the App View." 78 + }, 79 + "fullsize": { 80 + "type": "string", 81 + "format": "uri", 82 + "description": "Fully-qualified URL where a large version of the image can be fetched. May or may not be the exact original blob. For example, CDN location provided by the App View." 83 + }, 84 + "aspectRatio": { 85 + "ref": "app.bsky.embed.defs#aspectRatio", 86 + "type": "ref" 87 + } 88 + } 89 + } 90 + } 91 + }
+160
lexicons/app/bsky/embed/record.json
··· 1 + { 2 + "lexicon": 1, 3 + "id": "app.bsky.embed.record", 4 + "description": "A representation of a record embedded in a Bluesky record (eg, a post). For example, a quote-post, or sharing a feed generator record.", 5 + "defs": { 6 + "main": { 7 + "type": "object", 8 + "required": [ 9 + "record" 10 + ], 11 + "properties": { 12 + "record": { 13 + "ref": "com.atproto.repo.strongRef", 14 + "type": "ref" 15 + } 16 + } 17 + }, 18 + "view": { 19 + "type": "object", 20 + "required": [ 21 + "record" 22 + ], 23 + "properties": { 24 + "record": { 25 + "refs": [ 26 + "#viewRecord", 27 + "#viewNotFound", 28 + "#viewBlocked", 29 + "#viewDetached", 30 + "app.bsky.feed.defs#generatorView", 31 + "app.bsky.graph.defs#listView", 32 + "app.bsky.labeler.defs#labelerView", 33 + "app.bsky.graph.defs#starterPackViewBasic" 34 + ], 35 + "type": "union" 36 + } 37 + } 38 + }, 39 + "viewRecord": { 40 + "type": "object", 41 + "required": [ 42 + "uri", 43 + "cid", 44 + "author", 45 + "value", 46 + "indexedAt" 47 + ], 48 + "properties": { 49 + "cid": { 50 + "type": "string", 51 + "format": "cid" 52 + }, 53 + "uri": { 54 + "type": "string", 55 + "format": "at-uri" 56 + }, 57 + "value": { 58 + "type": "unknown", 59 + "description": "The record data itself." 60 + }, 61 + "author": { 62 + "ref": "app.bsky.actor.defs#profileViewBasic", 63 + "type": "ref" 64 + }, 65 + "embeds": { 66 + "type": "array", 67 + "items": { 68 + "refs": [ 69 + "app.bsky.embed.images#view", 70 + "app.bsky.embed.video#view", 71 + "app.bsky.embed.external#view", 72 + "app.bsky.embed.record#view", 73 + "app.bsky.embed.recordWithMedia#view" 74 + ], 75 + "type": "union" 76 + } 77 + }, 78 + "labels": { 79 + "type": "array", 80 + "items": { 81 + "ref": "com.atproto.label.defs#label", 82 + "type": "ref" 83 + } 84 + }, 85 + "indexedAt": { 86 + "type": "string", 87 + "format": "datetime" 88 + }, 89 + "likeCount": { 90 + "type": "integer" 91 + }, 92 + "quoteCount": { 93 + "type": "integer" 94 + }, 95 + "replyCount": { 96 + "type": "integer" 97 + }, 98 + "repostCount": { 99 + "type": "integer" 100 + } 101 + } 102 + }, 103 + "viewBlocked": { 104 + "type": "object", 105 + "required": [ 106 + "uri", 107 + "blocked", 108 + "author" 109 + ], 110 + "properties": { 111 + "uri": { 112 + "type": "string", 113 + "format": "at-uri" 114 + }, 115 + "author": { 116 + "ref": "app.bsky.feed.defs#blockedAuthor", 117 + "type": "ref" 118 + }, 119 + "blocked": { 120 + "type": "boolean", 121 + "const": true 122 + } 123 + } 124 + }, 125 + "viewDetached": { 126 + "type": "object", 127 + "required": [ 128 + "uri", 129 + "detached" 130 + ], 131 + "properties": { 132 + "uri": { 133 + "type": "string", 134 + "format": "at-uri" 135 + }, 136 + "detached": { 137 + "type": "boolean", 138 + "const": true 139 + } 140 + } 141 + }, 142 + "viewNotFound": { 143 + "type": "object", 144 + "required": [ 145 + "uri", 146 + "notFound" 147 + ], 148 + "properties": { 149 + "uri": { 150 + "type": "string", 151 + "format": "at-uri" 152 + }, 153 + "notFound": { 154 + "type": "boolean", 155 + "const": true 156 + } 157 + } 158 + } 159 + } 160 + }
+49
lexicons/app/bsky/embed/recordWithMedia.json
··· 1 + { 2 + "lexicon": 1, 3 + "id": "app.bsky.embed.recordWithMedia", 4 + "description": "A representation of a record embedded in a Bluesky record (eg, a post), alongside other compatible embeds. For example, a quote post and image, or a quote post and external URL card.", 5 + "defs": { 6 + "main": { 7 + "type": "object", 8 + "required": [ 9 + "record", 10 + "media" 11 + ], 12 + "properties": { 13 + "media": { 14 + "refs": [ 15 + "app.bsky.embed.images", 16 + "app.bsky.embed.video", 17 + "app.bsky.embed.external" 18 + ], 19 + "type": "union" 20 + }, 21 + "record": { 22 + "ref": "app.bsky.embed.record", 23 + "type": "ref" 24 + } 25 + } 26 + }, 27 + "view": { 28 + "type": "object", 29 + "required": [ 30 + "record", 31 + "media" 32 + ], 33 + "properties": { 34 + "media": { 35 + "refs": [ 36 + "app.bsky.embed.images#view", 37 + "app.bsky.embed.video#view", 38 + "app.bsky.embed.external#view" 39 + ], 40 + "type": "union" 41 + }, 42 + "record": { 43 + "ref": "app.bsky.embed.record#view", 44 + "type": "ref" 45 + } 46 + } 47 + } 48 + } 49 + }
+90
lexicons/app/bsky/embed/video.json
··· 1 + { 2 + "lexicon": 1, 3 + "id": "app.bsky.embed.video", 4 + "description": "A video embedded in a Bluesky record (eg, a post).", 5 + "defs": { 6 + "main": { 7 + "type": "object", 8 + "required": [ 9 + "video" 10 + ], 11 + "properties": { 12 + "alt": { 13 + "type": "string", 14 + "maxLength": 10000, 15 + "description": "Alt text description of the video, for accessibility.", 16 + "maxGraphemes": 1000 17 + }, 18 + "video": { 19 + "type": "blob", 20 + "accept": [ 21 + "video/mp4" 22 + ], 23 + "maxSize": 50000000 24 + }, 25 + "captions": { 26 + "type": "array", 27 + "items": { 28 + "ref": "#caption", 29 + "type": "ref" 30 + }, 31 + "maxLength": 20 32 + }, 33 + "aspectRatio": { 34 + "ref": "app.bsky.embed.defs#aspectRatio", 35 + "type": "ref" 36 + } 37 + } 38 + }, 39 + "view": { 40 + "type": "object", 41 + "required": [ 42 + "cid", 43 + "playlist" 44 + ], 45 + "properties": { 46 + "alt": { 47 + "type": "string", 48 + "maxLength": 10000, 49 + "maxGraphemes": 1000 50 + }, 51 + "cid": { 52 + "type": "string", 53 + "format": "cid" 54 + }, 55 + "playlist": { 56 + "type": "string", 57 + "format": "uri" 58 + }, 59 + "thumbnail": { 60 + "type": "string", 61 + "format": "uri" 62 + }, 63 + "aspectRatio": { 64 + "ref": "app.bsky.embed.defs#aspectRatio", 65 + "type": "ref" 66 + } 67 + } 68 + }, 69 + "caption": { 70 + "type": "object", 71 + "required": [ 72 + "lang", 73 + "file" 74 + ], 75 + "properties": { 76 + "file": { 77 + "type": "blob", 78 + "accept": [ 79 + "text/vtt" 80 + ], 81 + "maxSize": 20000 82 + }, 83 + "lang": { 84 + "type": "string", 85 + "format": "language" 86 + } 87 + } 88 + } 89 + } 90 + }
+515
lexicons/app/bsky/feed/defs.json
··· 1 + { 2 + "lexicon": 1, 3 + "id": "app.bsky.feed.defs", 4 + "defs": { 5 + "postView": { 6 + "type": "object", 7 + "required": [ 8 + "uri", 9 + "cid", 10 + "author", 11 + "record", 12 + "indexedAt" 13 + ], 14 + "properties": { 15 + "cid": { 16 + "type": "string", 17 + "format": "cid" 18 + }, 19 + "uri": { 20 + "type": "string", 21 + "format": "at-uri" 22 + }, 23 + "embed": { 24 + "refs": [ 25 + "app.bsky.embed.images#view", 26 + "app.bsky.embed.video#view", 27 + "app.bsky.embed.external#view", 28 + "app.bsky.embed.record#view", 29 + "app.bsky.embed.recordWithMedia#view" 30 + ], 31 + "type": "union" 32 + }, 33 + "author": { 34 + "ref": "app.bsky.actor.defs#profileViewBasic", 35 + "type": "ref" 36 + }, 37 + "labels": { 38 + "type": "array", 39 + "items": { 40 + "ref": "com.atproto.label.defs#label", 41 + "type": "ref" 42 + } 43 + }, 44 + "record": { 45 + "type": "unknown" 46 + }, 47 + "viewer": { 48 + "ref": "#viewerState", 49 + "type": "ref" 50 + }, 51 + "indexedAt": { 52 + "type": "string", 53 + "format": "datetime" 54 + }, 55 + "likeCount": { 56 + "type": "integer" 57 + }, 58 + "quoteCount": { 59 + "type": "integer" 60 + }, 61 + "replyCount": { 62 + "type": "integer" 63 + }, 64 + "threadgate": { 65 + "ref": "#threadgateView", 66 + "type": "ref" 67 + }, 68 + "repostCount": { 69 + "type": "integer" 70 + } 71 + } 72 + }, 73 + "replyRef": { 74 + "type": "object", 75 + "required": [ 76 + "root", 77 + "parent" 78 + ], 79 + "properties": { 80 + "root": { 81 + "refs": [ 82 + "#postView", 83 + "#notFoundPost", 84 + "#blockedPost" 85 + ], 86 + "type": "union" 87 + }, 88 + "parent": { 89 + "refs": [ 90 + "#postView", 91 + "#notFoundPost", 92 + "#blockedPost" 93 + ], 94 + "type": "union" 95 + }, 96 + "grandparentAuthor": { 97 + "ref": "app.bsky.actor.defs#profileViewBasic", 98 + "type": "ref", 99 + "description": "When parent is a reply to another post, this is the author of that post." 100 + } 101 + } 102 + }, 103 + "reasonPin": { 104 + "type": "object", 105 + "properties": {} 106 + }, 107 + "blockedPost": { 108 + "type": "object", 109 + "required": [ 110 + "uri", 111 + "blocked", 112 + "author" 113 + ], 114 + "properties": { 115 + "uri": { 116 + "type": "string", 117 + "format": "at-uri" 118 + }, 119 + "author": { 120 + "ref": "#blockedAuthor", 121 + "type": "ref" 122 + }, 123 + "blocked": { 124 + "type": "boolean", 125 + "const": true 126 + } 127 + } 128 + }, 129 + "interaction": { 130 + "type": "object", 131 + "properties": { 132 + "item": { 133 + "type": "string", 134 + "format": "at-uri" 135 + }, 136 + "event": { 137 + "type": "string", 138 + "knownValues": [ 139 + "app.bsky.feed.defs#requestLess", 140 + "app.bsky.feed.defs#requestMore", 141 + "app.bsky.feed.defs#clickthroughItem", 142 + "app.bsky.feed.defs#clickthroughAuthor", 143 + "app.bsky.feed.defs#clickthroughReposter", 144 + "app.bsky.feed.defs#clickthroughEmbed", 145 + "app.bsky.feed.defs#interactionSeen", 146 + "app.bsky.feed.defs#interactionLike", 147 + "app.bsky.feed.defs#interactionRepost", 148 + "app.bsky.feed.defs#interactionReply", 149 + "app.bsky.feed.defs#interactionQuote", 150 + "app.bsky.feed.defs#interactionShare" 151 + ] 152 + }, 153 + "feedContext": { 154 + "type": "string", 155 + "maxLength": 2000, 156 + "description": "Context on a feed item that was originally supplied by the feed generator on getFeedSkeleton." 157 + } 158 + } 159 + }, 160 + "requestLess": { 161 + "type": "token", 162 + "description": "Request that less content like the given feed item be shown in the feed" 163 + }, 164 + "requestMore": { 165 + "type": "token", 166 + "description": "Request that more content like the given feed item be shown in the feed" 167 + }, 168 + "viewerState": { 169 + "type": "object", 170 + "properties": { 171 + "like": { 172 + "type": "string", 173 + "format": "at-uri" 174 + }, 175 + "pinned": { 176 + "type": "boolean" 177 + }, 178 + "repost": { 179 + "type": "string", 180 + "format": "at-uri" 181 + }, 182 + "threadMuted": { 183 + "type": "boolean" 184 + }, 185 + "replyDisabled": { 186 + "type": "boolean" 187 + }, 188 + "embeddingDisabled": { 189 + "type": "boolean" 190 + } 191 + }, 192 + "description": "Metadata about the requesting account's relationship with the subject content. Only has meaningful content for authed requests." 193 + }, 194 + "feedViewPost": { 195 + "type": "object", 196 + "required": [ 197 + "post" 198 + ], 199 + "properties": { 200 + "post": { 201 + "ref": "#postView", 202 + "type": "ref" 203 + }, 204 + "reply": { 205 + "ref": "#replyRef", 206 + "type": "ref" 207 + }, 208 + "reason": { 209 + "refs": [ 210 + "#reasonRepost", 211 + "#reasonPin" 212 + ], 213 + "type": "union" 214 + }, 215 + "feedContext": { 216 + "type": "string", 217 + "maxLength": 2000, 218 + "description": "Context provided by feed generator that may be passed back alongside interactions." 219 + } 220 + } 221 + }, 222 + "notFoundPost": { 223 + "type": "object", 224 + "required": [ 225 + "uri", 226 + "notFound" 227 + ], 228 + "properties": { 229 + "uri": { 230 + "type": "string", 231 + "format": "at-uri" 232 + }, 233 + "notFound": { 234 + "type": "boolean", 235 + "const": true 236 + } 237 + } 238 + }, 239 + "reasonRepost": { 240 + "type": "object", 241 + "required": [ 242 + "by", 243 + "indexedAt" 244 + ], 245 + "properties": { 246 + "by": { 247 + "ref": "app.bsky.actor.defs#profileViewBasic", 248 + "type": "ref" 249 + }, 250 + "indexedAt": { 251 + "type": "string", 252 + "format": "datetime" 253 + } 254 + } 255 + }, 256 + "blockedAuthor": { 257 + "type": "object", 258 + "required": [ 259 + "did" 260 + ], 261 + "properties": { 262 + "did": { 263 + "type": "string", 264 + "format": "did" 265 + }, 266 + "viewer": { 267 + "ref": "app.bsky.actor.defs#viewerState", 268 + "type": "ref" 269 + } 270 + } 271 + }, 272 + "generatorView": { 273 + "type": "object", 274 + "required": [ 275 + "uri", 276 + "cid", 277 + "did", 278 + "creator", 279 + "displayName", 280 + "indexedAt" 281 + ], 282 + "properties": { 283 + "cid": { 284 + "type": "string", 285 + "format": "cid" 286 + }, 287 + "did": { 288 + "type": "string", 289 + "format": "did" 290 + }, 291 + "uri": { 292 + "type": "string", 293 + "format": "at-uri" 294 + }, 295 + "avatar": { 296 + "type": "string", 297 + "format": "uri" 298 + }, 299 + "labels": { 300 + "type": "array", 301 + "items": { 302 + "ref": "com.atproto.label.defs#label", 303 + "type": "ref" 304 + } 305 + }, 306 + "viewer": { 307 + "ref": "#generatorViewerState", 308 + "type": "ref" 309 + }, 310 + "creator": { 311 + "ref": "app.bsky.actor.defs#profileView", 312 + "type": "ref" 313 + }, 314 + "indexedAt": { 315 + "type": "string", 316 + "format": "datetime" 317 + }, 318 + "likeCount": { 319 + "type": "integer", 320 + "minimum": 0 321 + }, 322 + "contentMode": { 323 + "type": "string", 324 + "knownValues": [ 325 + "app.bsky.feed.defs#contentModeUnspecified", 326 + "app.bsky.feed.defs#contentModeVideo" 327 + ] 328 + }, 329 + "description": { 330 + "type": "string", 331 + "maxLength": 3000, 332 + "maxGraphemes": 300 333 + }, 334 + "displayName": { 335 + "type": "string" 336 + }, 337 + "descriptionFacets": { 338 + "type": "array", 339 + "items": { 340 + "ref": "app.bsky.richtext.facet", 341 + "type": "ref" 342 + } 343 + }, 344 + "acceptsInteractions": { 345 + "type": "boolean" 346 + } 347 + } 348 + }, 349 + "threadContext": { 350 + "type": "object", 351 + "properties": { 352 + "rootAuthorLike": { 353 + "type": "string", 354 + "format": "at-uri" 355 + } 356 + }, 357 + "description": "Metadata about this post within the context of the thread it is in." 358 + }, 359 + "threadViewPost": { 360 + "type": "object", 361 + "required": [ 362 + "post" 363 + ], 364 + "properties": { 365 + "post": { 366 + "ref": "#postView", 367 + "type": "ref" 368 + }, 369 + "parent": { 370 + "refs": [ 371 + "#threadViewPost", 372 + "#notFoundPost", 373 + "#blockedPost" 374 + ], 375 + "type": "union" 376 + }, 377 + "replies": { 378 + "type": "array", 379 + "items": { 380 + "refs": [ 381 + "#threadViewPost", 382 + "#notFoundPost", 383 + "#blockedPost" 384 + ], 385 + "type": "union" 386 + } 387 + }, 388 + "threadContext": { 389 + "ref": "#threadContext", 390 + "type": "ref" 391 + } 392 + } 393 + }, 394 + "threadgateView": { 395 + "type": "object", 396 + "properties": { 397 + "cid": { 398 + "type": "string", 399 + "format": "cid" 400 + }, 401 + "uri": { 402 + "type": "string", 403 + "format": "at-uri" 404 + }, 405 + "lists": { 406 + "type": "array", 407 + "items": { 408 + "ref": "app.bsky.graph.defs#listViewBasic", 409 + "type": "ref" 410 + } 411 + }, 412 + "record": { 413 + "type": "unknown" 414 + } 415 + } 416 + }, 417 + "interactionLike": { 418 + "type": "token", 419 + "description": "User liked the feed item" 420 + }, 421 + "interactionSeen": { 422 + "type": "token", 423 + "description": "Feed item was seen by user" 424 + }, 425 + "clickthroughItem": { 426 + "type": "token", 427 + "description": "User clicked through to the feed item" 428 + }, 429 + "contentModeVideo": { 430 + "type": "token", 431 + "description": "Declares the feed generator returns posts containing app.bsky.embed.video embeds." 432 + }, 433 + "interactionQuote": { 434 + "type": "token", 435 + "description": "User quoted the feed item" 436 + }, 437 + "interactionReply": { 438 + "type": "token", 439 + "description": "User replied to the feed item" 440 + }, 441 + "interactionShare": { 442 + "type": "token", 443 + "description": "User shared the feed item" 444 + }, 445 + "skeletonFeedPost": { 446 + "type": "object", 447 + "required": [ 448 + "post" 449 + ], 450 + "properties": { 451 + "post": { 452 + "type": "string", 453 + "format": "at-uri" 454 + }, 455 + "reason": { 456 + "refs": [ 457 + "#skeletonReasonRepost", 458 + "#skeletonReasonPin" 459 + ], 460 + "type": "union" 461 + }, 462 + "feedContext": { 463 + "type": "string", 464 + "maxLength": 2000, 465 + "description": "Context that will be passed through to client and may be passed to feed generator back alongside interactions." 466 + } 467 + } 468 + }, 469 + "clickthroughEmbed": { 470 + "type": "token", 471 + "description": "User clicked through to the embedded content of the feed item" 472 + }, 473 + "interactionRepost": { 474 + "type": "token", 475 + "description": "User reposted the feed item" 476 + }, 477 + "skeletonReasonPin": { 478 + "type": "object", 479 + "properties": {} 480 + }, 481 + "clickthroughAuthor": { 482 + "type": "token", 483 + "description": "User clicked through to the author of the feed item" 484 + }, 485 + "clickthroughReposter": { 486 + "type": "token", 487 + "description": "User clicked through to the reposter of the feed item" 488 + }, 489 + "generatorViewerState": { 490 + "type": "object", 491 + "properties": { 492 + "like": { 493 + "type": "string", 494 + "format": "at-uri" 495 + } 496 + } 497 + }, 498 + "skeletonReasonRepost": { 499 + "type": "object", 500 + "required": [ 501 + "repost" 502 + ], 503 + "properties": { 504 + "repost": { 505 + "type": "string", 506 + "format": "at-uri" 507 + } 508 + } 509 + }, 510 + "contentModeUnspecified": { 511 + "type": "token", 512 + "description": "Declares the feed generator returns any types of posts." 513 + } 514 + } 515 + }
+54
lexicons/app/bsky/feed/postgate.json
··· 1 + { 2 + "lexicon": 1, 3 + "id": "app.bsky.feed.postgate", 4 + "defs": { 5 + "main": { 6 + "key": "tid", 7 + "type": "record", 8 + "record": { 9 + "type": "object", 10 + "required": [ 11 + "post", 12 + "createdAt" 13 + ], 14 + "properties": { 15 + "post": { 16 + "type": "string", 17 + "format": "at-uri", 18 + "description": "Reference (AT-URI) to the post record." 19 + }, 20 + "createdAt": { 21 + "type": "string", 22 + "format": "datetime" 23 + }, 24 + "embeddingRules": { 25 + "type": "array", 26 + "items": { 27 + "refs": [ 28 + "#disableRule" 29 + ], 30 + "type": "union" 31 + }, 32 + "maxLength": 5, 33 + "description": "List of rules defining who can embed this post. If value is an empty array or is undefined, no particular rules apply and anyone can embed." 34 + }, 35 + "detachedEmbeddingUris": { 36 + "type": "array", 37 + "items": { 38 + "type": "string", 39 + "format": "at-uri" 40 + }, 41 + "maxLength": 50, 42 + "description": "List of AT-URIs embedding this post that the author has detached from." 43 + } 44 + } 45 + }, 46 + "description": "Record defining interaction rules for a post. The record key (rkey) of the postgate record must match the record key of the post, and that record must be in the same repository." 47 + }, 48 + "disableRule": { 49 + "type": "object", 50 + "properties": {}, 51 + "description": "Disables embedding of this post." 52 + } 53 + } 54 + }
+80
lexicons/app/bsky/feed/threadgate.json
··· 1 + { 2 + "lexicon": 1, 3 + "id": "app.bsky.feed.threadgate", 4 + "defs": { 5 + "main": { 6 + "key": "tid", 7 + "type": "record", 8 + "record": { 9 + "type": "object", 10 + "required": [ 11 + "post", 12 + "createdAt" 13 + ], 14 + "properties": { 15 + "post": { 16 + "type": "string", 17 + "format": "at-uri", 18 + "description": "Reference (AT-URI) to the post record." 19 + }, 20 + "allow": { 21 + "type": "array", 22 + "items": { 23 + "refs": [ 24 + "#mentionRule", 25 + "#followerRule", 26 + "#followingRule", 27 + "#listRule" 28 + ], 29 + "type": "union" 30 + }, 31 + "maxLength": 5, 32 + "description": "List of rules defining who can reply to this post. If value is an empty array, no one can reply. If value is undefined, anyone can reply." 33 + }, 34 + "createdAt": { 35 + "type": "string", 36 + "format": "datetime" 37 + }, 38 + "hiddenReplies": { 39 + "type": "array", 40 + "items": { 41 + "type": "string", 42 + "format": "at-uri" 43 + }, 44 + "maxLength": 50, 45 + "description": "List of hidden reply URIs." 46 + } 47 + } 48 + }, 49 + "description": "Record defining interaction gating rules for a thread (aka, reply controls). The record key (rkey) of the threadgate record must match the record key of the thread's root post, and that record must be in the same repository." 50 + }, 51 + "listRule": { 52 + "type": "object", 53 + "required": [ 54 + "list" 55 + ], 56 + "properties": { 57 + "list": { 58 + "type": "string", 59 + "format": "at-uri" 60 + } 61 + }, 62 + "description": "Allow replies from actors on a list." 63 + }, 64 + "mentionRule": { 65 + "type": "object", 66 + "properties": {}, 67 + "description": "Allow replies from actors mentioned in your post." 68 + }, 69 + "followerRule": { 70 + "type": "object", 71 + "properties": {}, 72 + "description": "Allow replies from actors who follow you." 73 + }, 74 + "followingRule": { 75 + "type": "object", 76 + "properties": {}, 77 + "description": "Allow replies from actors you follow." 78 + } 79 + } 80 + }
+332
lexicons/app/bsky/graph/defs.json
··· 1 + { 2 + "lexicon": 1, 3 + "id": "app.bsky.graph.defs", 4 + "defs": { 5 + "modlist": { 6 + "type": "token", 7 + "description": "A list of actors to apply an aggregate moderation action (mute/block) on." 8 + }, 9 + "listView": { 10 + "type": "object", 11 + "required": [ 12 + "uri", 13 + "cid", 14 + "creator", 15 + "name", 16 + "purpose", 17 + "indexedAt" 18 + ], 19 + "properties": { 20 + "cid": { 21 + "type": "string", 22 + "format": "cid" 23 + }, 24 + "uri": { 25 + "type": "string", 26 + "format": "at-uri" 27 + }, 28 + "name": { 29 + "type": "string", 30 + "maxLength": 64, 31 + "minLength": 1 32 + }, 33 + "avatar": { 34 + "type": "string", 35 + "format": "uri" 36 + }, 37 + "labels": { 38 + "type": "array", 39 + "items": { 40 + "ref": "com.atproto.label.defs#label", 41 + "type": "ref" 42 + } 43 + }, 44 + "viewer": { 45 + "ref": "#listViewerState", 46 + "type": "ref" 47 + }, 48 + "creator": { 49 + "ref": "app.bsky.actor.defs#profileView", 50 + "type": "ref" 51 + }, 52 + "purpose": { 53 + "ref": "#listPurpose", 54 + "type": "ref" 55 + }, 56 + "indexedAt": { 57 + "type": "string", 58 + "format": "datetime" 59 + }, 60 + "description": { 61 + "type": "string", 62 + "maxLength": 3000, 63 + "maxGraphemes": 300 64 + }, 65 + "listItemCount": { 66 + "type": "integer", 67 + "minimum": 0 68 + }, 69 + "descriptionFacets": { 70 + "type": "array", 71 + "items": { 72 + "ref": "app.bsky.richtext.facet", 73 + "type": "ref" 74 + } 75 + } 76 + } 77 + }, 78 + "curatelist": { 79 + "type": "token", 80 + "description": "A list of actors used for curation purposes such as list feeds or interaction gating." 81 + }, 82 + "listPurpose": { 83 + "type": "string", 84 + "knownValues": [ 85 + "app.bsky.graph.defs#modlist", 86 + "app.bsky.graph.defs#curatelist", 87 + "app.bsky.graph.defs#referencelist" 88 + ] 89 + }, 90 + "listItemView": { 91 + "type": "object", 92 + "required": [ 93 + "uri", 94 + "subject" 95 + ], 96 + "properties": { 97 + "uri": { 98 + "type": "string", 99 + "format": "at-uri" 100 + }, 101 + "subject": { 102 + "ref": "app.bsky.actor.defs#profileView", 103 + "type": "ref" 104 + } 105 + } 106 + }, 107 + "relationship": { 108 + "type": "object", 109 + "required": [ 110 + "did" 111 + ], 112 + "properties": { 113 + "did": { 114 + "type": "string", 115 + "format": "did" 116 + }, 117 + "following": { 118 + "type": "string", 119 + "format": "at-uri", 120 + "description": "if the actor follows this DID, this is the AT-URI of the follow record" 121 + }, 122 + "followedBy": { 123 + "type": "string", 124 + "format": "at-uri", 125 + "description": "if the actor is followed by this DID, contains the AT-URI of the follow record" 126 + } 127 + }, 128 + "description": "lists the bi-directional graph relationships between one actor (not indicated in the object), and the target actors (the DID included in the object)" 129 + }, 130 + "listViewBasic": { 131 + "type": "object", 132 + "required": [ 133 + "uri", 134 + "cid", 135 + "name", 136 + "purpose" 137 + ], 138 + "properties": { 139 + "cid": { 140 + "type": "string", 141 + "format": "cid" 142 + }, 143 + "uri": { 144 + "type": "string", 145 + "format": "at-uri" 146 + }, 147 + "name": { 148 + "type": "string", 149 + "maxLength": 64, 150 + "minLength": 1 151 + }, 152 + "avatar": { 153 + "type": "string", 154 + "format": "uri" 155 + }, 156 + "labels": { 157 + "type": "array", 158 + "items": { 159 + "ref": "com.atproto.label.defs#label", 160 + "type": "ref" 161 + } 162 + }, 163 + "viewer": { 164 + "ref": "#listViewerState", 165 + "type": "ref" 166 + }, 167 + "purpose": { 168 + "ref": "#listPurpose", 169 + "type": "ref" 170 + }, 171 + "indexedAt": { 172 + "type": "string", 173 + "format": "datetime" 174 + }, 175 + "listItemCount": { 176 + "type": "integer", 177 + "minimum": 0 178 + } 179 + } 180 + }, 181 + "notFoundActor": { 182 + "type": "object", 183 + "required": [ 184 + "actor", 185 + "notFound" 186 + ], 187 + "properties": { 188 + "actor": { 189 + "type": "string", 190 + "format": "at-identifier" 191 + }, 192 + "notFound": { 193 + "type": "boolean", 194 + "const": true 195 + } 196 + }, 197 + "description": "indicates that a handle or DID could not be resolved" 198 + }, 199 + "referencelist": { 200 + "type": "token", 201 + "description": "A list of actors used for only for reference purposes such as within a starter pack." 202 + }, 203 + "listViewerState": { 204 + "type": "object", 205 + "properties": { 206 + "muted": { 207 + "type": "boolean" 208 + }, 209 + "blocked": { 210 + "type": "string", 211 + "format": "at-uri" 212 + } 213 + } 214 + }, 215 + "starterPackView": { 216 + "type": "object", 217 + "required": [ 218 + "uri", 219 + "cid", 220 + "record", 221 + "creator", 222 + "indexedAt" 223 + ], 224 + "properties": { 225 + "cid": { 226 + "type": "string", 227 + "format": "cid" 228 + }, 229 + "uri": { 230 + "type": "string", 231 + "format": "at-uri" 232 + }, 233 + "list": { 234 + "ref": "#listViewBasic", 235 + "type": "ref" 236 + }, 237 + "feeds": { 238 + "type": "array", 239 + "items": { 240 + "ref": "app.bsky.feed.defs#generatorView", 241 + "type": "ref" 242 + }, 243 + "maxLength": 3 244 + }, 245 + "labels": { 246 + "type": "array", 247 + "items": { 248 + "ref": "com.atproto.label.defs#label", 249 + "type": "ref" 250 + } 251 + }, 252 + "record": { 253 + "type": "unknown" 254 + }, 255 + "creator": { 256 + "ref": "app.bsky.actor.defs#profileViewBasic", 257 + "type": "ref" 258 + }, 259 + "indexedAt": { 260 + "type": "string", 261 + "format": "datetime" 262 + }, 263 + "joinedWeekCount": { 264 + "type": "integer", 265 + "minimum": 0 266 + }, 267 + "listItemsSample": { 268 + "type": "array", 269 + "items": { 270 + "ref": "#listItemView", 271 + "type": "ref" 272 + }, 273 + "maxLength": 12 274 + }, 275 + "joinedAllTimeCount": { 276 + "type": "integer", 277 + "minimum": 0 278 + } 279 + } 280 + }, 281 + "starterPackViewBasic": { 282 + "type": "object", 283 + "required": [ 284 + "uri", 285 + "cid", 286 + "record", 287 + "creator", 288 + "indexedAt" 289 + ], 290 + "properties": { 291 + "cid": { 292 + "type": "string", 293 + "format": "cid" 294 + }, 295 + "uri": { 296 + "type": "string", 297 + "format": "at-uri" 298 + }, 299 + "labels": { 300 + "type": "array", 301 + "items": { 302 + "ref": "com.atproto.label.defs#label", 303 + "type": "ref" 304 + } 305 + }, 306 + "record": { 307 + "type": "unknown" 308 + }, 309 + "creator": { 310 + "ref": "app.bsky.actor.defs#profileViewBasic", 311 + "type": "ref" 312 + }, 313 + "indexedAt": { 314 + "type": "string", 315 + "format": "datetime" 316 + }, 317 + "listItemCount": { 318 + "type": "integer", 319 + "minimum": 0 320 + }, 321 + "joinedWeekCount": { 322 + "type": "integer", 323 + "minimum": 0 324 + }, 325 + "joinedAllTimeCount": { 326 + "type": "integer", 327 + "minimum": 0 328 + } 329 + } 330 + } 331 + } 332 + }
+28
lexicons/app/bsky/graph/follow.json
··· 1 + { 2 + "lexicon": 1, 3 + "id": "app.bsky.graph.follow", 4 + "defs": { 5 + "main": { 6 + "key": "tid", 7 + "type": "record", 8 + "record": { 9 + "type": "object", 10 + "required": [ 11 + "subject", 12 + "createdAt" 13 + ], 14 + "properties": { 15 + "subject": { 16 + "type": "string", 17 + "format": "did" 18 + }, 19 + "createdAt": { 20 + "type": "string", 21 + "format": "datetime" 22 + } 23 + } 24 + }, 25 + "description": "Record declaring a social 'follow' relationship of another account. Duplicate follows will be ignored by the AppView." 26 + } 27 + } 28 + }
+128
lexicons/app/bsky/labeler/defs.json
··· 1 + { 2 + "lexicon": 1, 3 + "id": "app.bsky.labeler.defs", 4 + "defs": { 5 + "labelerView": { 6 + "type": "object", 7 + "required": [ 8 + "uri", 9 + "cid", 10 + "creator", 11 + "indexedAt" 12 + ], 13 + "properties": { 14 + "cid": { 15 + "type": "string", 16 + "format": "cid" 17 + }, 18 + "uri": { 19 + "type": "string", 20 + "format": "at-uri" 21 + }, 22 + "labels": { 23 + "type": "array", 24 + "items": { 25 + "ref": "com.atproto.label.defs#label", 26 + "type": "ref" 27 + } 28 + }, 29 + "viewer": { 30 + "ref": "#labelerViewerState", 31 + "type": "ref" 32 + }, 33 + "creator": { 34 + "ref": "app.bsky.actor.defs#profileView", 35 + "type": "ref" 36 + }, 37 + "indexedAt": { 38 + "type": "string", 39 + "format": "datetime" 40 + }, 41 + "likeCount": { 42 + "type": "integer", 43 + "minimum": 0 44 + } 45 + } 46 + }, 47 + "labelerPolicies": { 48 + "type": "object", 49 + "required": [ 50 + "labelValues" 51 + ], 52 + "properties": { 53 + "labelValues": { 54 + "type": "array", 55 + "items": { 56 + "ref": "com.atproto.label.defs#labelValue", 57 + "type": "ref" 58 + }, 59 + "description": "The label values which this labeler publishes. May include global or custom labels." 60 + }, 61 + "labelValueDefinitions": { 62 + "type": "array", 63 + "items": { 64 + "ref": "com.atproto.label.defs#labelValueDefinition", 65 + "type": "ref" 66 + }, 67 + "description": "Label values created by this labeler and scoped exclusively to it. Labels defined here will override global label definitions for this labeler." 68 + } 69 + } 70 + }, 71 + "labelerViewerState": { 72 + "type": "object", 73 + "properties": { 74 + "like": { 75 + "type": "string", 76 + "format": "at-uri" 77 + } 78 + } 79 + }, 80 + "labelerViewDetailed": { 81 + "type": "object", 82 + "required": [ 83 + "uri", 84 + "cid", 85 + "creator", 86 + "policies", 87 + "indexedAt" 88 + ], 89 + "properties": { 90 + "cid": { 91 + "type": "string", 92 + "format": "cid" 93 + }, 94 + "uri": { 95 + "type": "string", 96 + "format": "at-uri" 97 + }, 98 + "labels": { 99 + "type": "array", 100 + "items": { 101 + "ref": "com.atproto.label.defs#label", 102 + "type": "ref" 103 + } 104 + }, 105 + "viewer": { 106 + "ref": "#labelerViewerState", 107 + "type": "ref" 108 + }, 109 + "creator": { 110 + "ref": "app.bsky.actor.defs#profileView", 111 + "type": "ref" 112 + }, 113 + "policies": { 114 + "ref": "app.bsky.labeler.defs#labelerPolicies", 115 + "type": "ref" 116 + }, 117 + "indexedAt": { 118 + "type": "string", 119 + "format": "datetime" 120 + }, 121 + "likeCount": { 122 + "type": "integer", 123 + "minimum": 0 124 + } 125 + } 126 + } 127 + } 128 + }
+89
lexicons/app/bsky/richtext/facet.json
··· 1 + { 2 + "lexicon": 1, 3 + "id": "app.bsky.richtext.facet", 4 + "defs": { 5 + "tag": { 6 + "type": "object", 7 + "required": [ 8 + "tag" 9 + ], 10 + "properties": { 11 + "tag": { 12 + "type": "string", 13 + "maxLength": 640, 14 + "maxGraphemes": 64 15 + } 16 + }, 17 + "description": "Facet feature for a hashtag. The text usually includes a '#' prefix, but the facet reference should not (except in the case of 'double hash tags')." 18 + }, 19 + "link": { 20 + "type": "object", 21 + "required": [ 22 + "uri" 23 + ], 24 + "properties": { 25 + "uri": { 26 + "type": "string", 27 + "format": "uri" 28 + } 29 + }, 30 + "description": "Facet feature for a URL. The text URL may have been simplified or truncated, but the facet reference should be a complete URL." 31 + }, 32 + "main": { 33 + "type": "object", 34 + "required": [ 35 + "index", 36 + "features" 37 + ], 38 + "properties": { 39 + "index": { 40 + "ref": "#byteSlice", 41 + "type": "ref" 42 + }, 43 + "features": { 44 + "type": "array", 45 + "items": { 46 + "refs": [ 47 + "#mention", 48 + "#link", 49 + "#tag" 50 + ], 51 + "type": "union" 52 + } 53 + } 54 + }, 55 + "description": "Annotation of a sub-string within rich text." 56 + }, 57 + "mention": { 58 + "type": "object", 59 + "required": [ 60 + "did" 61 + ], 62 + "properties": { 63 + "did": { 64 + "type": "string", 65 + "format": "did" 66 + } 67 + }, 68 + "description": "Facet feature for mention of another account. The text is usually a handle, including a '@' prefix, but the facet reference is a DID." 69 + }, 70 + "byteSlice": { 71 + "type": "object", 72 + "required": [ 73 + "byteStart", 74 + "byteEnd" 75 + ], 76 + "properties": { 77 + "byteEnd": { 78 + "type": "integer", 79 + "minimum": 0 80 + }, 81 + "byteStart": { 82 + "type": "integer", 83 + "minimum": 0 84 + } 85 + }, 86 + "description": "Specifies the sub-string range a facet feature applies to. Start index is inclusive, end index is exclusive. Indices are zero-indexed, counting bytes of the UTF-8 encoded text. NOTE: some languages, like Javascript, use UTF-16 or Unicode codepoints for string slice indexing; in these languages, convert to byte arrays before working with facets." 87 + } 88 + } 89 + }
-22
lexicons/network/slices/waiting.json
··· 1 - { 2 - "lexicon": 1, 3 - "id": "network.slices.waiting", 4 - "defs": { 5 - "main": { 6 - "type": "record", 7 - "description": "Existence of this record means you're on the waitist", 8 - "key": "literal:self", 9 - "record": { 10 - "type": "object", 11 - "required": ["createdAt"], 12 - "properties": { 13 - "createdAt": { 14 - "type": "string", 15 - "format": "datetime", 16 - "description": "When the user joined the waitlist" 17 - } 18 - } 19 - } 20 - } 21 - } 22 - }
+65
lexicons/network/slices/waitlist/defs.json
··· 1 + { 2 + "lexicon": 1, 3 + "id": "network.slices.waitlist.defs", 4 + "defs": { 5 + "requestView": { 6 + "type": "object", 7 + "description": "A request to join the waitlist with profile information", 8 + "required": ["slice", "createdAt"], 9 + "properties": { 10 + "slice": { 11 + "type": "string", 12 + "format": "at-uri", 13 + "description": "The AT URI of the slice being requested access to" 14 + }, 15 + "createdAt": { 16 + "type": "string", 17 + "format": "datetime", 18 + "description": "When the user joined the waitlist" 19 + }, 20 + "profile": { 21 + "type": "ref", 22 + "ref": "app.bsky.actor.defs#profileViewBasic", 23 + "description": "Profile of the requester" 24 + } 25 + } 26 + }, 27 + "inviteView": { 28 + "type": "object", 29 + "description": "An invite granting a DID access with profile information", 30 + "required": ["did", "slice", "createdAt"], 31 + "properties": { 32 + "did": { 33 + "type": "string", 34 + "format": "did", 35 + "description": "The DID being invited" 36 + }, 37 + "slice": { 38 + "type": "string", 39 + "format": "at-uri", 40 + "description": "The AT URI of the slice this invite is for" 41 + }, 42 + "createdAt": { 43 + "type": "string", 44 + "format": "datetime", 45 + "description": "When this invitation was created" 46 + }, 47 + "expiresAt": { 48 + "type": "string", 49 + "format": "datetime", 50 + "description": "Optional expiration date for this invitation" 51 + }, 52 + "uri": { 53 + "type": "string", 54 + "format": "at-uri", 55 + "description": "The AT URI of this invite record" 56 + }, 57 + "profile": { 58 + "type": "ref", 59 + "ref": "app.bsky.actor.defs#profileViewBasic", 60 + "description": "Profile of the invitee" 61 + } 62 + } 63 + } 64 + } 65 + }
+37
lexicons/network/slices/waitlist/invite.json
··· 1 + { 2 + "lexicon": 1, 3 + "id": "network.slices.waitlist.invite", 4 + "defs": { 5 + "main": { 6 + "type": "record", 7 + "description": "An invite granting a DID access, created by the slice owner", 8 + "key": "tid", 9 + "record": { 10 + "type": "object", 11 + "required": ["did", "slice", "createdAt"], 12 + "properties": { 13 + "did": { 14 + "type": "string", 15 + "format": "did", 16 + "description": "The DID being invited" 17 + }, 18 + "slice": { 19 + "type": "string", 20 + "format": "at-uri", 21 + "description": "The AT URI of the slice this invite is for" 22 + }, 23 + "createdAt": { 24 + "type": "string", 25 + "format": "datetime", 26 + "description": "When this invitation was created" 27 + }, 28 + "expiresAt": { 29 + "type": "string", 30 + "format": "datetime", 31 + "description": "Optional expiration date for this invitation" 32 + } 33 + } 34 + } 35 + } 36 + } 37 + }
+27
lexicons/network/slices/waitlist/request.json
··· 1 + { 2 + "lexicon": 1, 3 + "id": "network.slices.waitlist.request", 4 + "defs": { 5 + "main": { 6 + "type": "record", 7 + "description": "A request to join the waitlist", 8 + "key": "literal:self", 9 + "record": { 10 + "type": "object", 11 + "required": ["slice", "createdAt"], 12 + "properties": { 13 + "slice": { 14 + "type": "string", 15 + "format": "at-uri", 16 + "description": "The AT URI of the slice being requested access to" 17 + }, 18 + "createdAt": { 19 + "type": "string", 20 + "format": "datetime", 21 + "description": "When the user joined the waitlist" 22 + } 23 + } 24 + } 25 + } 26 + } 27 + }
+1 -1
packages/client/deno.json
··· 1 1 { 2 2 "name": "@slices/client", 3 - "version": "0.1.0-alpha.2", 3 + "version": "0.1.0-alpha.3", 4 4 "exports": "./src/mod.ts", 5 5 "imports": { 6 6 "@slices/oauth": "jsr:@slices/oauth@^0.4.1"
+30 -7
packages/client/src/mod.ts
··· 44 44 | "collection" 45 45 | "uri" 46 46 | "cid" 47 - | "indexedAt"; 47 + | "indexedAt" 48 + | "json"; 48 49 49 50 export interface SortField<TField extends string = string> { 50 51 field: TField; ··· 245 246 } 246 247 } 247 248 248 - throw new Error( 249 - `Request failed: ${response.status} ${response.statusText}` 250 - ); 249 + // Try to read the response body for detailed error information 250 + let errorMessage = `Request failed: ${response.status} ${response.statusText}`; 251 + try { 252 + const errorBody = await response.json(); 253 + if (errorBody?.message) { 254 + errorMessage += ` - ${errorBody.message}`; 255 + } else if (errorBody?.error) { 256 + errorMessage += ` - ${errorBody.error}`; 257 + } 258 + } catch { 259 + // If we can't parse the response body, just use the status message 260 + } 261 + 262 + throw new Error(errorMessage); 251 263 } 252 264 253 265 return (await response.json()) as T; ··· 327 339 } 328 340 } 329 341 330 - throw new Error( 331 - `Blob upload failed: ${response.status} ${response.statusText}` 332 - ); 342 + // Try to read the response body for detailed error information 343 + let errorMessage = `Blob upload failed: ${response.status} ${response.statusText}`; 344 + try { 345 + const errorBody = await response.json(); 346 + if (errorBody?.message) { 347 + errorMessage += ` - ${errorBody.message}`; 348 + } else if (errorBody?.error) { 349 + errorMessage += ` - ${errorBody.error}`; 350 + } 351 + } catch { 352 + // If we can't parse the response body, just use the status message 353 + } 354 + 355 + throw new Error(errorMessage); 333 356 } 334 357 335 358 return await response.json();