this repo has no description

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:

  1. Start local PDS: npx wrangler dev
  2. Trigger OAuth flow with login_hint parameter
  3. Verify profile card shows on consent page
  4. Verify it degrades gracefully with invalid login_hint