Social Annotations in the Atmosphere
15
fork

Configure Feed

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

WIP 2: electric bugaloo

+2879 -2500
-1
.beads/beads.base.jsonl
··· 1 - {"id":"seams.so-iji","title":"Update AT Protocol OAuth config with Chrome Web Store extension ID","status":"in_progress","priority":1,"issue_type":"task","created_at":"2025-11-14T20:54:15.969349171-08:00","updated_at":"2025-11-14T21:09:37.328022945-08:00"}
-1
.beads/beads.base.meta.json
··· 1 - {"version":"0.23.0","timestamp":"2025-11-14T21:10:33.177343555-08:00","commit":"8be3e44"}
-1
.beads/beads.left.jsonl
··· 1 - {"id":"seams.so-iji","content_hash":"3f105051d1c42a39ebc3af2d850ed6b60a58bfefa2753ce246692a44b3f57ab3","title":"Update AT Protocol OAuth config with Chrome Web Store extension ID","description":"","status":"in_progress","priority":1,"issue_type":"task","created_at":"2025-11-14T20:54:15.969349171-08:00","updated_at":"2025-11-14T21:09:37.328022945-08:00","source_repo":"."}
-1
.beads/beads.left.meta.json
··· 1 - {"version":"0.23.0","timestamp":"2025-11-14T21:10:31.078894927-08:00","commit":"8be3e44"}
+5
.beads/issues.jsonl
··· 1 + {"id":"seams.so-btm","content_hash":"cc23540734a41c32c1bfab62a7d682eeda62166167d038a47afc22dea5626273","title":"Implement URL Share Intent for Proxy PWA","description":"","status":"open","priority":2,"issue_type":"feature","created_at":"2025-11-18T23:26:19.717576506-08:00","updated_at":"2025-11-18T23:26:19.717576506-08:00","source_repo":"."} 2 + {"id":"seams.so-iji","content_hash":"3f105051d1c42a39ebc3af2d850ed6b60a58bfefa2753ce246692a44b3f57ab3","title":"Update AT Protocol OAuth config with Chrome Web Store extension ID","description":"","status":"closed","priority":1,"issue_type":"task","created_at":"2025-11-14T20:54:15.969349171-08:00","updated_at":"2025-11-14T21:10:31.150138092-08:00","closed_at":"2025-11-14T21:10:31.150138092-08:00","source_repo":"."} 3 + {"id":"seams.so-rlq","content_hash":"1812949c8de2a7f3465bee85295f92c3d923568d47e9b4df15dc25c01ddc798f","title":"Configure Proxy Service as PWA","description":"","status":"open","priority":2,"issue_type":"feature","created_at":"2025-11-18T23:26:16.152745718-08:00","updated_at":"2025-11-18T23:26:16.152745718-08:00","source_repo":"."} 4 + {"id":"seams.so-s8c","content_hash":"be3aca226e4e9749d46f83c0efa735455b25a44d019b4a005bc0669acb0f3596","title":"Ensure persistent login for Proxy PWA","description":"","status":"open","priority":2,"issue_type":"task","created_at":"2025-11-18T23:26:19.859051558-08:00","updated_at":"2025-11-18T23:26:19.859051558-08:00","source_repo":"."} 5 + {"id":"seams.so-vp1","content_hash":"3ba0850cb5f709e86796104a6804d665d1de9f9eddb63f919728ad711f8d4ed6","title":"Add Shadow DOM wrapper for sidebar iframe container","description":"","status":"open","priority":3,"issue_type":"task","created_at":"2025-11-15T02:07:42.906585543-08:00","updated_at":"2025-11-15T02:07:42.906585543-08:00","source_repo":"."}
+4 -3
.gitignore
··· 9 9 10 10 node_modules 11 11 .output 12 + result 12 13 stats.html 13 14 stats-*.json 14 15 .wxt ··· 43 44 dns/creds.json 44 45 45 46 # Via proxy build artifacts 46 - pywb-test/static/seams-*.js 47 - pywb-test/static/seams-*.js.map 48 - pywb-test/static/assets/ 47 + proxy/static/seams-*.js 48 + proxy/static/seams-*.js.map 49 + proxy/static/assets/
+12 -6
Caddyfile
··· 1 1 # Seams Via Proxy - Development Configuration 2 2 # Use http:// to disable automatic HTTPS 3 - http://localhost:8082 { 3 + :8082 { 4 + bind 0.0.0.0 4 5 # Proxy routes to pywb (must come first) 5 6 handle /proxy/* { 6 - reverse_proxy localhost:8080 7 + reverse_proxy 127.0.0.1:8081 { 8 + # Trust upstream headers for scheme/host 9 + header_up X-Forwarded-Proto {header.X-Forwarded-Proto} 10 + header_up X-Forwarded-Host {host} 11 + header_down -Content-Security-Policy 12 + } 7 13 } 8 14 9 15 # Proxy static files served by pywb 10 16 handle /static/* { 11 - reverse_proxy localhost:8080 17 + reverse_proxy 127.0.0.1:8081 12 18 } 13 19 14 20 # Via landing page at root 15 21 handle / { 16 - root * pywb-test/static 22 + root * proxy/static 17 23 rewrite * /via-landing.html 18 24 file_server 19 25 } 20 26 21 - # Serve static files (CSS, JS, etc) from pywb-test/static 27 + # Serve static files (CSS, JS, etc) from proxy/static 22 28 handle /* { 23 - root * pywb-test/static 29 + root * proxy/static 24 30 file_server 25 31 } 26 32
PENDING_TASKS.md history/PENDING_TASKS.md
+1 -1
README.md
··· 6 6 7 7 - **Browser Extension** (TypeScript/WXT) - Chrome/Firefox extension for creating and viewing annotations 8 8 - **Backend Server** (Go) - SQLite-backed indexing service for querying annotations by URL 9 - - **Landing Page** (`public/`) - Public feed of recent annotations 9 + - **Landing Page** (`landing/`) - Public feed of recent annotations 10 10 11 11 ## Quick Start 12 12
STORAGE_FIRST_ARCHITECTURE.md history/STORAGE_FIRST_ARCHITECTURE.md
+36
deploy-proxy.sh
··· 1 + #!/usr/bin/env bash 2 + set -e 3 + 4 + # Define app name 5 + APP_NAME="sure-seams-so" 6 + CONFIG_FILE="fly.proxy.toml" 7 + 8 + echo "Building proxy service" 9 + pnpm build:via 10 + 11 + echo "➕ Adding static assets to git index (for Nix)..." 12 + git add -f proxy/static 13 + 14 + echo "🏗️ Building Proxy Docker image with Nix..." 15 + nix build .#proxy 16 + 17 + echo "➖ Resetting git index..." 18 + git reset proxy/static 19 + 20 + echo "📦 Loading image into Docker..." 21 + docker load < result 22 + 23 + echo "🏷️ Tagging image for Fly.io registry..." 24 + docker tag seams-proxy:latest registry.fly.io/$APP_NAME:latest 25 + 26 + echo "🔐 Authenticating with Fly.io Docker registry..." 27 + flyctl auth docker 28 + 29 + echo "⬆️ Pushing image to Fly.io..." 30 + docker push registry.fly.io/$APP_NAME:latest 31 + 32 + echo "🚀 Deploying to Fly.io..." 33 + flyctl deploy --config $CONFIG_FILE --image registry.fly.io/$APP_NAME:latest 34 + 35 + echo "✅ Proxy deployment complete!" 36 + echo "🌐 Visit: https://sure.seams.so"
+4
dns/dnsconfig.js
··· 8 8 // Fly.io app endpoints 9 9 A("@", "66.241.124.49"), 10 10 AAAA("@", "2a09:8280:1::b0:1269:0"), 11 + 12 + // Proxy (sure.seams.so) endpoints 13 + A("sure", "66.241.124.243"), 14 + AAAA("sure", "2a09:8280:1::b1:efa2:0"), 11 15 12 16 // ACME/Let's Encrypt DNS challenge for Fly.io 13 17 CNAME("_acme-challenge", "seams.so.wdzq2d2.flydns.net."),
-7
entrypoints/background.ts
··· 1 1 import { BrowserStorageAdapter, ExtensionBackgroundWorker } from '@seams/core'; 2 - import { loadSession } from '@/lib/oauth'; 3 - import { listAnnotations, listComments } from '@/lib/pds'; 4 2 5 3 const BACKEND_URL = import.meta.env.BACKEND_URL || 'http://localhost:8080'; 6 4 ··· 9 7 10 8 const worker = new ExtensionBackgroundWorker({ 11 9 storage, 12 - fetchAnnotationsForUrl: async (url: string) => { 13 - return []; 14 - }, 15 - fetchUserAnnotations: listAnnotations, 16 - fetchComments: listComments, 17 10 backendUrl: BACKEND_URL, 18 11 }); 19 12
+45 -583
entrypoints/sidepanel/main.ts
··· 1 1 import './style.css'; 2 - import type { Annotation } from '@/lib/types/annotation'; 3 - import type { Comment } from '@/lib/types/comment'; 4 - import { initializeOAuth, startLoginProcess, loadSession, clearSession, getProfile } from '@/lib/oauth'; 5 - import { createAnnotation, createComment } from '@/lib/pds'; 2 + import { BrowserStorageAdapter, ExtensionOAuthLauncher, Sidebar } from '@seams/core'; 6 3 7 4 console.log('[sidepanel] sidepanel script loading...'); 8 5 9 - // Initialize OAuth 10 - initializeOAuth(); 11 - 12 - // Run immediately, not wrapped in defineUnlistedScript 13 6 (function() { 14 7 const app = document.getElementById('app'); 15 8 if (!app) return; 16 9 17 - let currentTabId: number | null = null; 18 - let currentUrl = ''; 19 - let currentSelection: { text: string; selectors: any[] } | null = null; 20 - let pageAnnotations: Annotation[] = []; 21 - let allComments: Comment[] = []; 22 - const collapsedThreads = new Set<string>(); 23 - const activeReplyForms = new Set<string>(); 24 - 25 - function normalizeUrl(url: string): string { 26 - try { 27 - const parsed = new URL(url); 28 - // Remove fragment 29 - parsed.hash = ''; 30 - // Remove trailing slash 31 - let path = parsed.pathname; 32 - if (path.endsWith('/') && path !== '/') { 33 - path = path.slice(0, -1); 34 - } 35 - parsed.pathname = path; 36 - return parsed.toString(); 37 - } catch { 38 - return url; 39 - } 40 - } 41 - 42 - app.innerHTML = ` 43 - <div class="sidebar"> 44 - <div class="auth-section" id="auth-section"> 45 - <div class="login-container"> 46 - <h2>Login to Seams</h2> 47 - <div class="input-wrapper"> 48 - <span class="at-symbol">@</span> 49 - <input type="text" id="handle-input" class="handle-input" placeholder="you.bsky.social" /> 50 - </div> 51 - <button id="login-btn">Login with ATProto</button> 52 - <div id="auth-status"></div> 53 - </div> 54 - </div> 55 - <div class="content-section" id="content-section" style="display: none;"> 56 - <div class="annotation-form" id="annotation-form" style="display: none;"> 57 - <div class="form-header"> 58 - <h2>Create Annotation</h2> 59 - <button id="clear-selection-btn" class="clear-btn">×</button> 60 - </div> 61 - <div id="selected-text" class="selected-text"></div> 62 - <textarea id="annotation-text" placeholder="Add your note..."></textarea> 63 - <button id="save-btn">Save Annotation</button> 64 - </div> 65 - <div class="annotations-list"> 66 - <h2>Annotations on this page</h2> 67 - <div id="annotations"></div> 68 - </div> 69 - </div> 70 - <div class="profile-menu"> 71 - <img id="profile-avatar" style="display: none; width: 40px; height: 40px; border-radius: 50%; cursor: pointer;" /> 72 - <div id="profile-dropdown" class="profile-dropdown" style="display: none;"> 73 - <button id="logout-btn">Logout</button> 74 - </div> 75 - </div> 76 - </div> 77 - `; 78 - 79 - const selectedTextEl = document.getElementById('selected-text'); 80 - const annotationTextarea = document.getElementById('annotation-text') as HTMLTextAreaElement; 81 - const saveBtn = document.getElementById('save-btn'); 82 - const annotationsContainer = document.getElementById('annotations'); 83 - const handleInput = document.getElementById('handle-input') as HTMLInputElement; 84 - const loginBtn = document.getElementById('login-btn'); 85 - const logoutBtn = document.getElementById('logout-btn'); 86 - const authStatus = document.getElementById('auth-status'); 87 - const profileAvatar = document.getElementById('profile-avatar') as HTMLImageElement; 88 - const profileDropdown = document.getElementById('profile-dropdown'); 89 - const authSection = document.getElementById('auth-section'); 90 - const contentSection = document.getElementById('content-section'); 91 - const annotationForm = document.getElementById('annotation-form'); 92 - const clearSelectionBtn = document.getElementById('clear-selection-btn'); 93 - 94 - // Check for existing session 95 - loadSession().then(async session => { 96 - if (session) { 97 - try { 98 - const profile = await getProfile(session); 99 - if (profileAvatar && profile.avatar) { 100 - profileAvatar.src = profile.avatar; 101 - profileAvatar.style.display = 'block'; 102 - } 103 - if (authSection) authSection.style.display = 'none'; 104 - if (contentSection) contentSection.style.display = 'block'; 105 - 106 - // Trigger sync on sidepanel open if already logged in 107 - browser.runtime.sendMessage({ type: 'SYNC_CACHE' }); 108 - } catch (error) { 109 - console.error('Failed to fetch profile:', error); 110 - if (authSection) authSection.style.display = 'none'; 111 - if (contentSection) contentSection.style.display = 'block'; 112 - if (profileAvatar) profileAvatar.style.display = 'block'; 113 - } 114 - } 115 - }); 116 - 117 - // Login handler 118 - const handleLogin = async () => { 119 - let handle = handleInput?.value.trim(); 120 - if (!handle) { 121 - alert('Please enter your handle'); 122 - return; 123 - } 124 - 125 - // Strip @ if present 126 - if (handle.startsWith('@')) { 127 - handle = handle.slice(1); 128 - } 129 - 130 - try { 131 - if (authStatus) authStatus.textContent = 'Logging in...'; 132 - await startLoginProcess(handle); 133 - 134 - // Reload to show logged in state 135 - const session = await loadSession(); 136 - if (session) { 137 - try { 138 - const profile = await getProfile(session); 139 - if (profileAvatar && profile.avatar) { 140 - profileAvatar.src = profile.avatar; 141 - profileAvatar.style.display = 'block'; 142 - } 143 - if (authSection) authSection.style.display = 'none'; 144 - if (contentSection) contentSection.style.display = 'block'; 145 - 146 - // Trigger initial sync after login 147 - browser.runtime.sendMessage({ type: 'SYNC_CACHE' }); 148 - } catch (error) { 149 - console.error('Failed to fetch profile:', error); 150 - if (authSection) authSection.style.display = 'none'; 151 - if (contentSection) contentSection.style.display = 'block'; 152 - } 153 - } 154 - } catch (error) { 155 - if (authStatus) authStatus.textContent = 'Login failed'; 156 - console.error('Login error:', error); 10 + const storage = new BrowserStorageAdapter(); 11 + const launcher = new ExtensionOAuthLauncher(); 12 + 13 + const sidebar = new Sidebar( 14 + app, 15 + storage, 16 + launcher, 17 + { 18 + oauth: { 19 + clientId: import.meta.env.VITE_OAUTH_CLIENT_ID, 20 + redirectUri: import.meta.env.VITE_OAUTH_REDIRECT_URI, 21 + scope: import.meta.env.VITE_OAUTH_SCOPE, 22 + }, 23 + pds: { 24 + backendUrl: import.meta.env.VITE_BACKEND_URL || import.meta.env.BACKEND_URL || 'https://seams.so', 25 + }, 26 + }, 27 + () => { 28 + browser.runtime.sendMessage({ type: 'SYNC_CACHE' }); 157 29 } 158 - }; 159 - 160 - loginBtn?.addEventListener('click', handleLogin); 30 + ); 161 31 162 - // Handle Enter key on handle input 163 - handleInput?.addEventListener('keydown', (e) => { 164 - if (e.key === 'Enter') { 165 - e.preventDefault(); 166 - handleLogin(); 32 + // Listen for messages from background/content 33 + browser.runtime.onMessage.addListener((message) => { 34 + if (message.type === 'SELECTION_CHANGED') { 35 + sidebar.setSelection(message.selection); 167 36 } 168 37 }); 169 38 170 - // Profile dropdown toggle 171 - profileAvatar?.addEventListener('click', () => { 172 - if (profileDropdown) { 173 - profileDropdown.style.display = profileDropdown.style.display === 'none' ? 'block' : 'none'; 39 + // Listen for active tab changes 40 + browser.tabs.onActivated.addListener(async (activeInfo: any) => { 41 + const tab = await browser.tabs.get(activeInfo.tabId); 42 + if (tab.url) { 43 + sidebar.setCurrentUrl(tab.url); 174 44 } 175 45 }); 176 46 177 - // Close dropdown when clicking outside 178 - document.addEventListener('click', (e) => { 179 - if (profileDropdown && profileAvatar && 180 - !profileAvatar.contains(e.target as Node) && 181 - !profileDropdown.contains(e.target as Node)) { 182 - profileDropdown.style.display = 'none'; 47 + // Listen for URL updates in current tab 48 + browser.tabs.onUpdated.addListener(async (tabId: number, changeInfo: any) => { 49 + if (changeInfo.url) { 50 + const activeTabs = await browser.tabs.query({ active: true, currentWindow: true }); 51 + if (activeTabs[0]?.id === tabId) { 52 + sidebar.setCurrentUrl(changeInfo.url); 53 + } 183 54 } 184 55 }); 185 56 186 - // Logout handler 187 - logoutBtn?.addEventListener('click', async () => { 188 - await clearSession(); 189 - await browser.storage.local.remove(['annotations', 'comments', 'lastSync', 'syncError', 'lastSyncAttempt']); 190 - if (profileAvatar) profileAvatar.style.display = 'none'; 191 - if (profileDropdown) profileDropdown.style.display = 'none'; 192 - if (authSection) authSection.style.display = 'flex'; 193 - if (contentSection) contentSection.style.display = 'none'; 194 - if (authStatus) authStatus.textContent = ''; 195 - }); 196 - 197 - // Clear selection handler 198 - clearSelectionBtn?.addEventListener('click', () => { 199 - currentSelection = null; 200 - if (selectedTextEl) selectedTextEl.innerHTML = ''; 201 - if (annotationTextarea) annotationTextarea.value = ''; 202 - if (annotationForm) annotationForm.style.display = 'none'; 203 - }); 204 - 205 - // Initialize on open 57 + // Get initial state 206 58 (async function init() { 207 - await refreshActiveTab(); 208 - })(); 209 - 210 - async function refreshActiveTab() { 211 - // Get active tab 212 59 const tabs = await browser.tabs.query({ active: true, currentWindow: true }); 213 60 const activeTab = tabs[0]; 214 61 215 - if (!activeTab?.id) { 216 - console.error('[sidepanel] No active tab'); 217 - return; 218 - } 219 - 220 - currentTabId = activeTab.id; 62 + if (!activeTab?.id || !activeTab.url) return; 221 63 222 - // Get state from content script 223 - try { 224 - const response = await browser.tabs.sendMessage(currentTabId, { 225 - type: 'GET_STATE' 226 - }); 227 - 228 - currentUrl = response.url; 229 - currentSelection = response.selection; 230 - } catch (error) { 231 - console.warn('[sidepanel] Content script not available; falling back to tab URL'); 232 - const tabs = await browser.tabs.query({ active: true, currentWindow: true }); 233 - currentUrl = tabs[0]?.url || ''; 234 - currentSelection = null; 235 - } 236 - 237 - // Update UI 238 - updateSelectionUI(); 239 - 240 - // Load annotations for this URL 241 - await loadAnnotationsForUrl(currentUrl); 242 - } 243 - 244 - function updateSelectionUI() { 245 - if (currentSelection && currentSelection.text && selectedTextEl) { 246 - selectedTextEl.innerHTML = `<blockquote>${currentSelection.text}</blockquote>`; 247 - if (annotationForm) annotationForm.style.display = 'block'; 248 - } else { 249 - if (annotationForm) annotationForm.style.display = 'none'; 250 - } 251 - } 252 - 253 - // Watch for tab changes 254 - browser.tabs.onActivated.addListener(({ tabId }) => { 255 - if (tabId !== currentTabId) { 256 - console.log('[sidepanel] Active tab changed:', tabId); 257 - refreshActiveTab(); 258 - } 259 - }); 260 - 261 - // Watch for URL changes in active tab 262 - browser.tabs.onUpdated.addListener((tabId, changeInfo, tab) => { 263 - if (tabId === currentTabId && changeInfo.url) { 264 - console.log('[sidepanel] Active tab URL changed:', changeInfo.url); 265 - currentUrl = changeInfo.url; 266 - currentSelection = null; 267 - updateSelectionUI(); 268 - loadAnnotationsForUrl(currentUrl); 269 - } 270 - }); 271 - 272 - // Watch for storage changes 273 - browser.storage.onChanged.addListener((changes, area) => { 274 - if (area === 'local' && (changes.annotations || changes.comments)) { 275 - console.log('[sidepanel] Cache updated, reloading for current URL'); 276 - loadAnnotationsForUrl(currentUrl); 277 - } 278 - }); 279 - 280 - // Listen for selection changes from content script 281 - browser.runtime.onMessage.addListener((message) => { 282 - if (message.type === 'SELECTION_CHANGED') { 283 - console.log('[sidepanel] Selection changed'); 284 - currentSelection = message.selection; 285 - updateSelectionUI(); 286 - } 287 - }); 288 - 289 - 290 - 291 - // Save annotation 292 - saveBtn?.addEventListener('click', async () => { 293 - if (!currentSelection) { 294 - alert('Please select text on the page first'); 295 - return; 296 - } 297 - 298 - const body = annotationTextarea.value.trim(); 299 - 300 - const annotation: Annotation = { 301 - $type: 'community.lexicon.annotation.annotation', 302 - target: [{ 303 - source: currentUrl, 304 - selector: currentSelection.selectors 305 - }], 306 - body: body || undefined, 307 - createdAt: new Date().toISOString() 308 - }; 309 - 310 - await createAndSyncAnnotation(annotation); 311 - }); 64 + sidebar.setCurrentUrl(activeTab.url); 312 65 313 - async function createAndSyncAnnotation(annotation: Annotation) { 314 66 try { 315 - // Save to PDS 316 - const saved = await createAnnotation(annotation); 317 - console.log('[sidepanel] Saved to PDS:', saved.uri); 318 - 319 - // Optimistically add to cache immediately 320 - const { annotations = [] } = await browser.storage.local.get('annotations'); 321 - const updatedAnnotations = [...annotations, saved]; 322 - await browser.storage.local.set({ annotations: updatedAnnotations }); 323 - 324 - // Request background to fetch fresh data from backend (to get indexed version with handle) 325 - browser.runtime.sendMessage({ 326 - type: 'FETCH_ANNOTATIONS_FOR_URL', 327 - url: currentUrl 67 + const response = await browser.tabs.sendMessage(activeTab.id, { 68 + type: 'GET_STATE' 328 69 }); 329 - 330 - // Also sync user's own annotations from PDS 331 - browser.runtime.sendMessage({ type: 'SYNC_CACHE' }); 332 - 333 - // Clear form 334 - annotationTextarea.value = ''; 335 - if (selectedTextEl) selectedTextEl.innerHTML = ''; 336 - currentSelection = null; 337 - if (annotationForm) annotationForm.style.display = 'none'; 338 - 339 - // UI will update automatically via storage.onChanged 70 + sidebar.setSelection(response.selection); 340 71 } catch (error) { 341 - console.error('[sidepanel] Failed to create annotation:', error); 342 - alert('Failed to save annotation'); 343 - } 344 - } 345 - 346 - function buildCommentThread(parentUri: string, parentHasReplies: boolean = false): string { 347 - const replies = allComments.filter(c => c.reply?.parent === parentUri); 348 - if (replies.length === 0) return ''; 349 - 350 - const isParentCollapsed = collapsedThreads.has(parentUri); 351 - 352 - if (isParentCollapsed) { 353 - return ` 354 - <div class="comment-thread collapsed"> 355 - <button class="thread-toggle-btn" data-uri="${parentUri}">▸</button> 356 - <div class="collapsed-indicator">${replies.length} hidden ${replies.length === 1 ? 'reply' : 'replies'}</div> 357 - </div> 358 - `; 359 - } 360 - 361 - return ` 362 - <div class="comment-thread"> 363 - ${parentHasReplies ? `<button class="thread-toggle-btn" data-uri="${parentUri}">▾</button>` : ''} 364 - <div class="thread-children ${replies.length === 1 ? 'single-child' : ''}"> 365 - ${replies.map(comment => { 366 - const hasReplies = allComments.some(c => c.reply?.parent === comment.uri); 367 - const isReplyFormActive = activeReplyForms.has(comment.uri!); 368 - 369 - return ` 370 - <div class="comment" data-uri="${comment.uri}"> 371 - <div class="comment-content"> 372 - <div class="comment-text">${comment.plaintext}</div> 373 - <div class="comment-meta"> 374 - <small>${new Date(comment.createdAt).toLocaleString()}</small> 375 - <button class="reply-btn" data-uri="${comment.uri}">Reply</button> 376 - </div> 377 - </div> 378 - ${isReplyFormActive ? ` 379 - <div class="reply-form" data-parent="${comment.uri}"> 380 - <textarea class="reply-input" placeholder="Write a reply..."></textarea> 381 - <div class="reply-actions"> 382 - <button class="save-reply-btn">Post</button> 383 - <button class="cancel-reply-btn">Cancel</button> 384 - </div> 385 - </div> 386 - ` : ''} 387 - ${hasReplies ? buildCommentThread(comment.uri!, true) : ''} 388 - </div> 389 - `; 390 - }).join('')} 391 - </div> 392 - </div> 393 - `; 394 - } 395 - 396 - function renderAnnotationCard(ann: Annotation): string { 397 - const quote = ann.target[0]?.selector?.find((s: any) => s.$type === 'community.lexicon.annotation.annotation#textQuoteSelector'); 398 - const text = quote?.exact || ''; 399 - const comments = allComments.filter(c => c.subject === ann.uri && !c.reply); 400 - const isCommentsCollapsed = collapsedThreads.has(ann.uri!); 401 - const isCommentFormActive = activeReplyForms.has(ann.uri!); 402 - 403 - return ` 404 - <div class="annotation-card" data-uri="${ann.uri}"> 405 - ${text ? `<blockquote>${text}</blockquote>` : ''} 406 - ${ann.body ? `<p>${ann.body}</p>` : ''} 407 - <div class="annotation-meta"> 408 - <small>${new Date(ann.createdAt).toLocaleString()}</small> 409 - </div> 410 - <div class="comments-section" style="display: none;"> 411 - <div class="comments-header"> 412 - <button class="toggle-comments-btn" data-uri="${ann.uri}"> 413 - ${isCommentsCollapsed ? '▸' : '▾'} ${comments.length} comment${comments.length !== 1 ? 's' : ''} 414 - </button> 415 - <button class="add-comment-btn" data-uri="${ann.uri}">Add comment</button> 416 - </div> 417 - ${!isCommentsCollapsed ? ` 418 - <div class="comments-list"> 419 - ${isCommentFormActive ? ` 420 - <div class="comment-form" data-subject="${ann.uri}"> 421 - <textarea class="comment-input" placeholder="Write a comment..."></textarea> 422 - <div class="comment-actions"> 423 - <button class="save-comment-btn">Post</button> 424 - <button class="cancel-comment-btn">Cancel</button> 425 - </div> 426 - </div> 427 - ` : ''} 428 - ${comments.map(comment => { 429 - const hasReplies = allComments.some(c => c.reply?.parent === comment.uri); 430 - 431 - return ` 432 - <div class="comment" data-uri="${comment.uri}"> 433 - <div class="comment-content"> 434 - <div class="comment-text">${comment.plaintext}</div> 435 - <div class="comment-meta"> 436 - <small>${new Date(comment.createdAt).toLocaleString()}</small> 437 - <button class="reply-btn" data-uri="${comment.uri}">Reply</button> 438 - </div> 439 - </div> 440 - ${activeReplyForms.has(comment.uri!) ? ` 441 - <div class="reply-form" data-parent="${comment.uri}"> 442 - <textarea class="reply-input" placeholder="Write a reply..."></textarea> 443 - <div class="reply-actions"> 444 - <button class="save-reply-btn">Post</button> 445 - <button class="cancel-reply-btn">Cancel</button> 446 - </div> 447 - </div> 448 - ` : ''} 449 - ${hasReplies ? buildCommentThread(comment.uri!, true) : ''} 450 - </div> 451 - `;}).join('')} 452 - </div> 453 - ` : ''} 454 - </div> 455 - </div> 456 - `; 457 - } 458 - 459 - function renderAnnotations() { 460 - if (!annotationsContainer) return; 461 - 462 - if (pageAnnotations.length === 0) { 463 - annotationsContainer.innerHTML = '<p class="empty">No annotations yet. Select text to create one.</p>'; 464 - return; 465 - } 466 - 467 - annotationsContainer.innerHTML = pageAnnotations.map(renderAnnotationCard).join(''); 468 - attachCommentEventListeners(); 469 - } 470 - 471 - async function loadAnnotationsForUrl(url: string) { 472 - const { annotations, comments, syncError, lastSync } = await browser.storage.local.get([ 473 - 'annotations', 474 - 'comments', 475 - 'syncError', 476 - 'lastSync' 477 - ]); 478 - 479 - const norm = normalizeUrl(url); 480 - 481 - // Filter by URL 482 - pageAnnotations = (annotations || []).filter( 483 - (ann: Annotation) => normalizeUrl(ann.target[0]?.source) === norm 484 - ); 485 - 486 - allComments = comments || []; 487 - 488 - // Show sync error if present 489 - if (syncError) { 490 - const lastSyncDate = lastSync ? new Date(lastSync).toLocaleString() : 'never'; 491 - console.warn(`[sidepanel] Sync failed: ${syncError}. Last successful sync: ${lastSyncDate}`); 72 + console.warn('[sidepanel] Content script not available'); 492 73 } 493 - 494 - renderAnnotations(); 495 - } 496 - 497 - function attachCommentEventListeners() { 498 - // Toggle comments visibility 499 - document.querySelectorAll('.toggle-comments-btn').forEach(btn => { 500 - btn.addEventListener('click', (e) => { 501 - const uri = (e.target as HTMLElement).dataset.uri!; 502 - if (collapsedThreads.has(uri)) { 503 - collapsedThreads.delete(uri); 504 - } else { 505 - collapsedThreads.add(uri); 506 - } 507 - renderAnnotations(); 508 - }); 509 - }); 510 - 511 - // Show comment form 512 - document.querySelectorAll('.add-comment-btn').forEach(btn => { 513 - btn.addEventListener('click', (e) => { 514 - const uri = (e.target as HTMLElement).dataset.uri!; 515 - activeReplyForms.add(uri); 516 - renderAnnotations(); 517 - }); 518 - }); 519 - 520 - // Show reply form 521 - document.querySelectorAll('.reply-btn').forEach(btn => { 522 - btn.addEventListener('click', (e) => { 523 - const uri = (e.target as HTMLElement).dataset.uri!; 524 - activeReplyForms.add(uri); 525 - renderAnnotations(); 526 - }); 527 - }); 528 - 529 - // Cancel comment/reply 530 - document.querySelectorAll('.cancel-comment-btn, .cancel-reply-btn').forEach(btn => { 531 - btn.addEventListener('click', (e) => { 532 - const form = (e.target as HTMLElement).closest('.comment-form, .reply-form')!; 533 - const uri = form.getAttribute('data-subject') || form.getAttribute('data-parent')!; 534 - activeReplyForms.delete(uri); 535 - renderAnnotations(); 536 - }); 537 - }); 538 - 539 - // Save comment 540 - document.querySelectorAll('.save-comment-btn').forEach(btn => { 541 - btn.addEventListener('click', async (e) => { 542 - const form = (e.target as HTMLElement).closest('.comment-form')!; 543 - const textarea = form.querySelector('.comment-input') as HTMLTextAreaElement; 544 - const subject = form.getAttribute('data-subject')!; 545 - const plaintext = textarea.value.trim(); 546 - 547 - if (!plaintext) return; 548 - 549 - try { 550 - await createComment({ 551 - $type: 'pub.leaflet.comment', 552 - subject, 553 - plaintext, 554 - createdAt: new Date().toISOString(), 555 - }); 556 - activeReplyForms.delete(subject); 557 - 558 - // Request sync after comment creation 559 - browser.runtime.sendMessage({ type: 'SYNC_CACHE' }); 560 - } catch (error) { 561 - console.error('Failed to create comment:', error); 562 - alert('Failed to post comment'); 563 - } 564 - }); 565 - }); 566 - 567 - // Save reply 568 - document.querySelectorAll('.save-reply-btn').forEach(btn => { 569 - btn.addEventListener('click', async (e) => { 570 - const form = (e.target as HTMLElement).closest('.reply-form')!; 571 - const textarea = form.querySelector('.reply-input') as HTMLTextAreaElement; 572 - const parent = form.getAttribute('data-parent')!; 573 - const plaintext = textarea.value.trim(); 574 - 575 - if (!plaintext) return; 576 - 577 - // Find the parent comment to get its subject 578 - const parentComment = allComments.find(c => c.uri === parent); 579 - if (!parentComment) return; 580 - 581 - try { 582 - await createComment({ 583 - $type: 'pub.leaflet.comment', 584 - subject: parentComment.subject, 585 - plaintext, 586 - createdAt: new Date().toISOString(), 587 - reply: { parent }, 588 - }); 589 - activeReplyForms.delete(parent); 590 - 591 - // Request sync after reply creation 592 - browser.runtime.sendMessage({ type: 'SYNC_CACHE' }); 593 - } catch (error) { 594 - console.error('Failed to create reply:', error); 595 - alert('Failed to post reply'); 596 - } 597 - }); 598 - }); 599 - 600 - // Collapse thread 601 - document.querySelectorAll('.thread-toggle-btn').forEach(btn => { 602 - btn.addEventListener('click', (e) => { 603 - const uri = (e.target as HTMLElement).dataset.uri!; 604 - if (collapsedThreads.has(uri)) { 605 - collapsedThreads.delete(uri); 606 - } else { 607 - collapsedThreads.add(uri); 608 - } 609 - renderAnnotations(); 610 - }); 611 - }); 612 - } 74 + })(); 613 75 })();
+108 -135
entrypoints/via-client/main.ts
··· 1 1 // Via proxy client - injects sidebar iframe and handles page interaction 2 - import { applyHighlights, clearHighlights } from '@/lib/highlights/apply'; 3 - import { WebStorageAdapter, BackgroundWorker, ContentScript } from '@seams/core'; 2 + import { WebStorageAdapter, BackgroundWorker, ContentScript, applyHighlights, clearHighlights, generateSelectors, createMobileAnnotateButton, createMobileSidebarToggle, createMobileAnnotationModal } from '@seams/core'; 4 3 import type { Annotation } from '@seams/core'; 5 - import { listAnnotationsForPage } from '@/lib/pds'; 6 4 7 5 console.log('🔬 Seams via client loaded!'); 8 6 ··· 11 9 12 10 const SIDEBAR_ID = 'seams-sidebar-iframe'; 13 11 const SIDEBAR_WIDTH = 400; 14 - const ADDER_ID = 'seams-selection-adder'; 15 - 16 - let currentAdder: HTMLElement | null = null; 12 + const IS_MOBILE = window.innerWidth <= 768; 17 13 18 14 // Initialize content script only (sidebar handles fetching via BackgroundWorker) 19 15 const contentScript = new ContentScript({ ··· 51 47 document.body.appendChild(iframe); 52 48 console.log('[seams-via] Sidebar injected'); 53 49 54 - // Adjust page content to avoid overlap 55 - document.body.style.marginRight = `${SIDEBAR_WIDTH}px`; 56 - } 57 - 58 - function showSelectionAdder() { 59 - const selection = window.getSelection(); 60 - if (!selection || selection.rangeCount === 0 || selection.toString().trim().length === 0) { 61 - hideSelectionAdder(); 62 - return; 63 - } 64 - 65 - const text = selection.toString().trim(); 66 - console.log('[seams-via] Selection:', text); 67 - 68 - // Get selection bounding rect 69 - const range = selection.getRangeAt(0); 70 - const rect = range.getBoundingClientRect(); 71 - 72 - // Remove existing adder 73 - hideSelectionAdder(); 74 - 75 - // Create adder button 76 - const adder = document.createElement('div'); 77 - adder.id = ADDER_ID; 78 - adder.innerHTML = ` 79 - <button style=" 80 - background: #0085ff; 81 - color: white; 82 - border: none; 83 - border-radius: 4px; 84 - padding: 8px 16px; 85 - font-size: 14px; 86 - font-weight: 500; 87 - cursor: pointer; 88 - box-shadow: 0 2px 8px rgba(0,0,0,0.15); 89 - font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; 90 - "> 91 - Annotate 92 - </button> 93 - `; 94 - 95 - adder.style.cssText = ` 96 - position: absolute; 97 - z-index: 2147483646; 98 - `; 99 - 100 - // Position above or below selection based on available space 101 - const spaceAbove = rect.top; 102 - const spaceBelow = window.innerHeight - rect.bottom; 103 - 104 - if (spaceAbove > spaceBelow && spaceAbove > 50) { 105 - // Position above 106 - adder.style.left = `${rect.left + window.scrollX + (rect.width / 2)}px`; 107 - adder.style.top = `${rect.top + window.scrollY - 40}px`; 108 - adder.style.transform = 'translateX(-50%)'; 50 + // Adjust page content to avoid overlap (only on desktop) 51 + if (!IS_MOBILE) { 52 + document.body.style.marginRight = `${SIDEBAR_WIDTH}px`; 109 53 } else { 110 - // Position below 111 - adder.style.left = `${rect.left + window.scrollX + (rect.width / 2)}px`; 112 - adder.style.top = `${rect.bottom + window.scrollY + 5}px`; 113 - adder.style.transform = 'translateX(-50%)'; 54 + iframe.style.width = '100%'; 55 + iframe.style.display = 'none'; // Hidden by default on mobile 114 56 } 115 57 116 - document.body.appendChild(adder); 117 - currentAdder = adder; 58 + // When iframe loads, send the URL 59 + iframe.onload = () => { 60 + console.log('[seams-via] Sidebar iframe loaded, sending URL'); 61 + iframe.contentWindow?.postMessage({ 62 + type: 'SEAMS_PAGE_URL', 63 + url: getActualUrl(), 64 + }, '*'); 65 + }; 118 66 119 - // Handle button click 120 - const button = adder.querySelector('button'); 121 - button?.addEventListener('click', (e) => { 122 - e.stopPropagation(); 123 - handleAnnotateClick(); 124 - }); 125 - 126 - // Close on click outside 127 - setTimeout(() => { 128 - document.addEventListener('click', handleClickOutside); 129 - }, 0); 130 - 131 - // Stop propagation on adder clicks 132 - adder.addEventListener('click', (e) => { 133 - e.stopPropagation(); 134 - }); 135 - } 136 - 137 - function hideSelectionAdder() { 138 - if (currentAdder) { 139 - currentAdder.remove(); 140 - currentAdder = null; 141 - document.removeEventListener('click', handleClickOutside); 67 + // Add toggle button for mobile 68 + if (IS_MOBILE) { 69 + const toggleBtn = createMobileSidebarToggle(() => { 70 + const isHidden = iframe.style.display === 'none'; 71 + iframe.style.display = isHidden ? 'block' : 'none'; 72 + toggleBtn.textContent = isHidden ? '>>' : '<<'; 73 + }); 142 74 } 143 75 } 144 76 145 - function handleClickOutside() { 146 - hideSelectionAdder(); 147 - } 148 - 149 - function handleAnnotateClick() { 150 - const selection = window.getSelection(); 151 - if (!selection || selection.toString().trim().length === 0) { 152 - return; 153 - } 154 - 155 - const text = selection.toString().trim(); 156 - console.log('[seams-via] Creating annotation for:', text); 157 - 158 - // TODO: Send to sidebar to create annotation 159 - window.postMessage({ 160 - type: 'SEAMS_CREATE_ANNOTATION', 161 - text, 162 - }, '*'); 163 - 164 - hideSelectionAdder(); 165 - } 166 - 167 - // Track text selection 168 - document.addEventListener('mouseup', (e) => { 169 - // Don't show adder if clicking on the adder itself 170 - if (currentAdder && currentAdder.contains(e.target as Node)) { 171 - return; 172 - } 173 - 174 - setTimeout(() => { 175 - showSelectionAdder(); 176 - }, 10); 177 - }); 178 - 179 77 function normalizeUrl(url: string): string { 180 78 try { 181 79 const parsed = new URL(url); ··· 234 132 235 133 // Start content script (loads and renders annotations from localStorage) 236 134 contentScript.start(); 135 + 136 + // Listen for text selection 137 + let activeAnnotateBtn: HTMLElement | null = null; 138 + 139 + document.addEventListener('mouseup', (e) => { 140 + const selection = window.getSelection(); 141 + const iframe = document.getElementById(SIDEBAR_ID) as HTMLIFrameElement; 142 + 143 + // Don't handle if clicking inside the mobile modal 144 + if ((e.target as HTMLElement).closest('.seams-mobile-annotate-btn')) return; 145 + 146 + // Remove existing annotate button if any (unless we just clicked it, but the above check handles that) 147 + if (activeAnnotateBtn) { 148 + activeAnnotateBtn.remove(); 149 + activeAnnotateBtn = null; 150 + } 151 + 152 + if (selection && selection.toString().trim().length > 0) { 153 + const text = selection.toString().trim(); 154 + const root = document.body; 155 + 156 + try { 157 + const selectors = generateSelectors(selection, root); 158 + console.log('[seams-via] Text selected:', text); 159 + 160 + if (IS_MOBILE) { 161 + // Mobile flow: Show annotate button 162 + const range = selection.getRangeAt(0); 163 + const rect = range.getBoundingClientRect(); 164 + 165 + // Show button above selection 166 + activeAnnotateBtn = createMobileAnnotateButton( 167 + rect.left + rect.width / 2, 168 + rect.top, 169 + () => { 170 + // Open modal 171 + createMobileAnnotationModal( 172 + text, 173 + (body) => { 174 + // Save 175 + if (iframe && iframe.contentWindow) { 176 + iframe.contentWindow.postMessage({ 177 + type: 'SEAMS_CREATE_ANNOTATION', 178 + payload: { 179 + text, 180 + selectors, 181 + body 182 + } 183 + }, '*'); 184 + } 185 + }, 186 + () => { 187 + // Cancel - clear selection? 188 + window.getSelection()?.removeAllRanges(); 189 + } 190 + ); 191 + // Remove button 192 + if (activeAnnotateBtn) { 193 + activeAnnotateBtn.remove(); 194 + activeAnnotateBtn = null; 195 + } 196 + } 197 + ); 198 + } else if (iframe && iframe.contentWindow) { 199 + // Desktop flow: Send to sidebar 200 + iframe.contentWindow.postMessage({ 201 + type: 'SEAMS_TEXT_SELECTED', 202 + payload: { 203 + text, 204 + selectors 205 + } 206 + }, '*'); 207 + } 208 + } catch (e) { 209 + console.error('[seams-via] Failed to generate selectors:', e); 210 + } 211 + } else if (iframe && iframe.contentWindow) { 212 + // Clear selection 213 + iframe.contentWindow.postMessage({ 214 + type: 'SEAMS_TEXT_SELECTED', 215 + payload: null 216 + }, '*'); 217 + } 218 + }); 237 219 } 238 220 239 221 init(); 240 222 241 - // Listen for sidebar ready, then send current URL so it can fetch 242 - window.addEventListener('message', (event) => { 243 - if (event.data.type === 'SEAMS_SIDEBAR_READY') { 244 - console.log('[seams-via] Sidebar ready, sending URL'); 245 - const iframe = document.getElementById(SIDEBAR_ID) as HTMLIFrameElement; 246 - iframe?.contentWindow?.postMessage({ 247 - type: 'SEAMS_PAGE_URL', 248 - url: getActualUrl(), 249 - }, '*'); 250 - } 251 - }); 223 + // Listen for messages from sidebar 224 + // window.addEventListener('message', (event) => { ... });
+1 -1
entrypoints/via-client/oauth-callback.html
··· 42 42 <h2>Completing login...</h2> 43 43 <p id="status">Processing OAuth response</p> 44 44 </div> 45 - <script type="module" src="/static/seams-oauth-callback.js"></script> 45 + <script type="module" src="./oauth-callback.ts"></script> 46 46 </body> 47 47 </html>
+40 -7
entrypoints/via-client/oauth-callback.ts
··· 1 1 // OAuth callback handler for via-client 2 - import { handleOAuthCallback } from '@/lib/oauth-web'; 2 + import { handleOAuthCallback, WebStorageAdapter } from '@seams/core'; 3 3 4 4 console.log('[oauth-callback] Processing OAuth callback'); 5 5 ··· 7 7 8 8 async function processCallback() { 9 9 try { 10 - const session = await handleOAuthCallback(); 10 + const storage = new WebStorageAdapter(); 11 + const config = { 12 + clientId: import.meta.env.VITE_OAUTH_CLIENT_ID || 'https://seams.so/oauth/client-metadata.json', 13 + redirectUri: import.meta.env.VITE_OAUTH_REDIRECT_URI || 'https://sure.seams.so/oauth-callback.html', 14 + scope: import.meta.env.VITE_OAUTH_SCOPE || 'atproto transition:generic', 15 + }; 16 + console.log('[oauth-callback] Config:', config); 17 + const session = await handleOAuthCallback(storage, config); 11 18 12 19 if (session) { 13 20 if (statusEl) statusEl.textContent = 'Login successful! Redirecting...'; 14 21 console.log('[oauth-callback] Login successful'); 15 22 16 - // Redirect back to the page the user was on 17 - // For now, redirect to the proxy home 18 - setTimeout(() => { 19 - window.location.href = '/proxy/https://example.com'; 20 - }, 1000); 23 + // Check if we are in a popup 24 + if (window.opener) { 25 + console.log('[oauth-callback] Sending message to opener'); 26 + window.opener.postMessage({ 27 + type: 'SEAMS_OAUTH_CALLBACK', 28 + url: window.location.href 29 + }, '*'); 30 + 31 + // Close popup after a delay 32 + setTimeout(() => { 33 + window.close(); 34 + }, 500); 35 + } else { 36 + // Not a popup (mobile or full page redirect) 37 + // Redirect back to the page the user was on 38 + // We can't easily know the "previous" page in a full redirect unless we stored it in sessionStorage BEFORE leaving. 39 + // But the sidebar iframe is where the login started. If we navigated the whole iframe, we lost state. 40 + // If we navigated the TOP window, we lost state too unless we stored it. 41 + 42 + // The sidebar puts `seams_login_redirect` in sessionStorage before starting? 43 + // Wait, `startLoginProcess` is called in Sidebar. 44 + 45 + // If we are here, we successfully logged in. 46 + if (statusEl) statusEl.textContent = 'Login successful! You can close this window.'; 47 + 48 + // For now, redirect to the proxy home or stored redirect 49 + const previousUrl = sessionStorage.getItem('seams_login_redirect') || '/'; 50 + setTimeout(() => { 51 + window.location.href = previousUrl; 52 + }, 1000); 53 + } 21 54 } else { 22 55 if (statusEl) statusEl.textContent = 'No OAuth response found'; 23 56 console.log('[oauth-callback] No OAuth response');
+1 -2
entrypoints/via-client/sidebar.html
··· 4 4 <meta charset="UTF-8"> 5 5 <meta name="viewport" content="width=device-width, initial-scale=1.0"> 6 6 <title>Seams Sidebar</title> 7 - <link rel="stylesheet" href="/static/assets/sidebar-J3iG1W2k.css"> 8 7 </head> 9 8 <body> 10 9 <div id="app"></div> 11 - <script type="module" src="/static/seams-sidebar.js"></script> 10 + <script type="module" src="./sidebar.ts"></script> 12 11 </body> 13 12 </html>
+73 -121
entrypoints/via-client/sidebar.ts
··· 1 - // Sidebar UI for via proxy client 2 - import './sidebar.css'; 3 - import { initializeOAuth, startLoginProcess, loadSession, clearSession, getProfile } from '@/lib/oauth-web'; 4 - import { WebStorageAdapter, BackgroundWorker } from '@seams/core'; 5 - import { listAnnotationsForPage } from '@/lib/pds'; 1 + import '../sidepanel/style.css'; 2 + import { WebStorageAdapter, WebOAuthLauncher, Sidebar, BackgroundWorker } from '@seams/core'; 6 3 7 4 console.log('[seams-sidebar] Loading sidebar...'); 8 5 9 - // Initialize web storage adapter AND background worker 10 - // Sidebar can fetch without pywb interference (not proxied) 11 - const storage = new WebStorageAdapter(); 12 - const backgroundWorker = new BackgroundWorker({ 13 - storage, 14 - fetchAnnotations: listAnnotationsForPage, 15 - }); 16 - 17 6 const app = document.getElementById('app'); 18 7 if (!app) { 19 8 throw new Error('App element not found'); 20 9 } 21 10 22 - // Initialize OAuth 23 - initializeOAuth(); 11 + const storage = new WebStorageAdapter(); 12 + const launcher = new WebOAuthLauncher(); 24 13 25 - let isLoggedIn = false; 14 + const backgroundWorker = new BackgroundWorker({ 15 + storage, 16 + fetchAnnotations: async (url: string) => { 17 + const backendUrl = import.meta.env.VITE_BACKEND_URL || import.meta.env.BACKEND_URL || 'https://seams.so'; 18 + const response = await fetch(`${backendUrl}/api/annotations?url=${encodeURIComponent(url)}&limit=100`); 19 + if (!response.ok) throw new Error(`Backend error: ${response.status}`); 20 + const data = await response.json(); 21 + 22 + return (data.annotations || []).map((ann: any) => { 23 + const selectors = JSON.parse(ann.selectors || '[]'); 24 + return { 25 + $type: 'community.lexicon.annotation.annotation', 26 + uri: ann.uri, 27 + cid: ann.cid, 28 + target: [{ 29 + source: ann.targetUrl, 30 + selector: selectors, 31 + }], 32 + body: ann.body || '', 33 + createdAt: ann.createdAt, 34 + author: ann.authorHandle ? { 35 + did: ann.authorDid, 36 + handle: ann.authorHandle, 37 + } : undefined, 38 + }; 39 + }); 40 + }, 41 + }); 26 42 27 - // Fetch annotations for the given URL 28 - function syncAnnotations(url: string) { 29 - console.log('[seams-sidebar] Syncing annotations for:', url); 30 - backgroundWorker.setCurrentUrl(url); 31 - } 32 - 33 - async function render() { 34 - const session = await loadSession(); 35 - 36 - if (session) { 37 - try { 38 - const profile = await getProfile(session); 39 - renderLoggedIn(profile); 40 - } catch (error) { 41 - console.error('[seams-sidebar] Failed to fetch profile:', error); 42 - renderLoggedIn(null); 43 + const sidebar = new Sidebar( 44 + app, 45 + storage, 46 + launcher, 47 + { 48 + oauth: { 49 + clientId: import.meta.env.VITE_OAUTH_CLIENT_ID || 'http://localhost:8081/static/client-metadata.json', 50 + redirectUri: import.meta.env.VITE_OAUTH_REDIRECT_URI || 'http://localhost:8081/static/oauth-callback.html', 51 + scope: import.meta.env.VITE_OAUTH_SCOPE || 'atproto transition:generic', 52 + }, 53 + pds: { 54 + backendUrl: import.meta.env.VITE_BACKEND_URL || import.meta.env.BACKEND_URL || 'https://seams.so', 55 + }, 56 + }, 57 + (url?: string) => { 58 + if (url) { 59 + backgroundWorker.setCurrentUrl(url); 43 60 } 44 - } else { 45 - renderLoginForm(); 46 61 } 47 - } 48 - 49 - function renderLoginForm() { 50 - isLoggedIn = false; 51 - app.innerHTML = ` 52 - <div class="sidebar"> 53 - <div class="sidebar-header"> 54 - <h1>Seams</h1> 55 - <p>Via Proxy Client</p> 56 - </div> 57 - <div class="sidebar-content"> 58 - <div class="login-container"> 59 - <h2>Login to Seams</h2> 60 - <div class="input-wrapper"> 61 - <span class="at-symbol">@</span> 62 - <input type="text" id="handle-input" class="handle-input" placeholder="you.bsky.social" /> 63 - </div> 64 - <button id="login-btn">Login with ATProto</button> 65 - <div id="auth-status"></div> 66 - </div> 67 - </div> 68 - </div> 69 - `; 70 - 71 - const handleInput = document.getElementById('handle-input') as HTMLInputElement; 72 - const loginBtn = document.getElementById('login-btn'); 73 - const authStatus = document.getElementById('auth-status'); 74 - 75 - const handleLogin = async () => { 76 - let handle = handleInput?.value.trim(); 77 - if (!handle) { 78 - alert('Please enter your handle'); 79 - return; 80 - } 81 - 82 - if (handle.startsWith('@')) { 83 - handle = handle.slice(1); 84 - } 85 - 86 - try { 87 - if (authStatus) authStatus.textContent = 'Logging in...'; 88 - await startLoginProcess(handle); 89 - } catch (error) { 90 - if (authStatus) authStatus.textContent = 'Login failed'; 91 - console.error('[seams-sidebar] Login error:', error); 92 - } 93 - }; 94 - 95 - loginBtn?.addEventListener('click', handleLogin); 96 - handleInput?.addEventListener('keydown', (e) => { 97 - if (e.key === 'Enter') { 98 - e.preventDefault(); 99 - handleLogin(); 100 - } 101 - }); 102 - } 103 - 104 - function renderLoggedIn(profile: any) { 105 - isLoggedIn = true; 106 - app.innerHTML = ` 107 - <div class="sidebar"> 108 - <div class="sidebar-header"> 109 - <h1>Seams</h1> 110 - ${profile ? ` 111 - <div class="profile-info"> 112 - ${profile.avatar ? `<img src="${profile.avatar}" class="profile-avatar" />` : ''} 113 - <span class="profile-handle">@${profile.handle || 'Unknown'}</span> 114 - </div> 115 - ` : ''} 116 - </div> 117 - <div class="sidebar-content"> 118 - <p>You are logged in!</p> 119 - <button id="logout-btn">Logout</button> 120 - </div> 121 - </div> 122 - `; 123 - 124 - const logoutBtn = document.getElementById('logout-btn'); 125 - logoutBtn?.addEventListener('click', async () => { 126 - await clearSession(); 127 - render(); 128 - }); 129 - } 130 - 131 - // Initial render 132 - render(); 62 + ); 133 63 134 64 // Listen for messages from parent (page URL) 135 65 window.addEventListener('message', (event) => { 136 66 if (event.data.type === 'SEAMS_PAGE_URL') { 137 67 const url = event.data.url; 138 68 console.log('[seams-sidebar] Received page URL:', url); 139 - syncAnnotations(url); 69 + sidebar.setCurrentUrl(url); 70 + backgroundWorker.setCurrentUrl(url); 71 + } else if (event.data.type === 'SEAMS_TEXT_SELECTED') { 72 + console.log('[seams-sidebar] Received selection update'); 73 + sidebar.setSelection(event.data.payload); 74 + } else if (event.data.type === 'SEAMS_CREATE_ANNOTATION') { 75 + console.log('[seams-sidebar] Received create annotation request'); 76 + const { text, selectors, body } = event.data.payload; 77 + // Use the current URL of the sidebar (which should match the page) 78 + // But we can also use the source URL from the payload if we sent it, 79 + // currently sidebar uses its own this.currentUrl 80 + 81 + // We need to make sure sidebar has the correct URL set 82 + // The sidebar tracks currentUrl via SEAMS_PAGE_URL messages 83 + 84 + sidebar.createAnnotation({ 85 + source: sidebar.getCurrentUrl(), // Assuming public getter or we can just rely on internal state 86 + selectors 87 + }, body).then(() => { 88 + console.log('[seams-sidebar] Annotation created from mobile request'); 89 + }).catch(err => { 90 + console.error('[seams-sidebar] Failed to create annotation from mobile request:', err); 91 + }); 140 92 } 141 93 }); 142 94
+86 -3
flake.nix
··· 18 18 version = "0.1.0"; 19 19 src = ./server; 20 20 21 - vendorHash = "sha256-kgJ78zcL7q3iwziS/6V6SQe/7izrvOm8BA84NhkmIKA="; 21 + vendorHash = null; 22 22 23 23 subPackages = [ "cmd/server" ]; 24 24 25 25 nativeBuildInputs = [ pkgs.gcc ]; 26 26 }; 27 + 28 + pythonWithPip = pkgs.python311.withPackages (ps: [ ps.pip ]); 27 29 in 28 30 { 29 31 packages = { ··· 41 43 server 42 44 (pkgs.runCommand "static-files" {} '' 43 45 mkdir -p $out/app/public 44 - cp -r ${./public}/* $out/app/public/ 46 + cp -r ${./landing}/* $out/app/public/ 45 47 '') 46 48 (pkgs.writeScriptBin "start-server.sh" '' 47 49 #!${pkgs.bash}/bin/bash ··· 69 71 ]; 70 72 }; 71 73 }; 74 + 75 + # Docker image for Via Proxy 76 + proxy = pkgs.dockerTools.buildImage { 77 + name = "seams-proxy"; 78 + tag = "latest"; 79 + 80 + copyToRoot = pkgs.buildEnv { 81 + name = "image-root"; 82 + paths = [ 83 + pkgs.coreutils 84 + pkgs.gnugrep 85 + pkgs.bash 86 + pkgs.caddy 87 + pkgs.uv 88 + pythonWithPip 89 + pkgs.gcc 90 + pkgs.cacert 91 + pkgs.stdenv.cc.cc.lib # For pywb/gevent dependencies 92 + 93 + # Config and static files 94 + # Note: This relies on proxy/static being populated by 'pnpm build:via' locally 95 + # and included in the source tree (e.g. git tracked or added). 96 + (pkgs.runCommand "proxy-files" {} '' 97 + mkdir -p $out/app/proxy 98 + cp -r ${./proxy}/* $out/app/proxy/ 99 + cp ${./Caddyfile} $out/app/Caddyfile 100 + '') 101 + 102 + # Startup script 103 + (pkgs.writeScriptBin "start-proxy.sh" '' 104 + #!${pkgs.bash}/bin/bash 105 + set -e 106 + 107 + echo "🚀 Starting Seams Proxy..." 108 + 109 + # Install pywb at runtime using uv 110 + echo "📦 Installing pywb..." 111 + # Ensure HOME exists 112 + ${pkgs.coreutils}/bin/mkdir -p /root 113 + 114 + export UV_CACHE_DIR=/root/.cache/uv 115 + export UV_TOOL_BIN_DIR=/root/.local/bin 116 + ${pkgs.uv}/bin/uv tool install pywb --with setuptools --python ${pythonWithPip}/bin/python3 117 + 118 + # Add local bin to PATH (where uv installs tools) 119 + export PATH=$PATH:/root/.local/bin 120 + 121 + # Start wayback (pywb) on port 8081 122 + # Must run from proxy dir to find config.yaml 123 + cd /app/proxy 124 + echo "📦 Starting wayback on :8081" 125 + # Explicitly bind to 127.0.0.1 since Caddy connects there 126 + export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:${pkgs.glibc}/lib 127 + wayback -p 8081 -b 127.0.0.1 & 128 + WB_PID=$! 129 + 130 + # Wait for wayback to initialize 131 + ${pkgs.coreutils}/bin/sleep 2 132 + 133 + # Start Caddy on port 8082 134 + cd /app 135 + echo "🌐 Starting Caddy on :8082" 136 + ${pkgs.caddy}/bin/caddy run --config /app/Caddyfile --adapter caddyfile 137 + '') 138 + ]; 139 + pathsToLink = [ "/bin" "/app" ]; 140 + }; 141 + 142 + config = { 143 + Cmd = [ "start-proxy.sh" ]; 144 + ExposedPorts = { "8082/tcp" = {}; "8081/tcp" = {}; }; 145 + WorkingDir = "/app"; 146 + Env = [ 147 + "PATH=/bin" 148 + "SSL_CERT_FILE=${pkgs.cacert}/etc/ssl/certs/ca-bundle.crt" 149 + "LD_LIBRARY_PATH=${pkgs.stdenv.cc.cc.lib}/lib:${pkgs.glibc}/lib" 150 + "HOME=/root" 151 + ]; 152 + }; 153 + }; 72 154 }; 73 155 74 156 devShells = { ··· 161 243 via = pkgs.mkShell { 162 244 buildInputs = with pkgs; [ 163 245 caddy 246 + wayback # Add wayback from nixpkgs 164 247 python311 165 248 uv 166 249 nodejs_20 ··· 176 259 echo " pnpm build:via - Build via client scripts" 177 260 echo "" 178 261 echo "🚀 Quick start:" 179 - echo " 1. Terminal 1: wayback" 262 + echo " 1. Terminal 1: wayback -p 8081" 180 263 echo " 2. Terminal 2: caddy run" 181 264 echo " 3. Visit http://localhost:8082" 182 265 '';
+16
fly.proxy.toml
··· 1 + # fly.proxy.toml app configuration file for sure.seams.so (Proxy) 2 + app = 'sure-seams-so' 3 + primary_region = 'sjc' 4 + 5 + [http_service] 6 + internal_port = 8082 7 + force_https = true 8 + auto_stop_machines = true 9 + auto_start_machines = true 10 + min_machines_running = 1 11 + processes = ['app'] 12 + 13 + [[vm]] 14 + memory = '512mb' 15 + cpu_kind = 'shared' 16 + cpus = 1
+60
history/SIMPLIFICATION_PLAN.md
··· 1 + # Comprehensive Simplification & Reliability Plan 2 + 3 + Based on the Oracle's review, this plan focuses on simplifying the architecture by enforcing "Storage as Source of Truth," standardizing messaging, and aligning with AT Protocol patterns. 4 + 5 + ## Phase 1: Core Protocol & Runtime Definition 6 + **Goal:** Establish a shared vocabulary for all components to prevent drift and ad-hoc logic. 7 + 8 + - [ ] **Create `packages/core/src/constants.ts`** 9 + - Move Protocol Constants: `ANNOTATION_COLLECTION`, `COMMENT_COLLECTION`. 10 + - Define Storage Keys: `STORAGE_KEY_PREFIX` (e.g., `seams:`), `ANNOTATIONS_KEY_PREFIX`. 11 + - Define XRPC Endpoints: `XRPC_CREATE_RECORD`, `XRPC_GET_RECORD`. 12 + - [ ] **Create `packages/core/src/messages.ts`** 13 + - Define a strict Union Type for all internal messages: 14 + - `SYNC_CACHE`: Trigger a sync from backend to storage. 15 + - `GET_STATE`: Request current selection/auth state. 16 + - `SELECTION_CHANGED`: Notify that user selected text. 17 + - `PAGE_URL_CHANGED`: Notify that the URL changed (SPA nav). 18 + - `LOGIN / LOGOUT`: Auth state changes. 19 + - [ ] **Refactor `packages/core/src/types.ts`** 20 + - Ensure `Annotation` type clearly distinguishes between "UI Shape" and "ATProto Record Shape" (if they differ). 21 + - Add strict types for `Selector`s. 22 + 23 + ## Phase 2: Unifying the Message Bus 24 + **Goal:** Remove ad-hoc `sendMessage` calls and use a type-safe wrapper. 25 + 26 + - [ ] **Create `Messenger` class in Core** 27 + - A simple wrapper around `browser.runtime.sendMessage` (Extension) and `postMessage` (Proxy). 28 + - Methods: `send(msg: Message)`, `on(type, handler)`. 29 + - [ ] **Refactor Extension to use `Messenger`** 30 + - Update `background/extension.ts` and `content/extension.ts`. 31 + - Remove any raw string message matching. 32 + - [ ] **Refactor Proxy to use `Messenger`** 33 + - Update `via-client/main.ts` and `via-client/sidebar.ts` to match the same protocol. 34 + 35 + ## Phase 3: Storage-First Enforcement 36 + **Goal:** Decouple UI from Network. The UI should *only* render what is in Storage. 37 + 38 + - [ ] **Audit & Refactor `ContentScript`** 39 + - Verify it *only* renders in response to `storage.onChanged`. 40 + - Remove any direct "fetch and render" logic triggered by page load (replace with "load from storage, then trigger background sync"). 41 + - [ ] **Audit & Refactor `Sidebar`** 42 + - Ensure it reads annotations from storage, not from a message response. 43 + - [ ] **Standardize Sync Logic** 44 + - **Extension:** Ensure `BackgroundWorker` is the *only* writer to `annotations:*` storage keys. 45 + - **Proxy:** Create a simple "Worker" in the client script (or sidebar) that polls the backend and writes to `localStorage`. 46 + 47 + ## Phase 4: AT Protocol & Backend Alignment 48 + **Goal:** Treat the PDS/Backend interaction as a stable API surface. 49 + 50 + - [ ] **Refactor `packages/core/src/pds/index.ts`** 51 + - Use constants from `constants.ts`. 52 + - Ensure `createAnnotation` uses the strict Record type. 53 + - [ ] **Align Backend (`server/`)** 54 + - Update Go structs to match the Core `Annotation` types/constants (manually for now, but strict). 55 + - Ensure the "Index" endpoint expects exactly what the frontend sends. 56 + 57 + ## Phase 5: Documentation 58 + - [ ] **Update `README.md` or create `ARCHITECTURE.md`** 59 + - Document the "Storage Flow" (Diagrams from Oracle). 60 + - Document the "Message Vocabulary".
+21
landing/oauth/client-metadata.json
··· 1 + { 2 + "client_id": "https://seams.so/oauth/client-metadata.json", 3 + "client_uri": "https://seams.so", 4 + "redirect_uris": [ 5 + "https://seams.so/oauth/callback", 6 + "https://seams.so/oauth/ff/callback", 7 + "https://sure.seams.so/oauth-callback.html" 8 + ], 9 + "application_type": "web", 10 + "client_name": "Seams", 11 + "dpop_bound_access_tokens": true, 12 + "grant_types": [ 13 + "authorization_code", 14 + "refresh_token" 15 + ], 16 + "response_types": [ 17 + "code" 18 + ], 19 + "scope": "atproto transition:generic", 20 + "token_endpoint_auth_method": "none" 21 + }
-256
lib/highlights/apply.ts
··· 1 - import { findAnnotationRange } from '@/lib/selectors/match'; 2 - import { showAnnotationPopover } from './popover'; 3 - import type { Annotation } from '@/lib/types/annotation'; 4 - 5 - export function applyHighlights(annotations: Annotation[], container?: HTMLElement) { 6 - // Use main/article as root to avoid matching text in script tags 7 - const root = container || document.querySelector('main') || document.querySelector('article') || document.body; 8 - 9 - // Clear existing highlights first 10 - clearHighlights(root); 11 - 12 - console.log(`[synthesis] Applying ${annotations.length} highlights`); 13 - console.log('[synthesis] Container:', root.tagName, root.textContent?.substring(0, 100)); 14 - 15 - annotations.forEach((annotation, index) => { 16 - console.log(`[synthesis] Processing annotation ${index + 1}/${annotations.length}`); 17 - const range = findAnnotationRange(annotation, root); 18 - 19 - if (!range) { 20 - console.warn('[synthesis] Could not anchor annotation:', annotation); 21 - return; 22 - } 23 - 24 - // Check if range is inside a script/style tag (not visible) 25 - let node = range.commonAncestorContainer; 26 - while (node && node !== container) { 27 - if (node.nodeName === 'SCRIPT' || node.nodeName === 'STYLE') { 28 - console.warn('[synthesis] Skipping highlight inside', node.nodeName, 'tag'); 29 - return; 30 - } 31 - node = node.parentNode as HTMLElement; 32 - } 33 - 34 - console.log('[synthesis] Found range, attempting to highlight'); 35 - try { 36 - highlightRange(range, annotation); 37 - console.log('[synthesis] Successfully highlighted annotation', index + 1); 38 - } catch (error) { 39 - console.warn('[synthesis] Failed to highlight range:', error); 40 - } 41 - }); 42 - 43 - console.log('[synthesis] Finished applying highlights'); 44 - } 45 - 46 - function highlightRange(range: Range, annotation: Annotation) { 47 - console.log('[synthesis] Highlighting range:', range.toString().substring(0, 50)); 48 - 49 - const highlight = document.createElement('span'); 50 - highlight.className = 'synthesis-highlight'; 51 - highlight.dataset.annotationId = annotation.uri || annotation.createdAt; 52 - highlight.style.cssText = ` 53 - background-color: rgba(255, 235, 59, 0.6) !important; 54 - cursor: pointer !important; 55 - transition: background-color 0.2s !important; 56 - `; 57 - 58 - // Hover effect 59 - highlight.addEventListener('mouseenter', () => { 60 - highlight.style.cssText = ` 61 - background-color: rgba(255, 235, 59, 0.8) !important; 62 - cursor: pointer !important; 63 - transition: background-color 0.2s !important; 64 - `; 65 - }); 66 - 67 - highlight.addEventListener('mouseleave', () => { 68 - highlight.style.cssText = ` 69 - background-color: rgba(255, 235, 59, 0.6) !important; 70 - cursor: pointer !important; 71 - transition: background-color 0.2s !important; 72 - `; 73 - }); 74 - 75 - // Click to show annotation popover 76 - highlight.addEventListener('click', (e) => { 77 - e.preventDefault(); 78 - e.stopPropagation(); 79 - console.log('[synthesis] Clicked annotation:', annotation); 80 - 81 - showAnnotationPopover( 82 - annotation, 83 - highlight, 84 - // On save 85 - async (updatedAnnotation) => { 86 - const stored = await browser.storage.local.get('annotations'); 87 - const annotations = stored.annotations || []; 88 - const index = annotations.findIndex((a: Annotation) => 89 - a.createdAt === annotation.createdAt 90 - ); 91 - if (index !== -1) { 92 - annotations[index] = updatedAnnotation; 93 - await browser.storage.local.set({ annotations }); 94 - console.log('[synthesis] Annotation updated:', updatedAnnotation); 95 - 96 - // Update the annotation object in memory so next click shows updated note 97 - Object.assign(annotation, updatedAnnotation); 98 - } 99 - }, 100 - // On delete 101 - async () => { 102 - const stored = await browser.storage.local.get('annotations'); 103 - const annotations = stored.annotations || []; 104 - const filtered = annotations.filter((a: Annotation) => 105 - a.createdAt !== annotation.createdAt 106 - ); 107 - await browser.storage.local.set({ annotations: filtered }); 108 - console.log('[synthesis] Annotation deleted'); 109 - 110 - // Remove highlight 111 - highlight.remove(); 112 - } 113 - ); 114 - }); 115 - 116 - // Wrap text nodes individually to preserve block structure 117 - try { 118 - const textNodes = getTextNodesInRange(range); 119 - console.log('[synthesis] Found', textNodes.length, 'text nodes to highlight'); 120 - 121 - if (textNodes.length === 0) { 122 - console.warn('[synthesis] No text nodes found in range'); 123 - return; 124 - } 125 - 126 - // Wrap each text node 127 - textNodes.forEach((textNode, index) => { 128 - const nodeRange = document.createRange(); 129 - nodeRange.selectNodeContents(textNode); 130 - 131 - // For first and last nodes, use the original range boundaries 132 - if (index === 0 && textNode === range.startContainer) { 133 - nodeRange.setStart(textNode, range.startOffset); 134 - } 135 - if (index === textNodes.length - 1 && textNode === range.endContainer) { 136 - nodeRange.setEnd(textNode, range.endOffset); 137 - } 138 - 139 - const span = document.createElement('span'); 140 - span.className = 'synthesis-highlight'; 141 - span.dataset.annotationId = annotation.uri || annotation.createdAt; 142 - span.style.cssText = highlight.style.cssText; 143 - 144 - // Copy event listeners 145 - span.addEventListener('mouseenter', () => { 146 - const allSpans = document.querySelectorAll( 147 - `.synthesis-highlight[data-annotation-id="${annotation.uri || annotation.createdAt}"]` 148 - ); 149 - allSpans.forEach(s => { 150 - (s as HTMLElement).style.cssText = ` 151 - background-color: rgba(255, 235, 59, 0.8) !important; 152 - cursor: pointer !important; 153 - transition: background-color 0.2s !important; 154 - `; 155 - }); 156 - }); 157 - 158 - span.addEventListener('mouseleave', () => { 159 - const allSpans = document.querySelectorAll( 160 - `.synthesis-highlight[data-annotation-id="${annotation.uri || annotation.createdAt}"]` 161 - ); 162 - allSpans.forEach(s => { 163 - (s as HTMLElement).style.cssText = ` 164 - background-color: rgba(255, 235, 59, 0.6) !important; 165 - cursor: pointer !important; 166 - transition: background-color 0.2s !important; 167 - `; 168 - }); 169 - }); 170 - 171 - span.addEventListener('click', (e) => { 172 - e.preventDefault(); 173 - e.stopPropagation(); 174 - console.log('[synthesis] Clicked annotation:', annotation); 175 - 176 - showAnnotationPopover( 177 - annotation, 178 - span, 179 - async (updatedAnnotation) => { 180 - const stored = await browser.storage.local.get('annotations'); 181 - const annotations = stored.annotations || []; 182 - const idx = annotations.findIndex((a: Annotation) => 183 - a.createdAt === annotation.createdAt 184 - ); 185 - if (idx !== -1) { 186 - annotations[idx] = updatedAnnotation; 187 - await browser.storage.local.set({ annotations }); 188 - console.log('[synthesis] Annotation updated:', updatedAnnotation); 189 - Object.assign(annotation, updatedAnnotation); 190 - } 191 - }, 192 - async () => { 193 - const stored = await browser.storage.local.get('annotations'); 194 - const annotations = stored.annotations || []; 195 - const filtered = annotations.filter((a: Annotation) => 196 - a.createdAt !== annotation.createdAt 197 - ); 198 - await browser.storage.local.set({ annotations: filtered }); 199 - console.log('[synthesis] Annotation deleted'); 200 - 201 - // Remove all highlight spans for this annotation 202 - const allSpans = document.querySelectorAll( 203 - `.synthesis-highlight[data-annotation-id="${annotation.uri || annotation.createdAt}"]` 204 - ); 205 - allSpans.forEach(s => s.remove()); 206 - } 207 - ); 208 - }); 209 - 210 - nodeRange.surroundContents(span); 211 - }); 212 - 213 - console.log('[synthesis] Successfully applied highlight across', textNodes.length, 'text nodes'); 214 - } catch (error) { 215 - console.error('[synthesis] Failed to apply highlight:', error); 216 - } 217 - } 218 - 219 - function getTextNodesInRange(range: Range): Text[] { 220 - const textNodes: Text[] = []; 221 - const walker = document.createTreeWalker( 222 - range.commonAncestorContainer, 223 - NodeFilter.SHOW_TEXT, 224 - { 225 - acceptNode: (node) => { 226 - if (range.intersectsNode(node)) { 227 - return NodeFilter.FILTER_ACCEPT; 228 - } 229 - return NodeFilter.FILTER_REJECT; 230 - } 231 - } 232 - ); 233 - 234 - let node: Node | null; 235 - while (node = walker.nextNode()) { 236 - textNodes.push(node as Text); 237 - } 238 - 239 - return textNodes; 240 - } 241 - 242 - export function clearHighlights(container: HTMLElement = document.body) { 243 - const highlights = container.querySelectorAll('.synthesis-highlight'); 244 - console.log(`[synthesis] Clearing ${highlights.length} highlights`); 245 - 246 - highlights.forEach(highlight => { 247 - const parent = highlight.parentNode; 248 - if (parent) { 249 - while (highlight.firstChild) { 250 - parent.insertBefore(highlight.firstChild, highlight); 251 - } 252 - parent.removeChild(highlight); 253 - parent.normalize(); 254 - } 255 - }); 256 - }
-68
lib/highlights/popover.ts
··· 1 - import type { Annotation } from '../types/annotation'; 2 - 3 - let currentPopover: HTMLElement | null = null; 4 - 5 - export function showAnnotationPopover( 6 - annotation: Annotation, 7 - targetElement: HTMLElement, 8 - onSave: (updatedAnnotation: Annotation) => void, 9 - onDelete: () => void 10 - ) { 11 - // Remove existing popover 12 - hidePopover(); 13 - 14 - const popover = document.createElement('div'); 15 - popover.className = 'synthesis-popover'; 16 - popover.style.cssText = ` 17 - position: absolute; 18 - z-index: 999999; 19 - background: white; 20 - border: 1px solid #ccc; 21 - border-radius: 8px; 22 - padding: 12px; 23 - box-shadow: 0 2px 8px rgba(0,0,0,0.15); 24 - min-width: 300px; 25 - max-width: 400px; 26 - `; 27 - 28 - const rect = targetElement.getBoundingClientRect(); 29 - popover.style.left = `${rect.left + window.scrollX}px`; 30 - popover.style.top = `${rect.bottom + window.scrollY + 5}px`; 31 - 32 - const quote = annotation.target[0]?.selector?.find((s: any) => s.$type === 'community.lexicon.annotation.annotation#textQuoteSelector'); 33 - const quotedText = quote?.exact || ''; 34 - 35 - popover.innerHTML = ` 36 - <div style="margin-bottom: 8px; font-size: 13px; color: #666; font-style: italic; border-left: 3px solid #0085ff; padding-left: 8px;"> 37 - ${quotedText} 38 - </div> 39 - <div style="padding: 8px 0; font-size: 14px; color: #333;"> 40 - ${annotation.body || '<em style="color: #999;">No note</em>'} 41 - </div> 42 - `; 43 - 44 - document.body.appendChild(popover); 45 - currentPopover = popover; 46 - 47 - // Close on click outside 48 - setTimeout(() => { 49 - document.addEventListener('click', handleClickOutside); 50 - }, 0); 51 - 52 - // Stop propagation on popover clicks 53 - popover.addEventListener('click', (e) => { 54 - e.stopPropagation(); 55 - }); 56 - } 57 - 58 - function handleClickOutside() { 59 - hidePopover(); 60 - } 61 - 62 - export function hidePopover() { 63 - if (currentPopover) { 64 - currentPopover.remove(); 65 - currentPopover = null; 66 - document.removeEventListener('click', handleClickOutside); 67 - } 68 - }
-101
lib/oauth-web.ts
··· 1 - // Web-compatible OAuth implementation (for via-client) 2 - // Adapted from lib/oauth.ts to work without browser.* APIs 3 - 4 - import { 5 - configureOAuth, 6 - createAuthorizationUrl, 7 - finalizeAuthorization, 8 - resolveFromIdentity, 9 - OAuthUserAgent, 10 - type OAuthSession, 11 - } from "@atcute/oauth-browser-client"; 12 - import { storage } from "./storage-adapter"; 13 - 14 - const OAUTH_SESSION_KEY = "synthesis-oauth:session"; 15 - 16 - let isOAuthInitialized = false; 17 - 18 - export function initializeOAuth() { 19 - if (typeof window !== "undefined" && !isOAuthInitialized) { 20 - // Use web redirect URL for via proxy 21 - configureOAuth({ 22 - metadata: { 23 - client_id: import.meta.env.VITE_OAUTH_CLIENT_ID || 'http://localhost:8081/static/client-metadata.json', 24 - redirect_uri: import.meta.env.VITE_OAUTH_REDIRECT_URI || 'http://localhost:8081/static/oauth-callback.html', 25 - }, 26 - }); 27 - isOAuthInitialized = true; 28 - } 29 - } 30 - 31 - export async function startLoginProcess(handle: string): Promise<void> { 32 - console.log('[oauth-web] Starting login process for handle:', handle); 33 - initializeOAuth(); 34 - 35 - console.log('[oauth-web] Resolving identity...'); 36 - const { metadata } = await resolveFromIdentity(handle); 37 - console.log('[oauth-web] PDS metadata:', metadata); 38 - 39 - console.log('[oauth-web] Creating authorization URL...'); 40 - const authUrl = await createAuthorizationUrl({ 41 - metadata: metadata, 42 - scope: import.meta.env.VITE_OAUTH_SCOPE || 'atproto transition:generic', 43 - }); 44 - console.log('[oauth-web] Auth URL:', authUrl.toString()); 45 - 46 - // For web context, redirect to auth URL 47 - window.location.href = authUrl.toString(); 48 - } 49 - 50 - export async function handleOAuthCallback(): Promise<OAuthSession | null> { 51 - console.log('[oauth-web] Handling OAuth callback'); 52 - 53 - // Parse OAuth response from URL (params can be in search or hash) 54 - const url = new URL(window.location.href); 55 - const paramString = url.search || url.hash.slice(1); 56 - const params = new URLSearchParams(paramString); 57 - 58 - console.log('[oauth-web] OAuth params:', Object.fromEntries(params)); 59 - 60 - if (!params.has('code') && !params.has('error')) { 61 - console.log('[oauth-web] No OAuth params found'); 62 - return null; 63 - } 64 - 65 - if (params.has('error')) { 66 - const error = params.get('error'); 67 - const errorDesc = params.get('error_description'); 68 - console.error('[oauth-web] OAuth error:', error, errorDesc); 69 - throw new Error(`OAuth error: ${error} - ${errorDesc}`); 70 - } 71 - 72 - // Finalize authorization with the params 73 - console.log('[oauth-web] Finalizing authorization...'); 74 - const session = await finalizeAuthorization(params); 75 - console.log('[oauth-web] Authorization complete, session:', session); 76 - 77 - // Store session 78 - await saveSession(session); 79 - console.log('[oauth-web] Session saved successfully'); 80 - 81 - return session; 82 - } 83 - 84 - export async function saveSession(session: OAuthSession): Promise<void> { 85 - await storage.local.set({ [OAUTH_SESSION_KEY]: session }); 86 - } 87 - 88 - export async function loadSession(): Promise<OAuthSession | null> { 89 - const result = await storage.local.get(OAUTH_SESSION_KEY); 90 - return result[OAUTH_SESSION_KEY] || null; 91 - } 92 - 93 - export async function clearSession(): Promise<void> { 94 - await storage.local.remove(OAUTH_SESSION_KEY); 95 - } 96 - 97 - export async function getProfile(session: OAuthSession): Promise<any> { 98 - const agent = new OAuthUserAgent(session); 99 - const response = await agent.handle('/xrpc/app.bsky.actor.getProfile?actor=' + session.info.sub); 100 - return await response.json(); 101 - }
-103
lib/oauth.ts
··· 1 - import { 2 - configureOAuth, 3 - createAuthorizationUrl, 4 - finalizeAuthorization, 5 - resolveFromIdentity, 6 - OAuthUserAgent, 7 - type OAuthSession, 8 - } from "@atcute/oauth-browser-client"; 9 - 10 - const OAUTH_SESSION_KEY = "synthesis-oauth:session"; 11 - 12 - let isOAuthInitialized = false; 13 - 14 - export function initializeOAuth() { 15 - if (typeof window !== "undefined" && !isOAuthInitialized) { 16 - // Always use the web redirect URL for AT Protocol OAuth 17 - // The web callback will relay to chromiumapp.org for extensions 18 - configureOAuth({ 19 - metadata: { 20 - client_id: import.meta.env.VITE_OAUTH_CLIENT_ID, 21 - redirect_uri: import.meta.env.VITE_OAUTH_REDIRECT_URI, 22 - }, 23 - }); 24 - isOAuthInitialized = true; 25 - } 26 - } 27 - 28 - export async function startLoginProcess(handle: string): Promise<void> { 29 - console.log('[oauth] Starting login process for handle:', handle); 30 - initializeOAuth(); 31 - 32 - console.log('[oauth] Resolving identity...'); 33 - // Resolve handle to get server metadata 34 - const { metadata } = await resolveFromIdentity(handle); 35 - console.log('[oauth] PDS metadata:', metadata); 36 - 37 - console.log('[oauth] Creating authorization URL...'); 38 - const authUrl = await createAuthorizationUrl({ 39 - metadata: metadata, 40 - scope: import.meta.env.VITE_OAUTH_SCOPE, 41 - }); 42 - console.log('[oauth] Auth URL:', authUrl.toString()); 43 - 44 - // Use browser.identity.launchWebAuthFlow for extension OAuth 45 - if (typeof browser !== "undefined" && browser.identity) { 46 - console.log('[oauth] Launching web auth flow...'); 47 - try { 48 - // launchWebAuthFlow will capture the chromiumapp.org redirect 49 - const capturedUrl = await browser.identity.launchWebAuthFlow({ 50 - url: authUrl.toString(), 51 - interactive: true, 52 - }); 53 - 54 - if (!capturedUrl) { 55 - throw new Error('OAuth flow cancelled or failed'); 56 - } 57 - 58 - console.log('[oauth] Captured redirect URL:', capturedUrl); 59 - 60 - // Parse OAuth response from redirect URL (params can be in search or hash) 61 - const url = new URL(capturedUrl); 62 - const paramString = url.search || url.hash.slice(1); // Remove '#' from hash 63 - const params = new URLSearchParams(paramString); 64 - 65 - console.log('[oauth] OAuth params:', Object.fromEntries(params)); 66 - 67 - // Finalize authorization with the params 68 - console.log('[oauth] Finalizing authorization...'); 69 - const session = await finalizeAuthorization(params); 70 - console.log('[oauth] Authorization complete, session:', session); 71 - 72 - // Store session 73 - await saveSession(session); 74 - console.log('[oauth] Session saved successfully'); 75 - } catch (error) { 76 - console.error('[oauth] launchWebAuthFlow error:', error); 77 - throw error; 78 - } 79 - } else { 80 - // Fallback for non-extension contexts 81 - console.log('[oauth] Redirecting to auth URL (non-extension)'); 82 - window.location.href = authUrl.toString(); 83 - } 84 - } 85 - 86 - export async function saveSession(session: OAuthSession): Promise<void> { 87 - await browser.storage.local.set({ [OAUTH_SESSION_KEY]: session }); 88 - } 89 - 90 - export async function loadSession(): Promise<OAuthSession | null> { 91 - const result = await browser.storage.local.get(OAUTH_SESSION_KEY); 92 - return result[OAUTH_SESSION_KEY] || null; 93 - } 94 - 95 - export async function clearSession(): Promise<void> { 96 - await browser.storage.local.remove(OAUTH_SESSION_KEY); 97 - } 98 - 99 - export async function getProfile(session: OAuthSession): Promise<any> { 100 - const agent = new OAuthUserAgent(session); 101 - const response = await agent.handle('/xrpc/app.bsky.actor.getProfile?actor=' + session.info.sub); 102 - return await response.json(); 103 - }
-261
lib/pds.ts
··· 1 - import { OAuthUserAgent } from "@atcute/oauth-browser-client"; 2 - import { loadSession } from "./oauth"; 3 - import type { Annotation } from "./types/annotation"; 4 - import type { Comment } from "./types/comment"; 5 - 6 - const ANNOTATION_COLLECTION = "community.lexicon.annotation.annotation"; 7 - const COMMENT_COLLECTION = "pub.leaflet.comment"; 8 - const BACKEND_URL = import.meta.env.VITE_BACKEND_URL || import.meta.env.BACKEND_URL || 'https://seams.so'; 9 - 10 - export async function createAnnotation(annotation: Annotation): Promise<Annotation> { 11 - const session = await loadSession(); 12 - if (!session) { 13 - throw new Error("Not authenticated"); 14 - } 15 - 16 - const agent = new OAuthUserAgent(session); 17 - 18 - const record = { 19 - $type: annotation.$type, 20 - target: annotation.target, 21 - body: annotation.body, 22 - tags: annotation.tags, 23 - document: annotation.document, 24 - createdAt: annotation.createdAt, 25 - }; 26 - 27 - const response = await agent.handle('/xrpc/com.atproto.repo.createRecord', { 28 - method: 'POST', 29 - headers: { 30 - 'Content-Type': 'application/json', 31 - }, 32 - body: JSON.stringify({ 33 - repo: session.info.sub, 34 - collection: ANNOTATION_COLLECTION, 35 - record, 36 - }), 37 - }); 38 - 39 - if (!response.ok) { 40 - const error = await response.json(); 41 - console.error('[pds] Create error:', error); 42 - throw new Error(`Failed to create annotation: ${response.status} - ${JSON.stringify(error)}`); 43 - } 44 - 45 - const result = await response.json(); 46 - 47 - // Index in backend 48 - try { 49 - await fetch(`${BACKEND_URL}/api/annotations/index`, { 50 - method: 'POST', 51 - headers: { 'Content-Type': 'application/json' }, 52 - body: JSON.stringify({ 53 - uri: result.uri, 54 - cid: result.cid, 55 - }), 56 - }); 57 - console.log('[pds] Annotation indexed in backend'); 58 - } catch (err) { 59 - console.error('[pds] Failed to index annotation in backend:', err); 60 - // Don't fail the create operation 61 - } 62 - 63 - return { 64 - ...annotation, 65 - uri: result.uri, 66 - cid: result.cid, 67 - }; 68 - } 69 - 70 - export async function listAnnotations(): Promise<Annotation[]> { 71 - const session = await loadSession(); 72 - if (!session) { 73 - throw new Error("Not authenticated"); 74 - } 75 - 76 - const agent = new OAuthUserAgent(session); 77 - 78 - const response = await agent.handle( 79 - `/xrpc/com.atproto.repo.listRecords?repo=${session.info.sub}&collection=${ANNOTATION_COLLECTION}`, 80 - { method: 'GET' } 81 - ); 82 - 83 - const result = await response.json(); 84 - 85 - return result.records.map((record: any) => ({ 86 - ...record.value, 87 - uri: record.uri, 88 - cid: record.cid, 89 - })); 90 - } 91 - 92 - export async function deleteAnnotation(uri: string): Promise<void> { 93 - const session = await loadSession(); 94 - if (!session) { 95 - throw new Error("Not authenticated"); 96 - } 97 - 98 - const agent = new OAuthUserAgent(session); 99 - 100 - // Parse rkey from URI: at://did:plc:xxx/collection/rkey 101 - const rkey = uri.split("/").pop(); 102 - if (!rkey) { 103 - throw new Error("Invalid URI"); 104 - } 105 - 106 - const response = await agent.handle('/xrpc/com.atproto.repo.deleteRecord', { 107 - method: 'POST', 108 - headers: { 109 - 'Content-Type': 'application/json', 110 - }, 111 - body: JSON.stringify({ 112 - repo: session.info.sub, 113 - collection: ANNOTATION_COLLECTION, 114 - rkey, 115 - }), 116 - }); 117 - 118 - if (!response.ok) { 119 - const error = await response.json(); 120 - console.error('[pds] Delete error:', error); 121 - throw new Error(`Failed to delete annotation: ${response.status} - ${JSON.stringify(error)}`); 122 - } 123 - } 124 - 125 - export async function createComment(comment: Omit<Comment, 'uri' | 'cid' | 'author'>): Promise<Comment> { 126 - const session = await loadSession(); 127 - if (!session) { 128 - throw new Error("Not authenticated"); 129 - } 130 - 131 - const agent = new OAuthUserAgent(session); 132 - 133 - const record = { 134 - $type: 'pub.leaflet.comment', 135 - subject: comment.subject, 136 - plaintext: comment.plaintext, 137 - createdAt: comment.createdAt, 138 - reply: comment.reply, 139 - facets: comment.facets, 140 - onPage: comment.onPage, 141 - }; 142 - 143 - const response = await agent.handle('/xrpc/com.atproto.repo.createRecord', { 144 - method: 'POST', 145 - headers: { 146 - 'Content-Type': 'application/json', 147 - }, 148 - body: JSON.stringify({ 149 - repo: session.info.sub, 150 - collection: COMMENT_COLLECTION, 151 - record, 152 - }), 153 - }); 154 - 155 - if (!response.ok) { 156 - const error = await response.json(); 157 - console.error('[pds] Create comment error:', error); 158 - throw new Error(`Failed to create comment: ${response.status} - ${JSON.stringify(error)}`); 159 - } 160 - 161 - const result = await response.json(); 162 - 163 - return { 164 - ...comment, 165 - uri: result.uri, 166 - cid: result.cid, 167 - }; 168 - } 169 - 170 - export async function listComments(): Promise<Comment[]> { 171 - const session = await loadSession(); 172 - if (!session) { 173 - throw new Error("Not authenticated"); 174 - } 175 - 176 - const agent = new OAuthUserAgent(session); 177 - 178 - const response = await agent.handle( 179 - `/xrpc/com.atproto.repo.listRecords?repo=${session.info.sub}&collection=${COMMENT_COLLECTION}`, 180 - { method: 'GET' } 181 - ); 182 - 183 - const result = await response.json(); 184 - 185 - return result.records.map((record: any) => ({ 186 - ...record.value, 187 - uri: record.uri, 188 - cid: record.cid, 189 - })); 190 - } 191 - 192 - export async function deleteComment(uri: string): Promise<void> { 193 - const session = await loadSession(); 194 - if (!session) { 195 - throw new Error("Not authenticated"); 196 - } 197 - 198 - const agent = new OAuthUserAgent(session); 199 - 200 - const rkey = uri.split("/").pop(); 201 - if (!rkey) { 202 - throw new Error("Invalid URI"); 203 - } 204 - 205 - const response = await agent.handle('/xrpc/com.atproto.repo.deleteRecord', { 206 - method: 'POST', 207 - headers: { 208 - 'Content-Type': 'application/json', 209 - }, 210 - body: JSON.stringify({ 211 - repo: session.info.sub, 212 - collection: COMMENT_COLLECTION, 213 - rkey, 214 - }), 215 - }); 216 - 217 - if (!response.ok) { 218 - const error = await response.json(); 219 - console.error('[pds] Delete comment error:', error); 220 - throw new Error(`Failed to delete comment: ${response.status} - ${JSON.stringify(error)}`); 221 - } 222 - } 223 - 224 - // Query annotations from backend by URL 225 - export async function listAnnotationsForPage(url: string): Promise<Annotation[]> { 226 - try { 227 - const response = await fetch( 228 - `${BACKEND_URL}/api/annotations?url=${encodeURIComponent(url)}&limit=100` 229 - ); 230 - 231 - if (!response.ok) { 232 - throw new Error(`Backend error: ${response.status}`); 233 - } 234 - 235 - const data = await response.json(); 236 - const annotations = data.annotations || []; 237 - 238 - // Transform backend format to extension format 239 - return annotations.map((ann: any) => { 240 - const selectors = JSON.parse(ann.selectors || '[]'); 241 - return { 242 - $type: ANNOTATION_COLLECTION, 243 - uri: ann.uri, 244 - cid: ann.cid, 245 - target: [{ 246 - source: ann.targetUrl, 247 - selector: selectors, 248 - }], 249 - body: ann.body || '', 250 - createdAt: ann.createdAt, 251 - author: ann.authorHandle ? { 252 - did: ann.authorDid, 253 - handle: ann.authorHandle, 254 - } : undefined, 255 - }; 256 - }); 257 - } catch (error) { 258 - console.error('[pds] Failed to fetch from backend:', error); 259 - return []; // Graceful fallback 260 - } 261 - }
-67
lib/selectors/generate.ts
··· 1 - import * as textQuote from 'dom-anchor-text-quote'; 2 - import * as textPosition from 'dom-anchor-text-position'; 3 - import type { Selector, TextQuoteSelector, TextPositionSelector } from '../types/annotation'; 4 - 5 - /** 6 - * Generate W3C selectors from a DOM selection using Hypothesis libraries 7 - */ 8 - export function generateSelectors(selection: Selection, root?: HTMLElement): Selector[] { 9 - if (!selection.rangeCount) return []; 10 - 11 - const range = selection.getRangeAt(0); 12 - const container = root || document.body; 13 - const selectors: Selector[] = []; 14 - 15 - // TextQuoteSelector - most robust for content changes 16 - const textQuoteSelector = generateTextQuoteSelector(range, container); 17 - if (textQuoteSelector) selectors.push(textQuoteSelector); 18 - 19 - // TextPositionSelector - precise but fragile 20 - const textPositionSelector = generateTextPositionSelector(range, container); 21 - if (textPositionSelector) selectors.push(textPositionSelector); 22 - 23 - return selectors; 24 - } 25 - 26 - function generateTextQuoteSelector( 27 - range: Range, 28 - root: HTMLElement 29 - ): TextQuoteSelector | null { 30 - const exact = range.toString().trim(); 31 - if (!exact) return null; 32 - 33 - try { 34 - const selector = textQuote.fromRange(root, range); 35 - 36 - return { 37 - $type: 'community.lexicon.annotation.annotation#textQuoteSelector', 38 - exact: selector.exact, 39 - prefix: selector.prefix || undefined, 40 - suffix: selector.suffix || undefined 41 - }; 42 - } catch (error) { 43 - console.warn('Failed to generate TextQuoteSelector:', error); 44 - return null; 45 - } 46 - } 47 - 48 - function generateTextPositionSelector( 49 - range: Range, 50 - root: HTMLElement 51 - ): TextPositionSelector | null { 52 - const exact = range.toString().trim(); 53 - if (!exact) return null; 54 - 55 - try { 56 - const selector = textPosition.fromRange(root, range); 57 - 58 - return { 59 - $type: 'community.lexicon.annotation.annotation#textPositionSelector', 60 - start: selector.start, 61 - end: selector.end 62 - }; 63 - } catch (error) { 64 - console.warn('Failed to generate TextPositionSelector:', error); 65 - return null; 66 - } 67 - }
-67
lib/selectors/match.ts
··· 1 - import * as textQuote from 'dom-anchor-text-quote'; 2 - import * as textPosition from 'dom-anchor-text-position'; 3 - import type { Annotation, TextQuoteSelector, TextPositionSelector } from '../types/annotation'; 4 - 5 - /** 6 - * Find the DOM Range for an annotation using its selectors 7 - */ 8 - export function findAnnotationRange( 9 - annotation: Annotation, 10 - container: HTMLElement = document.body 11 - ): Range | null { 12 - const selectors = annotation.target?.[0]?.selector; 13 - if (!selectors || selectors.length === 0) { 14 - console.warn('[synthesis] No selectors found in annotation'); 15 - return null; 16 - } 17 - 18 - console.log('[synthesis] Trying to match annotation with', selectors.length, 'selectors'); 19 - 20 - // Try each selector in order 21 - for (const selector of selectors) { 22 - let range: Range | null = null; 23 - 24 - console.log('[synthesis] Trying selector type:', selector.$type); 25 - 26 - switch (selector.$type) { 27 - case 'community.lexicon.annotation.annotation#textPositionSelector': 28 - range = matchTextPositionSelector(selector as TextPositionSelector, container); 29 - break; 30 - case 'community.lexicon.annotation.annotation#textQuoteSelector': 31 - range = matchTextQuoteSelector(selector as TextQuoteSelector, container); 32 - break; 33 - } 34 - 35 - if (range) { 36 - console.log('[synthesis] Successfully matched with', selector.$type); 37 - return range; 38 - } 39 - } 40 - 41 - console.warn('[synthesis] Could not match any selector'); 42 - return null; 43 - } 44 - 45 - function matchTextPositionSelector( 46 - selector: TextPositionSelector, 47 - container: HTMLElement 48 - ): Range | null { 49 - try { 50 - return textPosition.toRange(container, selector); 51 - } catch (e) { 52 - console.warn('TextPositionSelector match failed:', e); 53 - return null; 54 - } 55 - } 56 - 57 - function matchTextQuoteSelector( 58 - selector: TextQuoteSelector, 59 - container: HTMLElement 60 - ): Range | null { 61 - try { 62 - return textQuote.toRange(container, selector); 63 - } catch (e) { 64 - console.warn('TextQuoteSelector match failed:', e); 65 - return null; 66 - } 67 - }
-73
lib/storage-adapter.ts
··· 1 - // Storage adapter that mimics browser.storage.local API but uses localStorage 2 - // This allows sharing code between extension and via-client 3 - 4 - export const storage = { 5 - local: { 6 - async get(keys?: string | string[] | Record<string, any>): Promise<Record<string, any>> { 7 - if (!keys) { 8 - // Get all items 9 - const result: Record<string, any> = {}; 10 - for (let i = 0; i < localStorage.length; i++) { 11 - const key = localStorage.key(i); 12 - if (key) { 13 - try { 14 - result[key] = JSON.parse(localStorage.getItem(key) || 'null'); 15 - } catch { 16 - result[key] = localStorage.getItem(key); 17 - } 18 - } 19 - } 20 - return result; 21 - } 22 - 23 - const result: Record<string, any> = {}; 24 - 25 - if (typeof keys === 'string') { 26 - // Single key 27 - try { 28 - const value = localStorage.getItem(keys); 29 - result[keys] = value ? JSON.parse(value) : null; 30 - } catch { 31 - result[keys] = localStorage.getItem(keys); 32 - } 33 - } else if (Array.isArray(keys)) { 34 - // Array of keys 35 - keys.forEach(key => { 36 - try { 37 - const value = localStorage.getItem(key); 38 - result[key] = value ? JSON.parse(value) : null; 39 - } catch { 40 - result[key] = localStorage.getItem(key); 41 - } 42 - }); 43 - } else { 44 - // Object with default values 45 - Object.keys(keys).forEach(key => { 46 - try { 47 - const value = localStorage.getItem(key); 48 - result[key] = value ? JSON.parse(value) : keys[key]; 49 - } catch { 50 - result[key] = localStorage.getItem(key) || keys[key]; 51 - } 52 - }); 53 - } 54 - 55 - return result; 56 - }, 57 - 58 - async set(items: Record<string, any>): Promise<void> { 59 - Object.entries(items).forEach(([key, value]) => { 60 - localStorage.setItem(key, JSON.stringify(value)); 61 - }); 62 - }, 63 - 64 - async remove(keys: string | string[]): Promise<void> { 65 - const keysArray = Array.isArray(keys) ? keys : [keys]; 66 - keysArray.forEach(key => localStorage.removeItem(key)); 67 - }, 68 - 69 - async clear(): Promise<void> { 70 - localStorage.clear(); 71 - }, 72 - }, 73 - };
-46
lib/storage-web.ts
··· 1 - // Web storage adapter using localStorage + BroadcastChannel 2 - // Mimics browser.storage API for via proxy client 3 - 4 - export interface StorageAdapter { 5 - get(key: string): Promise<any>; 6 - set(key: string, value: any): Promise<void>; 7 - onChange(callback: (changes: { key: string; newValue: any; oldValue?: any }) => void): void; 8 - } 9 - 10 - export class WebStorage implements StorageAdapter { 11 - private channel: BroadcastChannel; 12 - private listeners: Array<(changes: { key: string; newValue: any; oldValue?: any }) => void> = []; 13 - 14 - constructor(channelName: string = 'seams-storage') { 15 - this.channel = new BroadcastChannel(channelName); 16 - 17 - // Listen for broadcasts from other contexts 18 - this.channel.onmessage = (event) => { 19 - console.log('[WebStorage] Received broadcast:', event.data); 20 - this.listeners.forEach(callback => callback(event.data)); 21 - }; 22 - } 23 - 24 - async get(key: string): Promise<any> { 25 - const value = localStorage.getItem(key); 26 - return value ? JSON.parse(value) : null; 27 - } 28 - 29 - async set(key: string, value: any): Promise<void> { 30 - const oldValue = await this.get(key); 31 - localStorage.setItem(key, JSON.stringify(value)); 32 - 33 - // Broadcast change to other contexts (sidebar, content script, other tabs) 34 - const change = { key, newValue: value, oldValue }; 35 - console.log('[WebStorage] Broadcasting change:', change); 36 - this.channel.postMessage(change); 37 - } 38 - 39 - onChange(callback: (changes: { key: string; newValue: any; oldValue?: any }) => void): void { 40 - this.listeners.push(callback); 41 - } 42 - 43 - close(): void { 44 - this.channel.close(); 45 - } 46 - }
-78
lib/types/annotation.ts
··· 1 - /** 2 - * W3C Web Annotation Data Model types 3 - * Based on lexicon.community annotation lexicon 4 - */ 5 - 6 - export interface Annotation { 7 - $type: 'community.lexicon.annotation.annotation'; 8 - target: Target[]; 9 - body?: string; 10 - tags?: string[]; 11 - document?: DocumentMetadata; 12 - createdAt: string; 13 - 14 - // ATProto metadata 15 - uri?: string; 16 - cid?: string; 17 - author?: { 18 - did: string; 19 - handle: string; 20 - displayName?: string; 21 - avatar?: string; 22 - }; 23 - } 24 - 25 - export interface Target { 26 - source: string; 27 - selector?: Selector[]; 28 - } 29 - 30 - export type Selector = 31 - | TextQuoteSelector 32 - | TextPositionSelector; 33 - 34 - export interface TextQuoteSelector { 35 - $type: 'community.lexicon.annotation.annotation#textQuoteSelector'; 36 - exact: string; 37 - prefix?: string; 38 - suffix?: string; 39 - } 40 - 41 - export interface TextPositionSelector { 42 - $type: 'community.lexicon.annotation.annotation#textPositionSelector'; 43 - start: number; 44 - end: number; 45 - } 46 - 47 - export interface DocumentMetadata { 48 - title?: string; 49 - canonicalUri?: string; 50 - } 51 - 52 - export function isAnnotation(record: unknown): record is Annotation { 53 - return ( 54 - typeof record === 'object' && 55 - record !== null && 56 - '$type' in record && 57 - record.$type === 'community.lexicon.annotation.annotation' 58 - ); 59 - } 60 - 61 - export function getAnnotationText(annotation: Annotation): string { 62 - return annotation.body || ''; 63 - } 64 - 65 - export function getQuotedText(annotation: Annotation): string | null { 66 - const target = annotation.target?.[0]; 67 - if (!target?.selector) return null; 68 - 69 - const textQuote = target.selector.find( 70 - (s): s is TextQuoteSelector => s.$type === 'community.lexicon.annotation.annotation#textQuoteSelector' 71 - ); 72 - 73 - return textQuote?.exact || null; 74 - } 75 - 76 - export function getSelectors(annotation: Annotation): Selector[] { 77 - return annotation.target?.[0]?.selector || []; 78 - }
-68
lib/types/comment.ts
··· 1 - export interface Comment { 2 - $type: 'pub.leaflet.comment'; 3 - subject: string; 4 - plaintext: string; 5 - createdAt: string; 6 - reply?: ReplyRef; 7 - facets?: RichtextFacet[]; 8 - onPage?: string; 9 - 10 - uri?: string; 11 - cid?: string; 12 - author?: { 13 - did: string; 14 - handle: string; 15 - displayName?: string; 16 - avatar?: string; 17 - }; 18 - } 19 - 20 - export interface ReplyRef { 21 - parent: string; 22 - } 23 - 24 - export interface RichtextFacet { 25 - index: { 26 - byteStart: number; 27 - byteEnd: number; 28 - }; 29 - features: FacetFeature[]; 30 - } 31 - 32 - export type FacetFeature = LinkFeature | MentionFeature | TagFeature; 33 - 34 - export interface LinkFeature { 35 - $type: 'app.bsky.richtext.facet#link'; 36 - uri: string; 37 - } 38 - 39 - export interface MentionFeature { 40 - $type: 'app.bsky.richtext.facet#mention'; 41 - did: string; 42 - } 43 - 44 - export interface TagFeature { 45 - $type: 'app.bsky.richtext.facet#tag'; 46 - tag: string; 47 - } 48 - 49 - export function isComment(record: unknown): record is Comment { 50 - return ( 51 - typeof record === 'object' && 52 - record !== null && 53 - '$type' in record && 54 - record.$type === 'pub.leaflet.comment' 55 - ); 56 - } 57 - 58 - export function getCommentThread(comments: Comment[]): Map<string, Comment[]> { 59 - const threads = new Map<string, Comment[]>(); 60 - 61 - for (const comment of comments) { 62 - const parent = comment.reply?.parent || comment.subject; 63 - const existing = threads.get(parent) || []; 64 - threads.set(parent, [...existing, comment]); 65 - } 66 - 67 - return threads; 68 - }
+2 -2
package.json
··· 1 1 { 2 2 "name": "seams", 3 - "version": "1.0.0", 3 + "version": "1.0.1", 4 4 "description": "To install dependencies:", 5 5 "main": "index.js", 6 6 "scripts": { 7 7 "dev": "wxt", 8 8 "build": "wxt build", 9 9 "zip": "wxt zip", 10 - "build:via": "vite build --config vite.via.config.ts", 10 + "build:via": "vite build --config vite.via.config.ts && bash scripts/postbuild-via.sh", 11 11 "dev:via": "vite build --config vite.via.config.ts --watch", 12 12 "via": "bash scripts/start-via.sh" 13 13 },
+1 -1
packages/core/package.json
··· 1 1 { 2 2 "name": "@seams/core", 3 - "version": "1.0.0", 3 + "version": "1.0.1", 4 4 "type": "module", 5 5 "main": "./src/index.ts", 6 6 "exports": {
+8 -80
packages/core/src/background/extension.ts
··· 7 7 8 8 export interface ExtensionBackgroundWorkerOptions { 9 9 storage: StorageAdapter; 10 - fetchAnnotationsForUrl: (url: string) => Promise<Annotation[]>; 11 - fetchUserAnnotations: () => Promise<Annotation[]>; 12 - fetchComments: () => Promise<any[]>; 13 10 backendUrl?: string; 14 11 } 15 12 16 13 export class ExtensionBackgroundWorker { 17 14 private storage: StorageAdapter; 18 - private fetchAnnotationsForUrl: (url: string) => Promise<Annotation[]>; 19 - private fetchUserAnnotations: () => Promise<Annotation[]>; 20 - private fetchComments: () => Promise<any[]>; 21 15 private backendUrl: string; 22 - private syncing = false; 23 16 24 17 constructor(options: ExtensionBackgroundWorkerOptions) { 25 18 this.storage = options.storage; 26 - this.fetchAnnotationsForUrl = options.fetchAnnotationsForUrl; 27 - this.fetchUserAnnotations = options.fetchUserAnnotations; 28 - this.fetchComments = options.fetchComments; 29 19 this.backendUrl = options.backendUrl || 'http://localhost:8080'; 30 20 } 31 21 ··· 34 24 } 35 25 36 26 private registerListeners(): void { 37 - // Sync on startup 38 - browser.runtime.onStartup.addListener(async () => { 39 - console.log('[background] Extension startup, syncing from PDS...'); 40 - await this.syncUserAnnotations(); 41 - }); 42 - 43 - // Sync on install (first run) 44 - browser.runtime.onInstalled.addListener(async () => { 45 - console.log('[background] Extension installed, syncing from PDS...'); 46 - await this.syncUserAnnotations(); 47 - }); 27 + // No sync needed on startup - annotations are fetched per-page from backend 48 28 49 29 // Open sidepanel when extension icon is clicked 50 30 const actionApi = browser.action || browser.browserAction; ··· 78 58 const normalized = normalizeUrl(tab.url); 79 59 console.log('[background] Tab updated, pre-fetching annotations for', normalized); 80 60 await this.fetchAndCacheAnnotations(normalized); 81 - 82 - // Notify content script of URL change 83 - browser.tabs.sendMessage(tabId, { 84 - type: 'URL_CHANGED', 85 - url: normalized 86 - }).catch(() => { 87 - // Content script might not be ready yet 88 - }); 89 61 } 90 62 }); 91 63 ··· 98 70 const normalized = normalizeUrl(details.url); 99 71 console.log('[background] SPA navigation detected:', normalized); 100 72 await this.fetchAndCacheAnnotations(normalized); 101 - 102 - // Notify content script of URL change 103 - browser.tabs.sendMessage(details.tabId, { 104 - type: 'URL_CHANGED', 105 - url: normalized 106 - }).catch(() => { 107 - console.log('[background] Failed to notify content script'); 108 - }); 109 73 }); 110 74 } else { 111 75 console.error('[background] browser.webNavigation is not available!'); ··· 114 78 // Handle messages 115 79 browser.runtime.onMessage.addListener((message: any, sender: any, sendResponse: any) => { 116 80 if (message.type === 'SYNC_CACHE') { 117 - console.log('[background] SYNC_CACHE requested'); 118 - this.syncUserAnnotations(); 81 + console.log('[background] SYNC_CACHE requested - triggering fetch for active tab'); 82 + browser.tabs.query({ active: true, currentWindow: true }).then((tabs: any[]) => { 83 + if (tabs[0]?.url) { 84 + const normalized = normalizeUrl(tabs[0].url); 85 + this.fetchAndCacheAnnotations(normalized); 86 + } 87 + }); 119 88 sendResponse({ success: true }); 120 - } 121 - 122 - if (message.type === 'FETCH_ANNOTATIONS_FOR_URL') { 123 - console.log('[background] FETCH_ANNOTATIONS_FOR_URL requested for', message.url); 124 - this.fetchAndCacheAnnotations(message.url) 125 - .then(() => sendResponse({ success: true })) 126 - .catch(err => sendResponse({ success: false, error: err.message })); 127 - return true; 128 89 } 129 90 }); 130 91 } ··· 183 144 } 184 145 } 185 146 186 - private async syncUserAnnotations(): Promise<void> { 187 - if (this.syncing) { 188 - console.log('[background] Sync already in progress, skipping'); 189 - return; 190 - } 191 - 192 - this.syncing = true; 193 - 194 - try { 195 - console.log('[background] Syncing user annotations from PDS...'); 196 - 197 - const userAnnotations = await this.fetchUserAnnotations(); 198 - const comments = await this.fetchComments(); 199 - 200 - await this.storage.set('userAnnotations', userAnnotations); 201 - await this.storage.set('comments', comments); 202 - await this.storage.set('lastSync', Date.now()); 203 - await this.storage.set('syncError', null); 204 - await this.storage.set('lastSyncAttempt', Date.now()); 205 - 206 - console.log('[background] Sync complete:', { 207 - userAnnotations: userAnnotations.length, 208 - comments: comments.length, 209 - source: 'User PDS' 210 - }); 211 - } catch (error) { 212 - console.error('[background] Sync error:', error); 213 - await this.storage.set('syncError', String((error as any)?.message || error)); 214 - await this.storage.set('lastSyncAttempt', Date.now()); 215 - } finally { 216 - this.syncing = false; 217 - } 218 - } 219 147 }
+32 -5
packages/core/src/background/worker.ts
··· 21 21 console.log('[BackgroundWorker] Syncing annotations for:', url); 22 22 23 23 try { 24 - const annotations = await this.fetchAnnotations(url); 25 - console.log('[BackgroundWorker] Fetched', annotations.length, 'annotations'); 24 + const newAnnotations = await this.fetchAnnotations(url); 25 + console.log('[BackgroundWorker] Fetched', newAnnotations.length, 'annotations'); 26 + 27 + // Get existing annotations from global storage 28 + const existingAnnotations = await this.storage.get('annotations') || []; 29 + 30 + // Create a Set of existing URIs for fast lookup 31 + const existingUris = new Set(existingAnnotations.map((a: any) => a.uri)); 32 + 33 + // Filter out annotations that we already have (though we might want to update them?) 34 + // For simplicity, let's just add new ones. 35 + // Actually, we should replace them if they exist to handle updates. 36 + 37 + // 1. Remove old annotations for this URL (or overwrite them) 38 + // Ideally, we merge: if URI exists, replace it. If new, add it. 39 + 40 + const merged = [...existingAnnotations]; 41 + 42 + for (const newAnn of newAnnotations) { 43 + const index = merged.findIndex((a: any) => a.uri === newAnn.uri); 44 + if (index >= 0) { 45 + merged[index] = newAnn; 46 + } else { 47 + merged.push(newAnn); 48 + } 49 + } 26 50 27 - const key = `annotations:${url}`; 28 - await this.storage.set(key, annotations); 29 - console.log('[BackgroundWorker] Stored', annotations.length, 'annotations'); 51 + // Limit total annotations to prevent unbounded memory growth (same as extension) 52 + const MAX_ANNOTATIONS = 500; 53 + const updated = merged.slice(-MAX_ANNOTATIONS); 54 + 55 + await this.storage.set('annotations', updated); 56 + console.log('[BackgroundWorker] Stored', updated.length, 'annotations (global)'); 30 57 } catch (error) { 31 58 console.error('[BackgroundWorker] Failed to sync annotations:', error); 32 59 throw error;
+52 -13
packages/core/src/content/extension.ts
··· 7 7 8 8 export interface ExtensionContentScriptOptions { 9 9 storage: StorageAdapter; 10 - applyHighlights: (annotations: Annotation[]) => void; 10 + applyHighlights: (annotations: Annotation[], storage: StorageAdapter) => void; 11 11 clearHighlights: () => void; 12 12 generateSelectors: (selection: Selection, root: Element) => any[]; 13 13 } 14 14 15 15 export class ExtensionContentScript { 16 16 private storage: StorageAdapter; 17 - private applyHighlights: (annotations: Annotation[]) => void; 17 + private applyHighlights: (annotations: Annotation[], storage: StorageAdapter) => void; 18 18 private clearHighlights: () => void; 19 19 private generateSelectors: (selection: Selection, root: Element) => any[]; 20 20 private currentUrl: string; ··· 71 71 } 72 72 }); 73 73 74 - // Listen for URL changes from background script 75 - browser.runtime.onMessage.addListener((message: any) => { 76 - if (message.type === 'URL_CHANGED') { 77 - const newUrl = normalizeUrl(message.url); 78 - if (newUrl !== this.currentUrl) { 79 - console.log('[content] URL changed (from background):', this.currentUrl, '→', newUrl); 80 - this.currentUrl = newUrl; 81 - this.currentSelection = null; 82 - this.loadAndRenderHighlights(); 74 + 75 + // Patch history API to detect SPA navigation 76 + const pushState = history.pushState; 77 + history.pushState = (...args) => { 78 + pushState.apply(history, args); 79 + this.handleUrlChange(); 80 + }; 81 + 82 + const replaceState = history.replaceState; 83 + history.replaceState = (...args) => { 84 + replaceState.apply(history, args); 85 + this.handleUrlChange(); 86 + }; 87 + 88 + window.addEventListener('popstate', () => this.handleUrlChange()); 89 + 90 + // Watch for DOM changes to handle lazy-loaded content (SPAs) 91 + const observer = new MutationObserver((mutations) => { 92 + let shouldRender = false; 93 + for (const mutation of mutations) { 94 + if (mutation.addedNodes.length > 0 || mutation.type === 'characterData') { 95 + shouldRender = true; 96 + break; 97 + } 98 + } 99 + 100 + if (shouldRender) { 101 + // Debounce re-rendering 102 + if ((this as any).renderTimeout) clearTimeout((this as any).renderTimeout); 103 + (this as any).renderTimeout = setTimeout(() => { 104 + console.log('[content] DOM changed, re-rendering highlights'); 105 + this.loadAndRenderHighlights(); 106 + }, 500); 83 107 } 84 - } 108 + }); 109 + 110 + observer.observe(document.body, { 111 + childList: true, 112 + subtree: true, 113 + characterData: true 85 114 }); 86 115 87 116 // Respond to GET_STATE requests ··· 95 124 }); 96 125 } 97 126 127 + private handleUrlChange() { 128 + const newUrl = normalizeUrl(window.location.href); 129 + if (newUrl !== this.currentUrl) { 130 + console.log('[content] URL changed:', this.currentUrl, '→', newUrl); 131 + this.currentUrl = newUrl; 132 + this.currentSelection = null; 133 + this.loadAndRenderHighlights(); 134 + } 135 + } 136 + 98 137 private async loadAndRenderHighlights(): Promise<void> { 99 138 this.clearHighlights(); 100 139 ··· 114 153 } 115 154 116 155 if (pageAnnotations.length > 0) { 117 - this.applyHighlights(pageAnnotations); 156 + this.applyHighlights(pageAnnotations, this.storage); 118 157 } 119 158 } 120 159 }
+1
packages/core/src/content/index.ts
··· 2 2 export type { ContentScriptOptions } from './script'; 3 3 export { ExtensionContentScript } from './extension'; 4 4 export type { ExtensionContentScriptOptions } from './extension'; 5 + export * from './mobile';
+187
packages/core/src/content/mobile.ts
··· 1 + export function createMobileAnnotateButton(x: number, y: number, onClick: () => void): HTMLElement { 2 + const btn = document.createElement('button'); 3 + btn.textContent = 'Annotate'; 4 + btn.className = 'seams-mobile-annotate-btn'; 5 + Object.assign(btn.style, { 6 + position: 'fixed', 7 + top: `${y + 20}px`, 8 + left: `${x}px`, 9 + transform: 'translateX(-50%)', 10 + zIndex: '2147483647', 11 + padding: '8px 16px', 12 + background: '#2d5016', // Forest green 13 + color: 'white', 14 + border: 'none', 15 + borderRadius: '20px', 16 + boxShadow: '0 2px 8px rgba(0,0,0,0.2)', 17 + fontSize: '14px', 18 + fontWeight: '600', 19 + cursor: 'pointer', 20 + }); 21 + 22 + // Use mousedown to prevent the document mouseup handler from removing the button 23 + // before the click event fires 24 + btn.addEventListener('mousedown', (e) => { 25 + e.stopPropagation(); 26 + }); 27 + 28 + btn.addEventListener('mouseup', (e) => { 29 + e.stopPropagation(); 30 + }); 31 + 32 + btn.addEventListener('click', (e) => { 33 + e.stopPropagation(); 34 + onClick(); 35 + }); 36 + 37 + document.body.appendChild(btn); 38 + return btn; 39 + } 40 + 41 + export function createMobileSidebarToggle(onClick: () => void): HTMLElement { 42 + const btn = document.createElement('button'); 43 + btn.className = 'seams-mobile-toggle-btn'; 44 + btn.textContent = '<<'; // Default state (closed -> open) 45 + Object.assign(btn.style, { 46 + position: 'fixed', 47 + top: '10px', 48 + right: '10px', 49 + zIndex: '2147483647', 50 + width: '40px', 51 + height: '40px', 52 + background: 'white', 53 + color: '#2d5016', // Forest green text 54 + border: '1px solid #ddd', 55 + borderRadius: '50%', 56 + boxShadow: '0 2px 8px rgba(0,0,0,0.1)', 57 + display: 'flex', 58 + alignItems: 'center', 59 + justifyContent: 'center', 60 + fontSize: '14px', 61 + fontWeight: 'bold', 62 + cursor: 'pointer', 63 + }); 64 + 65 + btn.addEventListener('click', (e) => { 66 + e.stopPropagation(); 67 + onClick(); 68 + }); 69 + 70 + document.body.appendChild(btn); 71 + return btn; 72 + } 73 + 74 + export function createMobileAnnotationModal( 75 + text: string, 76 + onSave: (body: string) => void, 77 + onCancel: () => void 78 + ): HTMLElement { 79 + const overlay = document.createElement('div'); 80 + Object.assign(overlay.style, { 81 + position: 'fixed', 82 + top: '0', 83 + left: '0', 84 + width: '100%', 85 + height: '100%', 86 + background: 'rgba(0,0,0,0.5)', 87 + zIndex: '2147483647', 88 + display: 'flex', 89 + alignItems: 'center', 90 + justifyContent: 'center', 91 + padding: '20px', 92 + boxSizing: 'border-box', 93 + }); 94 + 95 + const modal = document.createElement('div'); 96 + Object.assign(modal.style, { 97 + background: 'white', 98 + borderRadius: '12px', 99 + width: '100%', 100 + maxWidth: '400px', 101 + padding: '20px', 102 + boxShadow: '0 4px 12px rgba(0,0,0,0.2)', 103 + display: 'flex', 104 + flexDirection: 'column', 105 + gap: '12px', 106 + }); 107 + 108 + const quote = document.createElement('blockquote'); 109 + quote.textContent = text; 110 + Object.assign(quote.style, { 111 + borderLeft: '3px solid #2d5016', // Forest green 112 + margin: '0', 113 + paddingLeft: '10px', 114 + color: '#666', 115 + fontSize: '14px', 116 + maxHeight: '100px', 117 + overflowY: 'auto', 118 + }); 119 + 120 + const textarea = document.createElement('textarea'); 121 + textarea.placeholder = 'Add your note...'; 122 + Object.assign(textarea.style, { 123 + width: '100%', 124 + height: '100px', 125 + padding: '10px', 126 + border: '1px solid #ddd', 127 + borderRadius: '8px', 128 + resize: 'none', 129 + fontFamily: 'inherit', 130 + boxSizing: 'border-box', 131 + }); 132 + 133 + const buttons = document.createElement('div'); 134 + Object.assign(buttons.style, { 135 + display: 'flex', 136 + justifyContent: 'flex-end', 137 + gap: '8px', 138 + }); 139 + 140 + const cancelBtn = document.createElement('button'); 141 + cancelBtn.textContent = 'Cancel'; 142 + Object.assign(cancelBtn.style, { 143 + padding: '8px 16px', 144 + background: 'transparent', 145 + border: '1px solid #ddd', 146 + borderRadius: '6px', 147 + cursor: 'pointer', 148 + color: '#666', 149 + }); 150 + cancelBtn.onclick = () => { 151 + document.body.removeChild(overlay); 152 + onCancel(); 153 + }; 154 + 155 + const saveBtn = document.createElement('button'); 156 + saveBtn.textContent = 'Save'; 157 + Object.assign(saveBtn.style, { 158 + padding: '8px 16px', 159 + background: '#2d5016', // Forest green 160 + color: 'white', 161 + border: 'none', 162 + borderRadius: '6px', 163 + cursor: 'pointer', 164 + }); 165 + saveBtn.onclick = () => { 166 + const body = textarea.value.trim(); 167 + if (body) { 168 + onSave(body); 169 + document.body.removeChild(overlay); 170 + } 171 + }; 172 + 173 + buttons.appendChild(cancelBtn); 174 + buttons.appendChild(saveBtn); 175 + 176 + modal.appendChild(quote); 177 + modal.appendChild(textarea); 178 + modal.appendChild(buttons); 179 + overlay.appendChild(modal); 180 + 181 + document.body.appendChild(overlay); 182 + 183 + // Focus textarea 184 + setTimeout(() => textarea.focus(), 50); 185 + 186 + return overlay; 187 + }
+15 -13
packages/core/src/content/script.ts
··· 5 5 export interface ContentScriptOptions { 6 6 storage: StorageAdapter; 7 7 getCurrentUrl: () => string; 8 - applyHighlights: (annotations: Annotation[]) => void; 8 + applyHighlights: (annotations: Annotation[], storage: StorageAdapter) => void; 9 9 clearHighlights: () => void; 10 10 } 11 11 12 + import { normalizeUrl } from '../utils'; 13 + 12 14 export class ContentScript { 13 15 private storage: StorageAdapter; 14 16 private getCurrentUrl: () => string; 15 - private applyHighlights: (annotations: Annotation[]) => void; 17 + private applyHighlights: (annotations: Annotation[], storage: StorageAdapter) => void; 16 18 private clearHighlights: () => void; 17 19 private currentAnnotations: Annotation[] = []; 18 20 ··· 29 31 30 32 // Listen for storage changes 31 33 this.storage.onChange(({ key, newValue }) => { 32 - const currentUrl = this.getCurrentUrl(); 33 - const expectedKey = `annotations:${currentUrl}`; 34 - 35 - if (key === expectedKey) { 36 - this.renderAnnotations(newValue || []); 34 + if (key === 'annotations') { 35 + this.renderAnnotationsForCurrentUrl(); 37 36 } 38 37 }); 39 38 } 40 39 41 40 async renderAnnotationsForCurrentUrl(): Promise<void> { 42 - const url = this.getCurrentUrl(); 43 - const key = `annotations:${url}`; 44 - const annotations = await this.storage.get(key); 41 + const url = normalizeUrl(this.getCurrentUrl()); 42 + const allAnnotations = await this.storage.get('annotations') || []; 45 43 46 - if (annotations) { 47 - this.renderAnnotations(annotations); 44 + const pageAnnotations = allAnnotations.filter( 45 + (ann: Annotation) => normalizeUrl(ann.target[0]?.source) === url 46 + ); 47 + 48 + if (pageAnnotations.length > 0) { 49 + this.renderAnnotations(pageAnnotations); 48 50 } else { 49 51 this.clearHighlights(); 50 52 } ··· 55 57 this.clearHighlights(); 56 58 57 59 if (annotations.length > 0) { 58 - this.applyHighlights(annotations); 60 + this.applyHighlights(annotations, this.storage); 59 61 } 60 62 } 61 63
+4
packages/core/src/index.ts
··· 4 4 export * from './background'; 5 5 export * from './content'; 6 6 export * from './utils'; 7 + export * from './oauth'; 8 + export * from './oauth/launchers'; 9 + export * from './pds'; 10 + export * from './sidebar';
+171
packages/core/src/oauth/index.ts
··· 1 + import { 2 + configureOAuth, 3 + createAuthorizationUrl, 4 + finalizeAuthorization, 5 + resolveFromIdentity, 6 + OAuthUserAgent, 7 + type OAuthSession, 8 + } from "@atcute/oauth-browser-client"; 9 + import type { StorageAdapter } from "../storage"; 10 + 11 + const OAUTH_SESSION_KEY = "synthesis-oauth:session"; 12 + 13 + export interface OAuthLauncher { 14 + launch(authUrl: URL): Promise<string>; 15 + } 16 + 17 + export interface OAuthConfig { 18 + clientId: string; 19 + redirectUri: string; 20 + scope: string; 21 + } 22 + 23 + let isOAuthInitialized = false; 24 + 25 + export class OAuthManager { 26 + private storage: StorageAdapter; 27 + private launcher: OAuthLauncher; 28 + private config: OAuthConfig; 29 + 30 + constructor(storage: StorageAdapter, launcher: OAuthLauncher, config: OAuthConfig) { 31 + this.storage = storage; 32 + this.launcher = launcher; 33 + this.config = config; 34 + } 35 + 36 + initialize() { 37 + if (typeof window !== "undefined" && !isOAuthInitialized) { 38 + configureOAuth({ 39 + metadata: { 40 + client_id: this.config.clientId, 41 + redirect_uri: this.config.redirectUri, 42 + }, 43 + }); 44 + isOAuthInitialized = true; 45 + } 46 + } 47 + 48 + async startLoginProcess(handle: string): Promise<void> { 49 + console.log('[oauth] Starting login process for handle:', handle); 50 + 51 + // Store current location for redirect back after login (for web flow) 52 + if (typeof window !== 'undefined' && window.location) { 53 + try { 54 + sessionStorage.setItem('seams_login_redirect', window.location.href); 55 + } catch (e) { 56 + console.warn('[oauth] Failed to save redirect URL:', e); 57 + } 58 + } 59 + 60 + this.initialize(); 61 + 62 + console.log('[oauth] Resolving identity...'); 63 + const { metadata } = await resolveFromIdentity(handle); 64 + console.log('[oauth] PDS metadata:', metadata); 65 + 66 + console.log('[oauth] Creating authorization URL...'); 67 + const authUrl = await createAuthorizationUrl({ 68 + metadata: metadata, 69 + scope: this.config.scope, 70 + }); 71 + console.log('[oauth] Auth URL:', authUrl.toString()); 72 + 73 + console.log('[oauth] Launching auth flow...'); 74 + const capturedUrl = await this.launcher.launch(authUrl); 75 + 76 + if (!capturedUrl) { 77 + throw new Error('OAuth flow cancelled or failed'); 78 + } 79 + 80 + console.log('[oauth] Captured redirect URL:', capturedUrl); 81 + 82 + // Parse OAuth response from redirect URL (params can be in search or hash) 83 + const url = new URL(capturedUrl); 84 + const paramString = url.search || url.hash.slice(1); 85 + const params = new URLSearchParams(paramString); 86 + 87 + console.log('[oauth] OAuth params:', Object.fromEntries(params)); 88 + 89 + if (params.has('error')) { 90 + const error = params.get('error'); 91 + const errorDesc = params.get('error_description'); 92 + console.error('[oauth] OAuth error:', error, errorDesc); 93 + throw new Error(`OAuth error: ${error} - ${errorDesc}`); 94 + } 95 + 96 + // Finalize authorization with the params 97 + console.log('[oauth] Finalizing authorization...'); 98 + const session = await finalizeAuthorization(params); 99 + console.log('[oauth] Authorization complete, session:', session); 100 + 101 + // Store session 102 + await this.saveSession(session); 103 + console.log('[oauth] Session saved successfully'); 104 + } 105 + 106 + async saveSession(session: OAuthSession): Promise<void> { 107 + await this.storage.set(OAUTH_SESSION_KEY, session); 108 + } 109 + 110 + async loadSession(): Promise<OAuthSession | null> { 111 + return await this.storage.get(OAUTH_SESSION_KEY); 112 + } 113 + 114 + async clearSession(): Promise<void> { 115 + await this.storage.set(OAUTH_SESSION_KEY, null); 116 + } 117 + 118 + async getProfile(session: OAuthSession): Promise<any> { 119 + const agent = new OAuthUserAgent(session); 120 + const response = await agent.handle('/xrpc/app.bsky.actor.getProfile?actor=' + session.info.sub); 121 + return await response.json(); 122 + } 123 + } 124 + 125 + /** 126 + * Handle OAuth callback from URL parameters (for web contexts) 127 + * Call this from your oauth-callback page to process the redirect 128 + */ 129 + export async function handleOAuthCallback(storage: StorageAdapter, config?: OAuthConfig): Promise<OAuthSession | null> { 130 + console.log('[oauth] Handling OAuth callback'); 131 + 132 + if (config) { 133 + console.log('[oauth] Configuring OAuth client with:', config); 134 + configureOAuth({ 135 + metadata: { 136 + client_id: config.clientId, 137 + redirect_uri: config.redirectUri, 138 + }, 139 + }); 140 + } 141 + 142 + // Parse OAuth response from URL (params can be in search or hash) 143 + const url = new URL(window.location.href); 144 + const paramString = url.search || url.hash.slice(1); 145 + const params = new URLSearchParams(paramString); 146 + 147 + console.log('[oauth] OAuth params:', Object.fromEntries(params)); 148 + 149 + if (!params.has('code') && !params.has('error')) { 150 + console.log('[oauth] No OAuth params found'); 151 + return null; 152 + } 153 + 154 + if (params.has('error')) { 155 + const error = params.get('error'); 156 + const errorDesc = params.get('error_description'); 157 + console.error('[oauth] OAuth error:', error, errorDesc); 158 + throw new Error(`OAuth error: ${error} - ${errorDesc}`); 159 + } 160 + 161 + // Finalize authorization with the params 162 + console.log('[oauth] Finalizing authorization...'); 163 + const session = await finalizeAuthorization(params); 164 + console.log('[oauth] Authorization complete, session:', session); 165 + 166 + // Store session 167 + await storage.set(OAUTH_SESSION_KEY, session); 168 + console.log('[oauth] Session saved successfully'); 169 + 170 + return session; 171 + }
+69
packages/core/src/oauth/launchers.ts
··· 1 + import type { OAuthLauncher } from "./index"; 2 + 3 + /** 4 + * Extension OAuth launcher using browser.identity.launchWebAuthFlow 5 + */ 6 + export class ExtensionOAuthLauncher implements OAuthLauncher { 7 + async launch(authUrl: URL): Promise<string> { 8 + if (typeof browser === "undefined" || !browser.identity) { 9 + throw new Error('browser.identity not available'); 10 + } 11 + 12 + console.log('[oauth-launcher] Launching web auth flow...'); 13 + const capturedUrl = await browser.identity.launchWebAuthFlow({ 14 + url: authUrl.toString(), 15 + interactive: true, 16 + }); 17 + 18 + if (!capturedUrl) { 19 + throw new Error('OAuth flow cancelled or failed'); 20 + } 21 + 22 + return capturedUrl; 23 + } 24 + } 25 + 26 + /** 27 + * Web OAuth launcher using popup window with postMessage callback 28 + */ 29 + export class WebOAuthLauncher implements OAuthLauncher { 30 + async launch(authUrl: URL): Promise<string> { 31 + return new Promise((resolve, reject) => { 32 + const width = 600; 33 + const height = 700; 34 + const left = window.screenX + (window.outerWidth - width) / 2; 35 + const top = window.screenY + (window.outerHeight - height) / 2; 36 + 37 + const popup = window.open( 38 + authUrl.toString(), 39 + 'oauth-popup', 40 + `width=${width},height=${height},left=${left},top=${top},popup=yes` 41 + ); 42 + 43 + if (!popup) { 44 + reject(new Error('Failed to open OAuth popup')); 45 + return; 46 + } 47 + 48 + // Listen for message from callback page 49 + const messageHandler = (event: MessageEvent) => { 50 + if (event.data.type === 'SEAMS_OAUTH_CALLBACK') { 51 + window.removeEventListener('message', messageHandler); 52 + popup.close(); 53 + resolve(event.data.url); 54 + } 55 + }; 56 + 57 + window.addEventListener('message', messageHandler); 58 + 59 + // Poll for popup close 60 + const pollTimer = setInterval(() => { 61 + if (popup.closed) { 62 + clearInterval(pollTimer); 63 + window.removeEventListener('message', messageHandler); 64 + reject(new Error('OAuth popup closed by user')); 65 + } 66 + }, 500); 67 + }); 68 + } 69 + }
+269
packages/core/src/pds/index.ts
··· 1 + import { OAuthUserAgent } from "@atcute/oauth-browser-client"; 2 + import type { OAuthManager } from "../oauth"; 3 + import type { Annotation } from "../types"; 4 + 5 + const ANNOTATION_COLLECTION = "community.lexicon.annotation.annotation"; 6 + const COMMENT_COLLECTION = "pub.leaflet.comment"; 7 + 8 + export interface Comment { 9 + $type: string; 10 + uri?: string; 11 + cid?: string; 12 + subject: string; 13 + plaintext: string; 14 + createdAt: string; 15 + reply?: { 16 + parent: string; 17 + }; 18 + facets?: any[]; 19 + onPage?: string; 20 + author?: { 21 + did: string; 22 + handle: string; 23 + }; 24 + } 25 + 26 + export interface PDSConfig { 27 + backendUrl: string; 28 + } 29 + 30 + export class PDSClient { 31 + private oauth: OAuthManager; 32 + private config: PDSConfig; 33 + 34 + constructor(oauth: OAuthManager, config: PDSConfig) { 35 + this.oauth = oauth; 36 + this.config = config; 37 + } 38 + 39 + private async authenticatedAgent() { 40 + const session = await this.oauth.loadSession(); 41 + if (!session) { 42 + throw new Error("Not authenticated"); 43 + } 44 + return { agent: new OAuthUserAgent(session), session }; 45 + } 46 + 47 + private async indexInBackend(uri: string, cid: string) { 48 + try { 49 + await fetch(`${this.config.backendUrl}/api/annotations/index`, { 50 + method: 'POST', 51 + headers: { 'Content-Type': 'application/json' }, 52 + body: JSON.stringify({ uri, cid }), 53 + }); 54 + console.log('[pds] Annotation indexed in backend'); 55 + } catch (err) { 56 + console.error('[pds] Failed to index annotation in backend:', err); 57 + } 58 + } 59 + 60 + private async request(agent: OAuthUserAgent, path: string, options: any, retryCount = 0): Promise<Response> { 61 + const response = await agent.handle(path, options); 62 + 63 + if (response.status === 401 && retryCount < 3) { 64 + try { 65 + const data = await response.clone().json(); 66 + if (data.error === 'use_dpop_nonce') { 67 + console.log(`[pds] DPoP nonce mismatch (retry ${retryCount + 1}), retrying...`); 68 + // The OAuthUserAgent should automatically update its nonce from the response headers 69 + return this.request(agent, path, options, retryCount + 1); 70 + } else if (data.error === 'invalid_token' && retryCount < 3) { 71 + console.log(`[pds] Invalid token (retry ${retryCount + 1}), refreshing...`); 72 + try { 73 + // Force token refresh 74 + const newSession = await agent.getSession({ noCache: true }); 75 + // Update persistence 76 + await this.oauth.saveSession(newSession); 77 + 78 + // Create new agent with fresh credentials to ensure the new token is used 79 + const newAgent = new OAuthUserAgent(newSession); 80 + 81 + // Retry with the new agent 82 + return this.request(newAgent, path, options, retryCount + 1); 83 + } catch (refreshErr) { 84 + console.error('[pds] Token refresh failed:', refreshErr); 85 + // If refresh fails, we can't recover, so return original 401 86 + return response; 87 + } 88 + } 89 + } catch (e) { 90 + // Ignore JSON parse errors on 401 91 + } 92 + } 93 + 94 + return response; 95 + } 96 + 97 + async createAnnotation(annotation: Annotation): Promise<Annotation> { 98 + const { agent, session } = await this.authenticatedAgent(); 99 + 100 + const record = { 101 + $type: annotation.$type, 102 + target: annotation.target, 103 + body: annotation.body, 104 + tags: annotation.tags, 105 + document: annotation.document, 106 + createdAt: annotation.createdAt, 107 + }; 108 + 109 + const response = await this.request(agent, '/xrpc/com.atproto.repo.createRecord', { 110 + method: 'POST', 111 + headers: { 112 + 'Content-Type': 'application/json', 113 + }, 114 + body: JSON.stringify({ 115 + repo: session.info.sub, 116 + collection: ANNOTATION_COLLECTION, 117 + record, 118 + }), 119 + }); 120 + 121 + if (!response.ok) { 122 + const error = await response.json(); 123 + console.error('[pds] Create error:', error); 124 + throw new Error(`Failed to create annotation: ${response.status} - ${JSON.stringify(error)}`); 125 + } 126 + 127 + const result = await response.json(); 128 + 129 + // Index in backend 130 + await this.indexInBackend(result.uri, result.cid); 131 + 132 + return { 133 + ...annotation, 134 + uri: result.uri, 135 + cid: result.cid, 136 + }; 137 + } 138 + 139 + async deleteAnnotation(uri: string): Promise<void> { 140 + const { agent, session } = await this.authenticatedAgent(); 141 + 142 + const rkey = uri.split("/").pop(); 143 + if (!rkey) { 144 + throw new Error("Invalid URI"); 145 + } 146 + 147 + const response = await this.request(agent, '/xrpc/com.atproto.repo.deleteRecord', { 148 + method: 'POST', 149 + headers: { 150 + 'Content-Type': 'application/json', 151 + }, 152 + body: JSON.stringify({ 153 + repo: session.info.sub, 154 + collection: ANNOTATION_COLLECTION, 155 + rkey, 156 + }), 157 + }); 158 + 159 + if (!response.ok) { 160 + const error = await response.json(); 161 + console.error('[pds] Delete error:', error); 162 + throw new Error(`Failed to delete annotation: ${response.status} - ${JSON.stringify(error)}`); 163 + } 164 + } 165 + 166 + async createComment(comment: Omit<Comment, 'uri' | 'cid' | 'author'>): Promise<Comment> { 167 + const { agent, session } = await this.authenticatedAgent(); 168 + 169 + const record = { 170 + $type: 'pub.leaflet.comment', 171 + subject: comment.subject, 172 + plaintext: comment.plaintext, 173 + createdAt: comment.createdAt, 174 + reply: comment.reply, 175 + facets: comment.facets, 176 + onPage: comment.onPage, 177 + }; 178 + 179 + const response = await this.request(agent, '/xrpc/com.atproto.repo.createRecord', { 180 + method: 'POST', 181 + headers: { 182 + 'Content-Type': 'application/json', 183 + }, 184 + body: JSON.stringify({ 185 + repo: session.info.sub, 186 + collection: COMMENT_COLLECTION, 187 + record, 188 + }), 189 + }); 190 + 191 + if (!response.ok) { 192 + const error = await response.json(); 193 + console.error('[pds] Create comment error:', error); 194 + throw new Error(`Failed to create comment: ${response.status} - ${JSON.stringify(error)}`); 195 + } 196 + 197 + const result = await response.json(); 198 + 199 + return { 200 + ...comment, 201 + uri: result.uri, 202 + cid: result.cid, 203 + }; 204 + } 205 + 206 + async deleteComment(uri: string): Promise<void> { 207 + const { agent, session } = await this.authenticatedAgent(); 208 + 209 + const rkey = uri.split("/").pop(); 210 + if (!rkey) { 211 + throw new Error("Invalid URI"); 212 + } 213 + 214 + const response = await this.request(agent, '/xrpc/com.atproto.repo.deleteRecord', { 215 + method: 'POST', 216 + headers: { 217 + 'Content-Type': 'application/json', 218 + }, 219 + body: JSON.stringify({ 220 + repo: session.info.sub, 221 + collection: COMMENT_COLLECTION, 222 + rkey, 223 + }), 224 + }); 225 + 226 + if (!response.ok) { 227 + const error = await response.json(); 228 + console.error('[pds] Delete comment error:', error); 229 + throw new Error(`Failed to delete comment: ${response.status} - ${JSON.stringify(error)}`); 230 + } 231 + } 232 + 233 + async listAnnotationsForPage(url: string): Promise<Annotation[]> { 234 + try { 235 + const response = await fetch( 236 + `${this.config.backendUrl}/api/annotations?url=${encodeURIComponent(url)}&limit=100` 237 + ); 238 + 239 + if (!response.ok) { 240 + throw new Error(`Backend error: ${response.status}`); 241 + } 242 + 243 + const data = await response.json(); 244 + const annotations = data.annotations || []; 245 + 246 + return annotations.map((ann: any) => { 247 + const selectors = JSON.parse(ann.selectors || '[]'); 248 + return { 249 + $type: ANNOTATION_COLLECTION, 250 + uri: ann.uri, 251 + cid: ann.cid, 252 + target: [{ 253 + source: ann.targetUrl, 254 + selector: selectors, 255 + }], 256 + body: ann.body || '', 257 + createdAt: ann.createdAt, 258 + author: ann.authorHandle ? { 259 + did: ann.authorDid, 260 + handle: ann.authorHandle, 261 + } : undefined, 262 + }; 263 + }); 264 + } catch (error) { 265 + console.error('[pds] Failed to fetch from backend:', error); 266 + return []; 267 + } 268 + } 269 + }
+467
packages/core/src/sidebar/index.ts
··· 1 + import type { StorageAdapter } from '../storage'; 2 + import type { OAuthLauncher, OAuthConfig, OAuthManager } from '../oauth'; 3 + import { OAuthManager as OAuthManagerImpl } from '../oauth'; 4 + import type { PDSClient, Comment } from '../pds'; 5 + import { PDSClient as PDSClientImpl } from '../pds'; 6 + import type { Annotation } from '../types'; 7 + import { UIState } from './ui-state'; 8 + import { renderAnnotationCard } from './rendering'; 9 + import { normalizeUrl } from './utils'; 10 + 11 + export interface SidebarConfig { 12 + oauth: OAuthConfig; 13 + pds: { 14 + backendUrl: string; 15 + }; 16 + } 17 + 18 + export type SyncCallback = () => void; 19 + 20 + export class Sidebar { 21 + private container: HTMLElement; 22 + private storage: StorageAdapter; 23 + private oauth: OAuthManager; 24 + private pds: PDSClient; 25 + private onSyncNeeded?: SyncCallback; 26 + private uiState: UIState; 27 + 28 + private currentUrl = ''; 29 + private currentSelection: { text: string; selectors: any[] } | null = null; 30 + private pageAnnotations: Annotation[] = []; 31 + private allComments: Comment[] = []; 32 + 33 + constructor( 34 + container: HTMLElement, 35 + storage: StorageAdapter, 36 + launcher: OAuthLauncher, 37 + config: SidebarConfig, 38 + onSyncNeeded?: SyncCallback 39 + ) { 40 + this.container = container; 41 + this.storage = storage; 42 + this.oauth = new OAuthManagerImpl(storage, launcher, config.oauth); 43 + this.pds = new PDSClientImpl(this.oauth, config.pds); 44 + this.onSyncNeeded = onSyncNeeded; 45 + this.uiState = new UIState(); 46 + 47 + this.oauth.initialize(); 48 + this.initialize(); 49 + } 50 + 51 + private async initialize() { 52 + await this.render(); 53 + this.setupStorageListener(); 54 + } 55 + 56 + private setupStorageListener() { 57 + this.storage.onChange((change) => { 58 + if (change.key === 'annotations' || change.key === 'comments') { 59 + this.loadAnnotationsForCurrentUrl(); 60 + } 61 + }); 62 + } 63 + 64 + async setCurrentUrl(url: string) { 65 + this.currentUrl = url; 66 + await this.loadAnnotationsForCurrentUrl(); 67 + } 68 + 69 + getCurrentUrl() { 70 + return this.currentUrl; 71 + } 72 + 73 + setSelection(selection: { text: string; selectors: any[] } | null) { 74 + this.currentSelection = selection; 75 + this.updateSelectionUI(); 76 + } 77 + 78 + private async render() { 79 + const session = await this.oauth.loadSession(); 80 + 81 + if (session) { 82 + await this.renderLoggedIn(session); 83 + } else { 84 + this.renderLoginForm(); 85 + } 86 + } 87 + 88 + private renderLoginForm() { 89 + this.container.innerHTML = ` 90 + <div class="sidebar"> 91 + <div class="auth-section" id="auth-section"> 92 + <div class="login-container"> 93 + <h2>Login to Seams</h2> 94 + <div class="input-wrapper"> 95 + <span class="at-symbol">@</span> 96 + <input type="text" id="handle-input" class="handle-input" placeholder="you.bsky.social" /> 97 + </div> 98 + <button id="login-btn">Login with ATProto</button> 99 + <div id="auth-status"></div> 100 + </div> 101 + </div> 102 + </div> 103 + `; 104 + 105 + const handleInput = this.container.querySelector('#handle-input') as HTMLInputElement; 106 + const loginBtn = this.container.querySelector('#login-btn'); 107 + const authStatus = this.container.querySelector('#auth-status'); 108 + 109 + const handleLogin = async () => { 110 + let handle = handleInput?.value.trim(); 111 + if (!handle) { 112 + alert('Please enter your handle'); 113 + return; 114 + } 115 + 116 + if (handle.startsWith('@')) { 117 + handle = handle.slice(1); 118 + } 119 + 120 + try { 121 + if (authStatus) authStatus.textContent = 'Logging in...'; 122 + 123 + // Use window.location.href to redirect back to the same page 124 + // The sidebar iframe is on the same domain as the proxy, but it's an iframe. 125 + // If we are in a popup (native/extension), this launcher handles it. 126 + // If we are in web (via proxy), the launcher below handles it. 127 + 128 + // Important: The login flow might navigate the top window or open a popup depending on launcher. 129 + // For via proxy with WebOAuthLauncher (current setup), it opens a popup. 130 + // But if it's a redirect flow (e.g. mobile), we need to handle that. 131 + 132 + // However, since sidebar.ts initializes WebOAuthLauncher, it opens a popup. 133 + // The issue described is "redirected to /proxy/https://example.com". 134 + // This suggests the popup/redirect flow is bringing the user to the callback page, 135 + // and the callback page is hardcoded to redirect to example.com. 136 + 137 + // We need to make sure the callback page redirects to the RIGHT place. 138 + // But wait, the callback page is in the popup? Or is the main window redirecting? 139 + 140 + // If we are using AT Protocol OAuth client directly, `launch` returns a promise that resolves with the callback URL. 141 + // The `WebOAuthLauncher` opens a popup and waits for a message. 142 + // If the user is seeing a redirect in the main window, maybe they aren't using the popup launcher? 143 + // OR the popup launcher is working but the popup itself redirects to example.com? 144 + 145 + // The `oauth-callback.ts` page is what runs inside the popup (or the redirected tab). 146 + // It calls `handleOAuthCallback` and then redirects. 147 + // It currently redirects to `/proxy/https://example.com`. 148 + 149 + // In a popup flow: 150 + // 1. Sidebar opens popup to authUrl. 151 + // 2. User logs in at PDS. 152 + // 3. PDS redirects to callback URL (oauth-callback.html). 153 + // 4. oauth-callback.html runs `processCallback`. 154 + // 5. It saves session. 155 + // 6. It redirects to example.com. 156 + // 7. BUT `WebOAuthLauncher` is waiting for a message from the popup! 157 + // It listens for `SEAMS_OAUTH_CALLBACK`. 158 + 159 + // The `oauth-callback.ts` DOES NOT send this message! It just redirects. 160 + // So `WebOAuthLauncher` never resolves, and the popup (which is now on example.com) stays open? 161 + // Or if it's a full page redirect (mobile?), the user is stuck on example.com. 162 + 163 + // We need `oauth-callback.ts` to send the message if it's in a popup. 164 + // AND/OR if it's a full page redirect, we need to go back to the original page. 165 + 166 + await this.oauth.startLoginProcess(handle); 167 + 168 + const session = await this.oauth.loadSession(); 169 + if (session) { 170 + await this.renderLoggedIn(session); 171 + this.onSyncNeeded?.(); 172 + } 173 + } catch (error) { 174 + if (authStatus) authStatus.textContent = 'Login failed'; 175 + console.error('[sidebar] Login error:', error); 176 + } 177 + }; 178 + 179 + loginBtn?.addEventListener('click', handleLogin); 180 + handleInput?.addEventListener('keydown', (e) => { 181 + if (e.key === 'Enter') { 182 + e.preventDefault(); 183 + handleLogin(); 184 + } 185 + }); 186 + } 187 + 188 + private async renderLoggedIn(session: any) { 189 + let profile: any = null; 190 + try { 191 + profile = await this.oauth.getProfile(session); 192 + } catch (error) { 193 + console.error('[sidebar] Failed to fetch profile:', error); 194 + } 195 + 196 + this.container.innerHTML = ` 197 + <div class="sidebar"> 198 + <div class="content-section" id="content-section"> 199 + <div class="annotation-form" id="annotation-form" style="display: none;"> 200 + <div class="form-header"> 201 + <h2>Create Annotation</h2> 202 + <button id="clear-selection-btn" class="clear-btn">×</button> 203 + </div> 204 + <div id="selected-text" class="selected-text"></div> 205 + <textarea id="annotation-text" placeholder="Add your note..."></textarea> 206 + <button id="save-btn">Save Annotation</button> 207 + </div> 208 + <div class="annotations-list"> 209 + <h2>Annotations on this page</h2> 210 + <div id="annotations"></div> 211 + </div> 212 + </div> 213 + <div class="profile-menu"> 214 + <img id="profile-avatar" style="display: ${profile?.avatar ? 'block' : 'none'}; width: 40px; height: 40px; border-radius: 50%; cursor: pointer;" ${profile?.avatar ? `src="${profile.avatar}"` : ''} /> 215 + <div id="profile-dropdown" class="profile-dropdown" style="display: none;"> 216 + <button id="logout-btn">Logout</button> 217 + </div> 218 + </div> 219 + </div> 220 + `; 221 + 222 + this.attachEventListeners(); 223 + await this.loadAnnotationsForCurrentUrl(); 224 + } 225 + 226 + private attachEventListeners() { 227 + const clearSelectionBtn = this.container.querySelector('#clear-selection-btn'); 228 + const saveBtn = this.container.querySelector('#save-btn'); 229 + const logoutBtn = this.container.querySelector('#logout-btn'); 230 + const profileAvatar = this.container.querySelector('#profile-avatar'); 231 + const profileDropdown = this.container.querySelector('#profile-dropdown'); 232 + 233 + clearSelectionBtn?.addEventListener('click', () => { 234 + this.currentSelection = null; 235 + this.updateSelectionUI(); 236 + }); 237 + 238 + saveBtn?.addEventListener('click', async () => { 239 + await this.handleSaveAnnotation(); 240 + }); 241 + 242 + logoutBtn?.addEventListener('click', async () => { 243 + await this.oauth.clearSession(); 244 + await this.storage.set('annotations', null); 245 + await this.storage.set('comments', null); 246 + await this.storage.set('lastSync', null); 247 + await this.storage.set('syncError', null); 248 + await this.storage.set('lastSyncAttempt', null); 249 + await this.render(); 250 + }); 251 + 252 + profileAvatar?.addEventListener('click', () => { 253 + if (profileDropdown) { 254 + profileDropdown.setAttribute('style', 255 + profileDropdown.getAttribute('style')?.includes('none') ? 'display: block;' : 'display: none;' 256 + ); 257 + } 258 + }); 259 + 260 + document.addEventListener('click', (e) => { 261 + if (profileDropdown && profileAvatar && 262 + !profileAvatar.contains(e.target as Node) && 263 + !profileDropdown.contains(e.target as Node)) { 264 + profileDropdown.setAttribute('style', 'display: none;'); 265 + } 266 + }); 267 + } 268 + 269 + private updateSelectionUI() { 270 + const annotationForm = this.container.querySelector('#annotation-form'); 271 + const selectedTextEl = this.container.querySelector('#selected-text'); 272 + const annotationTextarea = this.container.querySelector('#annotation-text') as HTMLTextAreaElement; 273 + 274 + if (this.currentSelection && this.currentSelection.text && selectedTextEl) { 275 + selectedTextEl.innerHTML = `<blockquote>${this.currentSelection.text}</blockquote>`; 276 + if (annotationForm) annotationForm.setAttribute('style', 'display: block;'); 277 + } else { 278 + if (selectedTextEl) selectedTextEl.innerHTML = ''; 279 + if (annotationTextarea) annotationTextarea.value = ''; 280 + if (annotationForm) annotationForm.setAttribute('style', 'display: none;'); 281 + } 282 + } 283 + 284 + async createAnnotation(target: { source: string, selectors: any[] }, body: string) { 285 + try { 286 + await this.pds.createAnnotation({ 287 + $type: 'community.lexicon.annotation.annotation', 288 + target: [target], 289 + body, 290 + createdAt: new Date().toISOString(), 291 + }); 292 + 293 + this.currentSelection = null; 294 + this.updateSelectionUI(); 295 + this.onSyncNeeded?.(); 296 + } catch (error) { 297 + console.error('[sidebar] Failed to create annotation:', error); 298 + throw error; // Re-throw to handle in caller if needed 299 + } 300 + } 301 + 302 + private async handleSaveAnnotation() { 303 + if (!this.currentSelection) return; 304 + 305 + const annotationTextarea = this.container.querySelector('#annotation-text') as HTMLTextAreaElement; 306 + const body = annotationTextarea?.value.trim() || ''; 307 + 308 + try { 309 + await this.pds.createAnnotation({ 310 + $type: 'community.lexicon.annotation.annotation', 311 + target: [{ 312 + source: this.currentUrl, 313 + selector: this.currentSelection.selectors, 314 + }], 315 + body, 316 + createdAt: new Date().toISOString(), 317 + }); 318 + 319 + this.currentSelection = null; 320 + this.updateSelectionUI(); 321 + this.onSyncNeeded?.(); 322 + } catch (error) { 323 + console.error('[sidebar] Failed to create annotation:', error); 324 + alert('Failed to save annotation'); 325 + } 326 + } 327 + 328 + private async loadAnnotationsForCurrentUrl() { 329 + const result = await this.storage.get(['annotations', 'comments']); 330 + 331 + const norm = normalizeUrl(this.currentUrl); 332 + console.log('[sidebar] Current URL normalized:', norm); 333 + console.log('[sidebar] Total annotations in storage:', result.annotations?.length || 0); 334 + 335 + this.pageAnnotations = (result.annotations || []).filter( 336 + (ann: Annotation) => { 337 + const annUrl = normalizeUrl(ann.target[0]?.source); 338 + const matches = annUrl === norm; 339 + if (!matches) { 340 + console.log('[sidebar] Filtered out:', annUrl, '!==', norm); 341 + } 342 + return matches; 343 + } 344 + ); 345 + console.log('[sidebar] Filtered to', this.pageAnnotations.length, 'annotations for this page'); 346 + this.allComments = result.comments || []; 347 + 348 + this.renderAnnotations(); 349 + } 350 + 351 + private renderAnnotations() { 352 + const annotationsContainer = this.container.querySelector('#annotations'); 353 + if (!annotationsContainer) return; 354 + 355 + if (this.pageAnnotations.length === 0) { 356 + annotationsContainer.innerHTML = '<p class="empty">No annotations yet. Select text to create one.</p>'; 357 + return; 358 + } 359 + 360 + annotationsContainer.innerHTML = this.pageAnnotations 361 + .map(ann => renderAnnotationCard(ann, this.allComments, this.uiState)) 362 + .join(''); 363 + 364 + this.attachCommentEventListeners(); 365 + } 366 + 367 + private attachCommentEventListeners() { 368 + this.container.querySelectorAll('.toggle-comments-btn').forEach(btn => { 369 + btn.addEventListener('click', (e) => { 370 + const uri = (e.target as HTMLElement).dataset.uri!; 371 + this.uiState.toggleThreadCollapsed(uri); 372 + this.renderAnnotations(); 373 + }); 374 + }); 375 + 376 + this.container.querySelectorAll('.add-comment-btn').forEach(btn => { 377 + btn.addEventListener('click', (e) => { 378 + const uri = (e.target as HTMLElement).dataset.uri!; 379 + this.uiState.showReplyForm(uri); 380 + this.renderAnnotations(); 381 + }); 382 + }); 383 + 384 + this.container.querySelectorAll('.reply-btn').forEach(btn => { 385 + btn.addEventListener('click', (e) => { 386 + const uri = (e.target as HTMLElement).dataset.uri!; 387 + this.uiState.showReplyForm(uri); 388 + this.renderAnnotations(); 389 + }); 390 + }); 391 + 392 + this.container.querySelectorAll('.cancel-comment-btn, .cancel-reply-btn').forEach(btn => { 393 + btn.addEventListener('click', (e) => { 394 + const form = (e.target as HTMLElement).closest('.comment-form, .reply-form')!; 395 + const uri = form.getAttribute('data-subject') || form.getAttribute('data-parent')!; 396 + this.uiState.hideReplyForm(uri); 397 + this.renderAnnotations(); 398 + }); 399 + }); 400 + 401 + this.container.querySelectorAll('.save-comment-btn').forEach(btn => { 402 + btn.addEventListener('click', async (e) => { 403 + const form = (e.target as HTMLElement).closest('.comment-form')!; 404 + const textarea = form.querySelector('.comment-input') as HTMLTextAreaElement; 405 + const subject = form.getAttribute('data-subject')!; 406 + const plaintext = textarea.value.trim(); 407 + 408 + if (!plaintext) return; 409 + 410 + try { 411 + await this.pds.createComment({ 412 + $type: 'pub.leaflet.comment', 413 + subject, 414 + plaintext, 415 + createdAt: new Date().toISOString(), 416 + }); 417 + this.uiState.hideReplyForm(subject); 418 + this.onSyncNeeded?.(); 419 + } catch (error) { 420 + console.error('[sidebar] Failed to create comment:', error); 421 + alert('Failed to post comment'); 422 + } 423 + }); 424 + }); 425 + 426 + this.container.querySelectorAll('.save-reply-btn').forEach(btn => { 427 + btn.addEventListener('click', async (e) => { 428 + const form = (e.target as HTMLElement).closest('.reply-form')!; 429 + const textarea = form.querySelector('.reply-input') as HTMLTextAreaElement; 430 + const parent = form.getAttribute('data-parent')!; 431 + const plaintext = textarea.value.trim(); 432 + 433 + if (!plaintext) return; 434 + 435 + const parentComment = this.allComments.find(c => c.uri === parent); 436 + if (!parentComment) return; 437 + 438 + try { 439 + await this.pds.createComment({ 440 + $type: 'pub.leaflet.comment', 441 + subject: parentComment.subject, 442 + plaintext, 443 + createdAt: new Date().toISOString(), 444 + reply: { parent }, 445 + }); 446 + this.uiState.hideReplyForm(parent); 447 + this.onSyncNeeded?.(); 448 + } catch (error) { 449 + console.error('[sidebar] Failed to create reply:', error); 450 + alert('Failed to post reply'); 451 + } 452 + }); 453 + }); 454 + 455 + this.container.querySelectorAll('.thread-toggle-btn').forEach(btn => { 456 + btn.addEventListener('click', (e) => { 457 + const uri = (e.target as HTMLElement).dataset.uri!; 458 + this.uiState.toggleThreadCollapsed(uri); 459 + this.renderAnnotations(); 460 + }); 461 + }); 462 + } 463 + } 464 + 465 + export { UIState } from './ui-state'; 466 + export { renderAnnotationCard, buildCommentThread } from './rendering'; 467 + export { normalizeUrl } from './utils';
+120
packages/core/src/sidebar/rendering.ts
··· 1 + import type { Annotation } from '../types'; 2 + import type { Comment } from '../pds'; 3 + import type { UIState } from './ui-state'; 4 + 5 + export function buildCommentThread( 6 + parentUri: string, 7 + allComments: Comment[], 8 + uiState: UIState, 9 + isNested: boolean = false 10 + ): string { 11 + const replies = allComments.filter(c => c.reply?.parent === parentUri); 12 + if (replies.length === 0) return ''; 13 + 14 + const isCollapsed = uiState.isThreadCollapsed(parentUri); 15 + 16 + return ` 17 + <div class="comment-thread ${isNested ? 'nested' : ''}"> 18 + <button class="thread-toggle-btn" data-uri="${parentUri}"> 19 + ${isCollapsed ? '▸' : '▾'} ${replies.length} ${replies.length === 1 ? 'reply' : 'replies'} 20 + </button> 21 + ${!isCollapsed ? ` 22 + <div class="thread-children ${replies.length === 1 ? 'single-child' : ''}"> 23 + ${replies.map(comment => { 24 + const hasReplies = allComments.some(c => c.reply?.parent === comment.uri); 25 + const isReplyFormActive = uiState.isReplyFormActive(comment.uri!); 26 + 27 + return ` 28 + <div class="comment" data-uri="${comment.uri}"> 29 + <div class="comment-content"> 30 + <div class="comment-text">${comment.plaintext}</div> 31 + <div class="comment-meta"> 32 + <small>${new Date(comment.createdAt).toLocaleString()}</small> 33 + <button class="reply-btn" data-uri="${comment.uri}">Reply</button> 34 + </div> 35 + </div> 36 + ${isReplyFormActive ? ` 37 + <div class="reply-form" data-parent="${comment.uri}"> 38 + <textarea class="reply-input" placeholder="Write a reply..."></textarea> 39 + <div class="reply-actions"> 40 + <button class="save-reply-btn">Post</button> 41 + <button class="cancel-reply-btn">Cancel</button> 42 + </div> 43 + </div> 44 + ` : ''} 45 + ${hasReplies ? buildCommentThread(comment.uri!, allComments, uiState, true) : ''} 46 + </div> 47 + `; 48 + }).join('')} 49 + </div> 50 + ` : ''} 51 + </div> 52 + `; 53 + } 54 + 55 + export function renderAnnotationCard( 56 + ann: Annotation, 57 + allComments: Comment[], 58 + uiState: UIState 59 + ): string { 60 + const quote = ann.target[0]?.selector?.find((s: any) => s.$type === 'community.lexicon.annotation.annotation#textQuoteSelector'); 61 + const text = quote?.exact || ''; 62 + const comments = allComments.filter(c => c.subject === ann.uri && !c.reply); 63 + const isCommentsCollapsed = uiState.isThreadCollapsed(ann.uri!); 64 + const isCommentFormActive = uiState.isReplyFormActive(ann.uri!); 65 + 66 + return ` 67 + <div class="annotation-card" data-uri="${ann.uri}"> 68 + ${text ? `<blockquote>${text}</blockquote>` : ''} 69 + ${ann.body ? `<p>${ann.body}</p>` : ''} 70 + <div class="annotation-meta"> 71 + <small>${new Date(ann.createdAt).toLocaleString()}</small> 72 + </div> 73 + <div class="comments-section"> 74 + <div class="comments-header"> 75 + <button class="toggle-comments-btn" data-uri="${ann.uri}"> 76 + ${isCommentsCollapsed ? '▸' : '▾'} ${comments.length} comment${comments.length !== 1 ? 's' : ''} 77 + </button> 78 + <button class="add-comment-btn" data-uri="${ann.uri}">Add comment</button> 79 + </div> 80 + ${!isCommentsCollapsed ? ` 81 + <div class="comments-list"> 82 + ${isCommentFormActive ? ` 83 + <div class="comment-form" data-subject="${ann.uri}"> 84 + <textarea class="comment-input" placeholder="Write a comment..."></textarea> 85 + <div class="comment-actions"> 86 + <button class="save-comment-btn">Post</button> 87 + <button class="cancel-comment-btn">Cancel</button> 88 + </div> 89 + </div> 90 + ` : ''} 91 + ${comments.map(comment => { 92 + const hasReplies = allComments.some(c => c.reply?.parent === comment.uri); 93 + 94 + return ` 95 + <div class="comment" data-uri="${comment.uri}"> 96 + <div class="comment-content"> 97 + <div class="comment-text">${comment.plaintext}</div> 98 + <div class="comment-meta"> 99 + <small>${new Date(comment.createdAt).toLocaleString()}</small> 100 + <button class="reply-btn" data-uri="${comment.uri}">Reply</button> 101 + </div> 102 + </div> 103 + ${uiState.isReplyFormActive(comment.uri!) ? ` 104 + <div class="reply-form" data-parent="${comment.uri}"> 105 + <textarea class="reply-input" placeholder="Write a reply..."></textarea> 106 + <div class="reply-actions"> 107 + <button class="save-reply-btn">Post</button> 108 + <button class="cancel-reply-btn">Cancel</button> 109 + </div> 110 + </div> 111 + ` : ''} 112 + ${hasReplies ? buildCommentThread(comment.uri!, allComments, uiState, true) : ''} 113 + </div> 114 + `;}).join('')} 115 + </div> 116 + ` : ''} 117 + </div> 118 + </div> 119 + `; 120 + }
+32
packages/core/src/sidebar/ui-state.ts
··· 1 + export class UIState { 2 + private collapsedThreads = new Set<string>(); 3 + private activeReplyForms = new Set<string>(); 4 + 5 + isThreadCollapsed(uri: string): boolean { 6 + return this.collapsedThreads.has(uri); 7 + } 8 + 9 + toggleThreadCollapsed(uri: string): void { 10 + if (this.collapsedThreads.has(uri)) { 11 + this.collapsedThreads.delete(uri); 12 + } else { 13 + this.collapsedThreads.add(uri); 14 + } 15 + } 16 + 17 + isReplyFormActive(uri: string): boolean { 18 + return this.activeReplyForms.has(uri); 19 + } 20 + 21 + showReplyForm(uri: string): void { 22 + this.activeReplyForms.add(uri); 23 + } 24 + 25 + hideReplyForm(uri: string): void { 26 + this.activeReplyForms.delete(uri); 27 + } 28 + 29 + clearAllReplyForms(): void { 30 + this.activeReplyForms.clear(); 31 + } 32 + }
+14
packages/core/src/sidebar/utils.ts
··· 1 + export function normalizeUrl(url: string): string { 2 + try { 3 + const parsed = new URL(url); 4 + parsed.hash = ''; 5 + let path = parsed.pathname; 6 + if (path.endsWith('/') && path !== '/') { 7 + path = path.slice(0, -1); 8 + } 9 + parsed.pathname = path; 10 + return parsed.toString(); 11 + } catch { 12 + return url; 13 + } 14 + }
+1 -1
packages/core/src/storage/adapter.ts
··· 7 7 } 8 8 9 9 export interface StorageAdapter { 10 - get(key: string): Promise<any>; 10 + get(keys: string | string[]): Promise<any>; 11 11 set(key: string, value: any): Promise<void>; 12 12 onChange(callback: (change: StorageChange) => void): void; 13 13 }
+6 -3
packages/core/src/storage/browser.ts
··· 34 34 } 35 35 } 36 36 37 - async get(key: string): Promise<any> { 38 - const result = await browser.storage.local.get(key); 39 - return result[key] ?? null; 37 + async get(keys: string | string[]): Promise<any> { 38 + if (typeof keys === 'string') { 39 + const result = await browser.storage.local.get(keys); 40 + return result[keys] ?? null; 41 + } 42 + return await browser.storage.local.get(keys); 40 43 } 41 44 42 45 async set(key: string, value: any): Promise<void> {
+12 -3
packages/core/src/storage/web.ts
··· 13 13 }; 14 14 } 15 15 16 - async get(key: string): Promise<any> { 17 - const value = localStorage.getItem(key); 18 - return value ? JSON.parse(value) : null; 16 + async get(keys: string | string[]): Promise<any> { 17 + if (typeof keys === 'string') { 18 + const value = localStorage.getItem(keys); 19 + return value ? JSON.parse(value) : null; 20 + } 21 + 22 + const result: Record<string, any> = {}; 23 + keys.forEach(key => { 24 + const value = localStorage.getItem(key); 25 + result[key] = value ? JSON.parse(value) : null; 26 + }); 27 + return result; 19 28 } 20 29 21 30 async set(key: string, value: any): Promise<void> {
+47 -61
packages/core/src/utils/highlights/apply.ts
··· 1 1 import { findAnnotationRange } from '../selectors/match'; 2 2 import { showAnnotationPopover } from './popover'; 3 3 import type { Annotation } from '../../types'; 4 + import { TextRange } from './text-range'; 5 + import { wholeTextNodesInRange } from './range-util'; 6 + 7 + import type { StorageAdapter } from '../../storage/adapter'; 4 8 5 - export function applyHighlights(annotations: Annotation[], container?: HTMLElement) { 6 - // Use main/article as root to avoid matching text in script tags 9 + export function applyHighlights(annotations: Annotation[], storage: StorageAdapter, container?: HTMLElement) { 7 10 const root = container || document.querySelector('main') || document.querySelector('article') || document.body; 8 11 9 - // Clear existing highlights first 10 12 clearHighlights(root); 11 13 12 14 console.log(`[synthesis] Applying ${annotations.length} highlights`); 13 - console.log('[synthesis] Container:', root.tagName, root.textContent?.substring(0, 100)); 15 + 16 + // Convert all ranges to TextRange immediately (immune to DOM mutations) 17 + const textRangesWithAnnotations: Array<{ textRange: TextRange; annotation: Annotation }> = []; 14 18 15 19 annotations.forEach((annotation, index) => { 16 - console.log(`[synthesis] Processing annotation ${index + 1}/${annotations.length}`); 20 + console.log(`[synthesis] Finding range for annotation ${index + 1}/${annotations.length}`); 17 21 const range = findAnnotationRange(annotation, root); 18 22 19 23 if (!range) { ··· 21 25 return; 22 26 } 23 27 24 - // Check if range is inside a script/style tag (not visible) 28 + // Check if range is inside a script/style tag 25 29 let node = range.commonAncestorContainer; 26 30 while (node && node !== container) { 27 31 if (node.nodeName === 'SCRIPT' || node.nodeName === 'STYLE') { ··· 31 35 node = node.parentNode as HTMLElement; 32 36 } 33 37 34 - console.log('[synthesis] Found range, attempting to highlight'); 38 + // Convert to TextRange immediately - immune to DOM mutations 39 + const textRange = TextRange.fromRange(range); 40 + textRangesWithAnnotations.push({ textRange, annotation }); 41 + }); 42 + 43 + console.log(`[synthesis] Found ${textRangesWithAnnotations.length} valid annotations`); 44 + 45 + // Apply highlights in any order - TextRange protects us from invalidation 46 + textRangesWithAnnotations.forEach(({ textRange, annotation }, index) => { 47 + console.log(`[synthesis] Applying highlight ${index + 1}/${textRangesWithAnnotations.length}`); 35 48 try { 36 - highlightRange(range, annotation); 37 - console.log('[synthesis] Successfully highlighted annotation', index + 1); 49 + // Convert TextRange back to fresh DOM Range 50 + const range = textRange.toRange(); 51 + highlightRange(range, annotation, storage); 52 + console.log('[synthesis] Successfully highlighted annotation'); 38 53 } catch (error) { 39 54 console.warn('[synthesis] Failed to highlight range:', error); 40 55 } ··· 43 58 console.log('[synthesis] Finished applying highlights'); 44 59 } 45 60 46 - function highlightRange(range: Range, annotation: Annotation) { 61 + function highlightRange(range: Range, annotation: Annotation, storage: StorageAdapter) { 47 62 console.log('[synthesis] Highlighting range:', range.toString().substring(0, 50)); 48 63 49 64 const highlight = document.createElement('span'); ··· 83 98 highlight, 84 99 // On save 85 100 async (updatedAnnotation) => { 86 - const stored = await browser.storage.local.get('annotations'); 87 - const annotations = stored.annotations || []; 101 + const stored = await storage.get('annotations'); 102 + const annotations = stored.annotations || stored || []; // Handle both {annotations: [...]} and [...] 88 103 const index = annotations.findIndex((a: Annotation) => 89 104 a.createdAt === annotation.createdAt 90 105 ); 91 106 if (index !== -1) { 92 107 annotations[index] = updatedAnnotation; 93 - await browser.storage.local.set({ annotations }); 108 + await storage.set('annotations', annotations); 94 109 console.log('[synthesis] Annotation updated:', updatedAnnotation); 95 110 96 111 // Update the annotation object in memory so next click shows updated note ··· 99 114 }, 100 115 // On delete 101 116 async () => { 102 - const stored = await browser.storage.local.get('annotations'); 103 - const annotations = stored.annotations || []; 117 + const stored = await storage.get('annotations'); 118 + const annotations = stored.annotations || stored || []; // Handle both {annotations: [...]} and [...] 104 119 const filtered = annotations.filter((a: Annotation) => 105 120 a.createdAt !== annotation.createdAt 106 121 ); 107 - await browser.storage.local.set({ annotations: filtered }); 122 + await storage.set('annotations', filtered); 108 123 console.log('[synthesis] Annotation deleted'); 109 124 110 125 // Remove highlight ··· 113 128 ); 114 129 }); 115 130 116 - // Wrap text nodes individually to preserve block structure 131 + // Get whole text nodes (splits at boundaries) 117 132 try { 118 - const textNodes = getTextNodesInRange(range); 133 + const textNodes = wholeTextNodesInRange(range); 119 134 console.log('[synthesis] Found', textNodes.length, 'text nodes to highlight'); 120 135 121 136 if (textNodes.length === 0) { ··· 123 138 return; 124 139 } 125 140 126 - // Wrap each text node 127 - textNodes.forEach((textNode, index) => { 128 - const nodeRange = document.createRange(); 129 - nodeRange.selectNodeContents(textNode); 130 - 131 - // For first and last nodes, use the original range boundaries 132 - if (index === 0 && textNode === range.startContainer) { 133 - nodeRange.setStart(textNode, range.startOffset); 134 - } 135 - if (index === textNodes.length - 1 && textNode === range.endContainer) { 136 - nodeRange.setEnd(textNode, range.endOffset); 137 - } 138 - 141 + // Wrap each text node in a highlight span 142 + textNodes.forEach((textNode) => { 139 143 const span = document.createElement('span'); 140 144 span.className = 'synthesis-highlight'; 141 145 span.dataset.annotationId = annotation.uri || annotation.createdAt; ··· 177 181 annotation, 178 182 span, 179 183 async (updatedAnnotation) => { 180 - const stored = await browser.storage.local.get('annotations'); 181 - const annotations = stored.annotations || []; 184 + const stored = await storage.get('annotations'); 185 + const annotations = stored.annotations || stored || []; // Handle both {annotations: [...]} and [...] 182 186 const idx = annotations.findIndex((a: Annotation) => 183 187 a.createdAt === annotation.createdAt 184 188 ); 185 189 if (idx !== -1) { 186 190 annotations[idx] = updatedAnnotation; 187 - await browser.storage.local.set({ annotations }); 191 + await storage.set('annotations', annotations); 188 192 console.log('[synthesis] Annotation updated:', updatedAnnotation); 189 193 Object.assign(annotation, updatedAnnotation); 190 194 } 191 195 }, 192 196 async () => { 193 - const stored = await browser.storage.local.get('annotations'); 194 - const annotations = stored.annotations || []; 197 + const stored = await storage.get('annotations'); 198 + const annotations = stored.annotations || stored || []; // Handle both {annotations: [...]} and [...] 195 199 const filtered = annotations.filter((a: Annotation) => 196 200 a.createdAt !== annotation.createdAt 197 201 ); 198 - await browser.storage.local.set({ annotations: filtered }); 202 + await storage.set('annotations', filtered); 199 203 console.log('[synthesis] Annotation deleted'); 200 204 201 205 // Remove all highlight spans for this annotation ··· 207 211 ); 208 212 }); 209 213 210 - nodeRange.surroundContents(span); 214 + // Wrap the entire text node 215 + const parent = textNode.parentNode; 216 + if (parent) { 217 + parent.replaceChild(span, textNode); 218 + span.appendChild(textNode); 219 + } 211 220 }); 212 221 213 222 console.log('[synthesis] Successfully applied highlight across', textNodes.length, 'text nodes'); 214 223 } catch (error) { 215 224 console.error('[synthesis] Failed to apply highlight:', error); 216 225 } 217 - } 218 - 219 - function getTextNodesInRange(range: Range): Text[] { 220 - const textNodes: Text[] = []; 221 - const walker = document.createTreeWalker( 222 - range.commonAncestorContainer, 223 - NodeFilter.SHOW_TEXT, 224 - { 225 - acceptNode: (node) => { 226 - if (range.intersectsNode(node)) { 227 - return NodeFilter.FILTER_ACCEPT; 228 - } 229 - return NodeFilter.FILTER_REJECT; 230 - } 231 - } 232 - ); 233 - 234 - let node: Node | null; 235 - while (node = walker.nextNode()) { 236 - textNodes.push(node as Text); 237 - } 238 - 239 - return textNodes; 240 226 } 241 227 242 228 export function clearHighlights(container: HTMLElement = document.body) {
+56
packages/core/src/utils/highlights/range-util.ts
··· 1 + // Range utility functions 2 + // Adapted from Hypothesis client (BSD/MIT licensed) 3 + // https://github.com/hypothesis/client/blob/main/src/annotator/range-util.ts 4 + 5 + export function isNodeInRange(range: Range, node: Node): boolean { 6 + try { 7 + const length = node.nodeValue?.length ?? node.childNodes.length; 8 + return ( 9 + range.comparePoint(node, 0) <= 0 && 10 + range.comparePoint(node, length) >= 0 11 + ); 12 + } catch { 13 + return false; 14 + } 15 + } 16 + 17 + export function wholeTextNodesInRange(range: Range): Text[] { 18 + if (range.collapsed) { 19 + return []; 20 + } 21 + 22 + let root = range.commonAncestorContainer as Node | null; 23 + if (root && root.nodeType !== Node.ELEMENT_NODE) { 24 + root = root.parentElement; 25 + } 26 + if (!root) { 27 + return []; 28 + } 29 + 30 + const textNodes: Text[] = []; 31 + const nodeIter = root.ownerDocument!.createNodeIterator( 32 + root, 33 + NodeFilter.SHOW_TEXT 34 + ); 35 + 36 + let node; 37 + while ((node = nodeIter.nextNode())) { 38 + if (!isNodeInRange(range, node)) { 39 + continue; 40 + } 41 + const text = node as Text; 42 + 43 + if (text === range.startContainer && range.startOffset > 0) { 44 + text.splitText(range.startOffset); 45 + continue; 46 + } 47 + 48 + if (text === range.endContainer && range.endOffset < text.data.length) { 49 + text.splitText(range.endOffset); 50 + } 51 + 52 + textNodes.push(text); 53 + } 54 + 55 + return textNodes; 56 + }
+164
packages/core/src/utils/highlights/text-range.ts
··· 1 + // TextRange and TextPosition utilities 2 + // Adapted from Hypothesis client (BSD/MIT licensed) 3 + // https://github.com/hypothesis/client/blob/main/src/annotator/anchoring/text-range.ts 4 + 5 + function nodeTextLength(node: Node): number { 6 + switch (node.nodeType) { 7 + case Node.ELEMENT_NODE: 8 + case Node.TEXT_NODE: 9 + return node.textContent?.length ?? 0; 10 + default: 11 + return 0; 12 + } 13 + } 14 + 15 + function previousSiblingsTextLength(node: Node): number { 16 + let sibling = node.previousSibling; 17 + let length = 0; 18 + while (sibling) { 19 + length += nodeTextLength(sibling); 20 + sibling = sibling.previousSibling; 21 + } 22 + return length; 23 + } 24 + 25 + export class TextPosition { 26 + public element: Element; 27 + public offset: number; 28 + 29 + constructor(element: Element, offset: number) { 30 + if (offset < 0) { 31 + throw new Error('Offset is invalid'); 32 + } 33 + this.element = element; 34 + this.offset = offset; 35 + } 36 + 37 + static fromPoint(node: Node, offset: number): TextPosition { 38 + switch (node.nodeType) { 39 + case Node.TEXT_NODE: { 40 + if (!node.parentElement) { 41 + throw new Error('Text node has no parent'); 42 + } 43 + const textOffset = previousSiblingsTextLength(node) + offset; 44 + return new TextPosition(node.parentElement, textOffset); 45 + } 46 + case Node.ELEMENT_NODE: { 47 + let textOffset = 0; 48 + for (let i = 0; i < offset; i++) { 49 + textOffset += nodeTextLength(node.childNodes[i]); 50 + } 51 + return new TextPosition(node as Element, textOffset); 52 + } 53 + default: 54 + throw new Error('Node is not an element or text node'); 55 + } 56 + } 57 + 58 + relativeTo(parent: Element): TextPosition { 59 + if (!parent.contains(this.element)) { 60 + throw new Error('Parent is not an ancestor of current element'); 61 + } 62 + 63 + let el = this.element; 64 + let offset = this.offset; 65 + while (el !== parent) { 66 + offset += previousSiblingsTextLength(el); 67 + el = el.parentElement!; 68 + } 69 + 70 + return new TextPosition(el, offset); 71 + } 72 + 73 + resolve(): { node: Text; offset: number } { 74 + const result = resolveOffsets(this.element, this.offset); 75 + if (result.length === 0) { 76 + throw new RangeError('Offset exceeds text length'); 77 + } 78 + return result[0]; 79 + } 80 + } 81 + 82 + function resolveOffsets( 83 + element: Element, 84 + ...offsets: number[] 85 + ): Array<{ node: Text; offset: number }> { 86 + let nextOffset = offsets.shift(); 87 + const nodeIter = element.ownerDocument.createNodeIterator( 88 + element, 89 + NodeFilter.SHOW_TEXT 90 + ); 91 + const results: Array<{ node: Text; offset: number }> = []; 92 + 93 + let currentNode = nodeIter.nextNode() as Text | null; 94 + let textNode: Text | null = null; 95 + let length = 0; 96 + 97 + while (nextOffset !== undefined && currentNode) { 98 + textNode = currentNode; 99 + if (length + textNode.data.length > nextOffset) { 100 + results.push({ node: textNode, offset: nextOffset - length }); 101 + nextOffset = offsets.shift(); 102 + } else { 103 + currentNode = nodeIter.nextNode() as Text | null; 104 + length += textNode.data.length; 105 + } 106 + } 107 + 108 + // Boundary case 109 + while (nextOffset !== undefined && textNode && length === nextOffset) { 110 + results.push({ node: textNode, offset: textNode.data.length }); 111 + nextOffset = offsets.shift(); 112 + } 113 + 114 + if (nextOffset !== undefined) { 115 + throw new RangeError('Offset exceeds text length'); 116 + } 117 + 118 + return results; 119 + } 120 + 121 + export class TextRange { 122 + public start: TextPosition; 123 + public end: TextPosition; 124 + 125 + constructor(start: TextPosition, end: TextPosition) { 126 + this.start = start; 127 + this.end = end; 128 + } 129 + 130 + static fromRange(range: Range): TextRange { 131 + const start = TextPosition.fromPoint( 132 + range.startContainer, 133 + range.startOffset 134 + ); 135 + const end = TextPosition.fromPoint(range.endContainer, range.endOffset); 136 + return new TextRange(start, end); 137 + } 138 + 139 + toRange(): Range { 140 + let start; 141 + let end; 142 + 143 + if ( 144 + this.start.element === this.end.element && 145 + this.start.offset <= this.end.offset 146 + ) { 147 + const resolved = resolveOffsets( 148 + this.start.element, 149 + this.start.offset, 150 + this.end.offset 151 + ); 152 + start = resolved[0]; 153 + end = resolved[1]; 154 + } else { 155 + start = this.start.resolve(); 156 + end = this.end.resolve(); 157 + } 158 + 159 + const range = new Range(); 160 + range.setStart(start.node, start.offset); 161 + range.setEnd(end.node, end.offset); 162 + return range; 163 + } 164 + }
+8 -2
packages/core/src/utils/selectors/match.ts
··· 17 17 18 18 console.log('[synthesis] Trying to match annotation with', selectors.length, 'selectors'); 19 19 20 - // Try each selector in order 21 - for (const selector of selectors) { 20 + // Try TextQuoteSelector first (position-independent, works after DOM mutations) 21 + // Then fall back to TextPositionSelector (faster but fragile) 22 + const quoteSelector = selectors.find(s => s.$type === 'community.lexicon.annotation.annotation#textQuoteSelector'); 23 + const positionSelector = selectors.find(s => s.$type === 'community.lexicon.annotation.annotation#textPositionSelector'); 24 + 25 + const selectorsToTry = [quoteSelector, positionSelector].filter(Boolean); 26 + 27 + for (const selector of selectorsToTry) { 22 28 let range: Range | null = null; 23 29 24 30 console.log('[synthesis] Trying selector type:', selector.$type);
+21
proxy/static/client-metadata.json
··· 1 + { 2 + "client_id": "https://seams.so/oauth/client-metadata.json", 3 + "client_uri": "https://seams.so", 4 + "redirect_uris": [ 5 + "https://seams.so/oauth/callback", 6 + "https://seams.so/oauth/ff/callback", 7 + "https://sure.seams.so/oauth-callback.html" 8 + ], 9 + "application_type": "web", 10 + "client_name": "Seams", 11 + "dpop_bound_access_tokens": true, 12 + "grant_types": [ 13 + "authorization_code", 14 + "refresh_token" 15 + ], 16 + "response_types": [ 17 + "code" 18 + ], 19 + "scope": "atproto transition:generic", 20 + "token_endpoint_auth_method": "none" 21 + }
+48
proxy/static/entrypoints/via-client/oauth-callback.html
··· 1 + <!DOCTYPE html> 2 + <html lang="en"> 3 + <head> 4 + <meta charset="UTF-8"> 5 + <meta name="viewport" content="width=device-width, initial-scale=1.0"> 6 + <title>Seams OAuth Callback</title> 7 + <style> 8 + body { 9 + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; 10 + display: flex; 11 + align-items: center; 12 + justify-content: center; 13 + height: 100vh; 14 + margin: 0; 15 + background: #f5f5f5; 16 + } 17 + .message { 18 + text-align: center; 19 + padding: 32px; 20 + background: white; 21 + border-radius: 8px; 22 + box-shadow: 0 2px 8px rgba(0,0,0,0.1); 23 + } 24 + .spinner { 25 + border: 3px solid #f3f3f3; 26 + border-top: 3px solid #0085ff; 27 + border-radius: 50%; 28 + width: 40px; 29 + height: 40px; 30 + animation: spin 1s linear infinite; 31 + margin: 0 auto 16px; 32 + } 33 + @keyframes spin { 34 + 0% { transform: rotate(0deg); } 35 + 100% { transform: rotate(360deg); } 36 + } 37 + </style> 38 + <script type="module" crossorigin src="/seams-oauth-callback.js"></script> 39 + <link rel="modulepreload" crossorigin href="/assets/modulepreload-polyfill-59aEHZEI.js"> 40 + </head> 41 + <body> 42 + <div class="message"> 43 + <div class="spinner"></div> 44 + <h2>Completing login...</h2> 45 + <p id="status">Processing OAuth response</p> 46 + </div> 47 + </body> 48 + </html>
+15
proxy/static/entrypoints/via-client/sidebar.html
··· 1 + <!DOCTYPE html> 2 + <html lang="en"> 3 + <head> 4 + <meta charset="UTF-8"> 5 + <meta name="viewport" content="width=device-width, initial-scale=1.0"> 6 + <title>Seams Sidebar</title> 7 + <script type="module" crossorigin src="/seams-seams-sidebar.js"></script> 8 + <link rel="modulepreload" crossorigin href="/assets/modulepreload-polyfill-59aEHZEI.js"> 9 + <link rel="modulepreload" crossorigin href="/assets/web-Ts7v-0PE.js"> 10 + <link rel="stylesheet" crossorigin href="/assets/seams-sidebar-gO3IQ7ah.css"> 11 + </head> 12 + <body> 13 + <div id="app"></div> 14 + </body> 15 + </html>
+49
proxy/static/oauth-callback.html
··· 1 + <!DOCTYPE html> 2 + <html lang="en"> 3 + <head> 4 + <meta charset="UTF-8"> 5 + <meta name="viewport" content="width=device-width, initial-scale=1.0"> 6 + <title>Seams OAuth Callback</title> 7 + <style> 8 + body { 9 + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; 10 + display: flex; 11 + align-items: center; 12 + justify-content: center; 13 + height: 100vh; 14 + margin: 0; 15 + background: #f5f5f5; 16 + } 17 + .message { 18 + text-align: center; 19 + padding: 32px; 20 + background: white; 21 + border-radius: 8px; 22 + box-shadow: 0 2px 8px rgba(0,0,0,0.1); 23 + } 24 + .spinner { 25 + border: 3px solid #f3f3f3; 26 + border-top: 3px solid #0085ff; 27 + border-radius: 50%; 28 + width: 40px; 29 + height: 40px; 30 + animation: spin 1s linear infinite; 31 + margin: 0 auto 16px; 32 + } 33 + @keyframes spin { 34 + 0% { transform: rotate(0deg); } 35 + 100% { transform: rotate(360deg); } 36 + } 37 + </style> 38 + <script type="module" crossorigin src="/static/seams-oauth-callback.js"></script> 39 + <link rel="modulepreload" crossorigin href="/static/assets/modulepreload-polyfill-B384jI_7.js"> 40 + <link rel="modulepreload" crossorigin href="/static/assets/index-BKdQD0EM.js"> 41 + </head> 42 + <body> 43 + <div class="message"> 44 + <div class="spinner"></div> 45 + <h2>Completing login...</h2> 46 + <p id="status">Processing OAuth response</p> 47 + </div> 48 + </body> 49 + </html>
+15
proxy/static/seams-sidebar.html
··· 1 + <!DOCTYPE html> 2 + <html lang="en"> 3 + <head> 4 + <meta charset="UTF-8"> 5 + <meta name="viewport" content="width=device-width, initial-scale=1.0"> 6 + <title>Seams Sidebar</title> 7 + <script type="module" crossorigin src="/static/seams-seams-sidebar.js"></script> 8 + <link rel="modulepreload" crossorigin href="/static/assets/modulepreload-polyfill-B384jI_7.js"> 9 + <link rel="modulepreload" crossorigin href="/static/assets/index-BKdQD0EM.js"> 10 + <link rel="stylesheet" crossorigin href="/static/assets/seams-sidebar-gO3IQ7ah.css"> 11 + </head> 12 + <body> 13 + <div id="app"></div> 14 + </body> 15 + </html>
+49
proxy/static/via-html/oauth-callback.html
··· 1 + <!DOCTYPE html> 2 + <html lang="en"> 3 + <head> 4 + <meta charset="UTF-8"> 5 + <meta name="viewport" content="width=device-width, initial-scale=1.0"> 6 + <title>Seams OAuth Callback</title> 7 + <style> 8 + body { 9 + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; 10 + display: flex; 11 + align-items: center; 12 + justify-content: center; 13 + height: 100vh; 14 + margin: 0; 15 + background: #f5f5f5; 16 + } 17 + .message { 18 + text-align: center; 19 + padding: 32px; 20 + background: white; 21 + border-radius: 8px; 22 + box-shadow: 0 2px 8px rgba(0,0,0,0.1); 23 + } 24 + .spinner { 25 + border: 3px solid #f3f3f3; 26 + border-top: 3px solid #0085ff; 27 + border-radius: 50%; 28 + width: 40px; 29 + height: 40px; 30 + animation: spin 1s linear infinite; 31 + margin: 0 auto 16px; 32 + } 33 + @keyframes spin { 34 + 0% { transform: rotate(0deg); } 35 + 100% { transform: rotate(360deg); } 36 + } 37 + </style> 38 + <script type="module" crossorigin src="/seams-oauth-callback.js"></script> 39 + <link rel="modulepreload" crossorigin href="/assets/modulepreload-polyfill-CyEOUuNr.js"> 40 + <link rel="modulepreload" crossorigin href="/assets/index-BKdQD0EM.js"> 41 + </head> 42 + <body> 43 + <div class="message"> 44 + <div class="spinner"></div> 45 + <h2>Completing login...</h2> 46 + <p id="status">Processing OAuth response</p> 47 + </div> 48 + </body> 49 + </html>
+15
proxy/static/via-html/seams-sidebar.html
··· 1 + <!DOCTYPE html> 2 + <html lang="en"> 3 + <head> 4 + <meta charset="UTF-8"> 5 + <meta name="viewport" content="width=device-width, initial-scale=1.0"> 6 + <title>Seams Sidebar</title> 7 + <script type="module" crossorigin src="/seams-seams-sidebar.js"></script> 8 + <link rel="modulepreload" crossorigin href="/assets/modulepreload-polyfill-CyEOUuNr.js"> 9 + <link rel="modulepreload" crossorigin href="/assets/index-BKdQD0EM.js"> 10 + <link rel="stylesheet" crossorigin href="/assets/seams-sidebar-gO3IQ7ah.css"> 11 + </head> 12 + <body> 13 + <div id="app"></div> 14 + </body> 15 + </html>
+184
proxy/static/via-landing.html
··· 1 + <!DOCTYPE html> 2 + <html lang="en"> 3 + <head> 4 + <meta charset="UTF-8"> 5 + <meta name="viewport" content="width=device-width, initial-scale=1.0"> 6 + <title>Via - Seams Web Annotation Proxy</title> 7 + <link rel="stylesheet" href="landing.css"> 8 + <style> 9 + /* Extra styles for the proxy form in the main content area */ 10 + .proxy-container { 11 + padding: 60px 48px; 12 + max-width: 900px; 13 + } 14 + .url-form { 15 + display: flex; 16 + gap: 10px; 17 + margin-bottom: 30px; 18 + max-width: 600px; 19 + } 20 + 21 + input[type="url"] { 22 + flex: 1; 23 + padding: 14px 18px; 24 + border: 2px solid #ddd; 25 + border-radius: 2px; /* Match button radius */ 26 + font-size: 16px; 27 + transition: border-color 0.2s; 28 + font-family: inherit; 29 + } 30 + 31 + input[type="url"]:focus { 32 + outline: none; 33 + border-color: var(--forest-green); 34 + } 35 + 36 + button[type="submit"] { 37 + padding: 14px 32px; 38 + background: var(--forest-green); 39 + color: white; 40 + border: 1px dashed var(--forest-green-dark); 41 + border-radius: 2px; 42 + font-size: 16px; 43 + font-weight: 500; 44 + cursor: pointer; 45 + transition: all 0.2s; 46 + white-space: nowrap; 47 + font-family: inherit; 48 + } 49 + 50 + button[type="submit"]:hover { 51 + background: var(--forest-green-dark); 52 + transform: translateY(-1px); 53 + } 54 + 55 + .info-text { 56 + font-size: 16px; 57 + color: #333; 58 + line-height: 1.6; 59 + margin-bottom: 24px; 60 + max-width: 700px; 61 + } 62 + 63 + .example { 64 + margin-top: 24px; 65 + padding: 20px; 66 + background: #fff; 67 + border: 1px dashed #d0d0d0; 68 + border-radius: 2px; 69 + display: inline-block; 70 + } 71 + 72 + .example a { 73 + color: var(--forest-green); 74 + text-decoration: none; 75 + border-bottom: 1px dashed transparent; 76 + transition: border-color 0.2s; 77 + } 78 + .example a:hover { 79 + border-bottom-color: var(--forest-green); 80 + } 81 + 82 + /* Mobile tweaks */ 83 + @media (max-width: 640px) { 84 + .proxy-container { 85 + padding: 32px 24px; 86 + } 87 + .url-form { 88 + flex-direction: column; 89 + } 90 + button[type="submit"] { 91 + width: 100%; 92 + } 93 + } 94 + </style> 95 + </head> 96 + <body> 97 + <div class="layout"> 98 + <aside class="sidebar"> 99 + <div class="sidebar-content"> 100 + <header class="hero"> 101 + <div class="logo">Seams Via</div> 102 + <h1><span style="white-space: nowrap;">Wisdom is Made</span> <br>Accessible</h1> 103 + <p class="tagline">View and annotate any web page</p> 104 + <div class="cta-buttons"> 105 + <a href="https://seams.so" class="cta-primary">Go to Seams.so</a> 106 + </div> 107 + </header> 108 + 109 + <footer class="footer desktop-footer"> 110 + <div class="footer-icons"> 111 + <a href="https://tangled.org/@sealight.xyz/seams.so" target="_blank" aria-label="Tangled"> 112 + <img src="https://semble.so/_next/static/media/tangled-icon.b95d4d65.svg" alt="Tangled" /> 113 + </a> 114 + <a href="https://bsky.app" target="_blank" aria-label="Bluesky"> 115 + <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 360 320" style="width: 32px; height: 32px; opacity: 0.6;"> 116 + <path fill="currentColor" d="M180 142c-16.3-31.7-60.7-90.8-102-120C38.5-5.9 23.4-1 13.5 3.4 2.1 8.6 0 26.2 0 36.5c0 10.4 5.7 84.8 9.4 97.2 12.2 41 55.7 55 95.7 50.5-58.7 8.6-110.8 30-42.4 106.1 75.1 77.9 103-16.7 117.3-64.6 14.3 48 30.8 139 116 64.6 64-64.6 17.6-97.5-41.1-106.1 40 4.4 83.5-9.5 95.7-50.5 3.7-12.4 9.4-86.8 9.4-97.2 0-10.3-2-27.9-13.5-33C336.5-1 321.5-6 282 22c-41.3 29.2-85.7 88.3-102 120Z"/> 117 + </svg> 118 + </a> 119 + </div> 120 + </footer> 121 + </div> 122 + </aside> 123 + 124 + <main class="main-content"> 125 + <div class="proxy-container"> 126 + <div class="info-text"> 127 + <p>Via is a web annotation proxy that lets you view and create annotations on any web page using Seams.</p> 128 + <br> 129 + <p>Enter a URL below to view the page with all public annotations from the Seams community.</p> 130 + </div> 131 + 132 + <form class="url-form" id="via-form"> 133 + <input 134 + type="url" 135 + id="url-input" 136 + placeholder="Paste a link to annotate" 137 + required 138 + autocomplete="url" 139 + autofocus 140 + /> 141 + <button type="submit">Annotate</button> 142 + </form> 143 + 144 + <div class="example"> 145 + <strong>Try it:</strong> 146 + <a href="/proxy/https://newsletter.squishy.computer/p/places-to-intervene-in-a-system"> 147 + View example article with annotations 148 + </a> 149 + </div> 150 + </div> 151 + </main> 152 + 153 + <footer class="footer mobile-footer"> 154 + <div class="footer-icons"> 155 + <a href="https://tangled.org/@sealight.xyz/seams.so" target="_blank" aria-label="Tangled"> 156 + <img src="https://semble.so/_next/static/media/tangled-icon.b95d4d65.svg" alt="Tangled" /> 157 + </a> 158 + <a href="https://bsky.app" target="_blank" aria-label="Bluesky"> 159 + <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 360 320" style="width: 32px; height: 32px; opacity: 0.6;"> 160 + <path fill="currentColor" d="M180 142c-16.3-31.7-60.7-90.8-102-120C38.5-5.9 23.4-1 13.5 3.4 2.1 8.6 0 26.2 0 36.5c0 10.4 5.7 84.8 9.4 97.2 12.2 41 55.7 55 95.7 50.5-58.7 8.6-110.8 30-42.4 106.1 75.1 77.9 103-16.7 117.3-64.6 14.3 48 30.8 139 116 64.6 64-64.6 17.6-97.5-41.1-106.1 40 4.4 83.5-9.5 95.7-50.5 3.7-12.4 9.4-86.8 9.4-97.2 0-10.3-2-27.9-13.5-33C336.5-1 321.5-6 282 22c-41.3 29.2-85.7 88.3-102 120Z"/> 161 + </svg> 162 + </a> 163 + </div> 164 + </footer> 165 + </div> 166 + 167 + <script> 168 + document.getElementById('via-form').addEventListener('submit', (e) => { 169 + e.preventDefault(); 170 + let url = document.getElementById('url-input').value.trim(); 171 + 172 + if (!url) return; 173 + 174 + // Add protocol if missing 175 + if (!url.match(/^https?:\/\//i)) { 176 + url = 'https://' + url; 177 + } 178 + 179 + // Redirect to pywb proxy route 180 + window.location.href = `/proxy/${url}`; 181 + }); 182 + </script> 183 + </body> 184 + </html>
+184
proxy/via-html/via-landing.html
··· 1 + <!DOCTYPE html> 2 + <html lang="en"> 3 + <head> 4 + <meta charset="UTF-8"> 5 + <meta name="viewport" content="width=device-width, initial-scale=1.0"> 6 + <title>Via - Seams Web Annotation Proxy</title> 7 + <link rel="stylesheet" href="landing.css"> 8 + <style> 9 + /* Extra styles for the proxy form in the main content area */ 10 + .proxy-container { 11 + padding: 60px 48px; 12 + max-width: 900px; 13 + } 14 + .url-form { 15 + display: flex; 16 + gap: 10px; 17 + margin-bottom: 30px; 18 + max-width: 600px; 19 + } 20 + 21 + input[type="url"] { 22 + flex: 1; 23 + padding: 14px 18px; 24 + border: 2px solid #ddd; 25 + border-radius: 2px; /* Match button radius */ 26 + font-size: 16px; 27 + transition: border-color 0.2s; 28 + font-family: inherit; 29 + } 30 + 31 + input[type="url"]:focus { 32 + outline: none; 33 + border-color: var(--forest-green); 34 + } 35 + 36 + button[type="submit"] { 37 + padding: 14px 32px; 38 + background: var(--forest-green); 39 + color: white; 40 + border: 1px dashed var(--forest-green-dark); 41 + border-radius: 2px; 42 + font-size: 16px; 43 + font-weight: 500; 44 + cursor: pointer; 45 + transition: all 0.2s; 46 + white-space: nowrap; 47 + font-family: inherit; 48 + } 49 + 50 + button[type="submit"]:hover { 51 + background: var(--forest-green-dark); 52 + transform: translateY(-1px); 53 + } 54 + 55 + .info-text { 56 + font-size: 16px; 57 + color: #333; 58 + line-height: 1.6; 59 + margin-bottom: 24px; 60 + max-width: 700px; 61 + } 62 + 63 + .example { 64 + margin-top: 24px; 65 + padding: 20px; 66 + background: #fff; 67 + border: 1px dashed #d0d0d0; 68 + border-radius: 2px; 69 + display: inline-block; 70 + } 71 + 72 + .example a { 73 + color: var(--forest-green); 74 + text-decoration: none; 75 + border-bottom: 1px dashed transparent; 76 + transition: border-color 0.2s; 77 + } 78 + .example a:hover { 79 + border-bottom-color: var(--forest-green); 80 + } 81 + 82 + /* Mobile tweaks */ 83 + @media (max-width: 640px) { 84 + .proxy-container { 85 + padding: 32px 24px; 86 + } 87 + .url-form { 88 + flex-direction: column; 89 + } 90 + button[type="submit"] { 91 + width: 100%; 92 + } 93 + } 94 + </style> 95 + </head> 96 + <body> 97 + <div class="layout"> 98 + <aside class="sidebar"> 99 + <div class="sidebar-content"> 100 + <header class="hero"> 101 + <div class="logo">Seams Via</div> 102 + <h1><span style="white-space: nowrap;">Wisdom is Made</span> <br>Accessible</h1> 103 + <p class="tagline">View and annotate any web page</p> 104 + <div class="cta-buttons"> 105 + <a href="https://seams.so" class="cta-primary">Go to Seams.so</a> 106 + </div> 107 + </header> 108 + 109 + <footer class="footer desktop-footer"> 110 + <div class="footer-icons"> 111 + <a href="https://tangled.org/@sealight.xyz/seams.so" target="_blank" aria-label="Tangled"> 112 + <img src="https://semble.so/_next/static/media/tangled-icon.b95d4d65.svg" alt="Tangled" /> 113 + </a> 114 + <a href="https://bsky.app" target="_blank" aria-label="Bluesky"> 115 + <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 360 320" style="width: 32px; height: 32px; opacity: 0.6;"> 116 + <path fill="currentColor" d="M180 142c-16.3-31.7-60.7-90.8-102-120C38.5-5.9 23.4-1 13.5 3.4 2.1 8.6 0 26.2 0 36.5c0 10.4 5.7 84.8 9.4 97.2 12.2 41 55.7 55 95.7 50.5-58.7 8.6-110.8 30-42.4 106.1 75.1 77.9 103-16.7 117.3-64.6 14.3 48 30.8 139 116 64.6 64-64.6 17.6-97.5-41.1-106.1 40 4.4 83.5-9.5 95.7-50.5 3.7-12.4 9.4-86.8 9.4-97.2 0-10.3-2-27.9-13.5-33C336.5-1 321.5-6 282 22c-41.3 29.2-85.7 88.3-102 120Z"/> 117 + </svg> 118 + </a> 119 + </div> 120 + </footer> 121 + </div> 122 + </aside> 123 + 124 + <main class="main-content"> 125 + <div class="proxy-container"> 126 + <div class="info-text"> 127 + <p>Via is a web annotation proxy that lets you view and create annotations on any web page using Seams.</p> 128 + <br> 129 + <p>Enter a URL below to view the page with all public annotations from the Seams community.</p> 130 + </div> 131 + 132 + <form class="url-form" id="via-form"> 133 + <input 134 + type="url" 135 + id="url-input" 136 + placeholder="Paste a link to annotate" 137 + required 138 + autocomplete="url" 139 + autofocus 140 + /> 141 + <button type="submit">Annotate</button> 142 + </form> 143 + 144 + <div class="example"> 145 + <strong>Try it:</strong> 146 + <a href="/proxy/https://newsletter.squishy.computer/p/places-to-intervene-in-a-system"> 147 + View example article with annotations 148 + </a> 149 + </div> 150 + </div> 151 + </main> 152 + 153 + <footer class="footer mobile-footer"> 154 + <div class="footer-icons"> 155 + <a href="https://tangled.org/@sealight.xyz/seams.so" target="_blank" aria-label="Tangled"> 156 + <img src="https://semble.so/_next/static/media/tangled-icon.b95d4d65.svg" alt="Tangled" /> 157 + </a> 158 + <a href="https://bsky.app" target="_blank" aria-label="Bluesky"> 159 + <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 360 320" style="width: 32px; height: 32px; opacity: 0.6;"> 160 + <path fill="currentColor" d="M180 142c-16.3-31.7-60.7-90.8-102-120C38.5-5.9 23.4-1 13.5 3.4 2.1 8.6 0 26.2 0 36.5c0 10.4 5.7 84.8 9.4 97.2 12.2 41 55.7 55 95.7 50.5-58.7 8.6-110.8 30-42.4 106.1 75.1 77.9 103-16.7 117.3-64.6 14.3 48 30.8 139 116 64.6 64-64.6 17.6-97.5-41.1-106.1 40 4.4 83.5-9.5 95.7-50.5 3.7-12.4 9.4-86.8 9.4-97.2 0-10.3-2-27.9-13.5-33C336.5-1 321.5-6 282 22c-41.3 29.2-85.7 88.3-102 120Z"/> 161 + </svg> 162 + </a> 163 + </div> 164 + </footer> 165 + </div> 166 + 167 + <script> 168 + document.getElementById('via-form').addEventListener('submit', (e) => { 169 + e.preventDefault(); 170 + let url = document.getElementById('url-input').value.trim(); 171 + 172 + if (!url) return; 173 + 174 + // Add protocol if missing 175 + if (!url.match(/^https?:\/\//i)) { 176 + url = 'https://' + url; 177 + } 178 + 179 + // Redirect to pywb proxy route 180 + window.location.href = `/proxy/${url}`; 181 + }); 182 + </script> 183 + </body> 184 + </html>
public/about.css landing/about.css
public/about.html landing/about.html
public/extension-callback.html landing/extension-callback.html
public/fonts.css landing/fonts.css
public/fonts/fraunces-v38-latin-600.eot landing/fonts/fraunces-v38-latin-600.eot
public/fonts/fraunces-v38-latin-600.ttf landing/fonts/fraunces-v38-latin-600.ttf
public/fonts/fraunces-v38-latin-600.woff landing/fonts/fraunces-v38-latin-600.woff
public/fonts/fraunces-v38-latin-600.woff2 landing/fonts/fraunces-v38-latin-600.woff2
public/fonts/fraunces-v38-latin-700.eot landing/fonts/fraunces-v38-latin-700.eot
public/fonts/fraunces-v38-latin-700.ttf landing/fonts/fraunces-v38-latin-700.ttf
public/fonts/fraunces-v38-latin-700.woff landing/fonts/fraunces-v38-latin-700.woff
public/fonts/fraunces-v38-latin-700.woff2 landing/fonts/fraunces-v38-latin-700.woff2
public/fonts/fraunces-v38-latin-regular.eot landing/fonts/fraunces-v38-latin-regular.eot
public/fonts/fraunces-v38-latin-regular.ttf landing/fonts/fraunces-v38-latin-regular.ttf
public/fonts/fraunces-v38-latin-regular.woff landing/fonts/fraunces-v38-latin-regular.woff
public/fonts/fraunces-v38-latin-regular.woff2 landing/fonts/fraunces-v38-latin-regular.woff2
public/fonts/spectral-v15-latin-600.eot landing/fonts/spectral-v15-latin-600.eot
public/fonts/spectral-v15-latin-600.svg landing/fonts/spectral-v15-latin-600.svg
public/fonts/spectral-v15-latin-600.ttf landing/fonts/spectral-v15-latin-600.ttf
public/fonts/spectral-v15-latin-600.woff landing/fonts/spectral-v15-latin-600.woff
public/fonts/spectral-v15-latin-600.woff2 landing/fonts/spectral-v15-latin-600.woff2
public/fonts/spectral-v15-latin-700.eot landing/fonts/spectral-v15-latin-700.eot
public/fonts/spectral-v15-latin-700.ttf landing/fonts/spectral-v15-latin-700.ttf
public/fonts/spectral-v15-latin-700.woff landing/fonts/spectral-v15-latin-700.woff
public/fonts/spectral-v15-latin-700.woff2 landing/fonts/spectral-v15-latin-700.woff2
public/index.html landing/index.html
public/introspect.html landing/introspect.html
public/landing.css landing/landing.css
+2 -2
public/landing.js landing/landing.js
··· 113 113 114 114 // Render a single annotation card 115 115 async function renderAnnotation(annotation) { 116 - const { targetUrl, body, createdAt, authorDid, uri, authorHandle, exactText, selectorsJson } = annotation; 117 - const textQuoteSelector = getTextQuoteSelector(selectorsJson); 116 + const { targetUrl, body, createdAt, authorDid, uri, authorHandle, exactText, selectors } = annotation; 117 + const textQuoteSelector = getTextQuoteSelector(selectors); 118 118 const quotedText = exactText || textQuoteSelector?.exact; 119 119 const sourceUrl = targetUrl; 120 120 const fragmentUrl = buildTextFragmentUrl(sourceUrl, quotedText);
public/oauth/callback.html landing/oauth/callback.html
-24
public/oauth/client-metadata.json
··· 1 - { 2 - "client_id": "https://synthes-is.netlify.app/oauth/client-metadata.json", 3 - "client_uri": "https://synthes-is.netlify.app", 4 - "redirect_uris": [ 5 - "https://synthes-is.netlify.app", 6 - "https://synthes-is.netlify.app/oauth/callback", 7 - "https://synthes-is.netlify.app/oauth/ff/callback", 8 - "https://e7a0b7703dc3d21c8ef60539da3ed68d27b48e8c.extensions.allizom.org/", 9 - "https://synthesis@seams.so/oauth/ff/callback", 10 - "https://dmkgcehijkfpalmplnallinblhimageb.chromium.org/oauth/callback.html" 11 - ], 12 - "application_type": "web", 13 - "client_name": "Seams", 14 - "dpop_bound_access_tokens": true, 15 - "grant_types": [ 16 - "authorization_code", 17 - "refresh_token" 18 - ], 19 - "response_types": [ 20 - "code" 21 - ], 22 - "scope": "atproto transition:generic", 23 - "token_endpoint_auth_method": "none" 24 - }
public/oauth/ff/callback.html landing/oauth/ff/callback.html
pywb-test/collections/proxy/templates/head_insert.html proxy/collections/proxy/templates/head_insert.html
+2
pywb-test/config.yaml proxy/config.yaml
··· 6 6 # Minimal rewriting to inject head_insert only 7 7 enable_banner: false 8 8 enable_wombat: false 9 + # Disable CSP injection to allow proxied scripts to run 10 + enable_content_security_policy: false 9 11 # Exclude API calls from proxy interception 10 12 ignore_prefixes: 11 13 - https://seams.so/api/
pywb-test/static/about.css proxy/static/about.css
pywb-test/static/about.html proxy/static/about.html
-2
pywb-test/static/assets/oauth-web-B1Uotnw-.js
··· 1 - var ue=Object.defineProperty;var G=t=>{throw TypeError(t)};var de=(t,e,r)=>e in t?ue(t,e,{enumerable:!0,configurable:!0,writable:!0,value:r}):t[e]=r;var f=(t,e,r)=>de(t,typeof e!="symbol"?e+"":e,r),H=(t,e,r)=>e.has(t)||G("Cannot "+r);var m=(t,e,r)=>(H(t,e,"read from private field"),r?r.call(t):e.get(t)),E=(t,e,r)=>e.has(t)?G("Cannot add the same private member more than once"):e instanceof WeakSet?e.add(t):e.set(t,r),k=(t,e,r,s)=>(H(t,e,"write to private field"),s?s.call(t,r):e.set(t,r),r),L=(t,e,r)=>(H(t,e,"access private method"),r);const he="useandom-26T198340PX75pxJACKVERYMINDBUSHWOLF_GQZbfghjklqvwyzrict";let W=(t=21)=>{let e="",r=crypto.getRandomValues(new Uint8Array(t|=0));for(;t--;)e+=he[r[t]&63];return e};const pe=new TextEncoder;new TextDecoder;const fe=crypto.subtle,we=t=>new Uint8Array(t),ge=we,$=t=>pe.encode(t),ye=async t=>new Uint8Array(await fe.digest("SHA-256",t)),me=(t,e,r)=>s=>{const n=(1<<e)-1;let o="",a=0,i=0;for(let c=0;c<s.length;++c)for(i=i<<8|s[c],a+=8;a>e;)a-=e,o+=t[n&i>>a];if(a!==0&&(o+=t[n&i<<e-a]),r)for(;o.length*e&7;)o+="=";return o},_e=(t,e,r)=>{const s={};for(let n=0;n<t.length;++n)s[t[n]]=n;return n=>{let o=n.length;for(;r&&n[o-1]==="=";)--o;const a=ge(o*e/8|0);let i=0,c=0,l=0;for(let u=0;u<o;++u){const h=s[n[u]];if(h===void 0)throw new SyntaxError("invalid base string");c=c<<e|h,i+=e,i>=8&&(i-=8,a[l++]=255&c>>i)}if(i>=e||255&c<<8-i)throw new SyntaxError("unexpected end of data");return a}},ve=t=>Uint8Array.fromBase64(t,{alphabet:"base64url",lastChunkHandling:"loose"}),Se=t=>t.toBase64({alphabet:"base64url",omitPadding:!0}),te="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_",be=_e(te,6,!1),Ae=me(te,6,!1),re="fromBase64"in Uint8Array,Ee=re?ve:be,z=re?Se:Ae,N=typeof navigator<"u"?navigator.locks:void 0,se=async t=>{const e=$(t),r=await ye(e);return z(r)},ke=async()=>{const t=W(64);return{verifier:t,challenge:await se(t),method:"S256"}},Re=t=>{if(t!=null){const e=JSON.parse(t);if(e!=null)return e}return{}},xe=({name:t})=>{const e=new AbortController,r=e.signal,s=(n,o,a=!1)=>{let i;const c=`${t}:${n}`,l=()=>i&&localStorage.setItem(c,JSON.stringify(i)),u=()=>{if(r.aborted)throw new Error("store closed");return i??(i=Re(localStorage.getItem(c)))};{const h=d=>{d.key===c&&(i=void 0)};globalThis.addEventListener("storage",h,{signal:r})}{const h=async d=>{if(!d||r.aborted||(await new Promise(w=>setTimeout(w,1e4)),r.aborted))return;let g=Date.now(),v=!1;u();for(const w in i){const U=i[w].expiresAt;U!==null&&g>U&&(v=!0,delete i[w])}v&&l()};N?N.request(`${c}:cleanup`,{ifAvailable:!0},h):h(!0)}return{get(h){u();const d=i[h];if(!d)return;const g=d.expiresAt;if(g!==null&&Date.now()>g){delete i[h],l();return}return d.value},getWithLapsed(h){u();const d=i[h],g=Date.now();if(!d)return[void 0,1/0];const v=d.updatedAt;return v===void 0?[d.value,1/0]:[d.value,g-v]},set(h,d){u();const g={value:d,expiresAt:o(d),updatedAt:a?Date.now():void 0};i[h]=g,l()},delete(h){u(),i[h]!==void 0&&(delete i[h],l())},keys(){return u(),Object.keys(i)}}};return{dispose:()=>{e.abort()},sessions:s("sessions",({token:n})=>n.refresh?null:n.expires_at??null),states:s("states",n=>Date.now()+10*60*1e3),dpopNonces:s("dpopNonces",n=>Date.now()+24*60*60*1e3,!0),inflightDpop:new Map}};let M,Z,_;const Ue=t=>{({client_id:M,redirect_uri:Z}=t.metadata),_=xe({name:t.storageName??"atcute-oauth"})};class D extends Error{constructor(){super(...arguments);f(this,"name","LoginError")}}class je extends Error{constructor(){super(...arguments);f(this,"name","AuthorizationError")}}class p extends Error{constructor(){super(...arguments);f(this,"name","ResolverError")}}class q extends Error{constructor(r,s,n){super(s,n);f(this,"sub");f(this,"name","TokenRefreshError");this.sub=r}}class ne extends Error{constructor(r,s){var l,u;const n=Y((l=Q(s))==null?void 0:l.error),o=Y((u=Q(s))==null?void 0:u.error_description),a=n?`"${n}"`:"unknown",i=o?`: ${o}`:"",c=`OAuth ${a} error${i}`;super(c);f(this,"response");f(this,"data");f(this,"name","OAuthResponseError");f(this,"error");f(this,"description");this.response=r,this.data=s,this.error=n,this.description=o}get status(){return this.response.status}get headers(){return this.response.headers}}class De extends Error{constructor(r,s,n){super(n);f(this,"response");f(this,"status");f(this,"name","FetchResponseError");this.response=r,this.status=s}}const Y=t=>typeof t=="string"?t:void 0,Q=t=>typeof t=="object"&&t!==null&&!Array.isArray(t)?t:void 0,ze=/^did:([a-z]+):([a-zA-Z0-9._:%\-]*[a-zA-Z0-9._\-])$/,Oe=t=>typeof t=="string"&&t.length>=7&&t.length<=2048&&ze.test(t),Pe="parse"in URL,Ie=t=>{let e=null;if(Pe)e=URL.parse(t);else try{e=new URL(t)}catch{}return e!==null&&(e.protocol==="https:"||e.protocol==="http:")&&e.pathname==="/"&&e.search===""&&e.hash===""},Le=(t,e)=>{const r=t.service;if(r)for(let s=0,n=r.length;s<n;s++){const{id:o,type:a,serviceEndpoint:i}=r[s];if(!(o!==e.id&&o!==t.id+e.id)){if(e.type!==void 0){if(Array.isArray(a)){if(!a.includes(e.type))continue}else if(a!==e.type)continue}if(!(typeof i!="string"||!Ie(i)))return i}}},Te=t=>Le(t,{id:"#atproto_pds",type:"AtprotoPersonalDataServer"}),$e="https://public.api.bsky.app",B=t=>{var e;return(e=t.get("content-type"))==null?void 0:e.split(";")[0]},Ne="parse"in URL,qe=t=>{let e=null;if(Ne)e=URL.parse(t);else try{e=new URL(t)}catch{}return e!==null?e.protocol==="https:"||e.protocol==="http:":!1},Ke=/^([a-zA-Z0-9-]+(?:\.[a-zA-Z0-9-]+)*(?:\.[a-zA-Z]{2,}))$/,Be=async t=>{const e=$e+`/xrpc/com.atproto.identity.resolveHandle?handle=${t}`,r=await fetch(e);if(r.status===400)throw new p("domain handle not found");if(!r.ok)throw new p("directory is unreachable");return(await r.json()).did},Fe=async t=>{const e=t.indexOf(":",4),r=t.slice(4,e),s=t.slice(e+1);let n;if(r==="plc"){const o=await fetch(`https://plc.directory/${t}`);if(o.status===404)throw new p("did not found in directory");if(!o.ok)throw new p("directory is unreachable");n=await o.json()}else if(r==="web"){if(!Ke.test(s))throw new p("invalid identifier");const o=await fetch(`https://${s}/.well-known/did.json`);if(!o.ok)throw new p("did document is unreachable");n=await o.json()}else throw new p("unsupported did method");return n},Je=async t=>{const e=new URL("/.well-known/oauth-protected-resource",t),r=await fetch(e,{redirect:"manual",headers:{accept:"application/json"}});if(r.status!==200||B(r.headers)!=="application/json")throw new p("unexpected response");const s=await r.json();if(s.resource!==e.origin)throw new p("unexpected issuer");return s},He=async t=>{const e=new URL("/.well-known/oauth-authorization-server",t),r=await fetch(e,{redirect:"manual",headers:{accept:"application/json"}});if(r.status!==200||B(r.headers)!=="application/json")throw new p("unexpected response");const s=await r.json();if(s.issuer!==e.origin)throw new p("unexpected issuer");if(!qe(s.authorization_endpoint))throw new p("authorization server provided incorrect authorization endpoint");if(!s.client_id_metadata_document_supported)throw new p("authorization server does not support 'client_id_metadata_document'");if(!s.pushed_authorization_request_endpoint)throw new p("authorization server does not support 'pushed_authorization request'");if(s.response_types_supported&&!s.response_types_supported.includes("code"))throw new p("authorization server does not support 'code' response type");return s},oe=async t=>{let e;Oe(t)?e=t:e=await Be(t);const r=await Fe(e),s=Te(r);if(!s)throw new p("missing pds endpoint");return{identity:{id:e,raw:t,pds:new URL(s)},metadata:await Ce(s)}},Ce=async t=>{var n;const e=await Je(t);if(((n=e.authorization_servers)==null?void 0:n.length)!==1)throw new p("expected exactly one authorization server in the listing");const r=e.authorization_servers[0],s=await He(r);if(s.protected_resources&&!s.protected_resources.includes(e.resource))throw new p("server is not in authorization server's jurisdiction");return s},ae={name:"ECDSA",namedCurve:"P-256"},We=async()=>{const t=await crypto.subtle.generateKey(ae,!0,["sign","verify"]),e=await crypto.subtle.exportKey("pkcs8",t.privateKey),{ext:r,key_ops:s,...n}=await crypto.subtle.exportKey("jwk",t.publicKey);return{typ:"ES256",key:z(new Uint8Array(e)),jwt:z($(JSON.stringify({typ:"dpop+jwt",alg:"ES256",jwk:n})))}},Me=t=>{const e=t.jwt,r=crypto.subtle.importKey("pkcs8",Ee(t.key),ae,!0,["sign"]),s=(n,o,a,i)=>{const c={ath:i,htm:n,htu:o,iat:Math.floor(Date.now()/1e3),jti:W(24),nonce:a};return z($(JSON.stringify(c)))};return async(n,o,a,i)=>{const c=s(n,o,a,i),l=await crypto.subtle.sign({name:"ECDSA",hash:{name:"SHA-256"}},await r,$(e+"."+c)),u=z(new Uint8Array(l));return e+"."+c+"."+u}},ie=(t,e)=>{const r=_.dpopNonces,s=_.inflightDpop,n=Me(t);return async(o,a)=>{const i=new Request(o,a),c=i.headers.get("authorization"),l=c!=null&&c.startsWith("DPoP ")?await se(c.slice(5)):void 0,{method:u,url:h}=i,{origin:d,pathname:g}=new URL(h),v=d+g;let w=s.get(d);w&&(await w.promise,w=void 0);let I,U=!1;try{const[j,y]=r.getWithLapsed(d);I=j,U=y>3*60*1e3}catch{}U&&s.set(d,w=Promise.withResolvers());let A;try{const j=await n(u,v,I,l);i.headers.set("dpop",j);const y=await fetch(i);if(A=y.headers.get("dpop-nonce"),A===null||A===I)return y;try{r.set(d,A)}catch{}if(!await Ze(y,e)||o===i||(a==null?void 0:a.body)instanceof ReadableStream)return y}finally{w&&(s.delete(d),w.resolve())}{const j=await n(u,v,A,l),y=new Request(o,a);y.headers.set("dpop",j);const F=await fetch(y),J=F.headers.get("dpop-nonce");if(J!==null&&J!==A)try{r.set(d,J)}catch{}return F}}},Ze=async(t,e)=>{if((e===void 0||e===!1)&&t.status===401){const r=t.headers.get("www-authenticate");if(r!=null&&r.startsWith("DPoP"))return r.includes('error="use_dpop_nonce"')}if((e===void 0||e===!0)&&t.status===400&&B(t.headers)==="application/json")try{const r=await t.clone().json();return typeof r=="object"&&(r==null?void 0:r.error)==="use_dpop_nonce"}catch{return!1}return!1},Ve=(t,e)=>{const r={};for(let s=0,n=e.length;s<n;s++){const o=e[s];r[o]=t[o]}return r};var O,R,b,C,ce;class P{constructor(e,r){E(this,b);E(this,O);E(this,R);k(this,R,e),k(this,O,ie(r,!0))}async request(e,r){const s=m(this,R)[`${e}_endpoint`];if(!s)throw new Error(`no endpoint for ${e}`);const n=await m(this,O).call(this,s,{method:"post",headers:{"content-type":"application/json"},body:JSON.stringify({...r,client_id:M})});if(B(n.headers)!=="application/json")throw new De(n,2,"unexpected content-type");const o=await n.json();if(n.ok)return o;throw new ne(n,o)}async revoke(e){try{await this.request("revocation",{token:e})}catch{}}async exchangeCode(e,r){const s=await this.request("token",{grant_type:"authorization_code",redirect_uri:Z,code:e,code_verifier:r});try{return await L(this,b,ce).call(this,s)}catch(n){throw await this.revoke(s.access_token),n}}async refresh({sub:e,token:r}){if(!r.refresh)throw new q(e,"no refresh token available");const s=await this.request("token",{grant_type:"refresh_token",refresh_token:r.refresh});try{if(e!==s.sub)throw new q(e,`sub mismatch in token response; got ${s.sub}`);return L(this,b,C).call(this,s)}catch(n){throw await this.revoke(s.access_token),n}}}O=new WeakMap,R=new WeakMap,b=new WeakSet,C=function(e){if(!e.sub)throw new TypeError("missing sub field in token response");if(!e.scope)throw new TypeError("missing scope field in token response");if(e.token_type!=="DPoP")throw new TypeError("token response returned a non-dpop token");return{scope:e.scope,refresh:e.refresh_token,access:e.access_token,type:e.token_type,expires_at:typeof e.expires_in=="number"?Date.now()+e.expires_in*1e3:void 0}},ce=async function(e){const r=e.sub;if(!r)throw new TypeError("missing sub field in token response");const s=L(this,b,C).call(this,e),n=await oe(r);if(n.metadata.issuer!==m(this,R).issuer)throw new TypeError(`issuer mismatch; got ${n.metadata.issuer}`);return{token:s,info:{sub:r,aud:n.identity.pds.href,server:Ve(n.metadata,["issuer","authorization_endpoint","introspection_endpoint","pushed_authorization_request_endpoint","revocation_endpoint","token_endpoint"])}}};const T=new Map,X=async(t,e)=>{var i,c;(i=e==null?void 0:e.signal)==null||i.throwIfAborted();let r=tt;e!=null&&e.noCache?r=Qe:e!=null&&e.allowStale&&(r=Ye);let s;for(;s=T.get(t);){try{const{isFresh:l,value:u}=await s;if(l||r(u))return u}catch{}(c=e==null?void 0:e.signal)==null||c.throwIfAborted()}const n=async()=>{const l=_.sessions.get(t);if(l&&r(l))return{isFresh:!1,value:l};const u=await Xe(t,l);return await le(t,u),{isFresh:!0,value:u}};let o;if(N?o=N.request(`atcute-oauth:${t}`,n):o=n(),o=o.finally(()=>T.delete(t)),T.has(t))throw new Error("concurrent request for the same key");T.set(t,o);const{value:a}=await o;return a},le=async(t,e)=>{try{_.sessions.set(t,e)}catch(r){throw await et(e),r}},Ge=t=>{_.sessions.delete(t)},Ye=()=>!0,Qe=()=>!1,Xe=async(t,e)=>{if(e===void 0)throw new q(t,"session deleted by another tab");const{dpopKey:r,info:s,token:n}=e,o=new P(s.server,r);try{const a=await o.refresh({sub:s.sub,token:n});return{dpopKey:r,info:s,token:a}}catch(a){throw a instanceof ne&&a.status===400&&a.error==="invalid_grant"?new q(t,"session was revoked",{cause:a}):a}},et=async({dpopKey:t,info:e,token:r})=>{await new P(e.server,t).revoke(r.refresh??r.access)},tt=({token:t})=>{const e=t.expires_at;return e==null||Date.now()+6e4<=e},rt=async({metadata:t,identity:e,scope:r})=>{const s=W(24),n=await ke(),o=await We(),a={redirect_uri:Z,code_challenge:n.challenge,code_challenge_method:n.method,state:s,login_hint:e==null?void 0:e.raw,response_mode:"fragment",response_type:"code",display:"page",scope:r};_.states.set(s,{dpopKey:o,metadata:t,verifier:n.verifier});const c=await new P(t,o).request("pushed_authorization_request",a),l=new URL(t.authorization_endpoint);return l.searchParams.set("client_id",M),l.searchParams.set("request_uri",c.request_uri),l},st=async t=>{const e=t.get("iss"),r=t.get("state"),s=t.get("code"),n=t.get("error");if(!r||!(s||n))throw new D("missing parameters");const o=_.states.get(r);if(o)_.states.delete(r);else throw new D("unknown state provided");const a=o.dpopKey,i=o.metadata;if(n)throw new je(t.get("error_description")||n);if(!s)throw new D("missing code parameter");if(e===null)throw new D("missing issuer parameter");if(e!==i.issuer)throw new D("issuer mismatch");const c=new P(i,a),{info:l,token:u}=await c.exchangeCode(s,o.verifier),h=l.sub,d={dpopKey:a,info:l,token:u};return await le(h,d),d};var x,S;class nt{constructor(e){f(this,"session");E(this,x);E(this,S);this.session=e,k(this,x,ie(e.dpopKey,!1))}get sub(){return this.session.info.sub}getSession(e){const r=X(this.session.info.sub,e);return r.then(s=>{this.session=s}).finally(()=>{k(this,S,void 0)}),k(this,S,r)}async signOut(){const e=this.session.info.sub;try{const{dpopKey:r,info:s,token:n}=await X(e,{allowStale:!0});await new P(s.server,r).revoke(n.refresh??n.access)}finally{Ge(e)}}async handle(e,r){await m(this,S);const s=new Headers(r==null?void 0:r.headers);let n=this.session,o=new URL(e,n.info.aud);s.set("authorization",`${n.token.type} ${n.token.access}`);let a=await m(this,x).call(this,o,{...r,headers:s});if(!ot(a))return a;try{m(this,S)?n=await m(this,S):n=await this.getSession()}catch{return a}return(r==null?void 0:r.body)instanceof ReadableStream?a:(o=new URL(e,n.info.aud),s.set("authorization",`${n.token.type} ${n.token.access}`),await m(this,x).call(this,o,{...r,headers:s}))}}x=new WeakMap,S=new WeakMap;const ot=t=>{if(t.status!==401)return!1;const e=t.headers.get("www-authenticate");return e!=null&&(e.startsWith("Bearer ")||e.startsWith("DPoP "))&&e.includes('error="invalid_token"')},V={local:{async get(t){if(!t){const r={};for(let s=0;s<localStorage.length;s++){const n=localStorage.key(s);if(n)try{r[n]=JSON.parse(localStorage.getItem(n)||"null")}catch{r[n]=localStorage.getItem(n)}}return r}const e={};if(typeof t=="string")try{const r=localStorage.getItem(t);e[t]=r?JSON.parse(r):null}catch{e[t]=localStorage.getItem(t)}else Array.isArray(t)?t.forEach(r=>{try{const s=localStorage.getItem(r);e[r]=s?JSON.parse(s):null}catch{e[r]=localStorage.getItem(r)}}):Object.keys(t).forEach(r=>{try{const s=localStorage.getItem(r);e[r]=s?JSON.parse(s):t[r]}catch{e[r]=localStorage.getItem(r)||t[r]}});return e},async set(t){Object.entries(t).forEach(([e,r])=>{localStorage.setItem(e,JSON.stringify(r))})},async remove(t){(Array.isArray(t)?t:[t]).forEach(r=>localStorage.removeItem(r))},async clear(){localStorage.clear()}}},K="synthesis-oauth:session";let ee=!1;function at(){typeof window<"u"&&!ee&&(Ue({metadata:{client_id:"http://localhost:8081/static/client-metadata.json",redirect_uri:"http://localhost:8081/static/oauth-callback.html"}}),ee=!0)}async function lt(t){console.log("[oauth-web] Starting login process for handle:",t),at(),console.log("[oauth-web] Resolving identity...");const{metadata:e}=await oe(t);console.log("[oauth-web] PDS metadata:",e),console.log("[oauth-web] Creating authorization URL...");const r=await rt({metadata:e,scope:"atproto transition:generic"});console.log("[oauth-web] Auth URL:",r.toString()),window.location.href=r.toString()}async function ut(){console.log("[oauth-web] Handling OAuth callback");const t=new URL(window.location.href),e=t.search||t.hash.slice(1),r=new URLSearchParams(e);if(console.log("[oauth-web] OAuth params:",Object.fromEntries(r)),!r.has("code")&&!r.has("error"))return console.log("[oauth-web] No OAuth params found"),null;if(r.has("error")){const n=r.get("error"),o=r.get("error_description");throw console.error("[oauth-web] OAuth error:",n,o),new Error(`OAuth error: ${n} - ${o}`)}console.log("[oauth-web] Finalizing authorization...");const s=await st(r);return console.log("[oauth-web] Authorization complete, session:",s),await it(s),console.log("[oauth-web] Session saved successfully"),s}async function it(t){await V.local.set({[K]:t})}async function dt(){return(await V.local.get(K))[K]||null}async function ht(){await V.local.remove(K)}async function pt(t){return await(await new nt(t).handle("/xrpc/app.bsky.actor.getProfile?actor="+t.info.sub)).json()}export{ht as c,pt as g,ut as h,at as i,dt as l,lt as s}; 2 - //# sourceMappingURL=oauth-web-B1Uotnw-.js.map
-1
pywb-test/static/assets/oauth-web-B1Uotnw-.js.map
··· 1 - {"version":3,"file":"oauth-web-B1Uotnw-.js","sources":["../../../node_modules/.pnpm/nanoid@5.1.6/node_modules/nanoid/url-alphabet/index.js","../../../node_modules/.pnpm/nanoid@5.1.6/node_modules/nanoid/index.browser.js","../../../node_modules/.pnpm/@atcute+uint8array@1.0.5/node_modules/@atcute/uint8array/dist/index.js","../../../node_modules/.pnpm/@atcute+multibase@1.1.6/node_modules/@atcute/multibase/dist/utils.js","../../../node_modules/.pnpm/@atcute+multibase@1.1.6/node_modules/@atcute/multibase/dist/bases/base64-web-native.js","../../../node_modules/.pnpm/@atcute+multibase@1.1.6/node_modules/@atcute/multibase/dist/bases/base64-web-polyfill.js","../../../node_modules/.pnpm/@atcute+multibase@1.1.6/node_modules/@atcute/multibase/dist/bases/base64-web.js","../../../node_modules/.pnpm/@atcute+oauth-browser-client@1.0.27/node_modules/@atcute/oauth-browser-client/dist/utils/runtime.js","../../../node_modules/.pnpm/@atcute+oauth-browser-client@1.0.27/node_modules/@atcute/oauth-browser-client/dist/store/db.js","../../../node_modules/.pnpm/@atcute+oauth-browser-client@1.0.27/node_modules/@atcute/oauth-browser-client/dist/environment.js","../../../node_modules/.pnpm/@atcute+oauth-browser-client@1.0.27/node_modules/@atcute/oauth-browser-client/dist/errors.js","../../../node_modules/.pnpm/@atcute+lexicons@1.2.2/node_modules/@atcute/lexicons/dist/syntax/did.js","../../../node_modules/.pnpm/@atcute+identity@1.1.1/node_modules/@atcute/identity/dist/utils.js","../../../node_modules/.pnpm/@atcute+oauth-browser-client@1.0.27/node_modules/@atcute/oauth-browser-client/dist/constants.js","../../../node_modules/.pnpm/@atcute+oauth-browser-client@1.0.27/node_modules/@atcute/oauth-browser-client/dist/utils/response.js","../../../node_modules/.pnpm/@atcute+oauth-browser-client@1.0.27/node_modules/@atcute/oauth-browser-client/dist/utils/strings.js","../../../node_modules/.pnpm/@atcute+oauth-browser-client@1.0.27/node_modules/@atcute/oauth-browser-client/dist/resolvers.js","../../../node_modules/.pnpm/@atcute+oauth-browser-client@1.0.27/node_modules/@atcute/oauth-browser-client/dist/dpop.js","../../../node_modules/.pnpm/@atcute+oauth-browser-client@1.0.27/node_modules/@atcute/oauth-browser-client/dist/utils/misc.js","../../../node_modules/.pnpm/@atcute+oauth-browser-client@1.0.27/node_modules/@atcute/oauth-browser-client/dist/agents/server-agent.js","../../../node_modules/.pnpm/@atcute+oauth-browser-client@1.0.27/node_modules/@atcute/oauth-browser-client/dist/agents/sessions.js","../../../node_modules/.pnpm/@atcute+oauth-browser-client@1.0.27/node_modules/@atcute/oauth-browser-client/dist/agents/exchange.js","../../../node_modules/.pnpm/@atcute+oauth-browser-client@1.0.27/node_modules/@atcute/oauth-browser-client/dist/agents/user-agent.js","../../../lib/storage-adapter.ts","../../../lib/oauth-web.ts"],"sourcesContent":["export const urlAlphabet =\n 'useandom-26T198340PX75pxJACKVERYMINDBUSHWOLF_GQZbfghjklqvwyzrict'\n","/* @ts-self-types=\"./index.d.ts\" */\nimport { urlAlphabet as scopedUrlAlphabet } from './url-alphabet/index.js'\nexport { urlAlphabet } from './url-alphabet/index.js'\nexport let random = bytes => crypto.getRandomValues(new Uint8Array(bytes))\nexport let customRandom = (alphabet, defaultSize, getRandom) => {\n let mask = (2 << Math.log2(alphabet.length - 1)) - 1\n let step = -~((1.6 * mask * defaultSize) / alphabet.length)\n return (size = defaultSize) => {\n let id = ''\n while (true) {\n let bytes = getRandom(step)\n let j = step | 0\n while (j--) {\n id += alphabet[bytes[j] & mask] || ''\n if (id.length >= size) return id\n }\n }\n }\n}\nexport let customAlphabet = (alphabet, size = 21) =>\n customRandom(alphabet, size | 0, random)\nexport let nanoid = (size = 21) => {\n let id = ''\n let bytes = crypto.getRandomValues(new Uint8Array((size |= 0)))\n while (size--) {\n id += scopedUrlAlphabet[bytes[size] & 63]\n }\n return id\n}\n","const textEncoder = new TextEncoder();\nconst textDecoder = new TextDecoder();\nconst subtle = crypto.subtle;\n/**\n * creates an Uint8Array of the requested size, with the contents zeroed\n */\nexport const alloc = (size) => {\n return new Uint8Array(size);\n};\n/**\n * creates an Uint8Array of the requested size, where the contents may not be\n * zeroed out. only use if you're certain that the contents will be overwritten\n */\nexport const allocUnsafe = alloc;\n/**\n * compares two Uint8Array buffers\n */\nexport const compare = (a, b) => {\n const alen = a.length;\n const blen = b.length;\n if (alen > blen) {\n return 1;\n }\n if (alen < blen) {\n return -1;\n }\n for (let i = 0; i < alen; i++) {\n const ax = a[i];\n const bx = b[i];\n if (ax < bx) {\n return -1;\n }\n if (ax > bx) {\n return 1;\n }\n }\n return 0;\n};\n/**\n * checks if the two Uint8Array buffers are equal\n */\nexport const equals = (a, b) => {\n if (a === b) {\n return true;\n }\n let len;\n if ((len = a.length) === b.length) {\n while (len--) {\n if (a[len] !== b[len]) {\n return false;\n }\n }\n }\n return len === -1;\n};\n/**\n * checks if the two Uint8Array buffers are equal, timing-safe version\n */\nexport const timingSafeEquals = (a, b) => {\n let len;\n let out = 0;\n if ((len = a.length) === b.length) {\n while (len--) {\n out |= a[len] ^ b[len];\n }\n }\n return len === -1 && out === 0;\n};\n/**\n * concatenates multiple Uint8Array buffers into one\n */\nexport const concat = (arrays, size) => {\n let written = 0;\n let len = arrays.length;\n let idx;\n if (size === undefined) {\n for (idx = size = 0; idx < len; idx++) {\n const chunk = arrays[idx];\n size += chunk.length;\n }\n }\n const buffer = new Uint8Array(size);\n for (idx = 0; idx < len; idx++) {\n const chunk = arrays[idx];\n buffer.set(chunk, written);\n written += chunk.length;\n }\n return buffer;\n};\n/**\n * encodes a UTF-8 string\n */\nexport const encodeUtf8 = (str) => {\n return textEncoder.encode(str);\n};\n/**\n * encodes a UTF-8 string into a given buffer\n */\nexport const encodeUtf8Into = (to, str, offset, length) => {\n let buffer;\n if (offset === undefined) {\n buffer = to;\n }\n else if (length === undefined) {\n buffer = to.subarray(offset);\n }\n else {\n buffer = to.subarray(offset, offset + length);\n }\n const result = textEncoder.encodeInto(str, buffer);\n return result.written;\n};\nconst fromCharCode = String.fromCharCode;\n/**\n * decodes a UTF-8 string from a given buffer\n */\nexport const decodeUtf8From = (from, offset, length) => {\n let buffer;\n if (offset === undefined) {\n buffer = from;\n }\n else if (length === undefined) {\n buffer = from.subarray(offset);\n }\n else {\n buffer = from.subarray(offset, offset + length);\n }\n const end = buffer.length;\n if (end > 24) {\n return textDecoder.decode(buffer);\n }\n {\n let str = '';\n let idx = 0;\n for (; idx + 3 < end; idx += 4) {\n const a = buffer[idx];\n const b = buffer[idx + 1];\n const c = buffer[idx + 2];\n const d = buffer[idx + 3];\n if ((a | b | c | d) & 0x80) {\n return str + textDecoder.decode(buffer.subarray(idx));\n }\n str += fromCharCode(a, b, c, d);\n }\n for (; idx < end; idx++) {\n const x = buffer[idx];\n if (x & 0x80) {\n return str + textDecoder.decode(buffer.subarray(idx));\n }\n str += fromCharCode(x);\n }\n return str;\n }\n};\n/**\n * get a SHA-256 digest of this buffer\n */\nexport const toSha256 = async (buffer) => {\n return new Uint8Array(await subtle.digest('SHA-256', buffer));\n};\n//# sourceMappingURL=index.js.map","import { alloc, allocUnsafe } from '@atcute/uint8array';\nexport const createRfc4648Encode = (alphabet, bitsPerChar, pad) => {\n return (bytes) => {\n const mask = (1 << bitsPerChar) - 1;\n let str = '';\n let bits = 0; // Number of bits currently in the buffer\n let buffer = 0; // Bits waiting to be written out, MSB first\n for (let i = 0; i < bytes.length; ++i) {\n // Slurp data into the buffer:\n buffer = (buffer << 8) | bytes[i];\n bits += 8;\n // Write out as much as we can:\n while (bits > bitsPerChar) {\n bits -= bitsPerChar;\n str += alphabet[mask & (buffer >> bits)];\n }\n }\n // Partial character:\n if (bits !== 0) {\n str += alphabet[mask & (buffer << (bitsPerChar - bits))];\n }\n // Add padding characters until we hit a byte boundary:\n if (pad) {\n while (((str.length * bitsPerChar) & 7) !== 0) {\n str += '=';\n }\n }\n return str;\n };\n};\nexport const createRfc4648Decode = (alphabet, bitsPerChar, pad) => {\n // Build the character lookup table:\n const codes = {};\n for (let i = 0; i < alphabet.length; ++i) {\n codes[alphabet[i]] = i;\n }\n return (str) => {\n // Count the padding bytes:\n let end = str.length;\n while (pad && str[end - 1] === '=') {\n --end;\n }\n // Allocate the output:\n const bytes = allocUnsafe(((end * bitsPerChar) / 8) | 0);\n // Parse the data:\n let bits = 0; // Number of bits currently in the buffer\n let buffer = 0; // Bits waiting to be written out, MSB first\n let written = 0; // Next byte to write\n for (let i = 0; i < end; ++i) {\n // Read one character from the string:\n const value = codes[str[i]];\n if (value === undefined) {\n throw new SyntaxError(`invalid base string`);\n }\n // Append the bits to the buffer:\n buffer = (buffer << bitsPerChar) | value;\n bits += bitsPerChar;\n // Write out some bits if the buffer has a byte's worth:\n if (bits >= 8) {\n bits -= 8;\n bytes[written++] = 0xff & (buffer >> bits);\n }\n }\n // Verify that we have received just enough bits:\n if (bits >= bitsPerChar || (0xff & (buffer << (8 - bits))) !== 0) {\n throw new SyntaxError('unexpected end of data');\n }\n return bytes;\n };\n};\nexport const createBtcBaseEncode = (alphabet) => {\n if (alphabet.length >= 255) {\n throw new RangeError(`alphabet too long`);\n }\n const BASE = alphabet.length;\n const LEADER = alphabet.charAt(0);\n const iFACTOR = Math.log(256) / Math.log(BASE); // log(256) / log(BASE), rounded up\n return (source) => {\n if (source.length === 0) {\n return '';\n }\n // Skip & count leading zeroes.\n let zeroes = 0;\n let length = 0;\n let pbegin = 0;\n const pend = source.length;\n while (pbegin !== pend && source[pbegin] === 0) {\n pbegin++;\n zeroes++;\n }\n // Allocate enough space in big-endian base58 representation.\n const size = ((pend - pbegin) * iFACTOR + 1) >>> 0;\n const b58 = alloc(size);\n // Process the bytes.\n while (pbegin !== pend) {\n let carry = source[pbegin];\n // Apply \"b58 = b58 * 256 + ch\".\n let i = 0;\n for (let it1 = size - 1; (carry !== 0 || i < length) && it1 !== -1; it1--, i++) {\n carry += (256 * b58[it1]) >>> 0;\n b58[it1] = carry % BASE >>> 0;\n carry = (carry / BASE) >>> 0;\n }\n if (carry !== 0) {\n throw new Error('non-zero carry');\n }\n length = i;\n pbegin++;\n }\n // Skip leading zeroes in base58 result.\n let it2 = size - length;\n while (it2 !== size && b58[it2] === 0) {\n it2++;\n }\n // Translate the result into a string.\n let str = LEADER.repeat(zeroes);\n for (; it2 < size; ++it2) {\n str += alphabet.charAt(b58[it2]);\n }\n return str;\n };\n};\nexport const createBtcBaseDecode = (alphabet) => {\n if (alphabet.length >= 255) {\n throw new RangeError(`alphabet too long`);\n }\n const BASE_MAP = allocUnsafe(256).fill(255);\n for (let i = 0; i < alphabet.length; i++) {\n const xc = alphabet.charCodeAt(i);\n if (BASE_MAP[xc] !== 255) {\n throw new RangeError(`${alphabet[i]} is ambiguous`);\n }\n BASE_MAP[xc] = i;\n }\n const BASE = alphabet.length;\n const LEADER = alphabet.charAt(0);\n const FACTOR = Math.log(BASE) / Math.log(256); // log(BASE) / log(256), rounded up\n return (source) => {\n if (source.length === 0) {\n return allocUnsafe(0);\n }\n // Skip and count leading '1's.\n let psz = 0;\n let zeroes = 0;\n let length = 0;\n while (source[psz] === LEADER) {\n zeroes++;\n psz++;\n }\n // Allocate enough space in big-endian base256 representation.\n const size = ((source.length - psz) * FACTOR + 1) >>> 0; // log(58) / log(256), rounded up.\n const b256 = alloc(size);\n // Process the characters.\n while (psz < source.length) {\n // Decode character\n let carry = BASE_MAP[source.charCodeAt(psz)];\n // Invalid character\n if (carry === 255) {\n throw new Error(`invalid string`);\n }\n let i = 0;\n for (let it3 = size - 1; (carry !== 0 || i < length) && it3 !== -1; it3--, i++) {\n carry += (BASE * b256[it3]) >>> 0;\n b256[it3] = carry % 256 >>> 0;\n carry = (carry / 256) >>> 0;\n }\n if (carry !== 0) {\n throw new Error('non-zero carry');\n }\n length = i;\n psz++;\n }\n // Skip leading zeroes in b256.\n let it4 = size - length;\n while (it4 !== size && b256[it4] === 0) {\n it4++;\n }\n if (it4 === zeroes) {\n return b256;\n }\n const vch = allocUnsafe(zeroes + (size - it4));\n vch.fill(0, 0, zeroes);\n vch.set(b256.subarray(it4), zeroes);\n return vch;\n };\n};\n//# sourceMappingURL=utils.js.map","// #region base64\nexport const fromBase64 = (str) => {\n return Uint8Array.fromBase64(str, { alphabet: 'base64', lastChunkHandling: 'loose' });\n};\nexport const toBase64 = (bytes) => {\n return bytes.toBase64({ alphabet: 'base64', omitPadding: true });\n};\n// #endregion\n// #region base64pad\nexport const fromBase64Pad = (str) => {\n return Uint8Array.fromBase64(str, { alphabet: 'base64', lastChunkHandling: 'strict' });\n};\nexport const toBase64Pad = (bytes) => {\n return bytes.toBase64({ alphabet: 'base64', omitPadding: false });\n};\n// #endregion\n// #region base64url\nexport const fromBase64Url = (str) => {\n return Uint8Array.fromBase64(str, { alphabet: 'base64url', lastChunkHandling: 'loose' });\n};\nexport const toBase64Url = (bytes) => {\n return bytes.toBase64({ alphabet: 'base64url', omitPadding: true });\n};\n// #endregion\n// #region base64urlpad\nexport const fromBase64UrlPad = (str) => {\n return Uint8Array.fromBase64(str, { alphabet: 'base64url', lastChunkHandling: 'strict' });\n};\nexport const toBase64UrlPad = (bytes) => {\n return bytes.toBase64({ alphabet: 'base64url', omitPadding: false });\n};\n// #endregion\n//# sourceMappingURL=base64-web-native.js.map","import { createRfc4648Decode, createRfc4648Encode } from '../utils.js';\nconst BASE64_CHARSET = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/';\nconst BASE64URL_CHARSET = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_';\n// #region base64\nexport const fromBase64 = /*#__PURE__*/ createRfc4648Decode(BASE64_CHARSET, 6, false);\nexport const toBase64 = /*#__PURE__*/ createRfc4648Encode(BASE64_CHARSET, 6, false);\n// #endregion\n// #region base64pad\nexport const fromBase64Pad = /*#__PURE__*/ createRfc4648Decode(BASE64_CHARSET, 6, true);\nexport const toBase64Pad = /*#__PURE__*/ createRfc4648Encode(BASE64_CHARSET, 6, true);\n// #endregion\n// #region base64url\nexport const fromBase64Url = /*#__PURE__*/ createRfc4648Decode(BASE64URL_CHARSET, 6, false);\nexport const toBase64Url = /*#__PURE__*/ createRfc4648Encode(BASE64URL_CHARSET, 6, false);\n// #endregion\n// #region base64urlpad\nexport const fromBase64UrlPad = /*#__PURE__*/ createRfc4648Decode(BASE64URL_CHARSET, 6, true);\nexport const toBase64UrlPad = /*#__PURE__*/ createRfc4648Encode(BASE64URL_CHARSET, 6, true);\n// #endregion\n//# sourceMappingURL=base64-web-polyfill.js.map","import { fromBase64 as fromBase64Native, fromBase64Pad as fromBase64PadNative, fromBase64Url as fromBase64UrlNative, fromBase64UrlPad as fromBase64UrlPadNative, toBase64 as toBase64Native, toBase64Pad as toBase64PadNative, toBase64Url as toBase64UrlNative, toBase64UrlPad as toBase64UrlPadNative, } from './base64-web-native.js';\nimport { fromBase64Pad as fromBase64PadPolyfill, fromBase64 as fromBase64Polyfill, fromBase64UrlPad as fromBase64UrlPadPolyfill, fromBase64Url as fromBase64UrlPolyfill, toBase64Pad as toBase64PadPolyfill, toBase64 as toBase64Polyfill, toBase64UrlPad as toBase64UrlPadPolyfill, toBase64Url as toBase64UrlPolyfill, } from './base64-web-polyfill.js';\nconst HAS_NATIVE_SUPPORT = 'fromBase64' in Uint8Array;\n// #region base64\nexport const fromBase64 = !HAS_NATIVE_SUPPORT ? fromBase64Polyfill : fromBase64Native;\nexport const toBase64 = !HAS_NATIVE_SUPPORT ? toBase64Polyfill : toBase64Native;\n// #endregion\n// #region base64pad\nexport const fromBase64Pad = !HAS_NATIVE_SUPPORT ? fromBase64PadPolyfill : fromBase64PadNative;\nexport const toBase64Pad = !HAS_NATIVE_SUPPORT ? toBase64PadPolyfill : toBase64PadNative;\n// #endregion\n// #region base64url\nexport const fromBase64Url = !HAS_NATIVE_SUPPORT ? fromBase64UrlPolyfill : fromBase64UrlNative;\nexport const toBase64Url = !HAS_NATIVE_SUPPORT ? toBase64UrlPolyfill : toBase64UrlNative;\n// #endregion\n// #region base64urlpad\nexport const fromBase64UrlPad = !HAS_NATIVE_SUPPORT ? fromBase64UrlPadPolyfill : fromBase64UrlPadNative;\nexport const toBase64UrlPad = !HAS_NATIVE_SUPPORT ? toBase64UrlPadPolyfill : toBase64UrlPadNative;\n// #endregion\n//# sourceMappingURL=base64-web.js.map","import { nanoid } from 'nanoid';\nimport { toBase64Url } from '@atcute/multibase';\nimport { encodeUtf8, toSha256 } from '@atcute/uint8array';\nexport const locks = typeof navigator !== 'undefined' ? navigator.locks : undefined;\nexport const stringToSha256 = async (input) => {\n const bytes = encodeUtf8(input);\n const digest = await toSha256(bytes);\n return toBase64Url(digest);\n};\nexport const generatePKCE = async () => {\n const verifier = nanoid(64);\n return {\n verifier: verifier,\n challenge: await stringToSha256(verifier),\n method: 'S256',\n };\n};\n//# sourceMappingURL=runtime.js.map","import { locks } from '../utils/runtime.js';\nconst parse = (raw) => {\n if (raw != null) {\n const parsed = JSON.parse(raw);\n if (parsed != null) {\n return parsed;\n }\n }\n return {};\n};\nexport const createOAuthDatabase = ({ name }) => {\n const controller = new AbortController();\n const signal = controller.signal;\n const createStore = (subname, expiresAt, persistUpdatedAt = false) => {\n let store;\n const storageKey = `${name}:${subname}`;\n const persist = () => store && localStorage.setItem(storageKey, JSON.stringify(store));\n const read = () => {\n if (signal.aborted) {\n throw new Error(`store closed`);\n }\n return (store ??= parse(localStorage.getItem(storageKey)));\n };\n {\n const listener = (ev) => {\n if (ev.key === storageKey) {\n store = undefined;\n }\n };\n globalThis.addEventListener('storage', listener, { signal });\n }\n {\n const cleanup = async (lock) => {\n if (!lock || signal.aborted) {\n return;\n }\n await new Promise((resolve) => setTimeout(resolve, 10_000));\n if (signal.aborted) {\n return;\n }\n let now = Date.now();\n let changed = false;\n read();\n for (const key in store) {\n const item = store[key];\n const expiresAt = item.expiresAt;\n if (expiresAt !== null && now > expiresAt) {\n changed = true;\n delete store[key];\n }\n }\n if (changed) {\n persist();\n }\n };\n if (locks) {\n locks.request(`${storageKey}:cleanup`, { ifAvailable: true }, cleanup);\n }\n else {\n cleanup(true);\n }\n }\n return {\n get(key) {\n read();\n const item = store[key];\n if (!item) {\n return;\n }\n const expiresAt = item.expiresAt;\n if (expiresAt !== null && Date.now() > expiresAt) {\n delete store[key];\n persist();\n return;\n }\n return item.value;\n },\n getWithLapsed(key) {\n read();\n const item = store[key];\n const now = Date.now();\n if (!item) {\n return [undefined, Infinity];\n }\n const updatedAt = item.updatedAt;\n if (updatedAt === undefined) {\n return [item.value, Infinity];\n }\n return [item.value, now - updatedAt];\n },\n set(key, value) {\n read();\n const item = {\n value: value,\n expiresAt: expiresAt(value),\n updatedAt: persistUpdatedAt ? Date.now() : undefined,\n };\n store[key] = item;\n persist();\n },\n delete(key) {\n read();\n if (store[key] !== undefined) {\n delete store[key];\n persist();\n }\n },\n keys() {\n read();\n return Object.keys(store);\n },\n };\n };\n return {\n dispose: () => {\n controller.abort();\n },\n sessions: createStore('sessions', ({ token }) => {\n if (token.refresh) {\n return null;\n }\n return token.expires_at ?? null;\n }),\n states: createStore('states', (_item) => Date.now() + 10 * 60 * 1_000), // 10 minutes\n // The reference PDS have nonces that expire after 3 minutes, while other\n // implementations can have varying expiration times.\n // Stored for 24 hours.\n dpopNonces: createStore('dpopNonces', (_item) => Date.now() + 24 * 60 * 60 * 1_000, true),\n inflightDpop: new Map(),\n };\n};\n//# sourceMappingURL=db.js.map","import { createOAuthDatabase } from './store/db.js';\nexport let CLIENT_ID;\nexport let REDIRECT_URI;\nexport let database;\nexport const configureOAuth = (options) => {\n ({ client_id: CLIENT_ID, redirect_uri: REDIRECT_URI } = options.metadata);\n database = createOAuthDatabase({ name: options.storageName ?? 'atcute-oauth' });\n};\n//# sourceMappingURL=environment.js.map","export class LoginError extends Error {\n name = 'LoginError';\n}\nexport class AuthorizationError extends Error {\n name = 'AuthorizationError';\n}\nexport class ResolverError extends Error {\n name = 'ResolverError';\n}\nexport class TokenRefreshError extends Error {\n sub;\n name = 'TokenRefreshError';\n constructor(sub, message, options) {\n super(message, options);\n this.sub = sub;\n }\n}\nexport class OAuthResponseError extends Error {\n response;\n data;\n name = 'OAuthResponseError';\n error;\n description;\n constructor(response, data) {\n const error = ifString(ifObject(data)?.['error']);\n const errorDescription = ifString(ifObject(data)?.['error_description']);\n const messageError = error ? `\"${error}\"` : 'unknown';\n const messageDesc = errorDescription ? `: ${errorDescription}` : '';\n const message = `OAuth ${messageError} error${messageDesc}`;\n super(message);\n this.response = response;\n this.data = data;\n this.error = error;\n this.description = errorDescription;\n }\n get status() {\n return this.response.status;\n }\n get headers() {\n return this.response.headers;\n }\n}\nexport class FetchResponseError extends Error {\n response;\n status;\n name = 'FetchResponseError';\n constructor(response, status, message) {\n super(message);\n this.response = response;\n this.status = status;\n }\n}\nconst ifString = (v) => {\n return typeof v === 'string' ? v : undefined;\n};\nconst ifObject = (v) => {\n return typeof v === 'object' && v !== null && !Array.isArray(v) ? v : undefined;\n};\n//# sourceMappingURL=errors.js.map","const DID_RE = /^did:([a-z]+):([a-zA-Z0-9._:%\\-]*[a-zA-Z0-9._\\-])$/;\n// #__NO_SIDE_EFFECTS__\nexport const isDid = (input) => {\n return typeof input === 'string' && input.length >= 7 && input.length <= 2048 && DID_RE.test(input);\n};\n//# sourceMappingURL=did.js.map","import { isHandle } from '@atcute/lexicons/syntax';\nimport * as t from './types.js';\nconst isUrlParseSupported = 'parse' in URL;\nexport const isAtprotoServiceEndpoint = (input) => {\n let url = null;\n if (isUrlParseSupported) {\n url = URL.parse(input);\n }\n else {\n try {\n url = new URL(input);\n }\n catch { }\n }\n return (url !== null &&\n (url.protocol === 'https:' || url.protocol === 'http:') &&\n url.pathname === '/' &&\n url.search === '' &&\n url.hash === '');\n};\nexport const getVerificationMaterial = (doc, id) => {\n const verificationMethods = doc.verificationMethod;\n if (!verificationMethods) {\n return;\n }\n const expectedId = `${doc.id}${id}`;\n for (let idx = 0, len = verificationMethods.length; idx < len; idx++) {\n const { id, type, publicKeyMultibase } = verificationMethods[idx];\n if (id !== expectedId) {\n continue;\n }\n if (publicKeyMultibase === undefined) {\n continue;\n }\n return { type, publicKeyMultibase };\n }\n};\nexport const getAtprotoVerificationMaterial = (doc) => {\n return getVerificationMaterial(doc, '#atproto');\n};\nexport const getAtprotoLabelerVerificationMaterial = (doc) => {\n return getVerificationMaterial(doc, '#atproto_label');\n};\nexport const getAtprotoHandle = (doc) => {\n const alsoKnownAs = doc.alsoKnownAs;\n if (!alsoKnownAs) {\n return null;\n }\n const PREFIX = 'at://';\n for (let idx = 0, len = alsoKnownAs.length; idx < len; idx++) {\n const aka = alsoKnownAs[idx];\n if (!aka.startsWith(PREFIX)) {\n continue;\n }\n const raw = aka.slice(PREFIX.length);\n if (!isHandle(raw)) {\n return undefined;\n }\n return raw;\n }\n return null;\n};\nexport const getAtprotoServiceEndpoint = (doc, predicate) => {\n const services = doc.service;\n if (!services) {\n return;\n }\n for (let idx = 0, len = services.length; idx < len; idx++) {\n const { id, type, serviceEndpoint } = services[idx];\n if (id !== predicate.id && id !== doc.id + predicate.id) {\n continue;\n }\n if (predicate.type !== undefined) {\n if (Array.isArray(type)) {\n if (!type.includes(predicate.type)) {\n continue;\n }\n }\n else {\n if (type !== predicate.type) {\n continue;\n }\n }\n }\n if (typeof serviceEndpoint !== 'string' || !isAtprotoServiceEndpoint(serviceEndpoint)) {\n continue;\n }\n return serviceEndpoint;\n }\n};\nexport const getPdsEndpoint = (doc) => {\n return getAtprotoServiceEndpoint(doc, {\n id: '#atproto_pds',\n type: 'AtprotoPersonalDataServer',\n });\n};\nexport const getLabelerEndpoint = (doc) => {\n return getAtprotoServiceEndpoint(doc, {\n id: '#atproto_labeler',\n type: 'AtprotoLabeler',\n });\n};\nexport const getBlueskyChatEndpoint = (doc) => {\n return getAtprotoServiceEndpoint(doc, {\n id: '#bsky_chat',\n type: 'BskyChatService',\n });\n};\nexport const getBlueskyFeedgenEndpoint = (doc) => {\n return getAtprotoServiceEndpoint(doc, {\n id: '#bsky_fg',\n type: 'BskyFeedGenerator',\n });\n};\nexport const getBlueskyNotificationEndpoint = (doc) => {\n return getAtprotoServiceEndpoint(doc, {\n id: '#bsky_notif',\n type: 'BskyNotificationService',\n });\n};\n//# sourceMappingURL=utils.js.map","export const DEFAULT_APPVIEW_URL = 'https://public.api.bsky.app';\n//# sourceMappingURL=constants.js.map","export const extractContentType = (headers) => {\n return headers.get('content-type')?.split(';')[0];\n};\n//# sourceMappingURL=response.js.map","const isUrlParseSupported = 'parse' in URL;\nexport const isValidUrl = (urlString) => {\n let url = null;\n if (isUrlParseSupported) {\n url = URL.parse(urlString);\n }\n else {\n try {\n url = new URL(urlString);\n }\n catch { }\n }\n if (url !== null) {\n return url.protocol === 'https:' || url.protocol === 'http:';\n }\n return false;\n};\n//# sourceMappingURL=strings.js.map","import { getPdsEndpoint } from '@atcute/identity';\nimport { isDid } from '@atcute/lexicons/syntax';\nimport { DEFAULT_APPVIEW_URL } from './constants.js';\nimport { ResolverError } from './errors.js';\nimport { extractContentType } from './utils/response.js';\nimport { isValidUrl } from './utils/strings.js';\nconst DID_WEB_RE = /^([a-zA-Z0-9-]+(?:\\.[a-zA-Z0-9-]+)*(?:\\.[a-zA-Z]{2,}))$/;\n/**\n * Resolves domain handles into DID identifiers, by requesting Bluesky's AppView\n * for identity resolution.\n * @param handle Domain handle to resolve\n * @returns DID identifier resolved from the domain handle\n */\nexport const resolveHandle = async (handle) => {\n const url = DEFAULT_APPVIEW_URL + `/xrpc/com.atproto.identity.resolveHandle` + `?handle=${handle}`;\n const response = await fetch(url);\n if (response.status === 400) {\n throw new ResolverError(`domain handle not found`);\n }\n else if (!response.ok) {\n throw new ResolverError(`directory is unreachable`);\n }\n const json = (await response.json());\n return json.did;\n};\n/**\n * Get DID documents of did:plc (via plc.directory) and did:web identifiers\n * @param did DID identifier we're seeking DID doc from\n * @returns Retrieved DID document\n */\nexport const getDidDocument = async (did) => {\n const colon_index = did.indexOf(':', 4);\n const type = did.slice(4, colon_index);\n const ident = did.slice(colon_index + 1);\n // 2. retrieve their DID documents\n let doc;\n if (type === 'plc') {\n const response = await fetch(`https://plc.directory/${did}`);\n if (response.status === 404) {\n throw new ResolverError(`did not found in directory`);\n }\n else if (!response.ok) {\n throw new ResolverError(`directory is unreachable`);\n }\n const json = await response.json();\n doc = json;\n }\n else if (type === 'web') {\n if (!DID_WEB_RE.test(ident)) {\n throw new ResolverError(`invalid identifier`);\n }\n const response = await fetch(`https://${ident}/.well-known/did.json`);\n if (!response.ok) {\n throw new ResolverError(`did document is unreachable`);\n }\n const json = await response.json();\n doc = json;\n }\n else {\n throw new ResolverError(`unsupported did method`);\n }\n return doc;\n};\n/**\n * Get OAuth protected resource metadata from a host\n * @param host URL of the host\n * @returns Retrieved protected resource metadata\n */\nexport const getProtectedResourceMetadata = async (host) => {\n const url = new URL(`/.well-known/oauth-protected-resource`, host);\n const response = await fetch(url, {\n redirect: 'manual',\n headers: {\n accept: 'application/json',\n },\n });\n if (response.status !== 200 || extractContentType(response.headers) !== 'application/json') {\n throw new ResolverError(`unexpected response`);\n }\n const metadata = (await response.json());\n if (metadata.resource !== url.origin) {\n throw new ResolverError(`unexpected issuer`);\n }\n return metadata;\n};\n/**\n * Get OAuth authorization server metadata from a host\n * @param host URL of the host\n * @returns Retrieved authorization server metadata\n */\nexport const getAuthorizationServerMetadata = async (host) => {\n const url = new URL(`/.well-known/oauth-authorization-server`, host);\n const response = await fetch(url, {\n redirect: 'manual',\n headers: {\n accept: 'application/json',\n },\n });\n if (response.status !== 200 || extractContentType(response.headers) !== 'application/json') {\n throw new ResolverError(`unexpected response`);\n }\n const metadata = (await response.json());\n if (metadata.issuer !== url.origin) {\n throw new ResolverError(`unexpected issuer`);\n }\n if (!isValidUrl(metadata.authorization_endpoint)) {\n throw new ResolverError(`authorization server provided incorrect authorization endpoint`);\n }\n if (!metadata.client_id_metadata_document_supported) {\n throw new ResolverError(`authorization server does not support 'client_id_metadata_document'`);\n }\n if (!metadata.pushed_authorization_request_endpoint) {\n throw new ResolverError(`authorization server does not support 'pushed_authorization request'`);\n }\n if (metadata.response_types_supported) {\n if (!metadata.response_types_supported.includes('code')) {\n throw new ResolverError(`authorization server does not support 'code' response type`);\n }\n }\n return metadata;\n};\n/**\n * Resolve handle domains or DID identifiers to get their PDS and its authorization server metadata\n * @param ident Handle domain or DID identifier to resolve\n * @returns Resolved PDS and authorization server metadata\n */\nexport const resolveFromIdentity = async (ident) => {\n let did;\n if (isDid(ident)) {\n did = ident;\n }\n else {\n const resolved = await resolveHandle(ident);\n did = resolved;\n }\n const doc = await getDidDocument(did);\n const pds = getPdsEndpoint(doc);\n if (!pds) {\n throw new ResolverError(`missing pds endpoint`);\n }\n return {\n identity: {\n id: did,\n raw: ident,\n pds: new URL(pds),\n },\n metadata: await getMetadataFromResourceServer(pds),\n };\n};\n/**\n * Request authorization server metadata from a PDS\n * @param host URL of the host\n * @returns Resolved authorization server metadata\n */\nexport const resolveFromService = async (host) => {\n try {\n const metadata = await getMetadataFromResourceServer(host);\n return { metadata };\n }\n catch (err) {\n if (err instanceof ResolverError) {\n try {\n const metadata = await getAuthorizationServerMetadata(host);\n return { metadata };\n }\n catch { }\n }\n throw err;\n }\n};\n/**\n * Request authorization server metadata from its protected resource metadata\n * @param input URL of the host whose authorization server is delegated\n * @returns Resolved authorization server metadata\n */\nexport const getMetadataFromResourceServer = async (input) => {\n const rs_metadata = await getProtectedResourceMetadata(input);\n if (rs_metadata.authorization_servers?.length !== 1) {\n throw new ResolverError(`expected exactly one authorization server in the listing`);\n }\n const issuer = rs_metadata.authorization_servers[0];\n const as_metadata = await getAuthorizationServerMetadata(issuer);\n if (as_metadata.protected_resources) {\n if (!as_metadata.protected_resources.includes(rs_metadata.resource)) {\n throw new ResolverError(`server is not in authorization server's jurisdiction`);\n }\n }\n return as_metadata;\n};\n//# sourceMappingURL=resolvers.js.map","import { fromBase64Url, toBase64Url } from '@atcute/multibase';\nimport { encodeUtf8 } from '@atcute/uint8array';\nimport { nanoid } from 'nanoid';\nimport { database } from './environment.js';\nimport { extractContentType } from './utils/response.js';\nimport { stringToSha256 } from './utils/runtime.js';\nconst ES256_ALG = { name: 'ECDSA', namedCurve: 'P-256' };\nexport const createES256Key = async () => {\n const pair = await crypto.subtle.generateKey(ES256_ALG, true, ['sign', 'verify']);\n const key = await crypto.subtle.exportKey('pkcs8', pair.privateKey);\n const { ext: _ext, key_ops: _key_opts, ...jwk } = await crypto.subtle.exportKey('jwk', pair.publicKey);\n return {\n typ: 'ES256',\n key: toBase64Url(new Uint8Array(key)),\n jwt: toBase64Url(encodeUtf8(JSON.stringify({ typ: 'dpop+jwt', alg: 'ES256', jwk: jwk }))),\n };\n};\nexport const createDPoPSignage = (dpopKey) => {\n const headerString = dpopKey.jwt;\n const keyPromise = crypto.subtle.importKey('pkcs8', fromBase64Url(dpopKey.key), ES256_ALG, true, ['sign']);\n const constructPayload = (htm, htu, nonce, ath) => {\n const payload = {\n ath: ath,\n htm: htm,\n htu: htu,\n iat: Math.floor(Date.now() / 1_000),\n jti: nanoid(24),\n nonce: nonce,\n };\n return toBase64Url(encodeUtf8(JSON.stringify(payload)));\n };\n return async (method, htu, nonce, ath) => {\n const payloadString = constructPayload(method, htu, nonce, ath);\n const signed = await crypto.subtle.sign({ name: 'ECDSA', hash: { name: 'SHA-256' } }, await keyPromise, encodeUtf8(headerString + '.' + payloadString));\n const signatureString = toBase64Url(new Uint8Array(signed));\n return headerString + '.' + payloadString + '.' + signatureString;\n };\n};\nexport const createDPoPFetch = (dpopKey, isAuthServer) => {\n const nonces = database.dpopNonces;\n const pending = database.inflightDpop;\n const sign = createDPoPSignage(dpopKey);\n return async (input, init) => {\n const request = new Request(input, init);\n const authorizationHeader = request.headers.get('authorization');\n const ath = authorizationHeader?.startsWith('DPoP ')\n ? await stringToSha256(authorizationHeader.slice(5))\n : undefined;\n const { method, url } = request;\n const { origin, pathname } = new URL(url);\n const htu = origin + pathname;\n // See if we have a pending promise for this origin, we'll await before\n // proceeding with this request, next comment describes what the promise\n // is meant to be.\n let deferred = pending.get(origin);\n if (deferred) {\n await deferred.promise;\n deferred = undefined;\n }\n // Get our persisted nonce value for this origin\n let initNonce;\n let expiredOrMissing = false;\n try {\n const [nonce, lapsed] = nonces.getWithLapsed(origin);\n initNonce = nonce;\n // The problem with DPoP nonces is that we don't have insight as to when\n // they'll expire, either we have a nonce value or we don't.\n //\n // Which is very unfortunate, if the client makes multiple requests at the\n // same time, there's a chance that all of them will fail due to the nonce\n // value having expired.\n //\n // To make this less painful, if it's been over 3 minutes since we last\n // had a nonce value, or we never had one to begin with, we'll let this\n // request through and defer everyone else until we get a possibly fresh\n // nonce value.\n //\n // 3 minutes being the DPoP nonce expiration time set by the reference PDS\n // implementation.\n expiredOrMissing = lapsed > 3 * 60 * 1_000;\n }\n catch {\n // Ignore read errors, we'll just act like we're missing a nonce.\n }\n if (expiredOrMissing) {\n // Defer everyone else until this request finishes.\n pending.set(origin, (deferred = Promise.withResolvers()));\n }\n let nextNonce;\n try {\n const initProof = await sign(method, htu, initNonce, ath);\n request.headers.set('dpop', initProof);\n const initResponse = await fetch(request);\n nextNonce = initResponse.headers.get('dpop-nonce');\n if (nextNonce === null || nextNonce === initNonce) {\n // No nonce was returned or it is the same as the one we sent. No need to\n // update the nonce store, or retry the request.\n return initResponse;\n }\n // Store the fresh nonce for future requests\n try {\n nonces.set(origin, nextNonce);\n }\n catch {\n // Ignore write errors\n }\n const shouldRetry = await isUseDpopNonceError(initResponse, isAuthServer);\n if (!shouldRetry) {\n // Not a \"use_dpop_nonce\" error, so there is no need to retry\n return initResponse;\n }\n if (input === request || init?.body instanceof ReadableStream) {\n // If the input stream was already consumed, we cannot retry the request. A\n // solution would be to clone() the request but that would bufferize the\n // entire stream in memory which can lead to memory starvation. Instead, we\n // will return the original response and let the calling code handle retries.\n return initResponse;\n }\n }\n finally {\n // Now everyone can have their turn.\n if (deferred) {\n pending.delete(origin);\n deferred.resolve();\n }\n }\n // We got here because we were asked to retry the request (due to missing\n // nonce value in the first request), let's do just that.\n {\n const nextProof = await sign(method, htu, nextNonce, ath);\n const nextRequest = new Request(input, init);\n nextRequest.headers.set('dpop', nextProof);\n const retryResponse = await fetch(nextRequest);\n // Check if the server returned another new nonce in the retry response\n const retryNonce = retryResponse.headers.get('dpop-nonce');\n if (retryNonce !== null && retryNonce !== nextNonce) {\n try {\n nonces.set(origin, retryNonce);\n }\n catch {\n // Ignore write errors\n }\n }\n return retryResponse;\n }\n };\n};\nconst isUseDpopNonceError = async (response, isAuthServer) => {\n // https://datatracker.ietf.org/doc/html/rfc6750#section-3\n // https://datatracker.ietf.org/doc/html/rfc9449#name-resource-server-provided-no\n if (isAuthServer === undefined || isAuthServer === false) {\n if (response.status === 401) {\n const wwwAuth = response.headers.get('www-authenticate');\n if (wwwAuth?.startsWith('DPoP')) {\n return wwwAuth.includes('error=\"use_dpop_nonce\"');\n }\n }\n }\n // https://datatracker.ietf.org/doc/html/rfc9449#name-authorization-server-provid\n if (isAuthServer === undefined || isAuthServer === true) {\n if (response.status === 400 && extractContentType(response.headers) === 'application/json') {\n try {\n const json = await response.clone().json();\n return typeof json === 'object' && json?.['error'] === 'use_dpop_nonce';\n }\n catch {\n // Response too big (to be \"use_dpop_nonce\" error) or invalid JSON\n return false;\n }\n }\n }\n return false;\n};\n//# sourceMappingURL=dpop.js.map","export const pick = (obj, keys) => {\n const cloned = {};\n for (let idx = 0, len = keys.length; idx < len; idx++) {\n const key = keys[idx];\n // @ts-expect-error\n cloned[key] = obj[key];\n }\n return cloned;\n};\n//# sourceMappingURL=misc.js.map","import { createDPoPFetch } from '../dpop.js';\nimport { CLIENT_ID, REDIRECT_URI } from '../environment.js';\nimport { FetchResponseError, OAuthResponseError, TokenRefreshError } from '../errors.js';\nimport { resolveFromIdentity } from '../resolvers.js';\nimport { pick } from '../utils/misc.js';\nimport { extractContentType } from '../utils/response.js';\nexport class OAuthServerAgent {\n #fetch;\n #metadata;\n constructor(metadata, dpopKey) {\n this.#metadata = metadata;\n this.#fetch = createDPoPFetch(dpopKey, true);\n }\n async request(endpoint, payload) {\n const url = this.#metadata[`${endpoint}_endpoint`];\n if (!url) {\n throw new Error(`no endpoint for ${endpoint}`);\n }\n const response = await this.#fetch(url, {\n method: 'post',\n headers: { 'content-type': 'application/json' },\n body: JSON.stringify({ ...payload, client_id: CLIENT_ID }),\n });\n if (extractContentType(response.headers) !== 'application/json') {\n throw new FetchResponseError(response, 2, `unexpected content-type`);\n }\n const json = await response.json();\n if (response.ok) {\n return json;\n }\n else {\n throw new OAuthResponseError(response, json);\n }\n }\n async revoke(token) {\n try {\n await this.request('revocation', { token: token });\n }\n catch { }\n }\n async exchangeCode(code, verifier) {\n const response = await this.request('token', {\n grant_type: 'authorization_code',\n redirect_uri: REDIRECT_URI,\n code: code,\n code_verifier: verifier,\n });\n try {\n return await this.#processExchangeResponse(response);\n }\n catch (err) {\n await this.revoke(response.access_token);\n throw err;\n }\n }\n async refresh({ sub, token }) {\n if (!token.refresh) {\n throw new TokenRefreshError(sub, 'no refresh token available');\n }\n const response = await this.request('token', {\n grant_type: 'refresh_token',\n refresh_token: token.refresh,\n });\n try {\n if (sub !== response.sub) {\n throw new TokenRefreshError(sub, `sub mismatch in token response; got ${response.sub}`);\n }\n return this.#processTokenResponse(response);\n }\n catch (err) {\n await this.revoke(response.access_token);\n throw err;\n }\n }\n #processTokenResponse(res) {\n if (!res.sub) {\n throw new TypeError(`missing sub field in token response`);\n }\n if (!res.scope) {\n throw new TypeError(`missing scope field in token response`);\n }\n if (res.token_type !== 'DPoP') {\n throw new TypeError(`token response returned a non-dpop token`);\n }\n return {\n scope: res.scope,\n refresh: res.refresh_token,\n access: res.access_token,\n type: res.token_type,\n expires_at: typeof res.expires_in === 'number' ? Date.now() + res.expires_in * 1_000 : undefined,\n };\n }\n async #processExchangeResponse(res) {\n const sub = res.sub;\n if (!sub) {\n throw new TypeError(`missing sub field in token response`);\n }\n const token = this.#processTokenResponse(res);\n const resolved = await resolveFromIdentity(sub);\n if (resolved.metadata.issuer !== this.#metadata.issuer) {\n throw new TypeError(`issuer mismatch; got ${resolved.metadata.issuer}`);\n }\n return {\n token: token,\n info: {\n sub: sub,\n aud: resolved.identity.pds.href,\n server: pick(resolved.metadata, [\n 'issuer',\n 'authorization_endpoint',\n 'introspection_endpoint',\n 'pushed_authorization_request_endpoint',\n 'revocation_endpoint',\n 'token_endpoint',\n ]),\n },\n };\n }\n}\n//# sourceMappingURL=server-agent.js.map","import { database } from '../environment.js';\nimport { OAuthResponseError, TokenRefreshError } from '../errors.js';\nimport { locks } from '../utils/runtime.js';\nimport { OAuthServerAgent } from './server-agent.js';\nconst pending = new Map();\nexport const getSession = async (sub, options) => {\n options?.signal?.throwIfAborted();\n let allowStored = isTokenUsable;\n if (options?.noCache) {\n allowStored = returnFalse;\n }\n else if (options?.allowStale) {\n allowStored = returnTrue;\n }\n // As long as concurrent requests are made for the same key, only one\n // request will be made to the cache & getter function at a time. This works\n // because there is no async operation between the while() loop and the\n // pending.set() call. Because of the \"single threaded\" nature of\n // JavaScript, the pending item will be set before the next iteration of the\n // while loop.\n let previousExecutionFlow;\n while ((previousExecutionFlow = pending.get(sub))) {\n try {\n const { isFresh, value } = await previousExecutionFlow;\n if (isFresh || allowStored(value)) {\n return value;\n }\n }\n catch {\n // Ignore errors from previous execution flows (they will have been\n // propagated by that flow).\n }\n options?.signal?.throwIfAborted();\n }\n const run = async () => {\n const storedSession = database.sessions.get(sub);\n if (storedSession && allowStored(storedSession)) {\n // Use the stored value as return value for the current execution\n // flow. Notify other concurrent execution flows (that should be\n // \"stuck\" in the loop before until this promise resolves) that we got\n // a value, but that it came from the store (isFresh = false).\n return { isFresh: false, value: storedSession };\n }\n const newSession = await refreshToken(sub, storedSession);\n await storeSession(sub, newSession);\n return { isFresh: true, value: newSession };\n };\n let promise;\n if (locks) {\n promise = locks.request(`atcute-oauth:${sub}`, run);\n }\n else {\n promise = run();\n }\n promise = promise.finally(() => pending.delete(sub));\n if (pending.has(sub)) {\n // This should never happen. Indeed, there must not be any 'await'\n // statement between this and the loop iteration check meaning that\n // this.pending.get returned undefined. It is there to catch bugs that\n // would occur in future changes to the code.\n throw new Error('concurrent request for the same key');\n }\n pending.set(sub, promise);\n const { value } = await promise;\n return value;\n};\nexport const storeSession = async (sub, newSession) => {\n try {\n database.sessions.set(sub, newSession);\n }\n catch (err) {\n await onRefreshError(newSession);\n throw err;\n }\n};\nexport const deleteStoredSession = (sub) => {\n database.sessions.delete(sub);\n};\nexport const listStoredSessions = () => {\n return database.sessions.keys();\n};\nconst returnTrue = () => true;\nconst returnFalse = () => false;\nconst refreshToken = async (sub, storedSession) => {\n if (storedSession === undefined) {\n throw new TokenRefreshError(sub, `session deleted by another tab`);\n }\n const { dpopKey, info, token } = storedSession;\n const server = new OAuthServerAgent(info.server, dpopKey);\n try {\n const newToken = await server.refresh({ sub: info.sub, token });\n return { dpopKey, info, token: newToken };\n }\n catch (cause) {\n if (cause instanceof OAuthResponseError && cause.status === 400 && cause.error === 'invalid_grant') {\n throw new TokenRefreshError(sub, `session was revoked`, { cause });\n }\n throw cause;\n }\n};\nconst onRefreshError = async ({ dpopKey, info, token }) => {\n // If the token data cannot be stored, let's revoke it\n const server = new OAuthServerAgent(info.server, dpopKey);\n await server.revoke(token.refresh ?? token.access);\n};\nconst isTokenUsable = ({ token }) => {\n const expires = token.expires_at;\n return expires == null || Date.now() + 60_000 <= expires;\n};\n//# sourceMappingURL=sessions.js.map","import { nanoid } from 'nanoid';\nimport { createES256Key } from '../dpop.js';\nimport { CLIENT_ID, database, REDIRECT_URI } from '../environment.js';\nimport { AuthorizationError, LoginError } from '../errors.js';\nimport { generatePKCE } from '../utils/runtime.js';\nimport { OAuthServerAgent } from './server-agent.js';\nimport { storeSession } from './sessions.js';\n/**\n * Create authentication URL for authorization\n * @param options\n * @returns URL to redirect the user for authorization\n */\nexport const createAuthorizationUrl = async ({ metadata, identity, scope, }) => {\n const state = nanoid(24);\n const pkce = await generatePKCE();\n const dpopKey = await createES256Key();\n const params = {\n redirect_uri: REDIRECT_URI,\n code_challenge: pkce.challenge,\n code_challenge_method: pkce.method,\n state: state,\n login_hint: identity?.raw,\n response_mode: 'fragment',\n response_type: 'code',\n display: 'page',\n // id_token_hint: undefined,\n // max_age: undefined,\n // prompt: undefined,\n scope: scope,\n // ui_locales: undefined,\n };\n database.states.set(state, {\n dpopKey: dpopKey,\n metadata: metadata,\n verifier: pkce.verifier,\n });\n const server = new OAuthServerAgent(metadata, dpopKey);\n const response = await server.request('pushed_authorization_request', params);\n const authUrl = new URL(metadata.authorization_endpoint);\n authUrl.searchParams.set('client_id', CLIENT_ID);\n authUrl.searchParams.set('request_uri', response.request_uri);\n return authUrl;\n};\n/**\n * Finalize authorization\n * @param params Search params\n * @returns Session object, which you can use to instantiate user agents\n */\nexport const finalizeAuthorization = async (params) => {\n const issuer = params.get('iss');\n const state = params.get('state');\n const code = params.get('code');\n const error = params.get('error');\n if (!state || !(code || error)) {\n throw new LoginError(`missing parameters`);\n }\n const stored = database.states.get(state);\n if (stored) {\n // Delete now that we've caught it\n database.states.delete(state);\n }\n else {\n throw new LoginError(`unknown state provided`);\n }\n const dpopKey = stored.dpopKey;\n const metadata = stored.metadata;\n if (error) {\n throw new AuthorizationError(params.get('error_description') || error);\n }\n if (!code) {\n throw new LoginError(`missing code parameter`);\n }\n if (issuer === null) {\n throw new LoginError(`missing issuer parameter`);\n }\n else if (issuer !== metadata.issuer) {\n throw new LoginError(`issuer mismatch`);\n }\n // Retrieve authentication tokens\n const server = new OAuthServerAgent(metadata, dpopKey);\n const { info, token } = await server.exchangeCode(code, stored.verifier);\n // We're finished!\n const sub = info.sub;\n const session = { dpopKey, info, token };\n await storeSession(sub, session);\n return session;\n};\n//# sourceMappingURL=exchange.js.map","import { createDPoPFetch } from '../dpop.js';\nimport { OAuthServerAgent } from './server-agent.js';\nimport { deleteStoredSession, getSession } from './sessions.js';\nexport class OAuthUserAgent {\n session;\n #fetch;\n #getSessionPromise;\n constructor(session) {\n this.session = session;\n this.#fetch = createDPoPFetch(session.dpopKey, false);\n }\n get sub() {\n return this.session.info.sub;\n }\n getSession(options) {\n const promise = getSession(this.session.info.sub, options);\n promise\n .then((session) => {\n this.session = session;\n })\n .finally(() => {\n this.#getSessionPromise = undefined;\n });\n return (this.#getSessionPromise = promise);\n }\n async signOut() {\n const sub = this.session.info.sub;\n try {\n const { dpopKey, info, token } = await getSession(sub, { allowStale: true });\n const server = new OAuthServerAgent(info.server, dpopKey);\n await server.revoke(token.refresh ?? token.access);\n }\n finally {\n deleteStoredSession(sub);\n }\n }\n async handle(pathname, init) {\n await this.#getSessionPromise;\n const headers = new Headers(init?.headers);\n let session = this.session;\n let url = new URL(pathname, session.info.aud);\n headers.set('authorization', `${session.token.type} ${session.token.access}`);\n let response = await this.#fetch(url, { ...init, headers });\n if (!isInvalidTokenResponse(response)) {\n return response;\n }\n try {\n if (this.#getSessionPromise) {\n session = await this.#getSessionPromise;\n }\n else {\n session = await this.getSession();\n }\n }\n catch {\n return response;\n }\n // Stream already consumed, can't retry.\n if (init?.body instanceof ReadableStream) {\n return response;\n }\n url = new URL(pathname, session.info.aud);\n headers.set('authorization', `${session.token.type} ${session.token.access}`);\n return await this.#fetch(url, { ...init, headers });\n }\n}\nconst isInvalidTokenResponse = (response) => {\n if (response.status !== 401) {\n return false;\n }\n const auth = response.headers.get('www-authenticate');\n return (auth != null &&\n (auth.startsWith('Bearer ') || auth.startsWith('DPoP ')) &&\n auth.includes('error=\"invalid_token\"'));\n};\n//# sourceMappingURL=user-agent.js.map","// Storage adapter that mimics browser.storage.local API but uses localStorage\n// This allows sharing code between extension and via-client\n\nexport const storage = {\n local: {\n async get(keys?: string | string[] | Record<string, any>): Promise<Record<string, any>> {\n if (!keys) {\n // Get all items\n const result: Record<string, any> = {};\n for (let i = 0; i < localStorage.length; i++) {\n const key = localStorage.key(i);\n if (key) {\n try {\n result[key] = JSON.parse(localStorage.getItem(key) || 'null');\n } catch {\n result[key] = localStorage.getItem(key);\n }\n }\n }\n return result;\n }\n\n const result: Record<string, any> = {};\n \n if (typeof keys === 'string') {\n // Single key\n try {\n const value = localStorage.getItem(keys);\n result[keys] = value ? JSON.parse(value) : null;\n } catch {\n result[keys] = localStorage.getItem(keys);\n }\n } else if (Array.isArray(keys)) {\n // Array of keys\n keys.forEach(key => {\n try {\n const value = localStorage.getItem(key);\n result[key] = value ? JSON.parse(value) : null;\n } catch {\n result[key] = localStorage.getItem(key);\n }\n });\n } else {\n // Object with default values\n Object.keys(keys).forEach(key => {\n try {\n const value = localStorage.getItem(key);\n result[key] = value ? JSON.parse(value) : keys[key];\n } catch {\n result[key] = localStorage.getItem(key) || keys[key];\n }\n });\n }\n \n return result;\n },\n\n async set(items: Record<string, any>): Promise<void> {\n Object.entries(items).forEach(([key, value]) => {\n localStorage.setItem(key, JSON.stringify(value));\n });\n },\n\n async remove(keys: string | string[]): Promise<void> {\n const keysArray = Array.isArray(keys) ? keys : [keys];\n keysArray.forEach(key => localStorage.removeItem(key));\n },\n\n async clear(): Promise<void> {\n localStorage.clear();\n },\n },\n};\n","// Web-compatible OAuth implementation (for via-client)\n// Adapted from lib/oauth.ts to work without browser.* APIs\n\nimport {\n configureOAuth,\n createAuthorizationUrl,\n finalizeAuthorization,\n resolveFromIdentity,\n OAuthUserAgent,\n type OAuthSession,\n} from \"@atcute/oauth-browser-client\";\nimport { storage } from \"./storage-adapter\";\n\nconst OAUTH_SESSION_KEY = \"synthesis-oauth:session\";\n\nlet isOAuthInitialized = false;\n\nexport function initializeOAuth() {\n if (typeof window !== \"undefined\" && !isOAuthInitialized) {\n // Use web redirect URL for via proxy\n configureOAuth({\n metadata: {\n client_id: import.meta.env.VITE_OAUTH_CLIENT_ID || 'http://localhost:8081/static/client-metadata.json',\n redirect_uri: import.meta.env.VITE_OAUTH_REDIRECT_URI || 'http://localhost:8081/static/oauth-callback.html',\n },\n });\n isOAuthInitialized = true;\n }\n}\n\nexport async function startLoginProcess(handle: string): Promise<void> {\n console.log('[oauth-web] Starting login process for handle:', handle);\n initializeOAuth();\n \n console.log('[oauth-web] Resolving identity...');\n const { metadata } = await resolveFromIdentity(handle);\n console.log('[oauth-web] PDS metadata:', metadata);\n \n console.log('[oauth-web] Creating authorization URL...');\n const authUrl = await createAuthorizationUrl({\n metadata: metadata,\n scope: import.meta.env.VITE_OAUTH_SCOPE || 'atproto transition:generic',\n });\n console.log('[oauth-web] Auth URL:', authUrl.toString());\n\n // For web context, redirect to auth URL\n window.location.href = authUrl.toString();\n}\n\nexport async function handleOAuthCallback(): Promise<OAuthSession | null> {\n console.log('[oauth-web] Handling OAuth callback');\n \n // Parse OAuth response from URL (params can be in search or hash)\n const url = new URL(window.location.href);\n const paramString = url.search || url.hash.slice(1);\n const params = new URLSearchParams(paramString);\n \n console.log('[oauth-web] OAuth params:', Object.fromEntries(params));\n\n if (!params.has('code') && !params.has('error')) {\n console.log('[oauth-web] No OAuth params found');\n return null;\n }\n\n if (params.has('error')) {\n const error = params.get('error');\n const errorDesc = params.get('error_description');\n console.error('[oauth-web] OAuth error:', error, errorDesc);\n throw new Error(`OAuth error: ${error} - ${errorDesc}`);\n }\n\n // Finalize authorization with the params\n console.log('[oauth-web] Finalizing authorization...');\n const session = await finalizeAuthorization(params);\n console.log('[oauth-web] Authorization complete, session:', session);\n\n // Store session\n await saveSession(session);\n console.log('[oauth-web] Session saved successfully');\n\n return session;\n}\n\nexport async function saveSession(session: OAuthSession): Promise<void> {\n await storage.local.set({ [OAUTH_SESSION_KEY]: session });\n}\n\nexport async function loadSession(): Promise<OAuthSession | null> {\n const result = await storage.local.get(OAUTH_SESSION_KEY);\n return result[OAUTH_SESSION_KEY] || null;\n}\n\nexport async function clearSession(): Promise<void> {\n await storage.local.remove(OAUTH_SESSION_KEY);\n}\n\nexport async function getProfile(session: OAuthSession): Promise<any> {\n const agent = new OAuthUserAgent(session);\n const response = await agent.handle('/xrpc/app.bsky.actor.getProfile?actor=' + session.info.sub);\n return await response.json();\n}\n"],"names":["urlAlphabet","nanoid","size","id","bytes","scopedUrlAlphabet","textEncoder","subtle","alloc","allocUnsafe","encodeUtf8","str","toSha256","buffer","createRfc4648Encode","alphabet","bitsPerChar","pad","mask","bits","i","createRfc4648Decode","codes","end","written","value","fromBase64Url","toBase64Url","BASE64URL_CHARSET","HAS_NATIVE_SUPPORT","fromBase64UrlNative","fromBase64UrlPolyfill","toBase64UrlNative","toBase64UrlPolyfill","locks","stringToSha256","input","digest","generatePKCE","verifier","parse","raw","parsed","createOAuthDatabase","name","controller","signal","createStore","subname","expiresAt","persistUpdatedAt","store","storageKey","persist","read","listener","ev","cleanup","lock","resolve","now","changed","key","item","updatedAt","token","_item","CLIENT_ID","REDIRECT_URI","database","configureOAuth","options","LoginError","__publicField","AuthorizationError","ResolverError","TokenRefreshError","sub","message","OAuthResponseError","response","data","_a","_b","error","ifString","ifObject","errorDescription","messageError","messageDesc","FetchResponseError","status","v","DID_RE","isDid","isUrlParseSupported","isAtprotoServiceEndpoint","url","getAtprotoServiceEndpoint","doc","predicate","services","idx","len","type","serviceEndpoint","getPdsEndpoint","DEFAULT_APPVIEW_URL","extractContentType","headers","isValidUrl","urlString","DID_WEB_RE","resolveHandle","handle","getDidDocument","did","colon_index","ident","getProtectedResourceMetadata","host","metadata","getAuthorizationServerMetadata","resolveFromIdentity","pds","getMetadataFromResourceServer","rs_metadata","issuer","as_metadata","ES256_ALG","createES256Key","pair","_ext","_key_opts","jwk","createDPoPSignage","dpopKey","headerString","keyPromise","constructPayload","htm","htu","nonce","ath","payload","method","payloadString","signed","signatureString","createDPoPFetch","isAuthServer","nonces","pending","sign","init","request","authorizationHeader","origin","pathname","deferred","initNonce","expiredOrMissing","lapsed","nextNonce","initProof","initResponse","isUseDpopNonceError","nextProof","nextRequest","retryResponse","retryNonce","wwwAuth","json","pick","obj","keys","cloned","_fetch","_metadata","_OAuthServerAgent_instances","processTokenResponse_fn","processExchangeResponse_fn","OAuthServerAgent","__privateAdd","__privateSet","endpoint","__privateGet","code","__privateMethod","err","res","resolved","getSession","allowStored","isTokenUsable","returnFalse","returnTrue","previousExecutionFlow","isFresh","run","storedSession","newSession","refreshToken","storeSession","promise","onRefreshError","deleteStoredSession","info","server","newToken","cause","expires","createAuthorizationUrl","identity","scope","state","pkce","params","authUrl","finalizeAuthorization","stored","session","_getSessionPromise","OAuthUserAgent","isInvalidTokenResponse","auth","storage","result","items","OAUTH_SESSION_KEY","isOAuthInitialized","initializeOAuth","startLoginProcess","handleOAuthCallback","paramString","errorDesc","saveSession","loadSession","clearSession","getProfile"],"mappings":"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","x_google_ignoreList":[0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22]}
-2
pywb-test/static/assets/oauth-web-D0e6TxJF.js
··· 1 - var ue=Object.defineProperty;var G=t=>{throw TypeError(t)};var de=(t,e,r)=>e in t?ue(t,e,{enumerable:!0,configurable:!0,writable:!0,value:r}):t[e]=r;var f=(t,e,r)=>de(t,typeof e!="symbol"?e+"":e,r),H=(t,e,r)=>e.has(t)||G("Cannot "+r);var m=(t,e,r)=>(H(t,e,"read from private field"),r?r.call(t):e.get(t)),E=(t,e,r)=>e.has(t)?G("Cannot add the same private member more than once"):e instanceof WeakSet?e.add(t):e.set(t,r),k=(t,e,r,s)=>(H(t,e,"write to private field"),s?s.call(t,r):e.set(t,r),r),L=(t,e,r)=>(H(t,e,"access private method"),r);const he="useandom-26T198340PX75pxJACKVERYMINDBUSHWOLF_GQZbfghjklqvwyzrict";let W=(t=21)=>{let e="",r=crypto.getRandomValues(new Uint8Array(t|=0));for(;t--;)e+=he[r[t]&63];return e};const pe=new TextEncoder;new TextDecoder;const fe=crypto.subtle,we=t=>new Uint8Array(t),ge=we,$=t=>pe.encode(t),ye=async t=>new Uint8Array(await fe.digest("SHA-256",t)),me=(t,e,r)=>s=>{const n=(1<<e)-1;let o="",a=0,i=0;for(let c=0;c<s.length;++c)for(i=i<<8|s[c],a+=8;a>e;)a-=e,o+=t[n&i>>a];if(a!==0&&(o+=t[n&i<<e-a]),r)for(;o.length*e&7;)o+="=";return o},_e=(t,e,r)=>{const s={};for(let n=0;n<t.length;++n)s[t[n]]=n;return n=>{let o=n.length;for(;r&&n[o-1]==="=";)--o;const a=ge(o*e/8|0);let i=0,c=0,l=0;for(let u=0;u<o;++u){const h=s[n[u]];if(h===void 0)throw new SyntaxError("invalid base string");c=c<<e|h,i+=e,i>=8&&(i-=8,a[l++]=255&c>>i)}if(i>=e||255&c<<8-i)throw new SyntaxError("unexpected end of data");return a}},ve=t=>Uint8Array.fromBase64(t,{alphabet:"base64url",lastChunkHandling:"loose"}),Se=t=>t.toBase64({alphabet:"base64url",omitPadding:!0}),te="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_",be=_e(te,6,!1),Ae=me(te,6,!1),re="fromBase64"in Uint8Array,Ee=re?ve:be,z=re?Se:Ae,N=typeof navigator<"u"?navigator.locks:void 0,se=async t=>{const e=$(t),r=await ye(e);return z(r)},ke=async()=>{const t=W(64);return{verifier:t,challenge:await se(t),method:"S256"}},Re=t=>{if(t!=null){const e=JSON.parse(t);if(e!=null)return e}return{}},xe=({name:t})=>{const e=new AbortController,r=e.signal,s=(n,o,a=!1)=>{let i;const c=`${t}:${n}`,l=()=>i&&localStorage.setItem(c,JSON.stringify(i)),u=()=>{if(r.aborted)throw new Error("store closed");return i??(i=Re(localStorage.getItem(c)))};{const h=d=>{d.key===c&&(i=void 0)};globalThis.addEventListener("storage",h,{signal:r})}{const h=async d=>{if(!d||r.aborted||(await new Promise(w=>setTimeout(w,1e4)),r.aborted))return;let g=Date.now(),v=!1;u();for(const w in i){const U=i[w].expiresAt;U!==null&&g>U&&(v=!0,delete i[w])}v&&l()};N?N.request(`${c}:cleanup`,{ifAvailable:!0},h):h(!0)}return{get(h){u();const d=i[h];if(!d)return;const g=d.expiresAt;if(g!==null&&Date.now()>g){delete i[h],l();return}return d.value},getWithLapsed(h){u();const d=i[h],g=Date.now();if(!d)return[void 0,1/0];const v=d.updatedAt;return v===void 0?[d.value,1/0]:[d.value,g-v]},set(h,d){u();const g={value:d,expiresAt:o(d),updatedAt:a?Date.now():void 0};i[h]=g,l()},delete(h){u(),i[h]!==void 0&&(delete i[h],l())},keys(){return u(),Object.keys(i)}}};return{dispose:()=>{e.abort()},sessions:s("sessions",({token:n})=>n.refresh?null:n.expires_at??null),states:s("states",n=>Date.now()+10*60*1e3),dpopNonces:s("dpopNonces",n=>Date.now()+24*60*60*1e3,!0),inflightDpop:new Map}};let M,Z,_;const Ue=t=>{({client_id:M,redirect_uri:Z}=t.metadata),_=xe({name:t.storageName??"atcute-oauth"})};class D extends Error{constructor(){super(...arguments);f(this,"name","LoginError")}}class je extends Error{constructor(){super(...arguments);f(this,"name","AuthorizationError")}}class p extends Error{constructor(){super(...arguments);f(this,"name","ResolverError")}}class q extends Error{constructor(r,s,n){super(s,n);f(this,"sub");f(this,"name","TokenRefreshError");this.sub=r}}class ne extends Error{constructor(r,s){var l,u;const n=Y((l=Q(s))==null?void 0:l.error),o=Y((u=Q(s))==null?void 0:u.error_description),a=n?`"${n}"`:"unknown",i=o?`: ${o}`:"",c=`OAuth ${a} error${i}`;super(c);f(this,"response");f(this,"data");f(this,"name","OAuthResponseError");f(this,"error");f(this,"description");this.response=r,this.data=s,this.error=n,this.description=o}get status(){return this.response.status}get headers(){return this.response.headers}}class De extends Error{constructor(r,s,n){super(n);f(this,"response");f(this,"status");f(this,"name","FetchResponseError");this.response=r,this.status=s}}const Y=t=>typeof t=="string"?t:void 0,Q=t=>typeof t=="object"&&t!==null&&!Array.isArray(t)?t:void 0,ze=/^did:([a-z]+):([a-zA-Z0-9._:%\-]*[a-zA-Z0-9._\-])$/,Oe=t=>typeof t=="string"&&t.length>=7&&t.length<=2048&&ze.test(t),Pe="parse"in URL,Ie=t=>{let e=null;if(Pe)e=URL.parse(t);else try{e=new URL(t)}catch{}return e!==null&&(e.protocol==="https:"||e.protocol==="http:")&&e.pathname==="/"&&e.search===""&&e.hash===""},Le=(t,e)=>{const r=t.service;if(r)for(let s=0,n=r.length;s<n;s++){const{id:o,type:a,serviceEndpoint:i}=r[s];if(!(o!==e.id&&o!==t.id+e.id)){if(e.type!==void 0){if(Array.isArray(a)){if(!a.includes(e.type))continue}else if(a!==e.type)continue}if(!(typeof i!="string"||!Ie(i)))return i}}},Te=t=>Le(t,{id:"#atproto_pds",type:"AtprotoPersonalDataServer"}),$e="https://public.api.bsky.app",B=t=>{var e;return(e=t.get("content-type"))==null?void 0:e.split(";")[0]},Ne="parse"in URL,qe=t=>{let e=null;if(Ne)e=URL.parse(t);else try{e=new URL(t)}catch{}return e!==null?e.protocol==="https:"||e.protocol==="http:":!1},Ke=/^([a-zA-Z0-9-]+(?:\.[a-zA-Z0-9-]+)*(?:\.[a-zA-Z]{2,}))$/,Be=async t=>{const e=$e+`/xrpc/com.atproto.identity.resolveHandle?handle=${t}`,r=await fetch(e);if(r.status===400)throw new p("domain handle not found");if(!r.ok)throw new p("directory is unreachable");return(await r.json()).did},Fe=async t=>{const e=t.indexOf(":",4),r=t.slice(4,e),s=t.slice(e+1);let n;if(r==="plc"){const o=await fetch(`https://plc.directory/${t}`);if(o.status===404)throw new p("did not found in directory");if(!o.ok)throw new p("directory is unreachable");n=await o.json()}else if(r==="web"){if(!Ke.test(s))throw new p("invalid identifier");const o=await fetch(`https://${s}/.well-known/did.json`);if(!o.ok)throw new p("did document is unreachable");n=await o.json()}else throw new p("unsupported did method");return n},Je=async t=>{const e=new URL("/.well-known/oauth-protected-resource",t),r=await fetch(e,{redirect:"manual",headers:{accept:"application/json"}});if(r.status!==200||B(r.headers)!=="application/json")throw new p("unexpected response");const s=await r.json();if(s.resource!==e.origin)throw new p("unexpected issuer");return s},He=async t=>{const e=new URL("/.well-known/oauth-authorization-server",t),r=await fetch(e,{redirect:"manual",headers:{accept:"application/json"}});if(r.status!==200||B(r.headers)!=="application/json")throw new p("unexpected response");const s=await r.json();if(s.issuer!==e.origin)throw new p("unexpected issuer");if(!qe(s.authorization_endpoint))throw new p("authorization server provided incorrect authorization endpoint");if(!s.client_id_metadata_document_supported)throw new p("authorization server does not support 'client_id_metadata_document'");if(!s.pushed_authorization_request_endpoint)throw new p("authorization server does not support 'pushed_authorization request'");if(s.response_types_supported&&!s.response_types_supported.includes("code"))throw new p("authorization server does not support 'code' response type");return s},oe=async t=>{let e;Oe(t)?e=t:e=await Be(t);const r=await Fe(e),s=Te(r);if(!s)throw new p("missing pds endpoint");return{identity:{id:e,raw:t,pds:new URL(s)},metadata:await Ce(s)}},Ce=async t=>{var n;const e=await Je(t);if(((n=e.authorization_servers)==null?void 0:n.length)!==1)throw new p("expected exactly one authorization server in the listing");const r=e.authorization_servers[0],s=await He(r);if(s.protected_resources&&!s.protected_resources.includes(e.resource))throw new p("server is not in authorization server's jurisdiction");return s},ae={name:"ECDSA",namedCurve:"P-256"},We=async()=>{const t=await crypto.subtle.generateKey(ae,!0,["sign","verify"]),e=await crypto.subtle.exportKey("pkcs8",t.privateKey),{ext:r,key_ops:s,...n}=await crypto.subtle.exportKey("jwk",t.publicKey);return{typ:"ES256",key:z(new Uint8Array(e)),jwt:z($(JSON.stringify({typ:"dpop+jwt",alg:"ES256",jwk:n})))}},Me=t=>{const e=t.jwt,r=crypto.subtle.importKey("pkcs8",Ee(t.key),ae,!0,["sign"]),s=(n,o,a,i)=>{const c={ath:i,htm:n,htu:o,iat:Math.floor(Date.now()/1e3),jti:W(24),nonce:a};return z($(JSON.stringify(c)))};return async(n,o,a,i)=>{const c=s(n,o,a,i),l=await crypto.subtle.sign({name:"ECDSA",hash:{name:"SHA-256"}},await r,$(e+"."+c)),u=z(new Uint8Array(l));return e+"."+c+"."+u}},ie=(t,e)=>{const r=_.dpopNonces,s=_.inflightDpop,n=Me(t);return async(o,a)=>{const i=new Request(o,a),c=i.headers.get("authorization"),l=c!=null&&c.startsWith("DPoP ")?await se(c.slice(5)):void 0,{method:u,url:h}=i,{origin:d,pathname:g}=new URL(h),v=d+g;let w=s.get(d);w&&(await w.promise,w=void 0);let I,U=!1;try{const[j,y]=r.getWithLapsed(d);I=j,U=y>3*60*1e3}catch{}U&&s.set(d,w=Promise.withResolvers());let A;try{const j=await n(u,v,I,l);i.headers.set("dpop",j);const y=await fetch(i);if(A=y.headers.get("dpop-nonce"),A===null||A===I)return y;try{r.set(d,A)}catch{}if(!await Ze(y,e)||o===i||(a==null?void 0:a.body)instanceof ReadableStream)return y}finally{w&&(s.delete(d),w.resolve())}{const j=await n(u,v,A,l),y=new Request(o,a);y.headers.set("dpop",j);const F=await fetch(y),J=F.headers.get("dpop-nonce");if(J!==null&&J!==A)try{r.set(d,J)}catch{}return F}}},Ze=async(t,e)=>{if((e===void 0||e===!1)&&t.status===401){const r=t.headers.get("www-authenticate");if(r!=null&&r.startsWith("DPoP"))return r.includes('error="use_dpop_nonce"')}if((e===void 0||e===!0)&&t.status===400&&B(t.headers)==="application/json")try{const r=await t.clone().json();return typeof r=="object"&&(r==null?void 0:r.error)==="use_dpop_nonce"}catch{return!1}return!1},Ve=(t,e)=>{const r={};for(let s=0,n=e.length;s<n;s++){const o=e[s];r[o]=t[o]}return r};var O,R,b,C,ce;class P{constructor(e,r){E(this,b);E(this,O);E(this,R);k(this,R,e),k(this,O,ie(r,!0))}async request(e,r){const s=m(this,R)[`${e}_endpoint`];if(!s)throw new Error(`no endpoint for ${e}`);const n=await m(this,O).call(this,s,{method:"post",headers:{"content-type":"application/json"},body:JSON.stringify({...r,client_id:M})});if(B(n.headers)!=="application/json")throw new De(n,2,"unexpected content-type");const o=await n.json();if(n.ok)return o;throw new ne(n,o)}async revoke(e){try{await this.request("revocation",{token:e})}catch{}}async exchangeCode(e,r){const s=await this.request("token",{grant_type:"authorization_code",redirect_uri:Z,code:e,code_verifier:r});try{return await L(this,b,ce).call(this,s)}catch(n){throw await this.revoke(s.access_token),n}}async refresh({sub:e,token:r}){if(!r.refresh)throw new q(e,"no refresh token available");const s=await this.request("token",{grant_type:"refresh_token",refresh_token:r.refresh});try{if(e!==s.sub)throw new q(e,`sub mismatch in token response; got ${s.sub}`);return L(this,b,C).call(this,s)}catch(n){throw await this.revoke(s.access_token),n}}}O=new WeakMap,R=new WeakMap,b=new WeakSet,C=function(e){if(!e.sub)throw new TypeError("missing sub field in token response");if(!e.scope)throw new TypeError("missing scope field in token response");if(e.token_type!=="DPoP")throw new TypeError("token response returned a non-dpop token");return{scope:e.scope,refresh:e.refresh_token,access:e.access_token,type:e.token_type,expires_at:typeof e.expires_in=="number"?Date.now()+e.expires_in*1e3:void 0}},ce=async function(e){const r=e.sub;if(!r)throw new TypeError("missing sub field in token response");const s=L(this,b,C).call(this,e),n=await oe(r);if(n.metadata.issuer!==m(this,R).issuer)throw new TypeError(`issuer mismatch; got ${n.metadata.issuer}`);return{token:s,info:{sub:r,aud:n.identity.pds.href,server:Ve(n.metadata,["issuer","authorization_endpoint","introspection_endpoint","pushed_authorization_request_endpoint","revocation_endpoint","token_endpoint"])}}};const T=new Map,X=async(t,e)=>{var i,c;(i=e==null?void 0:e.signal)==null||i.throwIfAborted();let r=tt;e!=null&&e.noCache?r=Qe:e!=null&&e.allowStale&&(r=Ye);let s;for(;s=T.get(t);){try{const{isFresh:l,value:u}=await s;if(l||r(u))return u}catch{}(c=e==null?void 0:e.signal)==null||c.throwIfAborted()}const n=async()=>{const l=_.sessions.get(t);if(l&&r(l))return{isFresh:!1,value:l};const u=await Xe(t,l);return await le(t,u),{isFresh:!0,value:u}};let o;if(N?o=N.request(`atcute-oauth:${t}`,n):o=n(),o=o.finally(()=>T.delete(t)),T.has(t))throw new Error("concurrent request for the same key");T.set(t,o);const{value:a}=await o;return a},le=async(t,e)=>{try{_.sessions.set(t,e)}catch(r){throw await et(e),r}},Ge=t=>{_.sessions.delete(t)},Ye=()=>!0,Qe=()=>!1,Xe=async(t,e)=>{if(e===void 0)throw new q(t,"session deleted by another tab");const{dpopKey:r,info:s,token:n}=e,o=new P(s.server,r);try{const a=await o.refresh({sub:s.sub,token:n});return{dpopKey:r,info:s,token:a}}catch(a){throw a instanceof ne&&a.status===400&&a.error==="invalid_grant"?new q(t,"session was revoked",{cause:a}):a}},et=async({dpopKey:t,info:e,token:r})=>{await new P(e.server,t).revoke(r.refresh??r.access)},tt=({token:t})=>{const e=t.expires_at;return e==null||Date.now()+6e4<=e},rt=async({metadata:t,identity:e,scope:r})=>{const s=W(24),n=await ke(),o=await We(),a={redirect_uri:Z,code_challenge:n.challenge,code_challenge_method:n.method,state:s,login_hint:e==null?void 0:e.raw,response_mode:"fragment",response_type:"code",display:"page",scope:r};_.states.set(s,{dpopKey:o,metadata:t,verifier:n.verifier});const c=await new P(t,o).request("pushed_authorization_request",a),l=new URL(t.authorization_endpoint);return l.searchParams.set("client_id",M),l.searchParams.set("request_uri",c.request_uri),l},st=async t=>{const e=t.get("iss"),r=t.get("state"),s=t.get("code"),n=t.get("error");if(!r||!(s||n))throw new D("missing parameters");const o=_.states.get(r);if(o)_.states.delete(r);else throw new D("unknown state provided");const a=o.dpopKey,i=o.metadata;if(n)throw new je(t.get("error_description")||n);if(!s)throw new D("missing code parameter");if(e===null)throw new D("missing issuer parameter");if(e!==i.issuer)throw new D("issuer mismatch");const c=new P(i,a),{info:l,token:u}=await c.exchangeCode(s,o.verifier),h=l.sub,d={dpopKey:a,info:l,token:u};return await le(h,d),d};var x,S;class nt{constructor(e){f(this,"session");E(this,x);E(this,S);this.session=e,k(this,x,ie(e.dpopKey,!1))}get sub(){return this.session.info.sub}getSession(e){const r=X(this.session.info.sub,e);return r.then(s=>{this.session=s}).finally(()=>{k(this,S,void 0)}),k(this,S,r)}async signOut(){const e=this.session.info.sub;try{const{dpopKey:r,info:s,token:n}=await X(e,{allowStale:!0});await new P(s.server,r).revoke(n.refresh??n.access)}finally{Ge(e)}}async handle(e,r){await m(this,S);const s=new Headers(r==null?void 0:r.headers);let n=this.session,o=new URL(e,n.info.aud);s.set("authorization",`${n.token.type} ${n.token.access}`);let a=await m(this,x).call(this,o,{...r,headers:s});if(!ot(a))return a;try{m(this,S)?n=await m(this,S):n=await this.getSession()}catch{return a}return(r==null?void 0:r.body)instanceof ReadableStream?a:(o=new URL(e,n.info.aud),s.set("authorization",`${n.token.type} ${n.token.access}`),await m(this,x).call(this,o,{...r,headers:s}))}}x=new WeakMap,S=new WeakMap;const ot=t=>{if(t.status!==401)return!1;const e=t.headers.get("www-authenticate");return e!=null&&(e.startsWith("Bearer ")||e.startsWith("DPoP "))&&e.includes('error="invalid_token"')},V={local:{async get(t){if(!t){const r={};for(let s=0;s<localStorage.length;s++){const n=localStorage.key(s);if(n)try{r[n]=JSON.parse(localStorage.getItem(n)||"null")}catch{r[n]=localStorage.getItem(n)}}return r}const e={};if(typeof t=="string")try{const r=localStorage.getItem(t);e[t]=r?JSON.parse(r):null}catch{e[t]=localStorage.getItem(t)}else Array.isArray(t)?t.forEach(r=>{try{const s=localStorage.getItem(r);e[r]=s?JSON.parse(s):null}catch{e[r]=localStorage.getItem(r)}}):Object.keys(t).forEach(r=>{try{const s=localStorage.getItem(r);e[r]=s?JSON.parse(s):t[r]}catch{e[r]=localStorage.getItem(r)||t[r]}});return e},async set(t){Object.entries(t).forEach(([e,r])=>{localStorage.setItem(e,JSON.stringify(r))})},async remove(t){(Array.isArray(t)?t:[t]).forEach(r=>localStorage.removeItem(r))},async clear(){localStorage.clear()}}},K="synthesis-oauth:session";let ee=!1;function at(){typeof window<"u"&&!ee&&(Ue({metadata:{client_id:"http://localhost:8081/static/client-metadata.json",redirect_uri:"http://localhost:8081/static/oauth-callback.html"}}),ee=!0)}async function lt(t){console.log("[oauth-web] Starting login process for handle:",t),at(),console.log("[oauth-web] Resolving identity...");const{metadata:e}=await oe(t);console.log("[oauth-web] PDS metadata:",e),console.log("[oauth-web] Creating authorization URL...");const r=await rt({metadata:e,scope:"atproto transition:generic"});console.log("[oauth-web] Auth URL:",r.toString()),window.location.href=r.toString()}async function ut(){console.log("[oauth-web] Handling OAuth callback");const t=new URL(window.location.href),e=t.search||t.hash.slice(1),r=new URLSearchParams(e);if(console.log("[oauth-web] OAuth params:",Object.fromEntries(r)),!r.has("code")&&!r.has("error"))return console.log("[oauth-web] No OAuth params found"),null;if(r.has("error")){const n=r.get("error"),o=r.get("error_description");throw console.error("[oauth-web] OAuth error:",n,o),new Error(`OAuth error: ${n} - ${o}`)}console.log("[oauth-web] Finalizing authorization...");const s=await st(r);return console.log("[oauth-web] Authorization complete, session:",s),await it(s),console.log("[oauth-web] Session saved successfully"),s}async function it(t){await V.local.set({[K]:t})}async function dt(){return(await V.local.get(K))[K]||null}async function ht(){await V.local.remove(K)}async function pt(t){return await(await new nt(t).handle("/xrpc/app.bsky.actor.getProfile?actor="+t.info.sub)).json()}export{lt as a,ht as c,pt as g,ut as h,at as i,dt as l,V as s}; 2 - //# sourceMappingURL=oauth-web-D0e6TxJF.js.map
-1
pywb-test/static/assets/oauth-web-D0e6TxJF.js.map
··· 1 - {"version":3,"file":"oauth-web-D0e6TxJF.js","sources":["../../../node_modules/.pnpm/nanoid@5.1.6/node_modules/nanoid/url-alphabet/index.js","../../../node_modules/.pnpm/nanoid@5.1.6/node_modules/nanoid/index.browser.js","../../../node_modules/.pnpm/@atcute+uint8array@1.0.5/node_modules/@atcute/uint8array/dist/index.js","../../../node_modules/.pnpm/@atcute+multibase@1.1.6/node_modules/@atcute/multibase/dist/utils.js","../../../node_modules/.pnpm/@atcute+multibase@1.1.6/node_modules/@atcute/multibase/dist/bases/base64-web-native.js","../../../node_modules/.pnpm/@atcute+multibase@1.1.6/node_modules/@atcute/multibase/dist/bases/base64-web-polyfill.js","../../../node_modules/.pnpm/@atcute+multibase@1.1.6/node_modules/@atcute/multibase/dist/bases/base64-web.js","../../../node_modules/.pnpm/@atcute+oauth-browser-client@1.0.27/node_modules/@atcute/oauth-browser-client/dist/utils/runtime.js","../../../node_modules/.pnpm/@atcute+oauth-browser-client@1.0.27/node_modules/@atcute/oauth-browser-client/dist/store/db.js","../../../node_modules/.pnpm/@atcute+oauth-browser-client@1.0.27/node_modules/@atcute/oauth-browser-client/dist/environment.js","../../../node_modules/.pnpm/@atcute+oauth-browser-client@1.0.27/node_modules/@atcute/oauth-browser-client/dist/errors.js","../../../node_modules/.pnpm/@atcute+lexicons@1.2.2/node_modules/@atcute/lexicons/dist/syntax/did.js","../../../node_modules/.pnpm/@atcute+identity@1.1.1/node_modules/@atcute/identity/dist/utils.js","../../../node_modules/.pnpm/@atcute+oauth-browser-client@1.0.27/node_modules/@atcute/oauth-browser-client/dist/constants.js","../../../node_modules/.pnpm/@atcute+oauth-browser-client@1.0.27/node_modules/@atcute/oauth-browser-client/dist/utils/response.js","../../../node_modules/.pnpm/@atcute+oauth-browser-client@1.0.27/node_modules/@atcute/oauth-browser-client/dist/utils/strings.js","../../../node_modules/.pnpm/@atcute+oauth-browser-client@1.0.27/node_modules/@atcute/oauth-browser-client/dist/resolvers.js","../../../node_modules/.pnpm/@atcute+oauth-browser-client@1.0.27/node_modules/@atcute/oauth-browser-client/dist/dpop.js","../../../node_modules/.pnpm/@atcute+oauth-browser-client@1.0.27/node_modules/@atcute/oauth-browser-client/dist/utils/misc.js","../../../node_modules/.pnpm/@atcute+oauth-browser-client@1.0.27/node_modules/@atcute/oauth-browser-client/dist/agents/server-agent.js","../../../node_modules/.pnpm/@atcute+oauth-browser-client@1.0.27/node_modules/@atcute/oauth-browser-client/dist/agents/sessions.js","../../../node_modules/.pnpm/@atcute+oauth-browser-client@1.0.27/node_modules/@atcute/oauth-browser-client/dist/agents/exchange.js","../../../node_modules/.pnpm/@atcute+oauth-browser-client@1.0.27/node_modules/@atcute/oauth-browser-client/dist/agents/user-agent.js","../../../lib/storage-adapter.ts","../../../lib/oauth-web.ts"],"sourcesContent":["export const urlAlphabet =\n 'useandom-26T198340PX75pxJACKVERYMINDBUSHWOLF_GQZbfghjklqvwyzrict'\n","/* @ts-self-types=\"./index.d.ts\" */\nimport { urlAlphabet as scopedUrlAlphabet } from './url-alphabet/index.js'\nexport { urlAlphabet } from './url-alphabet/index.js'\nexport let random = bytes => crypto.getRandomValues(new Uint8Array(bytes))\nexport let customRandom = (alphabet, defaultSize, getRandom) => {\n let mask = (2 << Math.log2(alphabet.length - 1)) - 1\n let step = -~((1.6 * mask * defaultSize) / alphabet.length)\n return (size = defaultSize) => {\n let id = ''\n while (true) {\n let bytes = getRandom(step)\n let j = step | 0\n while (j--) {\n id += alphabet[bytes[j] & mask] || ''\n if (id.length >= size) return id\n }\n }\n }\n}\nexport let customAlphabet = (alphabet, size = 21) =>\n customRandom(alphabet, size | 0, random)\nexport let nanoid = (size = 21) => {\n let id = ''\n let bytes = crypto.getRandomValues(new Uint8Array((size |= 0)))\n while (size--) {\n id += scopedUrlAlphabet[bytes[size] & 63]\n }\n return id\n}\n","const textEncoder = new TextEncoder();\nconst textDecoder = new TextDecoder();\nconst subtle = crypto.subtle;\n/**\n * creates an Uint8Array of the requested size, with the contents zeroed\n */\nexport const alloc = (size) => {\n return new Uint8Array(size);\n};\n/**\n * creates an Uint8Array of the requested size, where the contents may not be\n * zeroed out. only use if you're certain that the contents will be overwritten\n */\nexport const allocUnsafe = alloc;\n/**\n * compares two Uint8Array buffers\n */\nexport const compare = (a, b) => {\n const alen = a.length;\n const blen = b.length;\n if (alen > blen) {\n return 1;\n }\n if (alen < blen) {\n return -1;\n }\n for (let i = 0; i < alen; i++) {\n const ax = a[i];\n const bx = b[i];\n if (ax < bx) {\n return -1;\n }\n if (ax > bx) {\n return 1;\n }\n }\n return 0;\n};\n/**\n * checks if the two Uint8Array buffers are equal\n */\nexport const equals = (a, b) => {\n if (a === b) {\n return true;\n }\n let len;\n if ((len = a.length) === b.length) {\n while (len--) {\n if (a[len] !== b[len]) {\n return false;\n }\n }\n }\n return len === -1;\n};\n/**\n * checks if the two Uint8Array buffers are equal, timing-safe version\n */\nexport const timingSafeEquals = (a, b) => {\n let len;\n let out = 0;\n if ((len = a.length) === b.length) {\n while (len--) {\n out |= a[len] ^ b[len];\n }\n }\n return len === -1 && out === 0;\n};\n/**\n * concatenates multiple Uint8Array buffers into one\n */\nexport const concat = (arrays, size) => {\n let written = 0;\n let len = arrays.length;\n let idx;\n if (size === undefined) {\n for (idx = size = 0; idx < len; idx++) {\n const chunk = arrays[idx];\n size += chunk.length;\n }\n }\n const buffer = new Uint8Array(size);\n for (idx = 0; idx < len; idx++) {\n const chunk = arrays[idx];\n buffer.set(chunk, written);\n written += chunk.length;\n }\n return buffer;\n};\n/**\n * encodes a UTF-8 string\n */\nexport const encodeUtf8 = (str) => {\n return textEncoder.encode(str);\n};\n/**\n * encodes a UTF-8 string into a given buffer\n */\nexport const encodeUtf8Into = (to, str, offset, length) => {\n let buffer;\n if (offset === undefined) {\n buffer = to;\n }\n else if (length === undefined) {\n buffer = to.subarray(offset);\n }\n else {\n buffer = to.subarray(offset, offset + length);\n }\n const result = textEncoder.encodeInto(str, buffer);\n return result.written;\n};\nconst fromCharCode = String.fromCharCode;\n/**\n * decodes a UTF-8 string from a given buffer\n */\nexport const decodeUtf8From = (from, offset, length) => {\n let buffer;\n if (offset === undefined) {\n buffer = from;\n }\n else if (length === undefined) {\n buffer = from.subarray(offset);\n }\n else {\n buffer = from.subarray(offset, offset + length);\n }\n const end = buffer.length;\n if (end > 24) {\n return textDecoder.decode(buffer);\n }\n {\n let str = '';\n let idx = 0;\n for (; idx + 3 < end; idx += 4) {\n const a = buffer[idx];\n const b = buffer[idx + 1];\n const c = buffer[idx + 2];\n const d = buffer[idx + 3];\n if ((a | b | c | d) & 0x80) {\n return str + textDecoder.decode(buffer.subarray(idx));\n }\n str += fromCharCode(a, b, c, d);\n }\n for (; idx < end; idx++) {\n const x = buffer[idx];\n if (x & 0x80) {\n return str + textDecoder.decode(buffer.subarray(idx));\n }\n str += fromCharCode(x);\n }\n return str;\n }\n};\n/**\n * get a SHA-256 digest of this buffer\n */\nexport const toSha256 = async (buffer) => {\n return new Uint8Array(await subtle.digest('SHA-256', buffer));\n};\n//# sourceMappingURL=index.js.map","import { alloc, allocUnsafe } from '@atcute/uint8array';\nexport const createRfc4648Encode = (alphabet, bitsPerChar, pad) => {\n return (bytes) => {\n const mask = (1 << bitsPerChar) - 1;\n let str = '';\n let bits = 0; // Number of bits currently in the buffer\n let buffer = 0; // Bits waiting to be written out, MSB first\n for (let i = 0; i < bytes.length; ++i) {\n // Slurp data into the buffer:\n buffer = (buffer << 8) | bytes[i];\n bits += 8;\n // Write out as much as we can:\n while (bits > bitsPerChar) {\n bits -= bitsPerChar;\n str += alphabet[mask & (buffer >> bits)];\n }\n }\n // Partial character:\n if (bits !== 0) {\n str += alphabet[mask & (buffer << (bitsPerChar - bits))];\n }\n // Add padding characters until we hit a byte boundary:\n if (pad) {\n while (((str.length * bitsPerChar) & 7) !== 0) {\n str += '=';\n }\n }\n return str;\n };\n};\nexport const createRfc4648Decode = (alphabet, bitsPerChar, pad) => {\n // Build the character lookup table:\n const codes = {};\n for (let i = 0; i < alphabet.length; ++i) {\n codes[alphabet[i]] = i;\n }\n return (str) => {\n // Count the padding bytes:\n let end = str.length;\n while (pad && str[end - 1] === '=') {\n --end;\n }\n // Allocate the output:\n const bytes = allocUnsafe(((end * bitsPerChar) / 8) | 0);\n // Parse the data:\n let bits = 0; // Number of bits currently in the buffer\n let buffer = 0; // Bits waiting to be written out, MSB first\n let written = 0; // Next byte to write\n for (let i = 0; i < end; ++i) {\n // Read one character from the string:\n const value = codes[str[i]];\n if (value === undefined) {\n throw new SyntaxError(`invalid base string`);\n }\n // Append the bits to the buffer:\n buffer = (buffer << bitsPerChar) | value;\n bits += bitsPerChar;\n // Write out some bits if the buffer has a byte's worth:\n if (bits >= 8) {\n bits -= 8;\n bytes[written++] = 0xff & (buffer >> bits);\n }\n }\n // Verify that we have received just enough bits:\n if (bits >= bitsPerChar || (0xff & (buffer << (8 - bits))) !== 0) {\n throw new SyntaxError('unexpected end of data');\n }\n return bytes;\n };\n};\nexport const createBtcBaseEncode = (alphabet) => {\n if (alphabet.length >= 255) {\n throw new RangeError(`alphabet too long`);\n }\n const BASE = alphabet.length;\n const LEADER = alphabet.charAt(0);\n const iFACTOR = Math.log(256) / Math.log(BASE); // log(256) / log(BASE), rounded up\n return (source) => {\n if (source.length === 0) {\n return '';\n }\n // Skip & count leading zeroes.\n let zeroes = 0;\n let length = 0;\n let pbegin = 0;\n const pend = source.length;\n while (pbegin !== pend && source[pbegin] === 0) {\n pbegin++;\n zeroes++;\n }\n // Allocate enough space in big-endian base58 representation.\n const size = ((pend - pbegin) * iFACTOR + 1) >>> 0;\n const b58 = alloc(size);\n // Process the bytes.\n while (pbegin !== pend) {\n let carry = source[pbegin];\n // Apply \"b58 = b58 * 256 + ch\".\n let i = 0;\n for (let it1 = size - 1; (carry !== 0 || i < length) && it1 !== -1; it1--, i++) {\n carry += (256 * b58[it1]) >>> 0;\n b58[it1] = carry % BASE >>> 0;\n carry = (carry / BASE) >>> 0;\n }\n if (carry !== 0) {\n throw new Error('non-zero carry');\n }\n length = i;\n pbegin++;\n }\n // Skip leading zeroes in base58 result.\n let it2 = size - length;\n while (it2 !== size && b58[it2] === 0) {\n it2++;\n }\n // Translate the result into a string.\n let str = LEADER.repeat(zeroes);\n for (; it2 < size; ++it2) {\n str += alphabet.charAt(b58[it2]);\n }\n return str;\n };\n};\nexport const createBtcBaseDecode = (alphabet) => {\n if (alphabet.length >= 255) {\n throw new RangeError(`alphabet too long`);\n }\n const BASE_MAP = allocUnsafe(256).fill(255);\n for (let i = 0; i < alphabet.length; i++) {\n const xc = alphabet.charCodeAt(i);\n if (BASE_MAP[xc] !== 255) {\n throw new RangeError(`${alphabet[i]} is ambiguous`);\n }\n BASE_MAP[xc] = i;\n }\n const BASE = alphabet.length;\n const LEADER = alphabet.charAt(0);\n const FACTOR = Math.log(BASE) / Math.log(256); // log(BASE) / log(256), rounded up\n return (source) => {\n if (source.length === 0) {\n return allocUnsafe(0);\n }\n // Skip and count leading '1's.\n let psz = 0;\n let zeroes = 0;\n let length = 0;\n while (source[psz] === LEADER) {\n zeroes++;\n psz++;\n }\n // Allocate enough space in big-endian base256 representation.\n const size = ((source.length - psz) * FACTOR + 1) >>> 0; // log(58) / log(256), rounded up.\n const b256 = alloc(size);\n // Process the characters.\n while (psz < source.length) {\n // Decode character\n let carry = BASE_MAP[source.charCodeAt(psz)];\n // Invalid character\n if (carry === 255) {\n throw new Error(`invalid string`);\n }\n let i = 0;\n for (let it3 = size - 1; (carry !== 0 || i < length) && it3 !== -1; it3--, i++) {\n carry += (BASE * b256[it3]) >>> 0;\n b256[it3] = carry % 256 >>> 0;\n carry = (carry / 256) >>> 0;\n }\n if (carry !== 0) {\n throw new Error('non-zero carry');\n }\n length = i;\n psz++;\n }\n // Skip leading zeroes in b256.\n let it4 = size - length;\n while (it4 !== size && b256[it4] === 0) {\n it4++;\n }\n if (it4 === zeroes) {\n return b256;\n }\n const vch = allocUnsafe(zeroes + (size - it4));\n vch.fill(0, 0, zeroes);\n vch.set(b256.subarray(it4), zeroes);\n return vch;\n };\n};\n//# sourceMappingURL=utils.js.map","// #region base64\nexport const fromBase64 = (str) => {\n return Uint8Array.fromBase64(str, { alphabet: 'base64', lastChunkHandling: 'loose' });\n};\nexport const toBase64 = (bytes) => {\n return bytes.toBase64({ alphabet: 'base64', omitPadding: true });\n};\n// #endregion\n// #region base64pad\nexport const fromBase64Pad = (str) => {\n return Uint8Array.fromBase64(str, { alphabet: 'base64', lastChunkHandling: 'strict' });\n};\nexport const toBase64Pad = (bytes) => {\n return bytes.toBase64({ alphabet: 'base64', omitPadding: false });\n};\n// #endregion\n// #region base64url\nexport const fromBase64Url = (str) => {\n return Uint8Array.fromBase64(str, { alphabet: 'base64url', lastChunkHandling: 'loose' });\n};\nexport const toBase64Url = (bytes) => {\n return bytes.toBase64({ alphabet: 'base64url', omitPadding: true });\n};\n// #endregion\n// #region base64urlpad\nexport const fromBase64UrlPad = (str) => {\n return Uint8Array.fromBase64(str, { alphabet: 'base64url', lastChunkHandling: 'strict' });\n};\nexport const toBase64UrlPad = (bytes) => {\n return bytes.toBase64({ alphabet: 'base64url', omitPadding: false });\n};\n// #endregion\n//# sourceMappingURL=base64-web-native.js.map","import { createRfc4648Decode, createRfc4648Encode } from '../utils.js';\nconst BASE64_CHARSET = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/';\nconst BASE64URL_CHARSET = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_';\n// #region base64\nexport const fromBase64 = /*#__PURE__*/ createRfc4648Decode(BASE64_CHARSET, 6, false);\nexport const toBase64 = /*#__PURE__*/ createRfc4648Encode(BASE64_CHARSET, 6, false);\n// #endregion\n// #region base64pad\nexport const fromBase64Pad = /*#__PURE__*/ createRfc4648Decode(BASE64_CHARSET, 6, true);\nexport const toBase64Pad = /*#__PURE__*/ createRfc4648Encode(BASE64_CHARSET, 6, true);\n// #endregion\n// #region base64url\nexport const fromBase64Url = /*#__PURE__*/ createRfc4648Decode(BASE64URL_CHARSET, 6, false);\nexport const toBase64Url = /*#__PURE__*/ createRfc4648Encode(BASE64URL_CHARSET, 6, false);\n// #endregion\n// #region base64urlpad\nexport const fromBase64UrlPad = /*#__PURE__*/ createRfc4648Decode(BASE64URL_CHARSET, 6, true);\nexport const toBase64UrlPad = /*#__PURE__*/ createRfc4648Encode(BASE64URL_CHARSET, 6, true);\n// #endregion\n//# sourceMappingURL=base64-web-polyfill.js.map","import { fromBase64 as fromBase64Native, fromBase64Pad as fromBase64PadNative, fromBase64Url as fromBase64UrlNative, fromBase64UrlPad as fromBase64UrlPadNative, toBase64 as toBase64Native, toBase64Pad as toBase64PadNative, toBase64Url as toBase64UrlNative, toBase64UrlPad as toBase64UrlPadNative, } from './base64-web-native.js';\nimport { fromBase64Pad as fromBase64PadPolyfill, fromBase64 as fromBase64Polyfill, fromBase64UrlPad as fromBase64UrlPadPolyfill, fromBase64Url as fromBase64UrlPolyfill, toBase64Pad as toBase64PadPolyfill, toBase64 as toBase64Polyfill, toBase64UrlPad as toBase64UrlPadPolyfill, toBase64Url as toBase64UrlPolyfill, } from './base64-web-polyfill.js';\nconst HAS_NATIVE_SUPPORT = 'fromBase64' in Uint8Array;\n// #region base64\nexport const fromBase64 = !HAS_NATIVE_SUPPORT ? fromBase64Polyfill : fromBase64Native;\nexport const toBase64 = !HAS_NATIVE_SUPPORT ? toBase64Polyfill : toBase64Native;\n// #endregion\n// #region base64pad\nexport const fromBase64Pad = !HAS_NATIVE_SUPPORT ? fromBase64PadPolyfill : fromBase64PadNative;\nexport const toBase64Pad = !HAS_NATIVE_SUPPORT ? toBase64PadPolyfill : toBase64PadNative;\n// #endregion\n// #region base64url\nexport const fromBase64Url = !HAS_NATIVE_SUPPORT ? fromBase64UrlPolyfill : fromBase64UrlNative;\nexport const toBase64Url = !HAS_NATIVE_SUPPORT ? toBase64UrlPolyfill : toBase64UrlNative;\n// #endregion\n// #region base64urlpad\nexport const fromBase64UrlPad = !HAS_NATIVE_SUPPORT ? fromBase64UrlPadPolyfill : fromBase64UrlPadNative;\nexport const toBase64UrlPad = !HAS_NATIVE_SUPPORT ? toBase64UrlPadPolyfill : toBase64UrlPadNative;\n// #endregion\n//# sourceMappingURL=base64-web.js.map","import { nanoid } from 'nanoid';\nimport { toBase64Url } from '@atcute/multibase';\nimport { encodeUtf8, toSha256 } from '@atcute/uint8array';\nexport const locks = typeof navigator !== 'undefined' ? navigator.locks : undefined;\nexport const stringToSha256 = async (input) => {\n const bytes = encodeUtf8(input);\n const digest = await toSha256(bytes);\n return toBase64Url(digest);\n};\nexport const generatePKCE = async () => {\n const verifier = nanoid(64);\n return {\n verifier: verifier,\n challenge: await stringToSha256(verifier),\n method: 'S256',\n };\n};\n//# sourceMappingURL=runtime.js.map","import { locks } from '../utils/runtime.js';\nconst parse = (raw) => {\n if (raw != null) {\n const parsed = JSON.parse(raw);\n if (parsed != null) {\n return parsed;\n }\n }\n return {};\n};\nexport const createOAuthDatabase = ({ name }) => {\n const controller = new AbortController();\n const signal = controller.signal;\n const createStore = (subname, expiresAt, persistUpdatedAt = false) => {\n let store;\n const storageKey = `${name}:${subname}`;\n const persist = () => store && localStorage.setItem(storageKey, JSON.stringify(store));\n const read = () => {\n if (signal.aborted) {\n throw new Error(`store closed`);\n }\n return (store ??= parse(localStorage.getItem(storageKey)));\n };\n {\n const listener = (ev) => {\n if (ev.key === storageKey) {\n store = undefined;\n }\n };\n globalThis.addEventListener('storage', listener, { signal });\n }\n {\n const cleanup = async (lock) => {\n if (!lock || signal.aborted) {\n return;\n }\n await new Promise((resolve) => setTimeout(resolve, 10_000));\n if (signal.aborted) {\n return;\n }\n let now = Date.now();\n let changed = false;\n read();\n for (const key in store) {\n const item = store[key];\n const expiresAt = item.expiresAt;\n if (expiresAt !== null && now > expiresAt) {\n changed = true;\n delete store[key];\n }\n }\n if (changed) {\n persist();\n }\n };\n if (locks) {\n locks.request(`${storageKey}:cleanup`, { ifAvailable: true }, cleanup);\n }\n else {\n cleanup(true);\n }\n }\n return {\n get(key) {\n read();\n const item = store[key];\n if (!item) {\n return;\n }\n const expiresAt = item.expiresAt;\n if (expiresAt !== null && Date.now() > expiresAt) {\n delete store[key];\n persist();\n return;\n }\n return item.value;\n },\n getWithLapsed(key) {\n read();\n const item = store[key];\n const now = Date.now();\n if (!item) {\n return [undefined, Infinity];\n }\n const updatedAt = item.updatedAt;\n if (updatedAt === undefined) {\n return [item.value, Infinity];\n }\n return [item.value, now - updatedAt];\n },\n set(key, value) {\n read();\n const item = {\n value: value,\n expiresAt: expiresAt(value),\n updatedAt: persistUpdatedAt ? Date.now() : undefined,\n };\n store[key] = item;\n persist();\n },\n delete(key) {\n read();\n if (store[key] !== undefined) {\n delete store[key];\n persist();\n }\n },\n keys() {\n read();\n return Object.keys(store);\n },\n };\n };\n return {\n dispose: () => {\n controller.abort();\n },\n sessions: createStore('sessions', ({ token }) => {\n if (token.refresh) {\n return null;\n }\n return token.expires_at ?? null;\n }),\n states: createStore('states', (_item) => Date.now() + 10 * 60 * 1_000), // 10 minutes\n // The reference PDS have nonces that expire after 3 minutes, while other\n // implementations can have varying expiration times.\n // Stored for 24 hours.\n dpopNonces: createStore('dpopNonces', (_item) => Date.now() + 24 * 60 * 60 * 1_000, true),\n inflightDpop: new Map(),\n };\n};\n//# sourceMappingURL=db.js.map","import { createOAuthDatabase } from './store/db.js';\nexport let CLIENT_ID;\nexport let REDIRECT_URI;\nexport let database;\nexport const configureOAuth = (options) => {\n ({ client_id: CLIENT_ID, redirect_uri: REDIRECT_URI } = options.metadata);\n database = createOAuthDatabase({ name: options.storageName ?? 'atcute-oauth' });\n};\n//# sourceMappingURL=environment.js.map","export class LoginError extends Error {\n name = 'LoginError';\n}\nexport class AuthorizationError extends Error {\n name = 'AuthorizationError';\n}\nexport class ResolverError extends Error {\n name = 'ResolverError';\n}\nexport class TokenRefreshError extends Error {\n sub;\n name = 'TokenRefreshError';\n constructor(sub, message, options) {\n super(message, options);\n this.sub = sub;\n }\n}\nexport class OAuthResponseError extends Error {\n response;\n data;\n name = 'OAuthResponseError';\n error;\n description;\n constructor(response, data) {\n const error = ifString(ifObject(data)?.['error']);\n const errorDescription = ifString(ifObject(data)?.['error_description']);\n const messageError = error ? `\"${error}\"` : 'unknown';\n const messageDesc = errorDescription ? `: ${errorDescription}` : '';\n const message = `OAuth ${messageError} error${messageDesc}`;\n super(message);\n this.response = response;\n this.data = data;\n this.error = error;\n this.description = errorDescription;\n }\n get status() {\n return this.response.status;\n }\n get headers() {\n return this.response.headers;\n }\n}\nexport class FetchResponseError extends Error {\n response;\n status;\n name = 'FetchResponseError';\n constructor(response, status, message) {\n super(message);\n this.response = response;\n this.status = status;\n }\n}\nconst ifString = (v) => {\n return typeof v === 'string' ? v : undefined;\n};\nconst ifObject = (v) => {\n return typeof v === 'object' && v !== null && !Array.isArray(v) ? v : undefined;\n};\n//# sourceMappingURL=errors.js.map","const DID_RE = /^did:([a-z]+):([a-zA-Z0-9._:%\\-]*[a-zA-Z0-9._\\-])$/;\n// #__NO_SIDE_EFFECTS__\nexport const isDid = (input) => {\n return typeof input === 'string' && input.length >= 7 && input.length <= 2048 && DID_RE.test(input);\n};\n//# sourceMappingURL=did.js.map","import { isHandle } from '@atcute/lexicons/syntax';\nimport * as t from './types.js';\nconst isUrlParseSupported = 'parse' in URL;\nexport const isAtprotoServiceEndpoint = (input) => {\n let url = null;\n if (isUrlParseSupported) {\n url = URL.parse(input);\n }\n else {\n try {\n url = new URL(input);\n }\n catch { }\n }\n return (url !== null &&\n (url.protocol === 'https:' || url.protocol === 'http:') &&\n url.pathname === '/' &&\n url.search === '' &&\n url.hash === '');\n};\nexport const getVerificationMaterial = (doc, id) => {\n const verificationMethods = doc.verificationMethod;\n if (!verificationMethods) {\n return;\n }\n const expectedId = `${doc.id}${id}`;\n for (let idx = 0, len = verificationMethods.length; idx < len; idx++) {\n const { id, type, publicKeyMultibase } = verificationMethods[idx];\n if (id !== expectedId) {\n continue;\n }\n if (publicKeyMultibase === undefined) {\n continue;\n }\n return { type, publicKeyMultibase };\n }\n};\nexport const getAtprotoVerificationMaterial = (doc) => {\n return getVerificationMaterial(doc, '#atproto');\n};\nexport const getAtprotoLabelerVerificationMaterial = (doc) => {\n return getVerificationMaterial(doc, '#atproto_label');\n};\nexport const getAtprotoHandle = (doc) => {\n const alsoKnownAs = doc.alsoKnownAs;\n if (!alsoKnownAs) {\n return null;\n }\n const PREFIX = 'at://';\n for (let idx = 0, len = alsoKnownAs.length; idx < len; idx++) {\n const aka = alsoKnownAs[idx];\n if (!aka.startsWith(PREFIX)) {\n continue;\n }\n const raw = aka.slice(PREFIX.length);\n if (!isHandle(raw)) {\n return undefined;\n }\n return raw;\n }\n return null;\n};\nexport const getAtprotoServiceEndpoint = (doc, predicate) => {\n const services = doc.service;\n if (!services) {\n return;\n }\n for (let idx = 0, len = services.length; idx < len; idx++) {\n const { id, type, serviceEndpoint } = services[idx];\n if (id !== predicate.id && id !== doc.id + predicate.id) {\n continue;\n }\n if (predicate.type !== undefined) {\n if (Array.isArray(type)) {\n if (!type.includes(predicate.type)) {\n continue;\n }\n }\n else {\n if (type !== predicate.type) {\n continue;\n }\n }\n }\n if (typeof serviceEndpoint !== 'string' || !isAtprotoServiceEndpoint(serviceEndpoint)) {\n continue;\n }\n return serviceEndpoint;\n }\n};\nexport const getPdsEndpoint = (doc) => {\n return getAtprotoServiceEndpoint(doc, {\n id: '#atproto_pds',\n type: 'AtprotoPersonalDataServer',\n });\n};\nexport const getLabelerEndpoint = (doc) => {\n return getAtprotoServiceEndpoint(doc, {\n id: '#atproto_labeler',\n type: 'AtprotoLabeler',\n });\n};\nexport const getBlueskyChatEndpoint = (doc) => {\n return getAtprotoServiceEndpoint(doc, {\n id: '#bsky_chat',\n type: 'BskyChatService',\n });\n};\nexport const getBlueskyFeedgenEndpoint = (doc) => {\n return getAtprotoServiceEndpoint(doc, {\n id: '#bsky_fg',\n type: 'BskyFeedGenerator',\n });\n};\nexport const getBlueskyNotificationEndpoint = (doc) => {\n return getAtprotoServiceEndpoint(doc, {\n id: '#bsky_notif',\n type: 'BskyNotificationService',\n });\n};\n//# sourceMappingURL=utils.js.map","export const DEFAULT_APPVIEW_URL = 'https://public.api.bsky.app';\n//# sourceMappingURL=constants.js.map","export const extractContentType = (headers) => {\n return headers.get('content-type')?.split(';')[0];\n};\n//# sourceMappingURL=response.js.map","const isUrlParseSupported = 'parse' in URL;\nexport const isValidUrl = (urlString) => {\n let url = null;\n if (isUrlParseSupported) {\n url = URL.parse(urlString);\n }\n else {\n try {\n url = new URL(urlString);\n }\n catch { }\n }\n if (url !== null) {\n return url.protocol === 'https:' || url.protocol === 'http:';\n }\n return false;\n};\n//# sourceMappingURL=strings.js.map","import { getPdsEndpoint } from '@atcute/identity';\nimport { isDid } from '@atcute/lexicons/syntax';\nimport { DEFAULT_APPVIEW_URL } from './constants.js';\nimport { ResolverError } from './errors.js';\nimport { extractContentType } from './utils/response.js';\nimport { isValidUrl } from './utils/strings.js';\nconst DID_WEB_RE = /^([a-zA-Z0-9-]+(?:\\.[a-zA-Z0-9-]+)*(?:\\.[a-zA-Z]{2,}))$/;\n/**\n * Resolves domain handles into DID identifiers, by requesting Bluesky's AppView\n * for identity resolution.\n * @param handle Domain handle to resolve\n * @returns DID identifier resolved from the domain handle\n */\nexport const resolveHandle = async (handle) => {\n const url = DEFAULT_APPVIEW_URL + `/xrpc/com.atproto.identity.resolveHandle` + `?handle=${handle}`;\n const response = await fetch(url);\n if (response.status === 400) {\n throw new ResolverError(`domain handle not found`);\n }\n else if (!response.ok) {\n throw new ResolverError(`directory is unreachable`);\n }\n const json = (await response.json());\n return json.did;\n};\n/**\n * Get DID documents of did:plc (via plc.directory) and did:web identifiers\n * @param did DID identifier we're seeking DID doc from\n * @returns Retrieved DID document\n */\nexport const getDidDocument = async (did) => {\n const colon_index = did.indexOf(':', 4);\n const type = did.slice(4, colon_index);\n const ident = did.slice(colon_index + 1);\n // 2. retrieve their DID documents\n let doc;\n if (type === 'plc') {\n const response = await fetch(`https://plc.directory/${did}`);\n if (response.status === 404) {\n throw new ResolverError(`did not found in directory`);\n }\n else if (!response.ok) {\n throw new ResolverError(`directory is unreachable`);\n }\n const json = await response.json();\n doc = json;\n }\n else if (type === 'web') {\n if (!DID_WEB_RE.test(ident)) {\n throw new ResolverError(`invalid identifier`);\n }\n const response = await fetch(`https://${ident}/.well-known/did.json`);\n if (!response.ok) {\n throw new ResolverError(`did document is unreachable`);\n }\n const json = await response.json();\n doc = json;\n }\n else {\n throw new ResolverError(`unsupported did method`);\n }\n return doc;\n};\n/**\n * Get OAuth protected resource metadata from a host\n * @param host URL of the host\n * @returns Retrieved protected resource metadata\n */\nexport const getProtectedResourceMetadata = async (host) => {\n const url = new URL(`/.well-known/oauth-protected-resource`, host);\n const response = await fetch(url, {\n redirect: 'manual',\n headers: {\n accept: 'application/json',\n },\n });\n if (response.status !== 200 || extractContentType(response.headers) !== 'application/json') {\n throw new ResolverError(`unexpected response`);\n }\n const metadata = (await response.json());\n if (metadata.resource !== url.origin) {\n throw new ResolverError(`unexpected issuer`);\n }\n return metadata;\n};\n/**\n * Get OAuth authorization server metadata from a host\n * @param host URL of the host\n * @returns Retrieved authorization server metadata\n */\nexport const getAuthorizationServerMetadata = async (host) => {\n const url = new URL(`/.well-known/oauth-authorization-server`, host);\n const response = await fetch(url, {\n redirect: 'manual',\n headers: {\n accept: 'application/json',\n },\n });\n if (response.status !== 200 || extractContentType(response.headers) !== 'application/json') {\n throw new ResolverError(`unexpected response`);\n }\n const metadata = (await response.json());\n if (metadata.issuer !== url.origin) {\n throw new ResolverError(`unexpected issuer`);\n }\n if (!isValidUrl(metadata.authorization_endpoint)) {\n throw new ResolverError(`authorization server provided incorrect authorization endpoint`);\n }\n if (!metadata.client_id_metadata_document_supported) {\n throw new ResolverError(`authorization server does not support 'client_id_metadata_document'`);\n }\n if (!metadata.pushed_authorization_request_endpoint) {\n throw new ResolverError(`authorization server does not support 'pushed_authorization request'`);\n }\n if (metadata.response_types_supported) {\n if (!metadata.response_types_supported.includes('code')) {\n throw new ResolverError(`authorization server does not support 'code' response type`);\n }\n }\n return metadata;\n};\n/**\n * Resolve handle domains or DID identifiers to get their PDS and its authorization server metadata\n * @param ident Handle domain or DID identifier to resolve\n * @returns Resolved PDS and authorization server metadata\n */\nexport const resolveFromIdentity = async (ident) => {\n let did;\n if (isDid(ident)) {\n did = ident;\n }\n else {\n const resolved = await resolveHandle(ident);\n did = resolved;\n }\n const doc = await getDidDocument(did);\n const pds = getPdsEndpoint(doc);\n if (!pds) {\n throw new ResolverError(`missing pds endpoint`);\n }\n return {\n identity: {\n id: did,\n raw: ident,\n pds: new URL(pds),\n },\n metadata: await getMetadataFromResourceServer(pds),\n };\n};\n/**\n * Request authorization server metadata from a PDS\n * @param host URL of the host\n * @returns Resolved authorization server metadata\n */\nexport const resolveFromService = async (host) => {\n try {\n const metadata = await getMetadataFromResourceServer(host);\n return { metadata };\n }\n catch (err) {\n if (err instanceof ResolverError) {\n try {\n const metadata = await getAuthorizationServerMetadata(host);\n return { metadata };\n }\n catch { }\n }\n throw err;\n }\n};\n/**\n * Request authorization server metadata from its protected resource metadata\n * @param input URL of the host whose authorization server is delegated\n * @returns Resolved authorization server metadata\n */\nexport const getMetadataFromResourceServer = async (input) => {\n const rs_metadata = await getProtectedResourceMetadata(input);\n if (rs_metadata.authorization_servers?.length !== 1) {\n throw new ResolverError(`expected exactly one authorization server in the listing`);\n }\n const issuer = rs_metadata.authorization_servers[0];\n const as_metadata = await getAuthorizationServerMetadata(issuer);\n if (as_metadata.protected_resources) {\n if (!as_metadata.protected_resources.includes(rs_metadata.resource)) {\n throw new ResolverError(`server is not in authorization server's jurisdiction`);\n }\n }\n return as_metadata;\n};\n//# sourceMappingURL=resolvers.js.map","import { fromBase64Url, toBase64Url } from '@atcute/multibase';\nimport { encodeUtf8 } from '@atcute/uint8array';\nimport { nanoid } from 'nanoid';\nimport { database } from './environment.js';\nimport { extractContentType } from './utils/response.js';\nimport { stringToSha256 } from './utils/runtime.js';\nconst ES256_ALG = { name: 'ECDSA', namedCurve: 'P-256' };\nexport const createES256Key = async () => {\n const pair = await crypto.subtle.generateKey(ES256_ALG, true, ['sign', 'verify']);\n const key = await crypto.subtle.exportKey('pkcs8', pair.privateKey);\n const { ext: _ext, key_ops: _key_opts, ...jwk } = await crypto.subtle.exportKey('jwk', pair.publicKey);\n return {\n typ: 'ES256',\n key: toBase64Url(new Uint8Array(key)),\n jwt: toBase64Url(encodeUtf8(JSON.stringify({ typ: 'dpop+jwt', alg: 'ES256', jwk: jwk }))),\n };\n};\nexport const createDPoPSignage = (dpopKey) => {\n const headerString = dpopKey.jwt;\n const keyPromise = crypto.subtle.importKey('pkcs8', fromBase64Url(dpopKey.key), ES256_ALG, true, ['sign']);\n const constructPayload = (htm, htu, nonce, ath) => {\n const payload = {\n ath: ath,\n htm: htm,\n htu: htu,\n iat: Math.floor(Date.now() / 1_000),\n jti: nanoid(24),\n nonce: nonce,\n };\n return toBase64Url(encodeUtf8(JSON.stringify(payload)));\n };\n return async (method, htu, nonce, ath) => {\n const payloadString = constructPayload(method, htu, nonce, ath);\n const signed = await crypto.subtle.sign({ name: 'ECDSA', hash: { name: 'SHA-256' } }, await keyPromise, encodeUtf8(headerString + '.' + payloadString));\n const signatureString = toBase64Url(new Uint8Array(signed));\n return headerString + '.' + payloadString + '.' + signatureString;\n };\n};\nexport const createDPoPFetch = (dpopKey, isAuthServer) => {\n const nonces = database.dpopNonces;\n const pending = database.inflightDpop;\n const sign = createDPoPSignage(dpopKey);\n return async (input, init) => {\n const request = new Request(input, init);\n const authorizationHeader = request.headers.get('authorization');\n const ath = authorizationHeader?.startsWith('DPoP ')\n ? await stringToSha256(authorizationHeader.slice(5))\n : undefined;\n const { method, url } = request;\n const { origin, pathname } = new URL(url);\n const htu = origin + pathname;\n // See if we have a pending promise for this origin, we'll await before\n // proceeding with this request, next comment describes what the promise\n // is meant to be.\n let deferred = pending.get(origin);\n if (deferred) {\n await deferred.promise;\n deferred = undefined;\n }\n // Get our persisted nonce value for this origin\n let initNonce;\n let expiredOrMissing = false;\n try {\n const [nonce, lapsed] = nonces.getWithLapsed(origin);\n initNonce = nonce;\n // The problem with DPoP nonces is that we don't have insight as to when\n // they'll expire, either we have a nonce value or we don't.\n //\n // Which is very unfortunate, if the client makes multiple requests at the\n // same time, there's a chance that all of them will fail due to the nonce\n // value having expired.\n //\n // To make this less painful, if it's been over 3 minutes since we last\n // had a nonce value, or we never had one to begin with, we'll let this\n // request through and defer everyone else until we get a possibly fresh\n // nonce value.\n //\n // 3 minutes being the DPoP nonce expiration time set by the reference PDS\n // implementation.\n expiredOrMissing = lapsed > 3 * 60 * 1_000;\n }\n catch {\n // Ignore read errors, we'll just act like we're missing a nonce.\n }\n if (expiredOrMissing) {\n // Defer everyone else until this request finishes.\n pending.set(origin, (deferred = Promise.withResolvers()));\n }\n let nextNonce;\n try {\n const initProof = await sign(method, htu, initNonce, ath);\n request.headers.set('dpop', initProof);\n const initResponse = await fetch(request);\n nextNonce = initResponse.headers.get('dpop-nonce');\n if (nextNonce === null || nextNonce === initNonce) {\n // No nonce was returned or it is the same as the one we sent. No need to\n // update the nonce store, or retry the request.\n return initResponse;\n }\n // Store the fresh nonce for future requests\n try {\n nonces.set(origin, nextNonce);\n }\n catch {\n // Ignore write errors\n }\n const shouldRetry = await isUseDpopNonceError(initResponse, isAuthServer);\n if (!shouldRetry) {\n // Not a \"use_dpop_nonce\" error, so there is no need to retry\n return initResponse;\n }\n if (input === request || init?.body instanceof ReadableStream) {\n // If the input stream was already consumed, we cannot retry the request. A\n // solution would be to clone() the request but that would bufferize the\n // entire stream in memory which can lead to memory starvation. Instead, we\n // will return the original response and let the calling code handle retries.\n return initResponse;\n }\n }\n finally {\n // Now everyone can have their turn.\n if (deferred) {\n pending.delete(origin);\n deferred.resolve();\n }\n }\n // We got here because we were asked to retry the request (due to missing\n // nonce value in the first request), let's do just that.\n {\n const nextProof = await sign(method, htu, nextNonce, ath);\n const nextRequest = new Request(input, init);\n nextRequest.headers.set('dpop', nextProof);\n const retryResponse = await fetch(nextRequest);\n // Check if the server returned another new nonce in the retry response\n const retryNonce = retryResponse.headers.get('dpop-nonce');\n if (retryNonce !== null && retryNonce !== nextNonce) {\n try {\n nonces.set(origin, retryNonce);\n }\n catch {\n // Ignore write errors\n }\n }\n return retryResponse;\n }\n };\n};\nconst isUseDpopNonceError = async (response, isAuthServer) => {\n // https://datatracker.ietf.org/doc/html/rfc6750#section-3\n // https://datatracker.ietf.org/doc/html/rfc9449#name-resource-server-provided-no\n if (isAuthServer === undefined || isAuthServer === false) {\n if (response.status === 401) {\n const wwwAuth = response.headers.get('www-authenticate');\n if (wwwAuth?.startsWith('DPoP')) {\n return wwwAuth.includes('error=\"use_dpop_nonce\"');\n }\n }\n }\n // https://datatracker.ietf.org/doc/html/rfc9449#name-authorization-server-provid\n if (isAuthServer === undefined || isAuthServer === true) {\n if (response.status === 400 && extractContentType(response.headers) === 'application/json') {\n try {\n const json = await response.clone().json();\n return typeof json === 'object' && json?.['error'] === 'use_dpop_nonce';\n }\n catch {\n // Response too big (to be \"use_dpop_nonce\" error) or invalid JSON\n return false;\n }\n }\n }\n return false;\n};\n//# sourceMappingURL=dpop.js.map","export const pick = (obj, keys) => {\n const cloned = {};\n for (let idx = 0, len = keys.length; idx < len; idx++) {\n const key = keys[idx];\n // @ts-expect-error\n cloned[key] = obj[key];\n }\n return cloned;\n};\n//# sourceMappingURL=misc.js.map","import { createDPoPFetch } from '../dpop.js';\nimport { CLIENT_ID, REDIRECT_URI } from '../environment.js';\nimport { FetchResponseError, OAuthResponseError, TokenRefreshError } from '../errors.js';\nimport { resolveFromIdentity } from '../resolvers.js';\nimport { pick } from '../utils/misc.js';\nimport { extractContentType } from '../utils/response.js';\nexport class OAuthServerAgent {\n #fetch;\n #metadata;\n constructor(metadata, dpopKey) {\n this.#metadata = metadata;\n this.#fetch = createDPoPFetch(dpopKey, true);\n }\n async request(endpoint, payload) {\n const url = this.#metadata[`${endpoint}_endpoint`];\n if (!url) {\n throw new Error(`no endpoint for ${endpoint}`);\n }\n const response = await this.#fetch(url, {\n method: 'post',\n headers: { 'content-type': 'application/json' },\n body: JSON.stringify({ ...payload, client_id: CLIENT_ID }),\n });\n if (extractContentType(response.headers) !== 'application/json') {\n throw new FetchResponseError(response, 2, `unexpected content-type`);\n }\n const json = await response.json();\n if (response.ok) {\n return json;\n }\n else {\n throw new OAuthResponseError(response, json);\n }\n }\n async revoke(token) {\n try {\n await this.request('revocation', { token: token });\n }\n catch { }\n }\n async exchangeCode(code, verifier) {\n const response = await this.request('token', {\n grant_type: 'authorization_code',\n redirect_uri: REDIRECT_URI,\n code: code,\n code_verifier: verifier,\n });\n try {\n return await this.#processExchangeResponse(response);\n }\n catch (err) {\n await this.revoke(response.access_token);\n throw err;\n }\n }\n async refresh({ sub, token }) {\n if (!token.refresh) {\n throw new TokenRefreshError(sub, 'no refresh token available');\n }\n const response = await this.request('token', {\n grant_type: 'refresh_token',\n refresh_token: token.refresh,\n });\n try {\n if (sub !== response.sub) {\n throw new TokenRefreshError(sub, `sub mismatch in token response; got ${response.sub}`);\n }\n return this.#processTokenResponse(response);\n }\n catch (err) {\n await this.revoke(response.access_token);\n throw err;\n }\n }\n #processTokenResponse(res) {\n if (!res.sub) {\n throw new TypeError(`missing sub field in token response`);\n }\n if (!res.scope) {\n throw new TypeError(`missing scope field in token response`);\n }\n if (res.token_type !== 'DPoP') {\n throw new TypeError(`token response returned a non-dpop token`);\n }\n return {\n scope: res.scope,\n refresh: res.refresh_token,\n access: res.access_token,\n type: res.token_type,\n expires_at: typeof res.expires_in === 'number' ? Date.now() + res.expires_in * 1_000 : undefined,\n };\n }\n async #processExchangeResponse(res) {\n const sub = res.sub;\n if (!sub) {\n throw new TypeError(`missing sub field in token response`);\n }\n const token = this.#processTokenResponse(res);\n const resolved = await resolveFromIdentity(sub);\n if (resolved.metadata.issuer !== this.#metadata.issuer) {\n throw new TypeError(`issuer mismatch; got ${resolved.metadata.issuer}`);\n }\n return {\n token: token,\n info: {\n sub: sub,\n aud: resolved.identity.pds.href,\n server: pick(resolved.metadata, [\n 'issuer',\n 'authorization_endpoint',\n 'introspection_endpoint',\n 'pushed_authorization_request_endpoint',\n 'revocation_endpoint',\n 'token_endpoint',\n ]),\n },\n };\n }\n}\n//# sourceMappingURL=server-agent.js.map","import { database } from '../environment.js';\nimport { OAuthResponseError, TokenRefreshError } from '../errors.js';\nimport { locks } from '../utils/runtime.js';\nimport { OAuthServerAgent } from './server-agent.js';\nconst pending = new Map();\nexport const getSession = async (sub, options) => {\n options?.signal?.throwIfAborted();\n let allowStored = isTokenUsable;\n if (options?.noCache) {\n allowStored = returnFalse;\n }\n else if (options?.allowStale) {\n allowStored = returnTrue;\n }\n // As long as concurrent requests are made for the same key, only one\n // request will be made to the cache & getter function at a time. This works\n // because there is no async operation between the while() loop and the\n // pending.set() call. Because of the \"single threaded\" nature of\n // JavaScript, the pending item will be set before the next iteration of the\n // while loop.\n let previousExecutionFlow;\n while ((previousExecutionFlow = pending.get(sub))) {\n try {\n const { isFresh, value } = await previousExecutionFlow;\n if (isFresh || allowStored(value)) {\n return value;\n }\n }\n catch {\n // Ignore errors from previous execution flows (they will have been\n // propagated by that flow).\n }\n options?.signal?.throwIfAborted();\n }\n const run = async () => {\n const storedSession = database.sessions.get(sub);\n if (storedSession && allowStored(storedSession)) {\n // Use the stored value as return value for the current execution\n // flow. Notify other concurrent execution flows (that should be\n // \"stuck\" in the loop before until this promise resolves) that we got\n // a value, but that it came from the store (isFresh = false).\n return { isFresh: false, value: storedSession };\n }\n const newSession = await refreshToken(sub, storedSession);\n await storeSession(sub, newSession);\n return { isFresh: true, value: newSession };\n };\n let promise;\n if (locks) {\n promise = locks.request(`atcute-oauth:${sub}`, run);\n }\n else {\n promise = run();\n }\n promise = promise.finally(() => pending.delete(sub));\n if (pending.has(sub)) {\n // This should never happen. Indeed, there must not be any 'await'\n // statement between this and the loop iteration check meaning that\n // this.pending.get returned undefined. It is there to catch bugs that\n // would occur in future changes to the code.\n throw new Error('concurrent request for the same key');\n }\n pending.set(sub, promise);\n const { value } = await promise;\n return value;\n};\nexport const storeSession = async (sub, newSession) => {\n try {\n database.sessions.set(sub, newSession);\n }\n catch (err) {\n await onRefreshError(newSession);\n throw err;\n }\n};\nexport const deleteStoredSession = (sub) => {\n database.sessions.delete(sub);\n};\nexport const listStoredSessions = () => {\n return database.sessions.keys();\n};\nconst returnTrue = () => true;\nconst returnFalse = () => false;\nconst refreshToken = async (sub, storedSession) => {\n if (storedSession === undefined) {\n throw new TokenRefreshError(sub, `session deleted by another tab`);\n }\n const { dpopKey, info, token } = storedSession;\n const server = new OAuthServerAgent(info.server, dpopKey);\n try {\n const newToken = await server.refresh({ sub: info.sub, token });\n return { dpopKey, info, token: newToken };\n }\n catch (cause) {\n if (cause instanceof OAuthResponseError && cause.status === 400 && cause.error === 'invalid_grant') {\n throw new TokenRefreshError(sub, `session was revoked`, { cause });\n }\n throw cause;\n }\n};\nconst onRefreshError = async ({ dpopKey, info, token }) => {\n // If the token data cannot be stored, let's revoke it\n const server = new OAuthServerAgent(info.server, dpopKey);\n await server.revoke(token.refresh ?? token.access);\n};\nconst isTokenUsable = ({ token }) => {\n const expires = token.expires_at;\n return expires == null || Date.now() + 60_000 <= expires;\n};\n//# sourceMappingURL=sessions.js.map","import { nanoid } from 'nanoid';\nimport { createES256Key } from '../dpop.js';\nimport { CLIENT_ID, database, REDIRECT_URI } from '../environment.js';\nimport { AuthorizationError, LoginError } from '../errors.js';\nimport { generatePKCE } from '../utils/runtime.js';\nimport { OAuthServerAgent } from './server-agent.js';\nimport { storeSession } from './sessions.js';\n/**\n * Create authentication URL for authorization\n * @param options\n * @returns URL to redirect the user for authorization\n */\nexport const createAuthorizationUrl = async ({ metadata, identity, scope, }) => {\n const state = nanoid(24);\n const pkce = await generatePKCE();\n const dpopKey = await createES256Key();\n const params = {\n redirect_uri: REDIRECT_URI,\n code_challenge: pkce.challenge,\n code_challenge_method: pkce.method,\n state: state,\n login_hint: identity?.raw,\n response_mode: 'fragment',\n response_type: 'code',\n display: 'page',\n // id_token_hint: undefined,\n // max_age: undefined,\n // prompt: undefined,\n scope: scope,\n // ui_locales: undefined,\n };\n database.states.set(state, {\n dpopKey: dpopKey,\n metadata: metadata,\n verifier: pkce.verifier,\n });\n const server = new OAuthServerAgent(metadata, dpopKey);\n const response = await server.request('pushed_authorization_request', params);\n const authUrl = new URL(metadata.authorization_endpoint);\n authUrl.searchParams.set('client_id', CLIENT_ID);\n authUrl.searchParams.set('request_uri', response.request_uri);\n return authUrl;\n};\n/**\n * Finalize authorization\n * @param params Search params\n * @returns Session object, which you can use to instantiate user agents\n */\nexport const finalizeAuthorization = async (params) => {\n const issuer = params.get('iss');\n const state = params.get('state');\n const code = params.get('code');\n const error = params.get('error');\n if (!state || !(code || error)) {\n throw new LoginError(`missing parameters`);\n }\n const stored = database.states.get(state);\n if (stored) {\n // Delete now that we've caught it\n database.states.delete(state);\n }\n else {\n throw new LoginError(`unknown state provided`);\n }\n const dpopKey = stored.dpopKey;\n const metadata = stored.metadata;\n if (error) {\n throw new AuthorizationError(params.get('error_description') || error);\n }\n if (!code) {\n throw new LoginError(`missing code parameter`);\n }\n if (issuer === null) {\n throw new LoginError(`missing issuer parameter`);\n }\n else if (issuer !== metadata.issuer) {\n throw new LoginError(`issuer mismatch`);\n }\n // Retrieve authentication tokens\n const server = new OAuthServerAgent(metadata, dpopKey);\n const { info, token } = await server.exchangeCode(code, stored.verifier);\n // We're finished!\n const sub = info.sub;\n const session = { dpopKey, info, token };\n await storeSession(sub, session);\n return session;\n};\n//# sourceMappingURL=exchange.js.map","import { createDPoPFetch } from '../dpop.js';\nimport { OAuthServerAgent } from './server-agent.js';\nimport { deleteStoredSession, getSession } from './sessions.js';\nexport class OAuthUserAgent {\n session;\n #fetch;\n #getSessionPromise;\n constructor(session) {\n this.session = session;\n this.#fetch = createDPoPFetch(session.dpopKey, false);\n }\n get sub() {\n return this.session.info.sub;\n }\n getSession(options) {\n const promise = getSession(this.session.info.sub, options);\n promise\n .then((session) => {\n this.session = session;\n })\n .finally(() => {\n this.#getSessionPromise = undefined;\n });\n return (this.#getSessionPromise = promise);\n }\n async signOut() {\n const sub = this.session.info.sub;\n try {\n const { dpopKey, info, token } = await getSession(sub, { allowStale: true });\n const server = new OAuthServerAgent(info.server, dpopKey);\n await server.revoke(token.refresh ?? token.access);\n }\n finally {\n deleteStoredSession(sub);\n }\n }\n async handle(pathname, init) {\n await this.#getSessionPromise;\n const headers = new Headers(init?.headers);\n let session = this.session;\n let url = new URL(pathname, session.info.aud);\n headers.set('authorization', `${session.token.type} ${session.token.access}`);\n let response = await this.#fetch(url, { ...init, headers });\n if (!isInvalidTokenResponse(response)) {\n return response;\n }\n try {\n if (this.#getSessionPromise) {\n session = await this.#getSessionPromise;\n }\n else {\n session = await this.getSession();\n }\n }\n catch {\n return response;\n }\n // Stream already consumed, can't retry.\n if (init?.body instanceof ReadableStream) {\n return response;\n }\n url = new URL(pathname, session.info.aud);\n headers.set('authorization', `${session.token.type} ${session.token.access}`);\n return await this.#fetch(url, { ...init, headers });\n }\n}\nconst isInvalidTokenResponse = (response) => {\n if (response.status !== 401) {\n return false;\n }\n const auth = response.headers.get('www-authenticate');\n return (auth != null &&\n (auth.startsWith('Bearer ') || auth.startsWith('DPoP ')) &&\n auth.includes('error=\"invalid_token\"'));\n};\n//# sourceMappingURL=user-agent.js.map","// Storage adapter that mimics browser.storage.local API but uses localStorage\n// This allows sharing code between extension and via-client\n\nexport const storage = {\n local: {\n async get(keys?: string | string[] | Record<string, any>): Promise<Record<string, any>> {\n if (!keys) {\n // Get all items\n const result: Record<string, any> = {};\n for (let i = 0; i < localStorage.length; i++) {\n const key = localStorage.key(i);\n if (key) {\n try {\n result[key] = JSON.parse(localStorage.getItem(key) || 'null');\n } catch {\n result[key] = localStorage.getItem(key);\n }\n }\n }\n return result;\n }\n\n const result: Record<string, any> = {};\n \n if (typeof keys === 'string') {\n // Single key\n try {\n const value = localStorage.getItem(keys);\n result[keys] = value ? JSON.parse(value) : null;\n } catch {\n result[keys] = localStorage.getItem(keys);\n }\n } else if (Array.isArray(keys)) {\n // Array of keys\n keys.forEach(key => {\n try {\n const value = localStorage.getItem(key);\n result[key] = value ? JSON.parse(value) : null;\n } catch {\n result[key] = localStorage.getItem(key);\n }\n });\n } else {\n // Object with default values\n Object.keys(keys).forEach(key => {\n try {\n const value = localStorage.getItem(key);\n result[key] = value ? JSON.parse(value) : keys[key];\n } catch {\n result[key] = localStorage.getItem(key) || keys[key];\n }\n });\n }\n \n return result;\n },\n\n async set(items: Record<string, any>): Promise<void> {\n Object.entries(items).forEach(([key, value]) => {\n localStorage.setItem(key, JSON.stringify(value));\n });\n },\n\n async remove(keys: string | string[]): Promise<void> {\n const keysArray = Array.isArray(keys) ? keys : [keys];\n keysArray.forEach(key => localStorage.removeItem(key));\n },\n\n async clear(): Promise<void> {\n localStorage.clear();\n },\n },\n};\n","// Web-compatible OAuth implementation (for via-client)\n// Adapted from lib/oauth.ts to work without browser.* APIs\n\nimport {\n configureOAuth,\n createAuthorizationUrl,\n finalizeAuthorization,\n resolveFromIdentity,\n OAuthUserAgent,\n type OAuthSession,\n} from \"@atcute/oauth-browser-client\";\nimport { storage } from \"./storage-adapter\";\n\nconst OAUTH_SESSION_KEY = \"synthesis-oauth:session\";\n\nlet isOAuthInitialized = false;\n\nexport function initializeOAuth() {\n if (typeof window !== \"undefined\" && !isOAuthInitialized) {\n // Use web redirect URL for via proxy\n configureOAuth({\n metadata: {\n client_id: import.meta.env.VITE_OAUTH_CLIENT_ID || 'http://localhost:8081/static/client-metadata.json',\n redirect_uri: import.meta.env.VITE_OAUTH_REDIRECT_URI || 'http://localhost:8081/static/oauth-callback.html',\n },\n });\n isOAuthInitialized = true;\n }\n}\n\nexport async function startLoginProcess(handle: string): Promise<void> {\n console.log('[oauth-web] Starting login process for handle:', handle);\n initializeOAuth();\n \n console.log('[oauth-web] Resolving identity...');\n const { metadata } = await resolveFromIdentity(handle);\n console.log('[oauth-web] PDS metadata:', metadata);\n \n console.log('[oauth-web] Creating authorization URL...');\n const authUrl = await createAuthorizationUrl({\n metadata: metadata,\n scope: import.meta.env.VITE_OAUTH_SCOPE || 'atproto transition:generic',\n });\n console.log('[oauth-web] Auth URL:', authUrl.toString());\n\n // For web context, redirect to auth URL\n window.location.href = authUrl.toString();\n}\n\nexport async function handleOAuthCallback(): Promise<OAuthSession | null> {\n console.log('[oauth-web] Handling OAuth callback');\n \n // Parse OAuth response from URL (params can be in search or hash)\n const url = new URL(window.location.href);\n const paramString = url.search || url.hash.slice(1);\n const params = new URLSearchParams(paramString);\n \n console.log('[oauth-web] OAuth params:', Object.fromEntries(params));\n\n if (!params.has('code') && !params.has('error')) {\n console.log('[oauth-web] No OAuth params found');\n return null;\n }\n\n if (params.has('error')) {\n const error = params.get('error');\n const errorDesc = params.get('error_description');\n console.error('[oauth-web] OAuth error:', error, errorDesc);\n throw new Error(`OAuth error: ${error} - ${errorDesc}`);\n }\n\n // Finalize authorization with the params\n console.log('[oauth-web] Finalizing authorization...');\n const session = await finalizeAuthorization(params);\n console.log('[oauth-web] Authorization complete, session:', session);\n\n // Store session\n await saveSession(session);\n console.log('[oauth-web] Session saved successfully');\n\n return session;\n}\n\nexport async function saveSession(session: OAuthSession): Promise<void> {\n await storage.local.set({ [OAUTH_SESSION_KEY]: session });\n}\n\nexport async function loadSession(): Promise<OAuthSession | null> {\n const result = await storage.local.get(OAUTH_SESSION_KEY);\n return result[OAUTH_SESSION_KEY] || null;\n}\n\nexport async function clearSession(): Promise<void> {\n await storage.local.remove(OAUTH_SESSION_KEY);\n}\n\nexport async function getProfile(session: OAuthSession): Promise<any> {\n const agent = new OAuthUserAgent(session);\n const response = await agent.handle('/xrpc/app.bsky.actor.getProfile?actor=' + session.info.sub);\n return await response.json();\n}\n"],"names":["urlAlphabet","nanoid","size","id","bytes","scopedUrlAlphabet","textEncoder","subtle","alloc","allocUnsafe","encodeUtf8","str","toSha256","buffer","createRfc4648Encode","alphabet","bitsPerChar","pad","mask","bits","i","createRfc4648Decode","codes","end","written","value","fromBase64Url","toBase64Url","BASE64URL_CHARSET","HAS_NATIVE_SUPPORT","fromBase64UrlNative","fromBase64UrlPolyfill","toBase64UrlNative","toBase64UrlPolyfill","locks","stringToSha256","input","digest","generatePKCE","verifier","parse","raw","parsed","createOAuthDatabase","name","controller","signal","createStore","subname","expiresAt","persistUpdatedAt","store","storageKey","persist","read","listener","ev","cleanup","lock","resolve","now","changed","key","item","updatedAt","token","_item","CLIENT_ID","REDIRECT_URI","database","configureOAuth","options","LoginError","__publicField","AuthorizationError","ResolverError","TokenRefreshError","sub","message","OAuthResponseError","response","data","_a","_b","error","ifString","ifObject","errorDescription","messageError","messageDesc","FetchResponseError","status","v","DID_RE","isDid","isUrlParseSupported","isAtprotoServiceEndpoint","url","getAtprotoServiceEndpoint","doc","predicate","services","idx","len","type","serviceEndpoint","getPdsEndpoint","DEFAULT_APPVIEW_URL","extractContentType","headers","isValidUrl","urlString","DID_WEB_RE","resolveHandle","handle","getDidDocument","did","colon_index","ident","getProtectedResourceMetadata","host","metadata","getAuthorizationServerMetadata","resolveFromIdentity","pds","getMetadataFromResourceServer","rs_metadata","issuer","as_metadata","ES256_ALG","createES256Key","pair","_ext","_key_opts","jwk","createDPoPSignage","dpopKey","headerString","keyPromise","constructPayload","htm","htu","nonce","ath","payload","method","payloadString","signed","signatureString","createDPoPFetch","isAuthServer","nonces","pending","sign","init","request","authorizationHeader","origin","pathname","deferred","initNonce","expiredOrMissing","lapsed","nextNonce","initProof","initResponse","isUseDpopNonceError","nextProof","nextRequest","retryResponse","retryNonce","wwwAuth","json","pick","obj","keys","cloned","_fetch","_metadata","_OAuthServerAgent_instances","processTokenResponse_fn","processExchangeResponse_fn","OAuthServerAgent","__privateAdd","__privateSet","endpoint","__privateGet","code","__privateMethod","err","res","resolved","getSession","allowStored","isTokenUsable","returnFalse","returnTrue","previousExecutionFlow","isFresh","run","storedSession","newSession","refreshToken","storeSession","promise","onRefreshError","deleteStoredSession","info","server","newToken","cause","expires","createAuthorizationUrl","identity","scope","state","pkce","params","authUrl","finalizeAuthorization","stored","session","_getSessionPromise","OAuthUserAgent","isInvalidTokenResponse","auth","storage","result","items","OAUTH_SESSION_KEY","isOAuthInitialized","initializeOAuth","startLoginProcess","handleOAuthCallback","paramString","errorDesc","saveSession","loadSession","clearSession","getProfile"],"mappings":"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","x_google_ignoreList":[0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22]}
-1
pywb-test/static/assets/sidebar-BBEPW7gD.css
··· 1 - *{margin:0;padding:0;box-sizing:border-box}body{font-family:-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Oxygen,Ubuntu,Cantarell,sans-serif;font-size:14px;line-height:1.5;color:#333}.sidebar{display:flex;flex-direction:column;height:100vh;background:#fff}.sidebar-header{padding:16px;border-bottom:1px solid #e0e0e0;background:#f5f5f5}.sidebar-header h1{font-size:20px;font-weight:600;margin-bottom:4px}.sidebar-header p{font-size:12px;color:#666}.sidebar-content{flex:1;overflow-y:auto;padding:16px}
-1
pywb-test/static/assets/sidebar-J3iG1W2k.css
··· 1 - *{margin:0;padding:0;box-sizing:border-box}body{font-family:-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Oxygen,Ubuntu,Cantarell,sans-serif;font-size:14px;line-height:1.5;color:#333}.sidebar{display:flex;flex-direction:column;height:100vh;background:#fff}.sidebar-header{padding:16px;border-bottom:1px solid #e0e0e0;background:#f5f5f5}.sidebar-header h1{font-size:20px;font-weight:600;margin-bottom:4px}.sidebar-header p{font-size:12px;color:#666}.profile-info{display:flex;align-items:center;gap:8px;margin-top:8px}.profile-avatar{width:32px;height:32px;border-radius:50%}.profile-handle{font-size:14px;color:#333}.sidebar-content{flex:1;overflow-y:auto;padding:16px}.login-container{display:flex;flex-direction:column;gap:12px}.login-container h2{font-size:18px;font-weight:600;margin-bottom:8px}.input-wrapper{position:relative;display:flex;align-items:center}.at-symbol{position:absolute;left:12px;color:#666;font-size:14px;pointer-events:none}.handle-input{width:100%;padding:10px 12px 10px 28px;border:1px solid #ccc;border-radius:4px;font-size:14px;font-family:inherit}.handle-input:focus{outline:none;border-color:#0085ff}button{padding:10px 16px;background:#0085ff;color:#fff;border:none;border-radius:4px;font-size:14px;font-weight:500;cursor:pointer;font-family:inherit}button:hover{background:#0073e6}button:active{background:#0061c2}#auth-status{font-size:12px;color:#666;min-height:16px}#logout-btn{background:#e74c3c;margin-top:8px}#logout-btn:hover{background:#c0392b}
-2
pywb-test/static/assets/storage-web-DVEDyBzH.js
··· 1 - class o{constructor(e="seams-storage"){this.listeners=[],this.channel=new BroadcastChannel(e),this.channel.onmessage=s=>{console.log("[WebStorage] Received broadcast:",s.data),this.listeners.forEach(a=>a(s.data))}}async get(e){const s=localStorage.getItem(e);return s?JSON.parse(s):null}async set(e,s){const a=await this.get(e);localStorage.setItem(e,JSON.stringify(s));const t={key:e,newValue:s,oldValue:a};console.log("[WebStorage] Broadcasting change:",t),this.channel.postMessage(t)}onChange(e){this.listeners.push(e)}close(){this.channel.close()}}export{o as W}; 2 - //# sourceMappingURL=storage-web-DVEDyBzH.js.map
-1
pywb-test/static/assets/storage-web-DVEDyBzH.js.map
··· 1 - {"version":3,"file":"storage-web-DVEDyBzH.js","sources":["../../../lib/storage-web.ts"],"sourcesContent":["// Web storage adapter using localStorage + BroadcastChannel\n// Mimics browser.storage API for via proxy client\n\nexport interface StorageAdapter {\n get(key: string): Promise<any>;\n set(key: string, value: any): Promise<void>;\n onChange(callback: (changes: { key: string; newValue: any; oldValue?: any }) => void): void;\n}\n\nexport class WebStorage implements StorageAdapter {\n private channel: BroadcastChannel;\n private listeners: Array<(changes: { key: string; newValue: any; oldValue?: any }) => void> = [];\n\n constructor(channelName: string = 'seams-storage') {\n this.channel = new BroadcastChannel(channelName);\n \n // Listen for broadcasts from other contexts\n this.channel.onmessage = (event) => {\n console.log('[WebStorage] Received broadcast:', event.data);\n this.listeners.forEach(callback => callback(event.data));\n };\n }\n\n async get(key: string): Promise<any> {\n const value = localStorage.getItem(key);\n return value ? JSON.parse(value) : null;\n }\n\n async set(key: string, value: any): Promise<void> {\n const oldValue = await this.get(key);\n localStorage.setItem(key, JSON.stringify(value));\n \n // Broadcast change to other contexts (sidebar, content script, other tabs)\n const change = { key, newValue: value, oldValue };\n console.log('[WebStorage] Broadcasting change:', change);\n this.channel.postMessage(change);\n }\n\n onChange(callback: (changes: { key: string; newValue: any; oldValue?: any }) => void): void {\n this.listeners.push(callback);\n }\n\n close(): void {\n this.channel.close();\n }\n}\n"],"names":["WebStorage","channelName","event","callback","key","value","oldValue","change"],"mappings":"AASO,MAAMA,CAAqC,CAIhD,YAAYC,EAAsB,gBAAiB,CAFnD,KAAQ,UAAsF,CAAA,EAG5F,KAAK,QAAU,IAAI,iBAAiBA,CAAW,EAG/C,KAAK,QAAQ,UAAaC,GAAU,CAClC,QAAQ,IAAI,mCAAoCA,EAAM,IAAI,EAC1D,KAAK,UAAU,QAAQC,GAAYA,EAASD,EAAM,IAAI,CAAC,CACzD,CACF,CAEA,MAAM,IAAIE,EAA2B,CACnC,MAAMC,EAAQ,aAAa,QAAQD,CAAG,EACtC,OAAOC,EAAQ,KAAK,MAAMA,CAAK,EAAI,IACrC,CAEA,MAAM,IAAID,EAAaC,EAA2B,CAChD,MAAMC,EAAW,MAAM,KAAK,IAAIF,CAAG,EACnC,aAAa,QAAQA,EAAK,KAAK,UAAUC,CAAK,CAAC,EAG/C,MAAME,EAAS,CAAE,IAAAH,EAAK,SAAUC,EAAO,SAAAC,CAAA,EACvC,QAAQ,IAAI,oCAAqCC,CAAM,EACvD,KAAK,QAAQ,YAAYA,CAAM,CACjC,CAEA,SAASJ,EAAmF,CAC1F,KAAK,UAAU,KAAKA,CAAQ,CAC9B,CAEA,OAAc,CACZ,KAAK,QAAQ,MAAA,CACf,CACF"}
-14
pywb-test/static/client-metadata.json
··· 1 - { 2 - "client_id": "http://localhost:8081/static/client-metadata.json", 3 - "client_name": "Seams (via proxy - development)", 4 - "client_uri": "https://seams.so", 5 - "redirect_uris": [ 6 - "http://localhost:8081/static/oauth-callback.html" 7 - ], 8 - "scope": "atproto transition:generic", 9 - "grant_types": ["authorization_code", "refresh_token"], 10 - "response_types": ["code"], 11 - "application_type": "web", 12 - "token_endpoint_auth_method": "none", 13 - "dpop_bound_access_tokens": true 14 - }
pywb-test/static/extension-callback.html proxy/static/extension-callback.html
pywb-test/static/fonts.css proxy/static/fonts.css
pywb-test/static/fonts/fraunces-v38-latin-600.eot proxy/static/fonts/fraunces-v38-latin-600.eot
pywb-test/static/fonts/fraunces-v38-latin-600.ttf proxy/static/fonts/fraunces-v38-latin-600.ttf
pywb-test/static/fonts/fraunces-v38-latin-600.woff proxy/static/fonts/fraunces-v38-latin-600.woff
pywb-test/static/fonts/fraunces-v38-latin-600.woff2 proxy/static/fonts/fraunces-v38-latin-600.woff2
pywb-test/static/fonts/fraunces-v38-latin-700.eot proxy/static/fonts/fraunces-v38-latin-700.eot
pywb-test/static/fonts/fraunces-v38-latin-700.ttf proxy/static/fonts/fraunces-v38-latin-700.ttf
pywb-test/static/fonts/fraunces-v38-latin-700.woff proxy/static/fonts/fraunces-v38-latin-700.woff
pywb-test/static/fonts/fraunces-v38-latin-700.woff2 proxy/static/fonts/fraunces-v38-latin-700.woff2
pywb-test/static/fonts/fraunces-v38-latin-regular.eot proxy/static/fonts/fraunces-v38-latin-regular.eot
pywb-test/static/fonts/fraunces-v38-latin-regular.ttf proxy/static/fonts/fraunces-v38-latin-regular.ttf
pywb-test/static/fonts/fraunces-v38-latin-regular.woff proxy/static/fonts/fraunces-v38-latin-regular.woff
pywb-test/static/fonts/fraunces-v38-latin-regular.woff2 proxy/static/fonts/fraunces-v38-latin-regular.woff2
pywb-test/static/fonts/spectral-v15-latin-600.eot proxy/static/fonts/spectral-v15-latin-600.eot
pywb-test/static/fonts/spectral-v15-latin-600.svg proxy/static/fonts/spectral-v15-latin-600.svg
pywb-test/static/fonts/spectral-v15-latin-600.ttf proxy/static/fonts/spectral-v15-latin-600.ttf
pywb-test/static/fonts/spectral-v15-latin-600.woff proxy/static/fonts/spectral-v15-latin-600.woff
pywb-test/static/fonts/spectral-v15-latin-600.woff2 proxy/static/fonts/spectral-v15-latin-600.woff2
pywb-test/static/fonts/spectral-v15-latin-700.eot proxy/static/fonts/spectral-v15-latin-700.eot
pywb-test/static/fonts/spectral-v15-latin-700.ttf proxy/static/fonts/spectral-v15-latin-700.ttf
pywb-test/static/fonts/spectral-v15-latin-700.woff proxy/static/fonts/spectral-v15-latin-700.woff
pywb-test/static/fonts/spectral-v15-latin-700.woff2 proxy/static/fonts/spectral-v15-latin-700.woff2
pywb-test/static/index.html proxy/static/index.html
pywb-test/static/introspect.html proxy/static/introspect.html
pywb-test/static/landing.css proxy/static/landing.css
+2 -2
pywb-test/static/landing.js proxy/static/landing.js
··· 113 113 114 114 // Render a single annotation card 115 115 async function renderAnnotation(annotation) { 116 - const { targetUrl, body, createdAt, authorDid, uri, authorHandle, exactText, selectorsJson } = annotation; 117 - const textQuoteSelector = getTextQuoteSelector(selectorsJson); 116 + const { targetUrl, body, createdAt, authorDid, uri, authorHandle, exactText, selectors } = annotation; 117 + const textQuoteSelector = getTextQuoteSelector(selectors); 118 118 const quotedText = exactText || textQuoteSelector?.exact; 119 119 const sourceUrl = targetUrl; 120 120 const fragmentUrl = buildTextFragmentUrl(sourceUrl, quotedText);
+1 -1
pywb-test/static/oauth-callback.html proxy/via-html/oauth-callback.html
··· 42 42 <h2>Completing login...</h2> 43 43 <p id="status">Processing OAuth response</p> 44 44 </div> 45 - <script type="module" src="/static/seams-oauth-callback.js"></script> 45 + <script type="module" src="../../entrypoints/via-client/oauth-callback.ts"></script> 46 46 </body> 47 47 </html>
pywb-test/static/oauth/callback.html proxy/static/oauth/callback.html
pywb-test/static/oauth/client-metadata.json proxy/static/oauth/client-metadata.json
pywb-test/static/oauth/ff/callback.html proxy/static/oauth/ff/callback.html
+1 -2
pywb-test/static/seams-sidebar.html proxy/via-html/seams-sidebar.html
··· 4 4 <meta charset="UTF-8"> 5 5 <meta name="viewport" content="width=device-width, initial-scale=1.0"> 6 6 <title>Seams Sidebar</title> 7 - <link rel="stylesheet" href="/static/assets/sidebar-J3iG1W2k.css"> 8 7 </head> 9 8 <body> 10 9 <div id="app"></div> 11 - <script type="module" src="/static/seams-sidebar.js"></script> 10 + <script type="module" src="../../entrypoints/via-client/sidebar.ts"></script> 12 11 </body> 13 12 </html>
pywb-test/static/sidebar-BBEPW7gD.css proxy/static/sidebar-BBEPW7gD.css
pywb-test/static/test-client.js proxy/static/test-client.js
-190
pywb-test/static/via-landing.html
··· 1 - <!DOCTYPE html> 2 - <html lang="en"> 3 - <head> 4 - <meta charset="UTF-8"> 5 - <meta name="viewport" content="width=device-width, initial-scale=1.0"> 6 - <title>Via - Seams Web Annotation Proxy</title> 7 - <style> 8 - * { 9 - margin: 0; 10 - padding: 0; 11 - box-sizing: border-box; 12 - } 13 - 14 - body { 15 - font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif; 16 - line-height: 1.6; 17 - color: #333; 18 - background: #f5f5f5; 19 - min-height: 100vh; 20 - display: flex; 21 - align-items: center; 22 - justify-content: center; 23 - padding: 20px; 24 - } 25 - 26 - .container { 27 - background: white; 28 - border-radius: 8px; 29 - box-shadow: 0 2px 8px rgba(0,0,0,0.1); 30 - max-width: 700px; 31 - width: 100%; 32 - padding: 50px 40px; 33 - } 34 - 35 - h1 { 36 - font-size: 2rem; 37 - margin-bottom: 10px; 38 - color: #222; 39 - font-weight: 600; 40 - } 41 - 42 - .tagline { 43 - font-size: 1rem; 44 - color: #666; 45 - margin-bottom: 30px; 46 - } 47 - 48 - .url-form { 49 - display: flex; 50 - gap: 10px; 51 - margin-bottom: 30px; 52 - } 53 - 54 - input[type="url"] { 55 - flex: 1; 56 - padding: 14px 18px; 57 - border: 2px solid #ddd; 58 - border-radius: 4px; 59 - font-size: 16px; 60 - transition: border-color 0.2s; 61 - } 62 - 63 - input[type="url"]:focus { 64 - outline: none; 65 - border-color: #0085ff; 66 - } 67 - 68 - button { 69 - padding: 14px 32px; 70 - background: #0085ff; 71 - color: white; 72 - border: none; 73 - border-radius: 4px; 74 - font-size: 16px; 75 - font-weight: 600; 76 - cursor: pointer; 77 - transition: background 0.2s; 78 - white-space: nowrap; 79 - } 80 - 81 - button:hover { 82 - background: #0070d9; 83 - } 84 - 85 - .info { 86 - padding: 20px; 87 - background: #f8f9fa; 88 - border-radius: 4px; 89 - font-size: 14px; 90 - color: #555; 91 - line-height: 1.6; 92 - } 93 - 94 - .info p { 95 - margin-bottom: 10px; 96 - } 97 - 98 - .info p:last-child { 99 - margin-bottom: 0; 100 - } 101 - 102 - .info a { 103 - color: #0085ff; 104 - text-decoration: none; 105 - } 106 - 107 - .info a:hover { 108 - text-decoration: underline; 109 - } 110 - 111 - .example { 112 - margin-top: 15px; 113 - font-size: 13px; 114 - color: #777; 115 - } 116 - 117 - .example a { 118 - color: #0085ff; 119 - text-decoration: none; 120 - } 121 - 122 - .example a:hover { 123 - text-decoration: underline; 124 - } 125 - 126 - .footer { 127 - margin-top: 40px; 128 - text-align: center; 129 - font-size: 14px; 130 - color: #999; 131 - } 132 - 133 - .footer a { 134 - color: #0085ff; 135 - text-decoration: none; 136 - } 137 - </style> 138 - </head> 139 - <body> 140 - <div class="container"> 141 - <h1>Seams Via</h1> 142 - <p class="tagline">View and annotate any web page</p> 143 - 144 - <form class="url-form" id="via-form"> 145 - <input 146 - type="url" 147 - id="url-input" 148 - placeholder="Paste a link to annotate" 149 - required 150 - autocomplete="url" 151 - autofocus 152 - /> 153 - <button type="submit">Annotate</button> 154 - </form> 155 - 156 - <div class="info"> 157 - <p>Via is a web annotation proxy that lets you view and create annotations on any web page using Seams.</p> 158 - <p>Enter a URL above to view the page with all public annotations from the Seams community.</p> 159 - </div> 160 - 161 - <div class="example"> 162 - <strong>Try it:</strong> 163 - <a href="/proxy/https://newsletter.squishy.computer/p/places-to-intervene-in-a-system"> 164 - View example article with annotations 165 - </a> 166 - </div> 167 - 168 - <div class="footer"> 169 - Powered by <a href="https://seams.so" target="_blank">Seams</a> 170 - </div> 171 - </div> 172 - 173 - <script> 174 - document.getElementById('via-form').addEventListener('submit', (e) => { 175 - e.preventDefault(); 176 - let url = document.getElementById('url-input').value.trim(); 177 - 178 - if (!url) return; 179 - 180 - // Add protocol if missing 181 - if (!url.match(/^https?:\/\//i)) { 182 - url = 'https://' + url; 183 - } 184 - 185 - // Redirect to pywb proxy route 186 - window.location.href = `/proxy/${url}`; 187 - }); 188 - </script> 189 - </body> 190 - </html>
+1 -1
scripts/inject-oauth-plugin.ts
··· 1 1 import type { PluginOption } from "vite"; 2 - import metadata from "../public/oauth/client-metadata.json"; 2 + import metadata from "../landing/oauth/client-metadata.json"; 3 3 4 4 type OAuthConfig = { 5 5 client_id: string;
+30
scripts/postbuild-via.sh
··· 1 + #!/usr/bin/env bash 2 + # Post-build script for via proxy - fixes HTML paths and copies to correct location 3 + 4 + set -e 5 + 6 + echo "📝 Post-processing via build..." 7 + 8 + # Copy BUILT HTML files from the nested output directory to static root 9 + # Vite outputs to proxy/static/proxy/via-html/ because of input structure 10 + cp proxy/static/proxy/via-html/*.html proxy/static/ 11 + 12 + # Clean up the nested directory structure created by Vite 13 + rm -rf proxy/static/proxy 14 + 15 + # Copy CSS and font files from landing to static root 16 + echo "🎨 Copying shared assets from landing..." 17 + cp landing/landing.css proxy/static/ 18 + cp landing/fonts.css proxy/static/ 19 + cp landing/landing.js proxy/static/ 20 + mkdir -p proxy/static/fonts 21 + cp landing/fonts/* proxy/static/fonts/ 22 + 23 + # Copy client-metadata.json from landing to static root (proxy shares the same client ID) 24 + cp landing/oauth/client-metadata.json proxy/static/ 25 + 26 + # Fix asset paths in HTML files (add /static prefix) 27 + # sed -i 's|src="/seams-|src="/static/seams-|g' proxy/static/*.html 28 + # sed -i 's|href="/assets/|href="/static/assets/|g' proxy/static/*.html 29 + 30 + echo "✅ Via build post-processing complete"
+2 -2
scripts/start-via.sh
··· 25 25 exit 1 26 26 fi 27 27 28 - # Start pywb in background (must run from pywb-test directory) 28 + # Start pywb in background (must run from proxy directory) 29 29 echo "📦 Starting pywb on port 8081..." 30 - (cd pywb-test && LD_LIBRARY_PATH="$LD_LIBRARY_PATH" wayback) & 30 + (cd proxy && LD_LIBRARY_PATH="$LD_LIBRARY_PATH" wayback -p 8081) & 31 31 PYWB_PID=$! 32 32 33 33 # Wait for pywb to start
+1 -1
server/cmd/server/main.go
··· 67 67 r.Get("/health", handler.Health) 68 68 69 69 // Serve static files for everything else 70 - publicDir := getEnv("PUBLIC_DIR", "../public") 70 + publicDir := getEnv("PUBLIC_DIR", "../landing") 71 71 log.Printf("Serving static files from %s", publicDir) 72 72 73 73 // Create final handler that checks API routes first, then static files
+6 -5
vite.via.config.ts
··· 2 2 import path from 'path'; 3 3 4 4 export default defineConfig({ 5 + base: '/static/', 5 6 build: { 6 - outDir: 'pywb-test/static', 7 + outDir: 'proxy/static', 7 8 emptyOutDir: false, 8 9 sourcemap: true, 9 10 rollupOptions: { 10 11 input: { 11 12 client: path.resolve(__dirname, 'entrypoints/via-client/main.ts'), 12 - sidebar: path.resolve(__dirname, 'entrypoints/via-client/sidebar.ts'), 13 - 'oauth-callback': path.resolve(__dirname, 'entrypoints/via-client/oauth-callback.ts'), 13 + 'seams-sidebar': path.resolve(__dirname, 'proxy/via-html/seams-sidebar.html'), 14 + 'oauth-callback': path.resolve(__dirname, 'proxy/via-html/oauth-callback.html'), 14 15 }, 15 16 output: { 16 17 entryFileNames: 'seams-[name].js', ··· 24 25 }, 25 26 }, 26 27 define: { 27 - 'import.meta.env.VITE_OAUTH_CLIENT_ID': JSON.stringify(process.env.VITE_OAUTH_CLIENT_ID || 'http://localhost:8081/static/client-metadata.json'), 28 - 'import.meta.env.VITE_OAUTH_REDIRECT_URI': JSON.stringify(process.env.VITE_OAUTH_REDIRECT_URI || 'http://localhost:8081/static/oauth-callback.html'), 28 + 'import.meta.env.VITE_OAUTH_CLIENT_ID': JSON.stringify(process.env.VITE_OAUTH_CLIENT_ID || 'https://seams.so/oauth/client-metadata.json'), 29 + 'import.meta.env.VITE_OAUTH_REDIRECT_URI': JSON.stringify(process.env.VITE_OAUTH_REDIRECT_URI || 'https://sure.seams.so/oauth-callback.html'), 29 30 'import.meta.env.VITE_OAUTH_SCOPE': JSON.stringify(process.env.VITE_OAUTH_SCOPE || 'atproto transition:generic'), 30 31 'import.meta.env.BACKEND_URL': JSON.stringify(process.env.BACKEND_URL || 'https://seams.so'), 31 32 },