Statusphere running on a slice 馃崟
at main 18 kB view raw
1// Generated TypeScript client for AT Protocol records 2// Generated at: 2025-08-28 00:22:31 UTC 3// Lexicons: 2 4 5/** 6 * @example Usage 7 * ```ts 8 * import { AtProtoClient } from "./generated_client.ts"; 9 * 10 * const client = new AtProtoClient( 11 * 'https://slices-api.fly.dev', 12 * 'at://did:plc:bcgltzqazw5tb6k2g3ttenbj/social.slices.slice/3lx5y476bws2q' 13 * ); 14 * 15 * // List records from the app.bsky.actor.profile collection 16 * const records = await client.app.bsky.actor.profile.listRecords(); 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 * // Search records in the collection 24 * const searchResults = await client.app.bsky.actor.profile.searchRecords({ 25 * query: "example search term" 26 * }); 27 * 28 * // Search specific field 29 * const fieldSearch = await client.app.bsky.actor.profile.searchRecords({ 30 * query: "blog", 31 * field: "title" 32 * }); 33 * 34 * // Serve the records as JSON 35 * Deno.serve(async () => new Response(JSON.stringify(records.records.map(r => r.value)))); 36 * ``` 37 */ 38 39import { OAuthClient } from "@slices/oauth"; 40 41export interface RecordResponse<T> { 42 uri: string; 43 cid: string; 44 did: string; 45 collection: string; 46 value: T; 47 indexedAt: string; 48} 49 50export interface ListRecordsResponse<T> { 51 records: RecordResponse<T>[]; 52 cursor?: string; 53} 54 55export interface GetActorsResponse { 56 actors: Actor[]; 57 cursor?: string; 58} 59 60export interface ListRecordsParams<TSortField extends string = string> { 61 author?: string; 62 authors?: string[]; 63 limit?: number; 64 cursor?: string; 65 sort?: 66 | `${TSortField}:${"asc" | "desc"}` 67 | `${TSortField}:${"asc" | "desc"},${TSortField}:${"asc" | "desc"}`; 68} 69 70export interface GetRecordParams { 71 uri: string; 72} 73 74export interface GetActorsParams { 75 search?: string; 76 dids?: string[]; 77 limit?: number; 78 cursor?: string; 79} 80 81export interface SearchRecordsParams<TSortField extends string = string> { 82 query: string; 83 field?: string; 84 limit?: number; 85 cursor?: string; 86 sort?: 87 | `${TSortField}:${"asc" | "desc"}` 88 | `${TSortField}:${"asc" | "desc"},${TSortField}:${"asc" | "desc"}`; 89} 90 91export interface IndexedRecord { 92 uri: string; 93 cid: string; 94 did: string; 95 collection: string; 96 value: Record<string, unknown>; 97 indexedAt: string; 98} 99 100export interface Actor { 101 did: string; 102 handle?: string; 103 sliceUri: string; 104 indexedAt: string; 105} 106 107export interface CodegenXrpcRequest { 108 target: string; 109 slice: string; 110} 111 112export interface CodegenXrpcResponse { 113 success: boolean; 114 generatedCode?: string; 115 error?: string; 116} 117 118export interface BulkSyncParams { 119 collections?: string[]; 120 externalCollections?: string[]; 121 repos?: string[]; 122 limitPerRepo?: number; 123} 124 125export interface BulkSyncOutput { 126 success: boolean; 127 totalRecords: number; 128 collectionsSynced: string[]; 129 reposProcessed: number; 130 message: string; 131} 132 133export interface SyncJobResponse { 134 success: boolean; 135 jobId?: string; 136 message: string; 137} 138 139export interface SyncJobResult { 140 success: boolean; 141 totalRecords: number; 142 collectionsSynced: string[]; 143 reposProcessed: number; 144 message: string; 145} 146 147export interface JobStatus { 148 jobId: string; 149 status: string; 150 createdAt: string; 151 startedAt?: string; 152 completedAt?: string; 153 result?: SyncJobResult; 154 error?: string; 155 retryCount: number; 156} 157 158export interface GetJobStatusParams { 159 jobId: string; 160} 161 162export interface GetJobHistoryParams { 163 userDid: string; 164 sliceUri: string; 165 limit?: number; 166} 167 168export type GetJobHistoryResponse = JobStatus[]; 169 170export interface CollectionStats { 171 collection: string; 172 recordCount: number; 173 uniqueActors: number; 174} 175 176export interface SliceStatsParams { 177 slice: string; 178} 179 180export interface SliceStatsOutput { 181 success: boolean; 182 collections: string[]; 183 collectionStats: CollectionStats[]; 184 totalLexicons: number; 185 totalRecords: number; 186 totalActors: number; 187 message?: string; 188} 189 190export interface SliceRecordsParams { 191 slice: string; 192 collection: string; 193 repo?: string; 194 limit?: number; 195 cursor?: string; 196} 197 198export interface SliceRecordsOutput { 199 success: boolean; 200 records: IndexedRecord[]; 201 cursor?: string; 202 message?: string; 203} 204 205export interface UploadBlobRequest { 206 data: ArrayBuffer | Uint8Array; 207 mimeType: string; 208} 209 210export interface BlobRef { 211 $type: string; 212 ref: string; 213 mimeType: string; 214 size: number; 215} 216 217export interface UploadBlobResponse { 218 blob: BlobRef; 219} 220 221export interface CollectionOperations<T> { 222 listRecords(params?: ListRecordsParams): Promise<ListRecordsResponse<T>>; 223 getRecord(params: GetRecordParams): Promise<RecordResponse<T>>; 224 searchRecords(params: SearchRecordsParams): Promise<ListRecordsResponse<T>>; 225} 226 227export interface AppBskyActorProfileRecord { 228 /** Small image to be displayed next to posts from account. AKA, 'profile picture' */ 229 avatar?: BlobRef; 230 /** Larger horizontal image to display behind profile view. */ 231 banner?: BlobRef; 232 createdAt?: string; 233 /** Free-form profile description text. */ 234 description?: string; 235 displayName?: string; 236} 237 238export type AppBskyActorProfileRecordSortFields = 239 | "createdAt" 240 | "description" 241 | "displayName"; 242 243export interface XyzStatusphereStatusRecord { 244 createdAt: string; 245 status: string; 246} 247 248export type XyzStatusphereStatusRecordSortFields = "createdAt" | "status"; 249 250class BaseClient { 251 protected readonly baseUrl: string; 252 protected oauthClient?: OAuthClient; 253 254 constructor(baseUrl: string, oauthClient?: OAuthClient) { 255 this.baseUrl = baseUrl; 256 this.oauthClient = oauthClient; 257 } 258 259 protected async ensureValidToken(): Promise<void> { 260 if (!this.oauthClient) { 261 throw new Error("OAuth client not configured"); 262 } 263 264 await this.oauthClient.ensureValidToken(); 265 } 266 267 protected async makeRequest<T = unknown>( 268 endpoint: string, 269 method?: "GET" | "POST" | "PUT" | "DELETE", 270 params?: Record<string, unknown> | unknown 271 ): Promise<T> { 272 return this.makeRequestWithRetry(endpoint, method, params, false); 273 } 274 275 private async makeRequestWithRetry<T = unknown>( 276 endpoint: string, 277 method?: "GET" | "POST" | "PUT" | "DELETE", 278 params?: Record<string, unknown> | unknown, 279 isRetry?: boolean 280 ): Promise<T> { 281 isRetry = isRetry ?? false; 282 const httpMethod = method || "GET"; 283 let url = `${this.baseUrl}/xrpc/${endpoint}`; 284 285 const requestInit: RequestInit = { 286 method: httpMethod, 287 headers: {}, 288 }; 289 290 // Add authorization header if OAuth client is available 291 if (this.oauthClient) { 292 try { 293 const tokens = await this.oauthClient.ensureValidToken(); 294 if (tokens.accessToken) { 295 (requestInit.headers as Record<string, string>)[ 296 "Authorization" 297 ] = `${tokens.tokenType} ${tokens.accessToken}`; 298 } 299 } catch (tokenError) { 300 // For write operations, OAuth tokens are required 301 if (httpMethod !== "GET") { 302 throw new Error( 303 `Authentication required: OAuth tokens are invalid or expired. Please log in again.` 304 ); 305 } 306 307 // For read operations, continue without auth (allow read-only operations) 308 } 309 } 310 311 if (httpMethod === "GET" && params) { 312 const searchParams = new URLSearchParams(); 313 Object.entries(params).forEach(([key, value]) => { 314 if (value !== undefined && value !== null) { 315 searchParams.append(key, String(value)); 316 } 317 }); 318 const queryString = searchParams.toString(); 319 if (queryString) { 320 url += "?" + queryString; 321 } 322 } else if (httpMethod !== "GET" && params) { 323 // Regular API endpoints expect JSON 324 (requestInit.headers as Record<string, string>)["Content-Type"] = 325 "application/json"; 326 requestInit.body = JSON.stringify(params); 327 } 328 329 const response = await fetch(url, requestInit); 330 if (!response.ok) { 331 // Handle 404 gracefully for GET requests 332 if (response.status === 404 && httpMethod === "GET") { 333 return null as T; 334 } 335 336 // Handle 401 Unauthorized - attempt token refresh and retry once 337 if ( 338 response.status === 401 && 339 !isRetry && 340 this.oauthClient && 341 httpMethod !== "GET" 342 ) { 343 try { 344 // Force token refresh by calling ensureValidToken again 345 await this.oauthClient.ensureValidToken(); 346 // Retry the request once with refreshed tokens 347 return this.makeRequestWithRetry(endpoint, method, params, true); 348 } catch (_refreshError) { 349 throw new Error( 350 `Authentication required: OAuth tokens are invalid or expired. Please log in again.` 351 ); 352 } 353 } 354 355 throw new Error( 356 `Request failed: ${response.status} ${response.statusText}` 357 ); 358 } 359 360 return (await response.json()) as T; 361 } 362} 363 364class ProfileActorBskyAppClient extends BaseClient { 365 private readonly sliceUri: string; 366 367 constructor(baseUrl: string, sliceUri: string, oauthClient?: OAuthClient) { 368 super(baseUrl, oauthClient); 369 this.sliceUri = sliceUri; 370 } 371 372 async listRecords( 373 params?: ListRecordsParams<AppBskyActorProfileRecordSortFields> 374 ): Promise<ListRecordsResponse<AppBskyActorProfileRecord>> { 375 const requestParams = { ...params, slice: this.sliceUri }; 376 return await this.makeRequest< 377 ListRecordsResponse<AppBskyActorProfileRecord> 378 >("app.bsky.actor.profile.list", "GET", requestParams); 379 } 380 381 async getRecord( 382 params: GetRecordParams 383 ): Promise<RecordResponse<AppBskyActorProfileRecord>> { 384 const requestParams = { ...params, slice: this.sliceUri }; 385 return await this.makeRequest<RecordResponse<AppBskyActorProfileRecord>>( 386 "app.bsky.actor.profile.get", 387 "GET", 388 requestParams 389 ); 390 } 391 392 async searchRecords( 393 params: SearchRecordsParams<AppBskyActorProfileRecordSortFields> 394 ): Promise<ListRecordsResponse<AppBskyActorProfileRecord>> { 395 const requestParams = { ...params, slice: this.sliceUri }; 396 return await this.makeRequest< 397 ListRecordsResponse<AppBskyActorProfileRecord> 398 >("app.bsky.actor.profile.searchRecords", "GET", requestParams); 399 } 400 401 async createRecord( 402 record: AppBskyActorProfileRecord, 403 useSelfRkey?: boolean 404 ): Promise<{ uri: string; cid: string }> { 405 const recordWithType = { $type: "app.bsky.actor.profile", ...record }; 406 const payload = useSelfRkey 407 ? { ...recordWithType, rkey: "self" } 408 : recordWithType; 409 return await this.makeRequest<{ uri: string; cid: string }>( 410 "app.bsky.actor.profile.create", 411 "POST", 412 payload 413 ); 414 } 415 416 async updateRecord( 417 rkey: string, 418 record: AppBskyActorProfileRecord 419 ): Promise<{ uri: string; cid: string }> { 420 const recordWithType = { $type: "app.bsky.actor.profile", ...record }; 421 return await this.makeRequest<{ uri: string; cid: string }>( 422 "app.bsky.actor.profile.update", 423 "POST", 424 { rkey, record: recordWithType } 425 ); 426 } 427 428 async deleteRecord(rkey: string): Promise<void> { 429 return await this.makeRequest<void>( 430 "app.bsky.actor.profile.delete", 431 "POST", 432 { rkey } 433 ); 434 } 435} 436 437class ActorBskyAppClient extends BaseClient { 438 readonly profile: ProfileActorBskyAppClient; 439 private readonly sliceUri: string; 440 441 constructor(baseUrl: string, sliceUri: string, oauthClient?: OAuthClient) { 442 super(baseUrl, oauthClient); 443 this.sliceUri = sliceUri; 444 this.profile = new ProfileActorBskyAppClient( 445 baseUrl, 446 sliceUri, 447 oauthClient 448 ); 449 } 450} 451 452class BskyAppClient extends BaseClient { 453 readonly actor: ActorBskyAppClient; 454 private readonly sliceUri: string; 455 456 constructor(baseUrl: string, sliceUri: string, oauthClient?: OAuthClient) { 457 super(baseUrl, oauthClient); 458 this.sliceUri = sliceUri; 459 this.actor = new ActorBskyAppClient(baseUrl, sliceUri, oauthClient); 460 } 461} 462 463class AppClient extends BaseClient { 464 readonly bsky: BskyAppClient; 465 private readonly sliceUri: string; 466 467 constructor(baseUrl: string, sliceUri: string, oauthClient?: OAuthClient) { 468 super(baseUrl, oauthClient); 469 this.sliceUri = sliceUri; 470 this.bsky = new BskyAppClient(baseUrl, sliceUri, oauthClient); 471 } 472} 473 474class StatusStatusphereXyzClient extends BaseClient { 475 private readonly sliceUri: string; 476 477 constructor(baseUrl: string, sliceUri: string, oauthClient?: OAuthClient) { 478 super(baseUrl, oauthClient); 479 this.sliceUri = sliceUri; 480 } 481 482 async listRecords( 483 params?: ListRecordsParams<XyzStatusphereStatusRecordSortFields> 484 ): Promise<ListRecordsResponse<XyzStatusphereStatusRecord>> { 485 const requestParams = { ...params, slice: this.sliceUri }; 486 return await this.makeRequest< 487 ListRecordsResponse<XyzStatusphereStatusRecord> 488 >("xyz.statusphere.status.list", "GET", requestParams); 489 } 490 491 async getRecord( 492 params: GetRecordParams 493 ): Promise<RecordResponse<XyzStatusphereStatusRecord>> { 494 const requestParams = { ...params, slice: this.sliceUri }; 495 return await this.makeRequest<RecordResponse<XyzStatusphereStatusRecord>>( 496 "xyz.statusphere.status.get", 497 "GET", 498 requestParams 499 ); 500 } 501 502 async searchRecords( 503 params: SearchRecordsParams<XyzStatusphereStatusRecordSortFields> 504 ): Promise<ListRecordsResponse<XyzStatusphereStatusRecord>> { 505 const requestParams = { ...params, slice: this.sliceUri }; 506 return await this.makeRequest< 507 ListRecordsResponse<XyzStatusphereStatusRecord> 508 >("xyz.statusphere.status.searchRecords", "GET", requestParams); 509 } 510 511 async createRecord( 512 record: XyzStatusphereStatusRecord, 513 useSelfRkey?: boolean 514 ): Promise<{ uri: string; cid: string }> { 515 const recordWithType = { $type: "xyz.statusphere.status", ...record }; 516 const payload = useSelfRkey 517 ? { ...recordWithType, rkey: "self" } 518 : recordWithType; 519 return await this.makeRequest<{ uri: string; cid: string }>( 520 "xyz.statusphere.status.create", 521 "POST", 522 payload 523 ); 524 } 525 526 async updateRecord( 527 rkey: string, 528 record: XyzStatusphereStatusRecord 529 ): Promise<{ uri: string; cid: string }> { 530 const recordWithType = { $type: "xyz.statusphere.status", ...record }; 531 return await this.makeRequest<{ uri: string; cid: string }>( 532 "xyz.statusphere.status.update", 533 "POST", 534 { rkey, record: recordWithType } 535 ); 536 } 537 538 async deleteRecord(rkey: string): Promise<void> { 539 return await this.makeRequest<void>( 540 "xyz.statusphere.status.delete", 541 "POST", 542 { rkey } 543 ); 544 } 545} 546 547class StatusphereXyzClient extends BaseClient { 548 readonly status: StatusStatusphereXyzClient; 549 private readonly sliceUri: string; 550 551 constructor(baseUrl: string, sliceUri: string, oauthClient?: OAuthClient) { 552 super(baseUrl, oauthClient); 553 this.sliceUri = sliceUri; 554 this.status = new StatusStatusphereXyzClient( 555 baseUrl, 556 sliceUri, 557 oauthClient 558 ); 559 } 560} 561 562class XyzClient extends BaseClient { 563 readonly statusphere: StatusphereXyzClient; 564 private readonly sliceUri: string; 565 566 constructor(baseUrl: string, sliceUri: string, oauthClient?: OAuthClient) { 567 super(baseUrl, oauthClient); 568 this.sliceUri = sliceUri; 569 this.statusphere = new StatusphereXyzClient(baseUrl, sliceUri, oauthClient); 570 } 571} 572 573export class AtProtoClient extends BaseClient { 574 readonly app: AppClient; 575 readonly xyz: XyzClient; 576 readonly oauth?: OAuthClient; 577 private readonly sliceUri: string; 578 579 constructor(baseUrl: string, sliceUri: string, oauthClient?: OAuthClient) { 580 super(baseUrl, oauthClient); 581 this.sliceUri = sliceUri; 582 this.app = new AppClient(baseUrl, sliceUri, oauthClient); 583 this.xyz = new XyzClient(baseUrl, sliceUri, oauthClient); 584 this.oauth = this.oauthClient; 585 } 586 587 async getActors(params: GetActorsParams): Promise<GetActorsResponse> { 588 const requestParams = { ...params, slice: this.sliceUri }; 589 return await this.makeRequest<GetActorsResponse>( 590 "social.slices.slice.getActors", 591 "GET", 592 requestParams 593 ); 594 } 595 596 uploadBlob(request: UploadBlobRequest): Promise<UploadBlobResponse> { 597 return this.uploadBlobWithRetry(request, false); 598 } 599 600 private async uploadBlobWithRetry( 601 request: UploadBlobRequest, 602 isRetry?: boolean 603 ): Promise<UploadBlobResponse> { 604 isRetry = isRetry ?? false; 605 // Special handling for blob upload with binary data 606 const httpMethod = "POST"; 607 const url = `${this.baseUrl}/xrpc/com.atproto.repo.uploadBlob`; 608 609 if (!this.oauthClient) { 610 throw new Error("OAuth client not configured"); 611 } 612 613 const tokens = await this.oauthClient.ensureValidToken(); 614 615 const requestInit: RequestInit = { 616 method: httpMethod, 617 headers: { 618 "Content-Type": request.mimeType, 619 Authorization: `${tokens.tokenType} ${tokens.accessToken}`, 620 }, 621 body: request.data, 622 }; 623 624 const response = await fetch(url, requestInit); 625 if (!response.ok) { 626 // Handle 401 Unauthorized - attempt token refresh and retry once 627 if (response.status === 401 && !isRetry && this.oauthClient) { 628 try { 629 // Force token refresh by calling ensureValidToken again 630 await this.oauthClient.ensureValidToken(); 631 // Retry the request once with refreshed tokens 632 return this.uploadBlobWithRetry(request, true); 633 } catch (_refreshError) { 634 throw new Error( 635 `Authentication required: OAuth tokens are invalid or expired. Please log in again.` 636 ); 637 } 638 } 639 640 throw new Error( 641 `Blob upload failed: ${response.status} ${response.statusText}` 642 ); 643 } 644 645 return await response.json(); 646 } 647}