Consent Page Profile Card Implementation Plan#
For Claude: REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.
Goal: Show the authorizing user's Bluesky profile (avatar, name, handle) on the OAuth consent page.
Architecture: Add inline HTML/CSS/JS to the consent page. Profile is fetched client-side from Bluesky's public API using the login_hint parameter. Graceful degradation if fetch fails.
Tech Stack: Vanilla JS, Bluesky public API (app.bsky.actor.getProfile)
Task 1: Update renderConsentPage signature#
Files:
- Modify:
src/pds.js:5008-5017(function signature and JSDoc)
Step 1: Add loginHint to JSDoc and parameters
Change the function signature from:
/**
* @param {{ clientName: string, clientId: string, scope: string, requestUri: string, error?: string }} params
* @returns {string} HTML page content
*/
function renderConsentPage({
clientName,
clientId,
scope,
requestUri,
error = '',
}) {
To:
/**
* @param {{ clientName: string, clientId: string, scope: string, requestUri: string, loginHint?: string, error?: string }} params
* @returns {string} HTML page content
*/
function renderConsentPage({
clientName,
clientId,
scope,
requestUri,
loginHint = '',
error = '',
}) {
Step 2: Verify syntax is correct
Run: node --check src/pds.js
Expected: No output (success)
Task 2: Add profile card CSS#
Files:
- Modify:
src/pds.js:5027-5055(inside the<style>block)
Step 1: Add profile card styles after existing styles
Add before </style></head>:
.profile-card{display:flex;align-items:center;gap:12px;padding:16px;background:#2a2a2a;border-radius:8px;margin-bottom:20px}
.profile-card.loading .avatar{background:#404040;animation:pulse 1.5s infinite}
.profile-card .avatar{width:48px;height:48px;border-radius:50%;background:#404040;flex-shrink:0}
.profile-card .avatar img{width:100%;height:100%;border-radius:50%;object-fit:cover}
.profile-card .info{min-width:0}
.profile-card .name{color:#fff;font-weight:500;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.profile-card .handle{color:#808080;font-size:14px}
@keyframes pulse{0%,100%{opacity:1}50%{opacity:0.5}}
Step 2: Verify syntax is correct
Run: node --check src/pds.js
Expected: No output (success)
Task 3: Add profile card HTML#
Files:
- Modify:
src/pds.js:5056-5057(after<body>opening, before<h2>)
Step 1: Add profile card HTML conditionally
Replace:
<body><h2>Sign in to authorize</h2>
With:
<body>
${loginHint ? `<div class="profile-card loading" id="profile-card">
<div class="avatar" id="profile-avatar"></div>
<div class="info"><div class="name" id="profile-name">Loading...</div>
<div class="handle" id="profile-handle">${escapeHtml(loginHint.startsWith('did:') ? loginHint : '@' + loginHint)}</div></div>
</div>` : ''}
<h2>Sign in to authorize</h2>
Step 2: Verify syntax is correct
Run: node --check src/pds.js
Expected: No output (success)
Task 4: Add profile fetch script#
Files:
- Modify:
src/pds.js:5066(before</body></html>)
Step 1: Add inline script to fetch profile
Replace:
</form></body></html>`;
With:
</form>
${loginHint ? `<script>
(async()=>{
const card=document.getElementById('profile-card');
if(!card)return;
try{
const r=await fetch('https://public.api.bsky.app/xrpc/app.bsky.actor.getProfile?actor='+encodeURIComponent('${escapeHtml(loginHint)}'));
if(!r.ok)throw new Error();
const p=await r.json();
document.getElementById('profile-avatar').innerHTML=p.avatar?'<img src="'+p.avatar+'" alt="">':'';
document.getElementById('profile-name').textContent=p.displayName||p.handle;
document.getElementById('profile-handle').textContent='@'+p.handle;
card.classList.remove('loading');
}catch(e){card.classList.remove('loading')}
})();
</script>` : ''}
</body></html>`;
Step 2: Verify syntax is correct
Run: node --check src/pds.js
Expected: No output (success)
Task 5: Pass loginHint from PAR flow#
Files:
- Modify:
src/pds.js:3954-3959(PAR flow renderConsentPage call)
Step 1: Add loginHint to renderConsentPage call
Change:
return new Response(
renderConsentPage({
clientName: clientMetadata.client_name || clientId,
clientId: clientId || '',
scope: parameters.scope || 'atproto',
requestUri: requestUri || '',
}),
To:
return new Response(
renderConsentPage({
clientName: clientMetadata.client_name || clientId,
clientId: clientId || '',
scope: parameters.scope || 'atproto',
requestUri: requestUri || '',
loginHint: parameters.login_hint || '',
}),
Step 2: Verify syntax is correct
Run: node --check src/pds.js
Expected: No output (success)
Task 6: Pass loginHint from direct flow#
Files:
- Modify:
src/pds.js:4022-4027(direct flow renderConsentPage call)
Step 1: Add loginHint to renderConsentPage call
Change:
return new Response(
renderConsentPage({
clientName: clientMetadata.client_name || clientId,
clientId: clientId,
scope: scope || 'atproto',
requestUri: newRequestUri,
}),
To:
return new Response(
renderConsentPage({
clientName: clientMetadata.client_name || clientId,
clientId: clientId,
scope: scope || 'atproto',
requestUri: newRequestUri,
loginHint: loginHint || '',
}),
Step 2: Verify syntax is correct
Run: node --check src/pds.js
Expected: No output (success)
Task 7: Run tests and commit#
Step 1: Run full test suite
Run: npm test
Expected: All 126 tests pass
Step 2: Commit changes
git add src/pds.js docs/plans/2025-01-09-consent-profile-card.md
git commit -m "feat: add profile card to OAuth consent page
Shows the authorizing user's avatar, display name, and handle
on the consent page. Fetches from Bluesky public API using
the login_hint parameter. Degrades gracefully if fetch fails."
Manual Testing#
After implementation, test by:
- Start local PDS:
npx wrangler dev - Trigger OAuth flow with login_hint parameter
- Verify profile card shows on consent page
- Verify it degrades gracefully with invalid login_hint