Highly ambitious ATProtocol AppView service and sdks

waitlists!

+94 -4
api/scripts/generate_typescript.ts
··· 19 interface LexiconDefinition { 20 type: string; 21 record?: LexiconRecord; 22 } 23 24 interface Lexicon { ··· 529 // Convert lexicon type to TypeScript type 530 function convertLexiconTypeToTypeScript( 531 def: any, 532 - currentLexicon: string 533 ): string { 534 const type = def.type; 535 switch (type) { 536 case "string": 537 return "string"; 538 case "integer": 539 return "number"; ··· 736 737 if (defValue.properties) { 738 for (const [propName, propDef] of Object.entries(defValue.properties)) { 739 - const tsType = convertLexiconTypeToTypeScript(propDef as any, lexicon.id); 740 const required = 741 defValue.required && defValue.required.includes(propName); 742 ··· 775 776 if (recordDef.properties) { 777 for (const [propName, propDef] of Object.entries(recordDef.properties)) { 778 - const tsType = convertLexiconTypeToTypeScript(propDef as any, lexicon.id); 779 const required = isPropertyRequired(recordDef, propName); 780 781 properties.push({ ··· 866 }); 867 } 868 869 // Add lexicon-specific interfaces and types 870 function addLexiconInterfaces(): void { 871 // First pass: Generate all individual definition interfaces/types ··· 932 isReadonly: true, 933 }); 934 break; 935 case "union": 936 case "array": 937 case "token": ··· 1144 { 1145 name: "client", 1146 type: "SlicesClient", 1147 - scope: "private", 1148 isReadonly: true, 1149 }, 1150 ]), ··· 1441 1442 // Generate the TypeScript 1443 addBaseInterfaces(); 1444 addLexiconInterfaces(); 1445 addClientClass(); 1446
··· 19 interface LexiconDefinition { 20 type: string; 21 record?: LexiconRecord; 22 + properties?: Record<string, LexiconProperty>; 23 } 24 25 interface Lexicon { ··· 530 // Convert lexicon type to TypeScript type 531 function convertLexiconTypeToTypeScript( 532 def: any, 533 + currentLexicon: string, 534 + propertyName?: string 535 ): string { 536 const type = def.type; 537 switch (type) { 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 + } 545 return "string"; 546 case "integer": 547 return "number"; ··· 744 745 if (defValue.properties) { 746 for (const [propName, propDef] of Object.entries(defValue.properties)) { 747 + const tsType = convertLexiconTypeToTypeScript(propDef as any, lexicon.id, propName); 748 const required = 749 defValue.required && defValue.required.includes(propName); 750 ··· 783 784 if (recordDef.properties) { 785 for (const [propName, propDef] of Object.entries(recordDef.properties)) { 786 + const tsType = convertLexiconTypeToTypeScript(propDef as any, lexicon.id, propName); 787 const required = isPropertyRequired(recordDef, propName); 788 789 properties.push({ ··· 874 }); 875 } 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 + 946 // Add lexicon-specific interfaces and types 947 function addLexiconInterfaces(): void { 948 // First pass: Generate all individual definition interfaces/types ··· 1009 isReadonly: true, 1010 }); 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; 1024 case "union": 1025 case "array": 1026 case "token": ··· 1233 { 1234 name: "client", 1235 type: "SlicesClient", 1236 + scope: "private" as any, 1237 isReadonly: true, 1238 }, 1239 ]), ··· 1530 1531 // Generate the TypeScript 1532 addBaseInterfaces(); 1533 + generateKnownValuesTypes(); 1534 addLexiconInterfaces(); 1535 addClientClass(); 1536
+2 -6
api/src/handler_xrpc_dynamic.rs
··· 575 576 match LexiconValidator::for_slice(&state.database, validation_slice_uri).await { 577 Ok(validator) => { 578 - // Debug: Get lexicons from the system slice to see what's there 579 - if collection == "network.slices.lexicon" {} 580 581 if let Err(e) = validator.validate_record(&collection, &record_data) { 582 return Err(( ··· 588 )); 589 } 590 } 591 - Err(e) => { 592 // If no lexicon found, continue without validation (backwards compatibility) 593 - eprintln!("Could not load lexicon validator: {:?}", e); 594 } 595 } 596 ··· 689 )); 690 } 691 } 692 - Err(e) => { 693 // If no lexicon found, continue without validation (backwards compatibility) 694 - eprintln!("Could not load lexicon validator: {:?}", e); 695 } 696 } 697
··· 575 576 match LexiconValidator::for_slice(&state.database, validation_slice_uri).await { 577 Ok(validator) => { 578 579 if let Err(e) = validator.validate_record(&collection, &record_data) { 580 return Err(( ··· 586 )); 587 } 588 } 589 + Err(_e) => { 590 // If no lexicon found, continue without validation (backwards compatibility) 591 } 592 } 593 ··· 686 )); 687 } 688 } 689 + Err(_e) => { 690 // If no lexicon found, continue without validation (backwards compatibility) 691 } 692 } 693
+1 -1
docker-compose.yml
··· 35 HTTP_PORT: "8081" 36 DATABASE_URL: "sqlite:///data/aip.db" 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" 39 ENABLE_CLIENT_API: "true" 40 ADMIN_DIDS: "did:plc:bcgltzqazw5tb6k2g3ttenbj" 41 DPOP_NONCE_SEED: "local-dev-nonce-seed"
··· 35 HTTP_PORT: "8081" 36 DATABASE_URL: "sqlite:///data/aip.db" 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.waitlist.request" 39 ENABLE_CLIENT_API: "true" 40 ADMIN_DIDS: "did:plc:bcgltzqazw5tb6k2g3ttenbj" 41 DPOP_NONCE_SEED: "local-dev-nonce-seed"
+5 -3
docs/sdk-usage.md
··· 118 where: { 119 releaseDate: { 120 gte: "1990-01-01", 121 - lte: "1999-12-31" 122 }, 123 }, 124 }); ··· 368 // Filter by exact handle 369 const exactHandle = await client.network.slices.slice.getActors({ 370 where: { 371 - handle: { eq: "alice.bsky.social" }, 372 }, 373 }); 374 ··· 492 // Limit to specific collections 493 const specificSearch = await client.network.slices.slice.getSliceRecords({ 494 where: { 495 - collection: { in: ["com.recordcollector.album", "com.recordcollector.review"] }, 496 json: { contains: "grunge" }, 497 }, 498 });
··· 118 where: { 119 releaseDate: { 120 gte: "1990-01-01", 121 + lte: "1999-12-31", 122 }, 123 }, 124 }); ··· 368 // Filter by exact handle 369 const exactHandle = await client.network.slices.slice.getActors({ 370 where: { 371 + handle: { eq: "user.bsky.social" }, 372 }, 373 }); 374 ··· 492 // Limit to specific collections 493 const specificSearch = await client.network.slices.slice.getSliceRecords({ 494 where: { 495 + collection: { 496 + in: ["com.recordcollector.album", "com.recordcollector.review"], 497 + }, 498 json: { contains: "grunge" }, 499 }, 500 });
+1 -1
frontend/deno.json
··· 8 "jsxImportSource": "preact" 9 }, 10 "imports": { 11 - "@slices/client": "jsr:@slices/client@^0.1.0-alpha.2", 12 "@slices/oauth": "jsr:@slices/oauth@^0.4.1", 13 "@slices/session": "jsr:@slices/session@^0.2.1", 14 "@std/assert": "jsr:@std/assert@^1.0.14",
··· 8 "jsxImportSource": "preact" 9 }, 10 "imports": { 11 + "@slices/client": "jsr:@slices/client@^0.1.0-alpha.3", 12 "@slices/oauth": "jsr:@slices/oauth@^0.4.1", 13 "@slices/session": "jsr:@slices/session@^0.2.1", 14 "@std/assert": "jsr:@std/assert@^1.0.14",
+4 -7
frontend/deno.lock
··· 2 "version": "5", 3 "specifiers": { 4 "jsr:@shikijs/shiki@*": "3.7.0", 5 - "jsr:@slices/client@~0.1.0-alpha.2": "0.1.0-alpha.2", 6 "jsr:@slices/oauth@~0.4.1": "0.4.1", 7 "jsr:@slices/session@~0.2.1": "0.2.1", 8 "jsr:@std/assert@^1.0.14": "1.0.14", ··· 43 "npm:shiki" 44 ] 45 }, 46 - "@slices/client@0.1.0-alpha.2": { 47 - "integrity": "d3c591e89ab5b7ed7988faf9428bb7b3539484c6b90005a7c66f2188cc60fe19", 48 - "dependencies": [ 49 - "jsr:@slices/oauth" 50 - ] 51 }, 52 "@slices/oauth@0.4.1": { 53 "integrity": "15f20df2ba81e9d1764291c8b4f6e3eb38cfc953750eeb3815872b7e22475492" ··· 672 }, 673 "workspace": { 674 "dependencies": [ 675 - "jsr:@slices/client@~0.1.0-alpha.2", 676 "jsr:@slices/oauth@~0.4.1", 677 "jsr:@slices/session@~0.2.1", 678 "jsr:@std/assert@^1.0.14",
··· 2 "version": "5", 3 "specifiers": { 4 "jsr:@shikijs/shiki@*": "3.7.0", 5 + "jsr:@slices/client@~0.1.0-alpha.3": "0.1.0-alpha.3", 6 "jsr:@slices/oauth@~0.4.1": "0.4.1", 7 "jsr:@slices/session@~0.2.1": "0.2.1", 8 "jsr:@std/assert@^1.0.14": "1.0.14", ··· 43 "npm:shiki" 44 ] 45 }, 46 + "@slices/client@0.1.0-alpha.3": { 47 + "integrity": "c18d6ad2dbe1043bbeb7da7c5a11724fa0fa388c3e6e96089bb033f518c4b23c" 48 }, 49 "@slices/oauth@0.4.1": { 50 "integrity": "15f20df2ba81e9d1764291c8b4f6e3eb38cfc953750eeb3815872b7e22475492" ··· 669 }, 670 "workspace": { 671 "dependencies": [ 672 + "jsr:@slices/client@~0.1.0-alpha.3", 673 "jsr:@slices/oauth@~0.4.1", 674 "jsr:@slices/session@~0.2.1", 675 "jsr:@std/assert@^1.0.14",
+2 -2
frontend/scripts/register-oauth-client.sh
··· 45 { 46 "client_name": "$CLIENT_NAME", 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", 49 "grant_types": ["authorization_code", "refresh_token"], 50 "response_types": ["code"], 51 "token_endpoint_auth_method": "client_secret_basic" ··· 106 echo " - Client ID: $CLIENT_ID" 107 echo " - Client Name: $CLIENT_NAME" 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" 110 echo " - Config saved to: $CONFIG_FILE" 111 echo 112 echo "🔧 Environment variables saved to $CONFIG_FILE:"
··· 45 { 46 "client_name": "$CLIENT_NAME", 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.waitlist.request", 49 "grant_types": ["authorization_code", "refresh_token"], 50 "response_types": ["code"], 51 "token_endpoint_auth_method": "client_secret_basic" ··· 106 echo " - Client ID: $CLIENT_ID" 107 echo " - Client Name: $CLIENT_NAME" 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.waitlist.request" 110 echo " - Config saved to: $CONFIG_FILE" 111 echo 112 echo "🔧 Environment variables saved to $CONFIG_FILE:"
+1484 -99
frontend/src/client.ts
··· 1 // Generated TypeScript client for AT Protocol records 2 - // Generated at: 2025-09-16 21:02:30 UTC 3 - // Lexicons: 9 4 5 /** 6 * @example Usage ··· 12 * 'at://did:plc:bcgltzqazw5tb6k2g3ttenbj/network.slices.slice/3lymhd4jhrd2z' 13 * ); 14 * 15 - * // Get records from the app.bsky.actor.profile collection 16 - * const records = await client.app.bsky.actor.profile.getRecords(); 17 * 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' 21 * }); 22 * 23 * // Get records with filtering and search 24 - * const filteredRecords = await client.app.bsky.actor.profile.getRecords({ 25 * where: { 26 * text: { contains: "example search term" } 27 * } 28 * }); 29 * 30 * // Use slice-level methods for cross-collection queries with type safety 31 - * const sliceRecords = await client.network.slices.slice.getSliceRecords<AppBskyActorProfile>({ 32 * where: { 33 - * collection: { eq: 'app.bsky.actor.profile' } 34 * } 35 * }); 36 * 37 * // Search across multiple collections using union types 38 - * const multiCollectionRecords = await client.network.slices.slice.getSliceRecords<AppBskyActorProfile | AppBskyActorProfile>({ 39 * where: { 40 - * collection: { in: ['app.bsky.actor.profile', 'app.bsky.actor.profile'] }, 41 * text: { contains: 'example search term' }, 42 * did: { in: ['did:plc:user1', 'did:plc:user2'] } 43 * }, ··· 263 message: string; 264 } 265 266 export interface AppBskyActorProfile { 267 /** Small image to be displayed next to posts from account. AKA, 'profile picture' */ 268 avatar?: BlobRef; ··· 288 | "description" 289 | "displayName"; 290 291 export interface NetworkSlicesSliceDefsSliceView { 292 uri: string; 293 cid: string; ··· 323 } 324 325 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 334 export interface NetworkSlicesLexicon { 335 /** Namespaced identifier for the lexicon */ ··· 406 407 export interface ComAtprotoLabelDefsLabelValueDefinition { 408 /** 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; 410 locales: ComAtprotoLabelDefs["LabelValueDefinitionStrings"][]; 411 /** 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; 413 /** Does the user need to have adult content enabled in order to configure this label? */ 414 adultOnly?: boolean; 415 /** The value of the label being defined. Must only include lowercase ascii and the '-' character ([a-z-]+). */ 416 identifier: string; 417 /** The default setting for this label. */ 418 - defaultSetting?: string; 419 } 420 421 export interface ComAtprotoLabelDefsLabelValueDefinitionStrings { ··· 432 uri: string; 433 } 434 435 export interface NetworkSlicesSliceDefs { 436 readonly SliceView: NetworkSlicesSliceDefsSliceView; 437 readonly SparklinePoint: NetworkSlicesSliceDefsSparklinePoint; ··· 444 export interface ComAtprotoLabelDefs { 445 readonly Label: ComAtprotoLabelDefsLabel; 446 readonly SelfLabel: ComAtprotoLabelDefsSelfLabel; 447 readonly SelfLabels: ComAtprotoLabelDefsSelfLabels; 448 readonly LabelValueDefinition: ComAtprotoLabelDefsLabelValueDefinition; 449 readonly LabelValueDefinitionStrings: ComAtprotoLabelDefsLabelValueDefinitionStrings; 450 } 451 452 class ProfileActorBskyAppClient { 453 private readonly client: SlicesClient; 454 ··· 536 } 537 538 class BskyAppClient { 539 readonly actor: ActorBskyAppClient; 540 private readonly client: SlicesClient; 541 542 constructor(client: SlicesClient) { 543 this.client = client; 544 this.actor = new ActorBskyAppClient(client); 545 } 546 } ··· 555 } 556 } 557 558 class SliceSlicesNetworkClient { 559 private readonly client: SlicesClient; 560 ··· 784 } 785 } 786 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 class LexiconSlicesNetworkClient { 864 private readonly client: SlicesClient; 865 ··· 1026 } 1027 1028 class SlicesNetworkClient { 1029 readonly slice: SliceSlicesNetworkClient; 1030 - readonly waiting: WaitingSlicesNetworkClient; 1031 readonly lexicon: LexiconSlicesNetworkClient; 1032 readonly actor: ActorSlicesNetworkClient; 1033 private readonly client: SlicesClient; 1034 1035 constructor(client: SlicesClient) { 1036 this.client = client; 1037 this.slice = new SliceSlicesNetworkClient(client); 1038 - this.waiting = new WaitingSlicesNetworkClient(client); 1039 this.lexicon = new LexiconSlicesNetworkClient(client); 1040 this.actor = new ActorSlicesNetworkClient(client); 1041 }
··· 1 // Generated TypeScript client for AT Protocol records 2 + // Generated at: 2025-09-17 17:42:43 UTC 3 + // Lexicons: 25 4 5 /** 6 * @example Usage ··· 12 * 'at://did:plc:bcgltzqazw5tb6k2g3ttenbj/network.slices.slice/3lymhd4jhrd2z' 13 * ); 14 * 15 + * // Get records from the app.bsky.graph.follow collection 16 + * const records = await client.app.bsky.graph.follow.getRecords(); 17 * 18 * // Get a specific record 19 + * const record = await client.app.bsky.graph.follow.getRecord({ 20 + * uri: 'at://did:plc:example/app.bsky.graph.follow/3abc123' 21 * }); 22 * 23 * // Get records with filtering and search 24 + * const filteredRecords = await client.app.bsky.graph.follow.getRecords({ 25 * where: { 26 * text: { contains: "example search term" } 27 * } 28 * }); 29 * 30 * // Use slice-level methods for cross-collection queries with type safety 31 + * const sliceRecords = await client.network.slices.slice.getSliceRecords<AppBskyGraphFollow>({ 32 * where: { 33 + * collection: { eq: 'app.bsky.graph.follow' } 34 * } 35 * }); 36 * 37 * // Search across multiple collections using union types 38 + * const multiCollectionRecords = await client.network.slices.slice.getSliceRecords<AppBskyGraphFollow | AppBskyActorProfile>({ 39 * where: { 40 + * collection: { in: ['app.bsky.graph.follow', 'app.bsky.actor.profile'] }, 41 * text: { contains: 'example search term' }, 42 * did: { in: ['did:plc:user1', 'did:plc:user2'] } 43 * }, ··· 263 message: string; 264 } 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 + 1058 export interface AppBskyActorProfile { 1059 /** Small image to be displayed next to posts from account. AKA, 'profile picture' */ 1060 avatar?: BlobRef; ··· 1080 | "description" 1081 | "displayName"; 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 + 1165 export interface NetworkSlicesSliceDefsSliceView { 1166 uri: string; 1167 cid: string; ··· 1197 } 1198 1199 export type NetworkSlicesSliceSortFields = "name" | "domain" | "createdAt"; 1200 1201 export interface NetworkSlicesLexicon { 1202 /** Namespaced identifier for the lexicon */ ··· 1273 1274 export interface ComAtprotoLabelDefsLabelValueDefinition { 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. */ 1276 + blurs: ComAtprotoLabelDefsBlurs; 1277 locales: ComAtprotoLabelDefs["LabelValueDefinitionStrings"][]; 1278 /** How should a client visually convey this label? 'inform' means neutral and informational; 'alert' means negative and warning; 'none' means show nothing. */ 1279 + severity: ComAtprotoLabelDefsSeverity; 1280 /** Does the user need to have adult content enabled in order to configure this label? */ 1281 adultOnly?: boolean; 1282 /** The value of the label being defined. Must only include lowercase ascii and the '-' character ([a-z-]+). */ 1283 identifier: string; 1284 /** The default setting for this label. */ 1285 + defaultSetting?: ComAtprotoLabelDefsDefaultSetting; 1286 } 1287 1288 export interface ComAtprotoLabelDefsLabelValueDefinitionStrings { ··· 1299 uri: string; 1300 } 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 + 1453 export interface NetworkSlicesSliceDefs { 1454 readonly SliceView: NetworkSlicesSliceDefsSliceView; 1455 readonly SparklinePoint: NetworkSlicesSliceDefsSparklinePoint; ··· 1462 export interface ComAtprotoLabelDefs { 1463 readonly Label: ComAtprotoLabelDefsLabel; 1464 readonly SelfLabel: ComAtprotoLabelDefsSelfLabel; 1465 + readonly LabelValue: ComAtprotoLabelDefsLabelValue; 1466 readonly SelfLabels: ComAtprotoLabelDefsSelfLabels; 1467 readonly LabelValueDefinition: ComAtprotoLabelDefsLabelValueDefinition; 1468 readonly LabelValueDefinitionStrings: ComAtprotoLabelDefsLabelValueDefinitionStrings; 1469 } 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 + 1721 class ProfileActorBskyAppClient { 1722 private readonly client: SlicesClient; 1723 ··· 1805 } 1806 1807 class BskyAppClient { 1808 + readonly graph: GraphBskyAppClient; 1809 + readonly feed: FeedBskyAppClient; 1810 readonly actor: ActorBskyAppClient; 1811 private readonly client: SlicesClient; 1812 1813 constructor(client: SlicesClient) { 1814 this.client = client; 1815 + this.graph = new GraphBskyAppClient(client); 1816 + this.feed = new FeedBskyAppClient(client); 1817 this.actor = new ActorBskyAppClient(client); 1818 } 1819 } ··· 1828 } 1829 } 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 + 2019 class SliceSlicesNetworkClient { 2020 private readonly client: SlicesClient; 2021 ··· 2245 } 2246 } 2247 2248 class LexiconSlicesNetworkClient { 2249 private readonly client: SlicesClient; 2250 ··· 2411 } 2412 2413 class SlicesNetworkClient { 2414 + readonly waitlist: WaitlistSlicesNetworkClient; 2415 readonly slice: SliceSlicesNetworkClient; 2416 readonly lexicon: LexiconSlicesNetworkClient; 2417 readonly actor: ActorSlicesNetworkClient; 2418 private readonly client: SlicesClient; 2419 2420 constructor(client: SlicesClient) { 2421 this.client = client; 2422 + this.waitlist = new WaitlistSlicesNetworkClient(client); 2423 this.slice = new SliceSlicesNetworkClient(client); 2424 this.lexicon = new LexiconSlicesNetworkClient(client); 2425 this.actor = new ActorSlicesNetworkClient(client); 2426 }
+3 -3
frontend/src/config.ts
··· 7 const OAUTH_REDIRECT_URI = Deno.env.get("OAUTH_REDIRECT_URI"); 8 const OAUTH_AIP_BASE_URL = Deno.env.get("OAUTH_AIP_BASE_URL"); 9 const API_URL = Deno.env.get("API_URL"); 10 - const SLICE_URI = Deno.env.get("SLICE_URI"); 11 12 if ( 13 !OAUTH_CLIENT_ID || ··· 19 ) { 20 throw new Error( 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", 23 ); 24 } 25 ··· 47 // "repo:network.slices.waiting", 48 ], 49 }, 50 - oauthStorage, 51 ); 52 53 // Session setup (shared database)
··· 7 const OAUTH_REDIRECT_URI = Deno.env.get("OAUTH_REDIRECT_URI"); 8 const OAUTH_AIP_BASE_URL = Deno.env.get("OAUTH_AIP_BASE_URL"); 9 const API_URL = Deno.env.get("API_URL"); 10 + export const SLICE_URI = Deno.env.get("SLICE_URI"); 11 12 if ( 13 !OAUTH_CLIENT_ID || ··· 19 ) { 20 throw new Error( 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" 23 ); 24 } 25 ··· 47 // "repo:network.slices.waiting", 48 ], 49 }, 50 + oauthStorage 51 ); 52 53 // Session setup (shared database)
+178 -57
frontend/src/features/auth/handlers.tsx
··· 3 import { atprotoClient, oauthSessions, sessionStore } from "../../config.ts"; 4 import { renderHTML } from "../../utils/render.tsx"; 5 import { LoginPage } from "./templates/LoginPage.tsx"; 6 7 // ============================================================================ 8 // LOGIN PAGE HANDLER ··· 14 15 const error = url.searchParams.get("error"); 16 return renderHTML( 17 - <LoginPage error={error || undefined} currentUser={context.currentUser} />, 18 ); 19 } 20 ··· 49 return Response.redirect( 50 new URL( 51 "/login?error=" + encodeURIComponent("OAuth initialization failed"), 52 - req.url, 53 ), 54 - 302, 55 ); 56 } 57 } ··· 82 return Response.redirect( 83 new URL( 84 "/login?error=" + encodeURIComponent("Invalid OAuth callback"), 85 - req.url, 86 ), 87 - 302, 88 ); 89 } 90 ··· 92 return Response.redirect( 93 new URL( 94 "/login?error=" + encodeURIComponent("OAuth client not configured"), 95 - req.url, 96 ), 97 - 302, 98 ); 99 } 100 ··· 111 return Response.redirect( 112 new URL( 113 "/login?error=" + encodeURIComponent("Failed to create session"), 114 - req.url, 115 ), 116 - 302, 117 ); 118 } 119 ··· 125 try { 126 userInfo = await atprotoClient.oauth?.getUserInfo(); 127 } catch (error) { 128 - console.log("Failed to get user info:", error); 129 } 130 131 - // Sync external collections if user doesn't have them yet 132 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 - }); 147 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"); 154 } 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", 159 ); 160 } 161 } 162 - } catch (error) { 163 - console.log( 164 - "Error during sync check, skipping external collections sync:", 165 - error, 166 - ); 167 } 168 169 // Redirect to user's profile page if handle is available ··· 181 return Response.redirect( 182 new URL( 183 "/login?error=" + encodeURIComponent("Authentication failed"), 184 - req.url, 185 ), 186 - 302, 187 ); 188 } 189 } ··· 235 isWaitlistFlow: true, 236 handle, 237 redirectUri: "/auth/waitlist/callback", 238 - }), 239 ); 240 241 // Initiate OAuth with minimal scope for waitlist, passing state directly 242 const authResult = await atprotoClient.oauth.authorize({ 243 loginHint: handle, 244 - scope: "atproto repo:network.slices.waiting", 245 state: waitlistState, 246 }); 247 ··· 263 if (!code || !state) { 264 return Response.redirect( 265 new URL("/waitlist?error=invalid_callback", req.url), 266 - 302, 267 ); 268 } 269 ··· 282 if (!atprotoClient.oauth) { 283 return Response.redirect( 284 new URL("/waitlist?error=oauth_not_configured", req.url), 285 - 302, 286 ); 287 } 288 ··· 293 const userInfo = await atprotoClient.oauth.getUserInfo(); 294 295 if (!userInfo) { 296 - return Response.redirect(new URL("/waitlist?error=no_user_info", req.url), 302); 297 } 298 299 // Create waitlist record 300 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( 309 { 310 createdAt: new Date().toISOString(), 311 }, 312 - true, 313 ); 314 } catch (error) { 315 console.error("Failed to create waitlist record:", error); 316 } ··· 326 return Response.redirect(redirectUrl.toString(), 302); 327 } catch (error) { 328 console.error("Waitlist callback error:", error); 329 - return Response.redirect(new URL("/waitlist?error=waitlist_failed", req.url), 302); 330 } 331 } 332
··· 3 import { atprotoClient, oauthSessions, sessionStore } from "../../config.ts"; 4 import { renderHTML } from "../../utils/render.tsx"; 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 + } 65 66 // ============================================================================ 67 // LOGIN PAGE HANDLER ··· 73 74 const error = url.searchParams.get("error"); 75 return renderHTML( 76 + <LoginPage error={error || undefined} currentUser={context.currentUser} /> 77 ); 78 } 79 ··· 108 return Response.redirect( 109 new URL( 110 "/login?error=" + encodeURIComponent("OAuth initialization failed"), 111 + req.url 112 ), 113 + 302 114 ); 115 } 116 } ··· 141 return Response.redirect( 142 new URL( 143 "/login?error=" + encodeURIComponent("Invalid OAuth callback"), 144 + req.url 145 ), 146 + 302 147 ); 148 } 149 ··· 151 return Response.redirect( 152 new URL( 153 "/login?error=" + encodeURIComponent("OAuth client not configured"), 154 + req.url 155 ), 156 + 302 157 ); 158 } 159 ··· 170 return Response.redirect( 171 new URL( 172 "/login?error=" + encodeURIComponent("Failed to create session"), 173 + req.url 174 ), 175 + 302 176 ); 177 } 178 ··· 184 try { 185 userInfo = await atprotoClient.oauth?.getUserInfo(); 186 } catch (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 + } 205 } 206 207 + // Sync external collections first to ensure actor records are populated 208 try { 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; 248 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 262 } 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 268 ); 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 276 } 277 } 278 279 // Redirect to user's profile page if handle is available ··· 291 return Response.redirect( 292 new URL( 293 "/login?error=" + encodeURIComponent("Authentication failed"), 294 + req.url 295 ), 296 + 302 297 ); 298 } 299 } ··· 345 isWaitlistFlow: true, 346 handle, 347 redirectUri: "/auth/waitlist/callback", 348 + }) 349 ); 350 351 // Initiate OAuth with minimal scope for waitlist, passing state directly 352 const authResult = await atprotoClient.oauth.authorize({ 353 loginHint: handle, 354 + scope: "atproto repo:network.slices.waitlist.request", 355 state: waitlistState, 356 }); 357 ··· 373 if (!code || !state) { 374 return Response.redirect( 375 new URL("/waitlist?error=invalid_callback", req.url), 376 + 302 377 ); 378 } 379 ··· 392 if (!atprotoClient.oauth) { 393 return Response.redirect( 394 new URL("/waitlist?error=oauth_not_configured", req.url), 395 + 302 396 ); 397 } 398 ··· 403 const userInfo = await atprotoClient.oauth.getUserInfo(); 404 405 if (!userInfo) { 406 + return Response.redirect( 407 + new URL("/waitlist?error=no_user_info", req.url), 408 + 302 409 + ); 410 } 411 412 // Create waitlist record 413 try { 414 + await atprotoClient.network.slices.waitlist.request.createRecord( 415 { 416 + slice: SLICE_URI!, 417 createdAt: new Date().toISOString(), 418 }, 419 + true 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 + } 432 } catch (error) { 433 console.error("Failed to create waitlist record:", error); 434 } ··· 444 return Response.redirect(redirectUrl.toString(), 302); 445 } catch (error) { 446 console.error("Waitlist callback error:", error); 447 + return Response.redirect( 448 + new URL("/waitlist?error=waitlist_failed", req.url), 449 + 302 450 + ); 451 } 452 } 453
+7 -7
frontend/src/features/dashboard/handlers.tsx
··· 10 11 async function handleProfilePage( 12 req: Request, 13 - params?: URLPatternResult, 14 ): Promise<Response> { 15 const context = await withAuth(req); 16 ··· 48 slices={slices} 49 currentUser={context.currentUser} 50 profile={profile} 51 - />, 52 ); 53 } 54 ··· 60 const authInfo = await atprotoClient.oauth?.getAuthenticationInfo(); 61 if (!authInfo?.isAuthenticated) { 62 return renderHTML( 63 - <CreateSliceDialog error="Session expired. Please log in again." />, 64 ); 65 } 66 ··· 75 error="Slice name is required" 76 name={name} 77 domain={domain} 78 - />, 79 ); 80 } 81 ··· 85 error="Primary domain is required" 86 name={name} 87 domain={domain} 88 - />, 89 ); 90 } 91 ··· 97 }; 98 99 const result = await atprotoClient.network.slices.slice.createRecord( 100 - recordData, 101 ); 102 103 const uriParts = result.uri.split("/"); ··· 117 error="Failed to create slice record. Please try again." 118 name={name} 119 domain={domain} 120 - />, 121 ); 122 } 123 } catch (_error) {
··· 10 11 async function handleProfilePage( 12 req: Request, 13 + params?: URLPatternResult 14 ): Promise<Response> { 15 const context = await withAuth(req); 16 ··· 48 slices={slices} 49 currentUser={context.currentUser} 50 profile={profile} 51 + /> 52 ); 53 } 54 ··· 60 const authInfo = await atprotoClient.oauth?.getAuthenticationInfo(); 61 if (!authInfo?.isAuthenticated) { 62 return renderHTML( 63 + <CreateSliceDialog error="Session expired. Please log in again." /> 64 ); 65 } 66 ··· 75 error="Slice name is required" 76 name={name} 77 domain={domain} 78 + /> 79 ); 80 } 81 ··· 85 error="Primary domain is required" 86 name={name} 87 domain={domain} 88 + /> 89 ); 90 } 91 ··· 97 }; 98 99 const result = await atprotoClient.network.slices.slice.createRecord( 100 + recordData 101 ); 102 103 const uriParts = result.uri.split("/"); ··· 117 error="Failed to create slice record. Please try again." 118 name={name} 119 domain={domain} 120 + /> 121 ); 122 } 123 } catch (_error) {
+1 -1
frontend/src/features/docs/templates/DocsIndexPage.tsx
··· 35 href={`/docs/${doc.slug}`} 36 className="block" 37 > 38 - <Card variant="hover"> 39 <Text as="h2" size="xl" className="font-semibold mb-2"> 40 {doc.title} 41 </Text>
··· 35 href={`/docs/${doc.slug}`} 36 className="block" 37 > 38 + <Card padding="md" variant="hover"> 39 <Text as="h2" size="xl" className="font-semibold mb-2"> 40 {doc.title} 41 </Text>
+2 -6
frontend/src/features/landing/templates/fragments/WaitlistFormModal.tsx
··· 8 <h2 class="font-mono text-xl font-bold text-gray-800 mb-4"> 9 Join the Waitlist 10 </h2> 11 - <form 12 - action="/auth/waitlist/initiate" 13 - method="POST" 14 - class="space-y-4" 15 - > 16 <div> 17 <label 18 for="handle-input" ··· 24 type="text" 25 id="handle-input" 26 name="handle" 27 - placeholder="alice.bsky.social" 28 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 required 30 />
··· 8 <h2 class="font-mono text-xl font-bold text-gray-800 mb-4"> 9 Join the Waitlist 10 </h2> 11 + <form action="/auth/waitlist/initiate" method="POST" class="space-y-4"> 12 <div> 13 <label 14 for="handle-input" ··· 20 type="text" 21 id="handle-input" 22 name="handle" 23 + placeholder="user.bsky.social" 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" 25 required 26 />
+1 -1
frontend/src/features/settings/templates/fragments/SettingsForm.tsx
··· 16 17 export function SettingsForm({ profile }: SettingsFormProps) { 18 return ( 19 - <Card> 20 <Text as="h2" size="xl" className="font-semibold mb-4"> 21 Profile Settings 22 </Text>
··· 16 17 export function SettingsForm({ profile }: SettingsFormProps) { 18 return ( 19 + <Card padding="md"> 20 <Text as="h2" size="xl" className="font-semibold mb-4"> 21 Profile Settings 22 </Text>
+2 -2
frontend/src/features/slices/codegen/templates/SliceCodegenPage.tsx
··· 44 hasSliceAccess={hasSliceAccess} 45 title={`${slice.name} - Code Generation`} 46 > 47 - <Card padding="none"> 48 <Card.Header 49 title="TypeScript Client" 50 action={ ··· 64 <Card.Content> 65 {error ? ( 66 <div className="p-6"> 67 - <Card variant="danger"> 68 <Text as="h3" size="lg" className="font-semibold mb-2"> 69 ❌ Generation Failed 70 </Text>
··· 44 hasSliceAccess={hasSliceAccess} 45 title={`${slice.name} - Code Generation`} 46 > 47 + <Card> 48 <Card.Header 49 title="TypeScript Client" 50 action={ ··· 64 <Card.Content> 65 {error ? ( 66 <div className="p-6"> 67 + <Card padding="md" variant="danger"> 68 <Text as="h3" size="lg" className="font-semibold mb-2"> 69 ❌ Generation Failed 70 </Text>
+1 -1
frontend/src/features/slices/lexicon/templates/LexiconDetailPage.tsx
··· 79 </Button> 80 </div> 81 82 - <Card padding="none"> 83 <Card.Header title="Lexicon Definitions" /> 84 <Card.Content className="text-sm overflow-x-auto [&_pre]:p-4 [&_pre]:m-0"> 85 <div dangerouslySetInnerHTML={{ __html: highlightedCode }} />
··· 79 </Button> 80 </div> 81 82 + <Card> 83 <Card.Header title="Lexicon Definitions" /> 84 <Card.Content className="text-sm overflow-x-auto [&_pre]:p-4 [&_pre]:m-0"> 85 <div dangerouslySetInnerHTML={{ __html: highlightedCode }} />
+1 -1
frontend/src/features/slices/lexicon/templates/SliceLexiconPage.tsx
··· 48 </Button> 49 </div> 50 )} 51 - <Card padding="none"> 52 <Card.Header 53 title={`${lexicons.length} ${ 54 lexicons.length === 1 ? "Lexicon" : "Lexicons"
··· 48 </Button> 49 </div> 50 )} 51 + <Card> 52 <Card.Header 53 title={`${lexicons.length} ${ 54 lexicons.length === 1 ? "Lexicon" : "Lexicons"
+3
frontend/src/features/slices/mod.ts
··· 9 import { syncRoutes } from "./sync/handlers.tsx"; 10 import { syncLogsRoutes } from "./sync-logs/handlers.tsx"; 11 import { jetstreamRoutes } from "./jetstream/handlers.tsx"; 12 13 // Export individual route groups 14 export { ··· 22 settingsRoutes, 23 syncLogsRoutes, 24 syncRoutes, 25 }; 26 27 // Export consolidated routes array for easy import ··· 36 ...syncRoutes, 37 ...syncLogsRoutes, 38 ...jetstreamRoutes, 39 ];
··· 9 import { syncRoutes } from "./sync/handlers.tsx"; 10 import { syncLogsRoutes } from "./sync-logs/handlers.tsx"; 11 import { jetstreamRoutes } from "./jetstream/handlers.tsx"; 12 + import { waitlistRoutes } from "./waitlist/handlers.tsx"; 13 14 // Export individual route groups 15 export { ··· 23 settingsRoutes, 24 syncLogsRoutes, 25 syncRoutes, 26 + waitlistRoutes, 27 }; 28 29 // Export consolidated routes array for easy import ··· 38 ...syncRoutes, 39 ...syncLogsRoutes, 40 ...jetstreamRoutes, 41 + ...waitlistRoutes, 42 ];
+1 -1
frontend/src/features/slices/oauth/templates/SliceOAuthPage.tsx
··· 64 </Button> 65 </div> 66 67 - <Card padding="none"> 68 <Card.Header title="OAuth Clients" /> 69 <Card.Content> 70 {clients.length === 0
··· 64 </Button> 65 </div> 66 67 + <Card> 68 <Card.Header title="OAuth Clients" /> 69 <Card.Content> 70 {clients.length === 0
+7 -7
frontend/src/features/slices/overview/templates/SliceOverview.tsx
··· 73 </div> 74 75 {(slice.indexedRecordCount ?? 0) > 0 && ( 76 - <Card className="mb-8"> 77 <Text as="h2" size="xl" className="font-semibold mb-4"> 78 📊 Database Status 79 </Text> ··· 107 )} 108 109 <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6"> 110 - <Card> 111 <Text as="h2" size="xl" className="font-semibold mb-4 block"> 112 📚 Lexicon Definitions 113 </Text> ··· 122 </Button> 123 </Card> 124 125 - <Card> 126 <Text as="h2" size="xl" className="font-semibold mb-4 block"> 127 📝 View Records 128 </Text> ··· 143 )} 144 </Card> 145 146 - <Card> 147 <Text as="h2" size="xl" className="font-semibold mb-4 block"> 148 ⚡ Code Generation 149 </Text> ··· 158 </Button> 159 </Card> 160 161 - <Card> 162 <Text as="h2" size="xl" className="font-semibold mb-4 block"> 163 📖 API Documentation 164 </Text> ··· 174 </Card> 175 176 {hasSliceAccess && ( 177 - <Card> 178 <Text as="h2" size="xl" className="font-semibold mb-4 block"> 179 🔄 Sync 180 </Text> ··· 191 )} 192 193 {collections.length > 0 && ( 194 - <Card> 195 <Text as="h2" size="xl" className="font-semibold mb-4 block"> 196 📊 Synced Collections 197 </Text>
··· 73 </div> 74 75 {(slice.indexedRecordCount ?? 0) > 0 && ( 76 + <Card padding="md" className="mb-8"> 77 <Text as="h2" size="xl" className="font-semibold mb-4"> 78 📊 Database Status 79 </Text> ··· 107 )} 108 109 <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6"> 110 + <Card padding="md"> 111 <Text as="h2" size="xl" className="font-semibold mb-4 block"> 112 📚 Lexicon Definitions 113 </Text> ··· 122 </Button> 123 </Card> 124 125 + <Card padding="md"> 126 <Text as="h2" size="xl" className="font-semibold mb-4 block"> 127 📝 View Records 128 </Text> ··· 143 )} 144 </Card> 145 146 + <Card padding="md"> 147 <Text as="h2" size="xl" className="font-semibold mb-4 block"> 148 ⚡ Code Generation 149 </Text> ··· 158 </Button> 159 </Card> 160 161 + <Card padding="md"> 162 <Text as="h2" size="xl" className="font-semibold mb-4 block"> 163 📖 API Documentation 164 </Text> ··· 174 </Card> 175 176 {hasSliceAccess && ( 177 + <Card padding="md"> 178 <Text as="h2" size="xl" className="font-semibold mb-4 block"> 179 🔄 Sync 180 </Text> ··· 191 )} 192 193 {collections.length > 0 && ( 194 + <Card padding="md"> 195 <Text as="h2" size="xl" className="font-semibold mb-4 block"> 196 📊 Synced Collections 197 </Text>
+43 -12
frontend/src/features/slices/records/templates/fragments/RecordsList.tsx
··· 1 - import type { IndexedRecord } from "../../../../../client.ts"; 2 import { Card } from "../../../../../shared/fragments/Card.tsx"; 3 import { Text } from "../../../../../shared/fragments/Text.tsx"; 4 ··· 12 13 export function RecordsList({ records }: RecordsListProps) { 14 return ( 15 - <Card padding="none"> 16 - <Card.Header 17 - title={`Records (${records.length})`} 18 - /> 19 <Card.Content className="divide-y divide-zinc-200 dark:divide-zinc-700"> 20 {records.map((record) => ( 21 <div key={record.uri} className="p-6"> ··· 26 </Text> 27 <dl className="grid grid-cols-1 gap-x-4 gap-y-2 text-sm"> 28 <div className="grid grid-cols-3 gap-4"> 29 - <Text as="dt" size="sm" variant="muted" className="font-medium">URI:</Text> 30 <Text as="dd" size="sm" className="col-span-2 break-all"> 31 {record.uri} 32 </Text> 33 </div> 34 <div className="grid grid-cols-3 gap-4"> 35 - <Text as="dt" size="sm" variant="muted" className="font-medium"> 36 Collection: 37 </Text> 38 <Text as="dd" size="sm" className="col-span-2"> ··· 40 </Text> 41 </div> 42 <div className="grid grid-cols-3 gap-4"> 43 - <Text as="dt" size="sm" variant="muted" className="font-medium">DID:</Text> 44 <Text as="dd" size="sm" className="col-span-2 break-all"> 45 {record.did} 46 </Text> 47 </div> 48 <div className="grid grid-cols-3 gap-4"> 49 - <Text as="dt" size="sm" variant="muted" className="font-medium">CID:</Text> 50 <Text as="dd" size="sm" className="col-span-2 break-all"> 51 {record.cid} 52 </Text> 53 </div> 54 <div className="grid grid-cols-3 gap-4"> 55 - <Text as="dt" size="sm" variant="muted" className="font-medium"> 56 Indexed: 57 </Text> 58 <Text as="dd" size="sm" className="col-span-2"> ··· 66 Record Data 67 </Text> 68 <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> 71 </pre> 72 </div> 73 </div>
··· 1 + import { IndexedRecord } from "@slices/client"; 2 import { Card } from "../../../../../shared/fragments/Card.tsx"; 3 import { Text } from "../../../../../shared/fragments/Text.tsx"; 4 ··· 12 13 export function RecordsList({ records }: RecordsListProps) { 14 return ( 15 + <Card> 16 + <Card.Header title={`Records (${records.length})`} /> 17 <Card.Content className="divide-y divide-zinc-200 dark:divide-zinc-700"> 18 {records.map((record) => ( 19 <div key={record.uri} className="p-6"> ··· 24 </Text> 25 <dl className="grid grid-cols-1 gap-x-4 gap-y-2 text-sm"> 26 <div className="grid grid-cols-3 gap-4"> 27 + <Text 28 + as="dt" 29 + size="sm" 30 + variant="muted" 31 + className="font-medium" 32 + > 33 + URI: 34 + </Text> 35 <Text as="dd" size="sm" className="col-span-2 break-all"> 36 {record.uri} 37 </Text> 38 </div> 39 <div className="grid grid-cols-3 gap-4"> 40 + <Text 41 + as="dt" 42 + size="sm" 43 + variant="muted" 44 + className="font-medium" 45 + > 46 Collection: 47 </Text> 48 <Text as="dd" size="sm" className="col-span-2"> ··· 50 </Text> 51 </div> 52 <div className="grid grid-cols-3 gap-4"> 53 + <Text 54 + as="dt" 55 + size="sm" 56 + variant="muted" 57 + className="font-medium" 58 + > 59 + DID: 60 + </Text> 61 <Text as="dd" size="sm" className="col-span-2 break-all"> 62 {record.did} 63 </Text> 64 </div> 65 <div className="grid grid-cols-3 gap-4"> 66 + <Text 67 + as="dt" 68 + size="sm" 69 + variant="muted" 70 + className="font-medium" 71 + > 72 + CID: 73 + </Text> 74 <Text as="dd" size="sm" className="col-span-2 break-all"> 75 {record.cid} 76 </Text> 77 </div> 78 <div className="grid grid-cols-3 gap-4"> 79 + <Text 80 + as="dt" 81 + size="sm" 82 + variant="muted" 83 + className="font-medium" 84 + > 85 Indexed: 86 </Text> 87 <Text as="dd" size="sm" className="col-span-2"> ··· 95 Record Data 96 </Text> 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"> 98 + <Text as="span" size="xs"> 99 + {record.pretty_value || 100 + JSON.stringify(record.value, null, 2)} 101 + </Text> 102 </pre> 103 </div> 104 </div>
+2 -2
frontend/src/features/slices/settings/templates/SliceSettings.tsx
··· 56 {/* Settings Content */} 57 <div className="space-y-8"> 58 {/* Edit Slice Settings */} 59 - <Card> 60 <Text as="h2" size="xl" className="font-semibold mb-4"> 61 Edit Slice Settings 62 </Text> ··· 104 </Card> 105 106 {/* Danger Zone */} 107 - <Card className="border-l-4 border-l-red-500"> 108 <Text 109 as="h2" 110 size="xl"
··· 56 {/* Settings Content */} 57 <div className="space-y-8"> 58 {/* Edit Slice Settings */} 59 + <Card padding="md"> 60 <Text as="h2" size="xl" className="font-semibold mb-4"> 61 Edit Slice Settings 62 </Text> ··· 104 </Card> 105 106 {/* Danger Zone */} 107 + <Card padding="md" className="border-l-4 border-l-red-500"> 108 <Text 109 as="h2" 110 size="xl"
+6 -1
frontend/src/features/slices/shared/fragments/SliceTabs.tsx
··· 48 } 49 ); 50 51 - // Add oauth and settings tabs only if user owns the slice 52 if (hasSliceAccess) { 53 tabs.push( 54 { 55 id: "oauth", 56 name: "OAuth Clients", 57 href: buildSliceUrlFromView(slice, sliceId, "oauth"), 58 }, 59 { 60 id: "settings",
··· 48 } 49 ); 50 51 + // Add oauth, waitlist and settings tabs only if user owns the slice 52 if (hasSliceAccess) { 53 tabs.push( 54 { 55 id: "oauth", 56 name: "OAuth Clients", 57 href: buildSliceUrlFromView(slice, sliceId, "oauth"), 58 + }, 59 + { 60 + id: "waitlist", 61 + name: "Waitlist", 62 + href: buildSliceUrlFromView(slice, sliceId, "waitlist"), 63 }, 64 { 65 id: "settings",
+1 -1
frontend/src/features/slices/sync/templates/SliceSyncPage.tsx
··· 38 <span className="flex items-center gap-2">Start Sync</span> 39 </Button> 40 </div> 41 - <Card padding="none"> 42 <Card.Header title="Recent Sync History" /> 43 <Card.Content 44 hx-get={`/api/slices/${sliceId}/job-history?handle=${slice.creator?.handle}`}
··· 38 <span className="flex items-center gap-2">Start Sync</span> 39 </Button> 40 </div> 41 + <Card> 42 <Card.Header title="Recent Sync History" /> 43 <Card.Content 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 import { withAuth } from "../../routes/middleware.ts"; 3 import { renderHTML } from "../../utils/render.tsx"; 4 import { WaitlistPage } from "./templates/WaitlistPage.tsx"; 5 6 async function handleWaitlistPage(req: Request): Promise<Response> { 7 const context = await withAuth(req); ··· 12 const handle = url.searchParams.get("handle"); 13 const error = url.searchParams.get("error"); 14 15 return renderHTML( 16 <WaitlistPage 17 success={success} 18 handle={handle || undefined} 19 error={error || undefined} 20 currentUser={context.currentUser} 21 /> 22 ); 23 }
··· 2 import { withAuth } from "../../routes/middleware.ts"; 3 import { renderHTML } from "../../utils/render.tsx"; 4 import { WaitlistPage } from "./templates/WaitlistPage.tsx"; 5 + import { publicClient, SLICE_URI } from "../../config.ts"; 6 + import { getHydratedWaitlistRequests } from "../slices/waitlist/api.ts"; 7 8 async function handleWaitlistPage(req: Request): Promise<Response> { 9 const context = await withAuth(req); ··· 14 const handle = url.searchParams.get("handle"); 15 const error = url.searchParams.get("error"); 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 + 30 return renderHTML( 31 <WaitlistPage 32 success={success} 33 handle={handle || undefined} 34 error={error || undefined} 35 currentUser={context.currentUser} 36 + recentRequests={recentRequests} 37 /> 38 ); 39 }
+6 -3
frontend/src/features/waitlist/templates/WaitlistPage.tsx
··· 3 import { WaitlistSuccess } from "./fragments/WaitlistSuccess.tsx"; 4 import { Text } from "../../../shared/fragments/Text.tsx"; 5 import type { AuthenticatedUser } from "../../../routes/middleware.ts"; 6 7 interface WaitlistPageProps { 8 success?: boolean; 9 handle?: string; 10 error?: string; 11 currentUser?: AuthenticatedUser; 12 } 13 14 export function WaitlistPage({ ··· 16 handle, 17 error, 18 currentUser, 19 }: WaitlistPageProps) { 20 return ( 21 <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"> 23 <div className="w-full max-w-md"> 24 {success ? ( 25 - <WaitlistSuccess handle={handle} /> 26 ) : ( 27 <> 28 <div className="text-center mb-8"> ··· 33 Be among the first to experience the future of AT Protocol ecosystem tools. 34 </Text> 35 </div> 36 - <WaitlistForm error={error} /> 37 </> 38 )} 39 </div>
··· 3 import { WaitlistSuccess } from "./fragments/WaitlistSuccess.tsx"; 4 import { Text } from "../../../shared/fragments/Text.tsx"; 5 import type { AuthenticatedUser } from "../../../routes/middleware.ts"; 6 + import type { NetworkSlicesWaitlistDefsRequestView } from "../../../client.ts"; 7 8 interface WaitlistPageProps { 9 success?: boolean; 10 handle?: string; 11 error?: string; 12 currentUser?: AuthenticatedUser; 13 + recentRequests?: NetworkSlicesWaitlistDefsRequestView[]; 14 } 15 16 export function WaitlistPage({ ··· 18 handle, 19 error, 20 currentUser, 21 + recentRequests, 22 }: WaitlistPageProps) { 23 return ( 24 <Layout title="Join the Waitlist - Slices" currentUser={currentUser}> 25 + <div className="min-h-[calc(100vh-3.5rem)] bg-white dark:bg-zinc-900 flex items-center justify-center px-4 py-16"> 26 <div className="w-full max-w-md"> 27 {success ? ( 28 + <WaitlistSuccess handle={handle} recentRequests={recentRequests} /> 29 ) : ( 30 <> 31 <div className="text-center mb-8"> ··· 36 Be among the first to experience the future of AT Protocol ecosystem tools. 37 </Text> 38 </div> 39 + <WaitlistForm error={error} recentRequests={recentRequests} /> 40 </> 41 )} 42 </div>
+44 -6
frontend/src/features/waitlist/templates/fragments/WaitlistForm.tsx
··· 3 import { Card } from "../../../../shared/fragments/Card.tsx"; 4 import { Text } from "../../../../shared/fragments/Text.tsx"; 5 import { FlashMessage } from "../../../../shared/fragments/FlashMessage.tsx"; 6 7 interface WaitlistFormProps { 8 error?: string; 9 } 10 11 - export function WaitlistForm({ error }: WaitlistFormProps) { 12 const getErrorMessage = (error: string) => { 13 switch (error) { 14 case "oauth_not_configured": ··· 19 return "Could not retrieve user information."; 20 case "waitlist_failed": 21 return "Failed to join waitlist. Please try again."; 22 default: 23 return "An error occurred. Please try again."; 24 } 25 }; 26 27 return ( 28 - <Card> 29 <form action="/auth/waitlist/initiate" method="POST"> 30 <div className="space-y-6"> 31 {error && ( ··· 36 <Input 37 label="Your handle" 38 name="handle" 39 - placeholder="alice.bsky.social" 40 required 41 /> 42 <Text as="p" size="xs" variant="muted"> ··· 45 </div> 46 47 <div className="space-y-4"> 48 - <Button type="submit" variant="primary" className="w-full justify-center"> 49 Join Waitlist 50 </Button> 51 52 <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. 54 </Text> 55 </div> 56 </div> 57 </form> 58 </Card> 59 ); 60 - }
··· 3 import { Card } from "../../../../shared/fragments/Card.tsx"; 4 import { Text } from "../../../../shared/fragments/Text.tsx"; 5 import { FlashMessage } from "../../../../shared/fragments/FlashMessage.tsx"; 6 + import { ActorAvatar } from "../../../../shared/fragments/ActorAvatar.tsx"; 7 + import type { NetworkSlicesWaitlistDefsRequestView } from "../../../../client.ts"; 8 9 interface WaitlistFormProps { 10 error?: string; 11 + recentRequests?: NetworkSlicesWaitlistDefsRequestView[]; 12 } 13 14 + export function WaitlistForm({ error, recentRequests }: WaitlistFormProps) { 15 const getErrorMessage = (error: string) => { 16 switch (error) { 17 case "oauth_not_configured": ··· 22 return "Could not retrieve user information."; 23 case "waitlist_failed": 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."; 29 default: 30 return "An error occurred. Please try again."; 31 } 32 }; 33 34 return ( 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 + 62 <form action="/auth/waitlist/initiate" method="POST"> 63 <div className="space-y-6"> 64 {error && ( ··· 69 <Input 70 label="Your handle" 71 name="handle" 72 + placeholder="user.bsky.social" 73 required 74 /> 75 <Text as="p" size="xs" variant="muted"> ··· 78 </div> 79 80 <div className="space-y-4"> 81 + <Button 82 + type="submit" 83 + variant="primary" 84 + className="w-full justify-center" 85 + > 86 Join Waitlist 87 </Button> 88 89 <Text as="p" size="xs" variant="muted" className="text-center"> 90 + By joining the waitlist, you'll be notified when Slices is ready 91 + for you. 92 </Text> 93 </div> 94 </div> 95 </form> 96 </Card> 97 ); 98 + }
+47 -4
frontend/src/features/waitlist/templates/fragments/WaitlistSuccess.tsx
··· 2 import { Card } from "../../../../shared/fragments/Card.tsx"; 3 import { Text } from "../../../../shared/fragments/Text.tsx"; 4 import { Link } from "../../../../shared/fragments/Link.tsx"; 5 import { Check } from "lucide-preact"; 6 7 interface WaitlistSuccessProps { 8 handle?: string; 9 } 10 11 - export function WaitlistSuccess({ handle }: WaitlistSuccessProps) { 12 return ( 13 - <Card className="text-center"> 14 <div className="flex justify-center mb-6"> 15 <div className="w-16 h-16 bg-green-100 dark:bg-green-900/30 rounded-full flex items-center justify-center"> 16 <Check size={32} className="text-green-600 dark:text-green-400" /> ··· 22 </Text> 23 24 <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. 26 </Text> 27 28 <div className="space-y-4"> 29 <Button href="/" variant="primary" className="w-full justify-center"> 30 Back to Home ··· 44 </div> 45 </Card> 46 ); 47 - }
··· 2 import { Card } from "../../../../shared/fragments/Card.tsx"; 3 import { Text } from "../../../../shared/fragments/Text.tsx"; 4 import { Link } from "../../../../shared/fragments/Link.tsx"; 5 + import { ActorAvatar } from "../../../../shared/fragments/ActorAvatar.tsx"; 6 import { Check } from "lucide-preact"; 7 + import type { NetworkSlicesWaitlistDefsRequestView } from "../../../../client.ts"; 8 9 interface WaitlistSuccessProps { 10 handle?: string; 11 + recentRequests?: NetworkSlicesWaitlistDefsRequestView[]; 12 } 13 14 + export function WaitlistSuccess({ 15 + handle, 16 + recentRequests, 17 + }: WaitlistSuccessProps) { 18 return ( 19 + <Card padding="md" className="text-center"> 20 <div className="flex justify-center mb-6"> 21 <div className="w-16 h-16 bg-green-100 dark:bg-green-900/30 rounded-full flex items-center justify-center"> 22 <Check size={32} className="text-green-600 dark:text-green-400" /> ··· 28 </Text> 29 30 <Text as="p" variant="secondary" className="mb-6"> 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. 43 </Text> 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 + 71 <div className="space-y-4"> 72 <Button href="/" variant="primary" className="w-full justify-center"> 73 Back to Home ··· 87 </div> 88 </Card> 89 ); 90 + }
+9 -3
frontend/src/lib/api.ts
··· 176 if (creator) { 177 const sparklineData = sparklinesMap[sliceRecord.uri]; 178 const statsData = statsMap[sliceRecord.uri]; 179 - sliceViews.push(sliceToView(sliceRecord, creator, sparklineData, statsData)); 180 } 181 } 182 ··· 215 if (creator) { 216 const sparklineData = sparklinesMap[sliceRecord.uri]; 217 const statsData = statsMap[sliceRecord.uri]; 218 - sliceViews.push(sliceToView(sliceRecord, creator, sparklineData, statsData)); 219 } 220 } 221 ··· 251 if (creator) { 252 const sparklineData = sparklinesMap[sliceRecord.uri]; 253 const statsData = statsMap[sliceRecord.uri]; 254 - sliceViews.push(sliceToView(sliceRecord, creator, sparklineData, statsData)); 255 } 256 } 257
··· 176 if (creator) { 177 const sparklineData = sparklinesMap[sliceRecord.uri]; 178 const statsData = statsMap[sliceRecord.uri]; 179 + sliceViews.push( 180 + sliceToView(sliceRecord, creator, sparklineData, statsData) 181 + ); 182 } 183 } 184 ··· 217 if (creator) { 218 const sparklineData = sparklinesMap[sliceRecord.uri]; 219 const statsData = statsMap[sliceRecord.uri]; 220 + sliceViews.push( 221 + sliceToView(sliceRecord, creator, sparklineData, statsData) 222 + ); 223 } 224 } 225 ··· 255 if (creator) { 256 const sparklineData = sparklinesMap[sliceRecord.uri]; 257 const statsData = statsMap[sliceRecord.uri]; 258 + sliceViews.push( 259 + sliceToView(sliceRecord, creator, sparklineData, statsData) 260 + ); 261 } 262 } 263
+5 -3
frontend/src/routes/mod.ts
··· 1 import type { Route } from "@std/http/unstable-route"; 2 import { landingRoutes } from "../features/landing/handlers.tsx"; 3 import { authRoutes } from "../features/auth/handlers.tsx"; 4 - import { waitlistRoutes } from "../features/waitlist/handlers.tsx"; 5 import { dashboardRoutes } from "../features/dashboard/handlers.tsx"; 6 import { 7 apiDocsRoutes, 8 codegenRoutes, ··· 14 settingsRoutes as sliceSettingsRoutes, 15 syncLogsRoutes, 16 syncRoutes, 17 } from "../features/slices/mod.ts"; 18 import { settingsRoutes } from "../features/settings/handlers.tsx"; 19 import { docsRoutes } from "../features/docs/handlers.tsx"; ··· 25 // Auth routes (login, oauth, logout) 26 ...authRoutes, 27 28 - // Waitlist page 29 - ...waitlistRoutes, 30 31 // Documentation routes 32 ...docsRoutes, ··· 45 ...syncRoutes, 46 ...syncLogsRoutes, 47 ...jetstreamRoutes, 48 49 // Dashboard routes (home page, create slice) 50 ...dashboardRoutes,
··· 1 import type { Route } from "@std/http/unstable-route"; 2 import { landingRoutes } from "../features/landing/handlers.tsx"; 3 import { authRoutes } from "../features/auth/handlers.tsx"; 4 import { dashboardRoutes } from "../features/dashboard/handlers.tsx"; 5 + import { waitlistRoutes as globalWaitlistRoutes } from "../features/waitlist/handlers.tsx"; 6 import { 7 apiDocsRoutes, 8 codegenRoutes, ··· 14 settingsRoutes as sliceSettingsRoutes, 15 syncLogsRoutes, 16 syncRoutes, 17 + waitlistRoutes as sliceWaitlistRoutes, 18 } from "../features/slices/mod.ts"; 19 import { settingsRoutes } from "../features/settings/handlers.tsx"; 20 import { docsRoutes } from "../features/docs/handlers.tsx"; ··· 26 // Auth routes (login, oauth, logout) 27 ...authRoutes, 28 29 + // Global waitlist page 30 + ...globalWaitlistRoutes, 31 32 // Documentation routes 33 ...docsRoutes, ··· 46 ...syncRoutes, 47 ...syncLogsRoutes, 48 ...jetstreamRoutes, 49 + ...sliceWaitlistRoutes, 50 51 // Dashboard routes (home page, create slice) 52 ...dashboardRoutes,
+2 -2
frontend/src/shared/fragments/Card.tsx
··· 8 variant?: CardVariant; 9 padding?: "none" | "sm" | "md" | "lg"; 10 className?: string; 11 - children: JSX.Element | JSX.Element[]; 12 } 13 14 interface CardHeaderProps { ··· 37 38 export function Card({ 39 variant = "default", 40 - padding = "md", 41 className, 42 children, 43 ...props
··· 8 variant?: CardVariant; 9 padding?: "none" | "sm" | "md" | "lg"; 10 className?: string; 11 + children: ComponentChildren; 12 } 13 14 interface CardHeaderProps { ··· 37 38 export function Card({ 39 variant = "default", 40 + padding = "none", 41 className, 42 children, 43 ...props
+8 -6
frontend/src/shared/fragments/FlashMessage.tsx
··· 14 if (type === "success") { 15 return ( 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"> 18 <CheckCircle2 19 - size={16} 20 style={{ fill: '#16a34a', stroke: 'white', strokeWidth: 1 }} 21 /> 22 - <Text variant="success"> 23 {message} 24 </Text> 25 </div> ··· 29 30 return ( 31 <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 <XCircle 34 - size={16} 35 style={{ fill: '#dc2626', stroke: 'white', strokeWidth: 1 }} 36 /> 37 - <Text variant="error"> 38 {message} 39 </Text> 40 </div>
··· 14 if (type === "success") { 15 return ( 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-3"> 18 <CheckCircle2 19 + size={20} 20 + className="flex-shrink-0" 21 style={{ fill: '#16a34a', stroke: 'white', strokeWidth: 1 }} 22 /> 23 + <Text variant="success" className="flex-1"> 24 {message} 25 </Text> 26 </div> ··· 30 31 return ( 32 <Card padding="sm" className={`mb-4 bg-red-50 dark:bg-red-900/20 border-red-200 dark:border-red-800 ${className}`}> 33 + <div className="flex items-center gap-3"> 34 <XCircle 35 + size={20} 36 + className="flex-shrink-0" 37 style={{ fill: '#dc2626', stroke: 'white', strokeWidth: 1 }} 38 /> 39 + <Text variant="error" className="flex-1"> 40 {message} 41 </Text> 42 </div>
+29 -13
frontend/src/shared/fragments/Layout.tsx
··· 31 <meta name="description" content={description} /> 32 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>" /> 35 36 {/* Open Graph / Facebook */} 37 <meta property="og:type" content="website" /> ··· 78 {showNavigation && <Navigation currentUser={currentUser} />} 79 <div 80 className={`min-h-screen flex flex-col ${ 81 - fullWidth ? "" : "max-w-5xl mx-auto sm:border-x border-zinc-200 dark:border-zinc-800" 82 }`} 83 style={backgroundStyle} 84 > 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 - )} 96 </div> 97 </body> 98 </html> 99 );
··· 31 <meta name="description" content={description} /> 32 33 {/* Favicon - Letter S */} 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 + /> 38 39 {/* Open Graph / Facebook */} 40 <meta property="og:type" content="website" /> ··· 81 {showNavigation && <Navigation currentUser={currentUser} />} 82 <div 83 className={`min-h-screen flex flex-col ${ 84 + fullWidth 85 + ? "" 86 + : "max-w-5xl mx-auto sm:border-x border-zinc-200 dark:border-zinc-800" 87 }`} 88 style={backgroundStyle} 89 > 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 + )} 109 </div> 110 + 111 + {/* Modal container for HTMX modals */} 112 + <div id="modal-container"></div> 113 </body> 114 </html> 115 );
+1 -1
frontend/src/shared/fragments/LogViewer.tsx
··· 27 const infoCount = logs.filter((l) => l.level === "info").length; 28 29 return ( 30 - <Card padding="none"> 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 <div className="flex items-center gap-4"> 33 <Text as="span" size="sm">
··· 27 const infoCount = logs.filter((l) => l.level === "info").length; 28 29 return ( 30 + <Card> 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 <div className="flex items-center gap-4"> 33 <Text as="span" size="sm">
+8 -6
frontend/src/shared/fragments/Navigation.tsx
··· 16 > 17 Slices 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> 25 </div> 26 <div className="flex items-center space-x-2"> 27 {currentUser?.isAuthenticated
··· 16 > 17 Slices 18 </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 + )} 27 </div> 28 <div className="flex items-center space-x-2"> 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 | "h3" 25 | "h4" 26 | "h5" 27 - | "h6"; 28 29 interface TextProps { 30 variant?: TextVariant;
··· 24 | "h3" 25 | "h4" 26 | "h5" 27 + | "h6" 28 + | "dt" 29 + | "dd"; 30 31 interface TextProps { 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 "properties": { 11 "avatar": { 12 "type": "blob", 13 - "accept": ["image/png", "image/jpeg"], 14 "maxSize": 1000000, 15 "description": "Small image to be displayed next to posts from account. AKA, 'profile picture'" 16 }, 17 "banner": { 18 "type": "blob", 19 - "accept": ["image/png", "image/jpeg"], 20 "maxSize": 1000000, 21 "description": "Larger horizontal image to display behind profile view." 22 }, 23 "labels": { 24 - "refs": ["com.atproto.label.defs#selfLabels"], 25 "type": "union", 26 "description": "Self-label values, specific to the Bluesky application, on the overall account." 27 }, ··· 53 "description": "A declaration of a Bluesky account profile." 54 } 55 } 56 - }
··· 10 "properties": { 11 "avatar": { 12 "type": "blob", 13 + "accept": [ 14 + "image/png", 15 + "image/jpeg" 16 + ], 17 "maxSize": 1000000, 18 "description": "Small image to be displayed next to posts from account. AKA, 'profile picture'" 19 }, 20 "banner": { 21 "type": "blob", 22 + "accept": [ 23 + "image/png", 24 + "image/jpeg" 25 + ], 26 "maxSize": 1000000, 27 "description": "Larger horizontal image to display behind profile view." 28 }, 29 "labels": { 30 + "refs": [ 31 + "com.atproto.label.defs#selfLabels" 32 + ], 33 "type": "union", 34 "description": "Self-label values, specific to the Bluesky application, on the overall account." 35 }, ··· 61 "description": "A declaration of a Bluesky account profile." 62 } 63 } 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 { 2 "name": "@slices/client", 3 - "version": "0.1.0-alpha.2", 4 "exports": "./src/mod.ts", 5 "imports": { 6 "@slices/oauth": "jsr:@slices/oauth@^0.4.1"
··· 1 { 2 "name": "@slices/client", 3 + "version": "0.1.0-alpha.3", 4 "exports": "./src/mod.ts", 5 "imports": { 6 "@slices/oauth": "jsr:@slices/oauth@^0.4.1"
+30 -7
packages/client/src/mod.ts
··· 44 | "collection" 45 | "uri" 46 | "cid" 47 - | "indexedAt"; 48 49 export interface SortField<TField extends string = string> { 50 field: TField; ··· 245 } 246 } 247 248 - throw new Error( 249 - `Request failed: ${response.status} ${response.statusText}` 250 - ); 251 } 252 253 return (await response.json()) as T; ··· 327 } 328 } 329 330 - throw new Error( 331 - `Blob upload failed: ${response.status} ${response.statusText}` 332 - ); 333 } 334 335 return await response.json();
··· 44 | "collection" 45 | "uri" 46 | "cid" 47 + | "indexedAt" 48 + | "json"; 49 50 export interface SortField<TField extends string = string> { 51 field: TField; ··· 246 } 247 } 248 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); 263 } 264 265 return (await response.json()) as T; ··· 339 } 340 } 341 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); 356 } 357 358 return await response.json();