~ knotless Search/
↑↓ move↵ open⇧↵ all results

chadtmiller.com/pds.js

An atproto PDS in JavaScript, for Cloudflare Workers and Node

0
0
main
255 lines · 6.4 KB Raw History
# 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:
```javascript
/**
 * @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:
```javascript
/**
 * @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>`:
```css
.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:
```javascript
<body><h2>Sign in to authorize</h2>
```

With:
```javascript
<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:
```javascript
</form></body></html>`;
```

With:
```javascript
</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:
```javascript
      return new Response(
        renderConsentPage({
          clientName: clientMetadata.client_name || clientId,
          clientId: clientId || '',
          scope: parameters.scope || 'atproto',
          requestUri: requestUri || '',
        }),
```

To:
```javascript
      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:
```javascript
    return new Response(
      renderConsentPage({
        clientName: clientMetadata.client_name || clientId,
        clientId: clientId,
        scope: scope || 'atproto',
        requestUri: newRequestUri,
      }),
```

To:
```javascript
    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**

```bash
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