From 2c55d78c481948ef24a5455557d4f0b35ef9cce7 Mon Sep 17 00:00:00 2001 From: Mark Bennett Date: Tue, 10 Feb 2026 00:16:20 +0000 Subject: [PATCH] Fix repository AT-URI to use record rkey instead of name Issue records were not appearing on tangled.org because the repo field used an incorrect AT-URI format with the repository name as the rkey instead of the actual record key. Changes: - Update buildRepoAtUri() to query PDS for sh.tangled.repo records - Find matching repository by name field in record value - Return record's URI which contains the correct rkey - Add comprehensive tests for new query-based resolution Before: at://did:plc:xxx/sh.tangled.repo/tangled-cli After: at://did:plc:xxx/sh.tangled.repo/3mef23waqwq22 Co-Authored-By: Claude Sonnet 4.5 --- src/utils/at-uri.ts | 41 +++++++++++++++++++++++++++++------------ tests/utils/at-uri.test.ts | 133 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++------- 2 file(s) changed, 155 insertion(s)(+), 19 deletion(s)(-) diff --git a/src/utils/at-uri.ts b/src/utils/at-uri.ts --- a/src/utils/at-uri.ts +++ b/src/utils/at-uri.ts @@ -64,25 +64,42 @@ * @param ownerDidOrHandle - DID (e.g., "did:plc:abc") or handle (e.g., "mark.bsky.social") * @param repoName - Repository name * @param client - Authenticated API client - * @returns AT-URI string (e.g., "at://did:plc:abc/sh.tangled.repo/repoName") + * @returns AT-URI string (e.g., "at://did:plc:abc/sh.tangled.repo/3mef23waqwq22") + * @throws Error if repository not found */ export async function buildRepoAtUri( ownerDidOrHandle: string, repoName: string, client: TangledApiClient ): Promise { - // Check if owner is already a DID + // Resolve owner to DID const isDid = ownerDidOrHandle.startsWith('did:'); + const did = isDid ? ownerDidOrHandle : await resolveHandleToDid(ownerDidOrHandle, client); - let did: string; - if (isDid) { - did = ownerDidOrHandle; - } else { - // Resolve handle to DID - did = await resolveHandleToDid(ownerDidOrHandle, client); + try { + // Query for sh.tangled.repo records + const response = await client.getAgent().com.atproto.repo.listRecords({ + repo: did, + collection: 'sh.tangled.repo', + limit: 100, // Reasonable limit for most users + }); + + // Find the record matching the repo name + const repoRecord = response.data.records.find((record) => { + const recordData = record.value as { name?: string }; + return recordData.name === repoName; + }); + + if (!repoRecord) { + throw new Error(`Repository '${repoName}' not found for ${ownerDidOrHandle}`); + } + + // Return the record's URI (which includes the correct rkey) + return repoRecord.uri; + } catch (error) { + if (error instanceof Error) { + throw new Error(`Failed to resolve repository AT-URI: ${error.message}`); + } + throw new Error('Failed to resolve repository AT-URI: Unknown error'); } - - // Construct AT-URI for repository - // Format: at://{did}/sh.tangled.repo/{repoName} - return `at://${did}/sh.tangled.repo/${repoName}`; } diff --git a/tests/utils/at-uri.test.ts b/tests/utils/at-uri.test.ts --- a/tests/utils/at-uri.test.ts +++ b/tests/utils/at-uri.test.ts @@ -163,15 +163,52 @@ mockClient = createMockClient(); }); - it('should build AT-URI from DID', async () => { + it('should query PDS and use repo record rkey', async () => { + const mockListRecords = vi.fn().mockResolvedValue({ + data: { + records: [ + { + uri: 'at://did:plc:abc123/sh.tangled.repo/3mef23waqwq22', + value: { name: 'my-repo', description: 'Test repo' }, + }, + ], + }, + }); + + vi.mocked(mockClient.getAgent).mockReturnValue({ + com: { + atproto: { + repo: { + listRecords: mockListRecords, + }, + }, + }, + } as never); + const result = await buildRepoAtUri('did:plc:abc123', 'my-repo', mockClient); - expect(result).toBe('at://did:plc:abc123/sh.tangled.repo/my-repo'); + expect(result).toBe('at://did:plc:abc123/sh.tangled.repo/3mef23waqwq22'); + expect(mockListRecords).toHaveBeenCalledWith({ + repo: 'did:plc:abc123', + collection: 'sh.tangled.repo', + limit: 100, + }); }); - it('should build AT-URI from handle', async () => { + it('should resolve handle then query for repo record', async () => { const mockResolve = vi.fn().mockResolvedValue({ data: { did: 'did:plc:abc123' }, + }); + + const mockListRecords = vi.fn().mockResolvedValue({ + data: { + records: [ + { + uri: 'at://did:plc:abc123/sh.tangled.repo/xyz789', + value: { name: 'my-repo' }, + }, + ], + }, }); vi.mocked(mockClient.getAgent).mockReturnValue({ @@ -180,20 +217,84 @@ identity: { resolveHandle: mockResolve, }, + repo: { + listRecords: mockListRecords, + }, }, }, } as never); const result = await buildRepoAtUri('mark.bsky.social', 'my-repo', mockClient); - expect(result).toBe('at://did:plc:abc123/sh.tangled.repo/my-repo'); + expect(result).toBe('at://did:plc:abc123/sh.tangled.repo/xyz789'); expect(mockResolve).toHaveBeenCalledWith({ handle: 'mark.bsky.social' }); + expect(mockListRecords).toHaveBeenCalledWith({ + repo: 'did:plc:abc123', + collection: 'sh.tangled.repo', + limit: 100, + }); }); - it('should handle repository names with special characters', async () => { - const result = await buildRepoAtUri('did:plc:abc123', 'repo-name_123', mockClient); + it('should find correct repo among multiple records', async () => { + const mockListRecords = vi.fn().mockResolvedValue({ + data: { + records: [ + { + uri: 'at://did:plc:abc123/sh.tangled.repo/aaa111', + value: { name: 'other-repo' }, + }, + { + uri: 'at://did:plc:abc123/sh.tangled.repo/bbb222', + value: { name: 'target-repo' }, + }, + { + uri: 'at://did:plc:abc123/sh.tangled.repo/ccc333', + value: { name: 'another-repo' }, + }, + ], + }, + }); - expect(result).toBe('at://did:plc:abc123/sh.tangled.repo/repo-name_123'); + vi.mocked(mockClient.getAgent).mockReturnValue({ + com: { + atproto: { + repo: { + listRecords: mockListRecords, + }, + }, + }, + } as never); + + const result = await buildRepoAtUri('did:plc:abc123', 'target-repo', mockClient); + + expect(result).toBe('at://did:plc:abc123/sh.tangled.repo/bbb222'); + }); + + it('should throw error when repository not found', async () => { + const mockListRecords = vi.fn().mockResolvedValue({ + data: { + records: [ + { + uri: 'at://did:plc:abc123/sh.tangled.repo/xyz789', + value: { name: 'different-repo' }, + }, + ], + }, + }); + + vi.mocked(mockClient.getAgent).mockReturnValue({ + com: { + atproto: { + repo: { + listRecords: mockListRecords, + }, + }, + }, + } as never); + + await expect(buildRepoAtUri('did:plc:abc123', 'nonexistent-repo', mockClient)).rejects.toThrow( + "Repository 'nonexistent-repo' not found for did:plc:abc123" + ); }); it('should throw error when handle resolution fails', async () => { @@ -211,6 +312,24 @@ await expect(buildRepoAtUri('mark.bsky.social', 'my-repo', mockClient)).rejects.toThrow( "Failed to resolve handle 'mark.bsky.social': Resolution failed" + ); + }); + + it('should throw error when listRecords fails', async () => { + const mockListRecords = vi.fn().mockRejectedValue(new Error('API error')); + + vi.mocked(mockClient.getAgent).mockReturnValue({ + com: { + atproto: { + repo: { + listRecords: mockListRecords, + }, + }, + }, + } as never); + + await expect(buildRepoAtUri('did:plc:abc123', 'my-repo', mockClient)).rejects.toThrow( + 'Failed to resolve repository AT-URI: API error' ); }); }); -- tangled.sh