import { defineHook, type GrainActorProfile } from "$hatk"; import { getSpaceSupport, pdsEndpointFor } from "../helpers/spaceSupport.ts"; export default defineHook("on-login", async (ctx) => { const { did, ensureRepo, lookup, db } = ctx; // Backfill repo in the background — large repos can block the login redirect ensureRepo(did).catch((err) => console.error(`[on-login] ensureRepo failed for ${did}:`, err)); // Warm the permissioned-spaces probe so the first page asking about it is // answered from cache. Backgrounded and swallowed: a PDS that never answers // must not hold up a login, and "no answer" is already read as "no spaces". pdsEndpointFor(db, did) .then((pds) => (pds ? getSpaceSupport(db, pds) : null)) .catch((err) => console.error(`[on-login] space support probe failed for ${did}:`, err)); // Check if user already has a populated grain profile const grainProfiles = await lookup("social.grain.actor.profile", "did", [did]); const grainProfile = grainProfiles.get(did); if (grainProfile?.value.displayName) return; // Fetch bsky profile directly from the user's PDS (fast, no backfill needed) const rows = (await db.query("SELECT pds_endpoint FROM _oauth_sessions WHERE did = $1", [ did, ])) as { pds_endpoint: string }[]; const pdsEndpoint = rows[0]?.pds_endpoint; if (!pdsEndpoint) return; const url = `${pdsEndpoint}/xrpc/com.atproto.repo.getRecord?repo=${encodeURIComponent(did)}&collection=app.bsky.actor.profile&rkey=self`; const res = await fetch(url); if (!res.ok) return; const { value: bsky } = (await res.json()) as { value: Record }; if (!bsky) return; const record: Record = { createdAt: grainProfile?.value.createdAt ?? new Date().toISOString(), }; if (bsky.displayName) record.displayName = bsky.displayName; if (bsky.description) record.description = bsky.description; if (bsky.avatar) record.avatar = bsky.avatar; if (grainProfile) { await ctx.putRecord("social.grain.actor.profile", "self", record); } else { await ctx.createRecord("social.grain.actor.profile", record, { rkey: "self" }); } });