grain.social is a photo sharing platform built on atproto. grain.social
atproto photography appview
57
fork

Configure Feed

Select the types of activity you want to include in your feed.

feat: resolve handles in AT URIs at XRPC layer

Add resolveHandle helper to convert handles to DIDs via _repos table.
Apply AT URI handle resolution in getGallery and getStory so URLs like
/profile/chadtmiller.com/gallery/... work without frontend resolution.
Bump hatk to alpha.54 for feed-layer handle resolution.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

+47 -7
+4 -4
package-lock.json
··· 6 6 "": { 7 7 "name": "grain", 8 8 "dependencies": { 9 - "@hatk/hatk": "^0.0.1-alpha.53", 9 + "@hatk/hatk": "^0.0.1-alpha.54", 10 10 "@sveltejs/adapter-node": "^5.5.4", 11 11 "@sveltejs/kit": "^2.55.0", 12 12 "@tanstack/svelte-query": "^6.1.0", ··· 163 163 } 164 164 }, 165 165 "node_modules/@hatk/hatk": { 166 - "version": "0.0.1-alpha.53", 167 - "resolved": "https://registry.npmjs.org/@hatk/hatk/-/hatk-0.0.1-alpha.53.tgz", 168 - "integrity": "sha512-gZAzTOHz/JfiRngfsePD6v5O3czzIh2Msi8rvzjsJcCDvOaisXQKqb5RoT17PVx+zDGSiQATT+xyqUpGYOeUow==", 166 + "version": "0.0.1-alpha.54", 167 + "resolved": "https://registry.npmjs.org/@hatk/hatk/-/hatk-0.0.1-alpha.54.tgz", 168 + "integrity": "sha512-ywPdR2FEi9N2Pg0R6IbWFRapn/U9yUXwILIgFpNlYUKR/i90zNRuha2+jeWgJgKk1xbJAb2DkipRP4Jukg5MKg==", 169 169 "license": "MIT", 170 170 "dependencies": { 171 171 "@bigmoves/lexicon": "^0.2.2",
+1 -1
package.json
··· 12 12 "test:browser": "npx playwright test" 13 13 }, 14 14 "dependencies": { 15 - "@hatk/hatk": "^0.0.1-alpha.53", 15 + "@hatk/hatk": "^0.0.1-alpha.54", 16 16 "@sveltejs/adapter-node": "^5.5.4", 17 17 "@sveltejs/kit": "^2.55.0", 18 18 "@tanstack/svelte-query": "^6.1.0",
+30
server/helpers/resolveHandle.ts
··· 1 + /** 2 + * Resolve a handle to a DID using _repos. 3 + * Returns the DID if already a DID, or looks up the handle. 4 + */ 5 + export async function resolveHandle( 6 + db: { query: (sql: string, params?: unknown[]) => Promise<unknown[]> }, 7 + actor: string, 8 + ): Promise<string | null> { 9 + if (actor.startsWith("did:")) return actor; 10 + const rows = (await db.query(`SELECT did FROM _repos WHERE handle = $1`, [actor])) as { 11 + did: string; 12 + }[]; 13 + return rows[0]?.did ?? null; 14 + } 15 + 16 + /** 17 + * Resolve a handle inside an AT URI (at://handle/...) to a DID-based URI. 18 + * Returns the URI unchanged if it already uses a DID. 19 + */ 20 + export async function resolveAtUri( 21 + db: { query: (sql: string, params?: unknown[]) => Promise<unknown[]> }, 22 + uri: string, 23 + ): Promise<string | null> { 24 + const match = uri.match(/^at:\/\/([^/]+)\/(.+)$/); 25 + if (!match) return null; 26 + const [, authority, path] = match; 27 + const did = await resolveHandle(db, authority); 28 + if (!did) return null; 29 + return `at://${did}/${path}`; 30 + }
+6 -1
server/xrpc/getGallery.ts
··· 1 1 import { defineQuery, InvalidRequestError } from "$hatk"; 2 2 import type { Gallery } from "$hatk"; 3 3 import { hydrateGalleries } from "../hydrate/galleries.ts"; 4 + import { resolveAtUri } from "../helpers/resolveHandle.ts"; 4 5 5 6 export default defineQuery("social.grain.unspecced.getGallery", async (ctx) => { 6 7 const { ok, params, db } = ctx; 7 - const { gallery: galleryUri } = params; 8 + let { gallery: galleryUri } = params; 9 + 10 + // Resolve handle in AT URI if needed 11 + const resolved = await resolveAtUri(db, galleryUri); 12 + if (resolved) galleryUri = resolved; 8 13 9 14 const rows = (await db.query(`SELECT * FROM "social.grain.gallery" WHERE uri = $1`, [ 10 15 galleryUri,
+6 -1
server/xrpc/getStory.ts
··· 3 3 import type { GrainActorProfile, Story, Label } from "$hatk"; 4 4 import { lookupCrossPosts } from "../hydrate/galleries.ts"; 5 5 import { lookupHandles } from "../helpers/lookupHandles.ts"; 6 + import { resolveAtUri } from "../helpers/resolveHandle.ts"; 6 7 7 8 export default defineQuery("social.grain.unspecced.getStory", async (ctx) => { 8 9 const { db, ok } = ctx; 9 - const storyUri = ctx.params.story; 10 + let storyUri = ctx.params.story; 10 11 if (!storyUri) return ok({}); 12 + 13 + // Resolve handle in AT URI if needed 14 + const resolved = await resolveAtUri(db, storyUri); 15 + if (resolved) storyUri = resolved; 11 16 12 17 const rows = (await db.query( 13 18 `SELECT uri, cid, did, media, aspect_ratio, location, address, created_at