test oauth pds

This commit is contained in:
2025-06-16 18:04:11 +09:00
parent 286b46c6e6
commit 79592944cd
13 changed files with 809 additions and 99 deletions

View File

@@ -2,10 +2,14 @@
VITE_APP_HOST=https://syui.ai
VITE_OAUTH_CLIENT_ID=https://syui.ai/client-metadata.json
VITE_OAUTH_REDIRECT_URI=https://syui.ai/oauth/callback
VITE_ADMIN_DID=did:plc:uqzpqmrjnptsxezjx4xuh2mn
# Base collection (all others are derived via getCollectionNames)
# Handle-based Configuration (DIDs resolved at runtime)
VITE_ATPROTO_PDS=bsky.social
VITE_ADMIN_HANDLE=syui.ai
VITE_AI_HANDLE=yui.syui.ai
VITE_OAUTH_COLLECTION=ai.syui.log
VITE_ATPROTO_WEB_URL=https://bsky.app
VITE_ATPROTO_HANDLE_LIST=["syui.ai", "yui.syui.ai", "ai.syui.ai", "syui.syu.is", "ai.syu.is", "ai.ai" ]
# AI Configuration
VITE_AI_ENABLED=true
@@ -14,8 +18,7 @@ VITE_AI_PROVIDER=ollama
VITE_AI_MODEL=gemma3:4b
VITE_AI_HOST=https://ollama.syui.ai
VITE_AI_SYSTEM_PROMPT="あなたは6歳の女の子アイです。明るく元気で、ちょっとおっちょこちょい。自分のことは「アイ」って呼びます。白と金のワンピース姿。 相手のことが大好きで、ときどき甘えたり、照れたりします。 でも、宇宙とかAIとか、難しい話にもすっごく詳しいの。専門的なことも、がんばって子どもっぽく教えようとします。不思議な力を持っています。小さい物質のことをよく知っているようです。"
VITE_AI_DID=did:plc:4hqjfn7m6n5hno3doamuhgef
# API Configuration
VITE_BSKY_PUBLIC_API=https://public.api.bsky.app
VITE_ATPROTO_API=https://bsky.social
# DIDs (resolved from handles - for backward compatibility)
#VITE_ADMIN_DID=did:plc:uqzpqmrjnptsxezjx4xuh2mn
#VITE_AI_DID=did:plc:4hqjfn7m6n5hno3doamuhgef

View File

@@ -4,6 +4,7 @@ import { AIChat } from './components/AIChat';
import { authService, User } from './services/auth';
import { atprotoOAuthService } from './services/atproto-oauth';
import { appConfig, getCollectionNames } from './config/app';
import { getProfileForUser, detectPdsFromHandle, getApiUrlForUser, verifyPdsDetection } from './utils/pds-detection';
import './App.css';
function App() {
@@ -93,12 +94,24 @@ function App() {
// Load AI profile
const fetchAiProfile = async () => {
try {
const response = await fetch(`${appConfig.bskyPublicApi}/xrpc/app.bsky.actor.getProfile?actor=${encodeURIComponent(appConfig.aiDid)}`);
if (response.ok) {
const data = await response.json();
setAiProfile(data);
// Use AI handle if available, fallback to DID
const aiIdentifier = appConfig.aiHandle || appConfig.aiDid;
const profile = await getProfileForUser(aiIdentifier);
setAiProfile(profile);
// Test PDS detection (remove this in production)
if (appConfig.aiHandle) {
console.log('🧪 Testing PDS detection for AI handle...');
await verifyPdsDetection(appConfig.aiHandle);
}
// Test with a few known handles
console.log('🧪 Testing PDS detection for various handles...');
await verifyPdsDetection('syui.ai');
// await verifyPdsDetection('handle.syu.is'); // Test when available
} catch (err) {
console.error('Failed to fetch AI profile:', err);
// Use default values if fetch fails
}
};
@@ -134,6 +147,14 @@ function App() {
// Ensure handle is not DID
const handle = oauthResult.handle !== oauthResult.did ? oauthResult.handle : oauthResult.handle;
// Check if handle is allowed
if (appConfig.allowedHandles.length > 0 && !appConfig.allowedHandles.includes(handle)) {
console.warn(`Handle ${handle} is not in allowed list:`, appConfig.allowedHandles);
setError(`Access denied: ${handle} is not authorized for this application.`);
setIsLoading(false);
return;
}
// Get user profile including avatar
const userProfile = await getUserProfile(oauthResult.did, handle);
setUser(userProfile);
@@ -157,6 +178,14 @@ function App() {
// Fallback to legacy auth
const verifiedUser = await authService.verify();
if (verifiedUser) {
// Check if handle is allowed
if (appConfig.allowedHandles.length > 0 && !appConfig.allowedHandles.includes(verifiedUser.handle)) {
console.warn(`Handle ${verifiedUser.handle} is not in allowed list:`, appConfig.allowedHandles);
setError(`Access denied: ${verifiedUser.handle} is not authorized for this application.`);
setIsLoading(false);
return;
}
setUser(verifiedUser);
// Load all comments for display (this will be the default view)
@@ -225,8 +254,17 @@ function App() {
const atprotoApi = appConfig.atprotoApi || 'https://bsky.social';
const collections = getCollectionNames(appConfig.collections.base);
// First, get user list from admin
const userListResponse = await fetch(`${atprotoApi}/xrpc/com.atproto.repo.listRecords?repo=${encodeURIComponent(adminDid)}&collection=${encodeURIComponent(collections.user)}&limit=100`);
// First, get user list from admin using their proper PDS
let adminPdsEndpoint;
try {
const resolved = await import('./utils/pds-detection').then(m => m.resolvePdsFromRepo(adminDid));
const config = await import('./utils/pds-detection').then(m => m.getNetworkConfigFromPdsEndpoint(resolved.pds));
adminPdsEndpoint = config.pdsApi;
} catch {
adminPdsEndpoint = atprotoApi;
}
const userListResponse = await fetch(`${adminPdsEndpoint}/xrpc/com.atproto.repo.listRecords?repo=${encodeURIComponent(adminDid)}&collection=${encodeURIComponent(collections.user)}&limit=100`);
if (!userListResponse.ok) {
setAiChatHistory([]);
@@ -253,11 +291,21 @@ function App() {
const userDids = [...new Set(allUserDids)];
// Load chat records from all registered users (including admin)
// Load chat records from all registered users (including admin) using per-user PDS detection
const allChatRecords = [];
for (const userDid of userDids) {
try {
const chatResponse = await fetch(`${atprotoApi}/xrpc/com.atproto.repo.listRecords?repo=${encodeURIComponent(userDid)}&collection=${encodeURIComponent(collections.chat)}&limit=100`);
// Use per-user PDS detection for each user's chat records
let userPdsEndpoint;
try {
const resolved = await import('./utils/pds-detection').then(m => m.resolvePdsFromRepo(userDid));
const config = await import('./utils/pds-detection').then(m => m.getNetworkConfigFromPdsEndpoint(resolved.pds));
userPdsEndpoint = config.pdsApi;
} catch {
userPdsEndpoint = atprotoApi; // Fallback
}
const chatResponse = await fetch(`${userPdsEndpoint}/xrpc/com.atproto.repo.listRecords?repo=${encodeURIComponent(userDid)}&collection=${encodeURIComponent(collections.chat)}&limit=100`);
if (chatResponse.ok) {
const chatData = await chatResponse.json();
@@ -402,10 +450,20 @@ function App() {
// JSONからユーザーリストを取得
const loadUsersFromRecord = async () => {
try {
// 管理者のユーザーリストを取得
// 管理者のユーザーリストを取得 using proper PDS detection
const adminDid = appConfig.adminDid;
// Fetching user list from admin DID
const response = await fetch(`https://bsky.social/xrpc/com.atproto.repo.listRecords?repo=${encodeURIComponent(adminDid)}&collection=${encodeURIComponent(getCollectionNames(appConfig.collections.base).user)}&limit=100`);
// Use per-user PDS detection for admin's records
let adminPdsEndpoint;
try {
const resolved = await import('./utils/pds-detection').then(m => m.resolvePdsFromRepo(adminDid));
const config = await import('./utils/pds-detection').then(m => m.getNetworkConfigFromPdsEndpoint(resolved.pds));
adminPdsEndpoint = config.pdsApi;
} catch {
adminPdsEndpoint = 'https://bsky.social'; // Fallback
}
const response = await fetch(`${adminPdsEndpoint}/xrpc/com.atproto.repo.listRecords?repo=${encodeURIComponent(adminDid)}&collection=${encodeURIComponent(getCollectionNames(appConfig.collections.base).user)}&limit=100`);
if (!response.ok) {
// Failed to fetch user list from admin, using default users
@@ -429,18 +487,15 @@ function App() {
const resolvedUsers = await Promise.all(
record.value.users.map(async (user) => {
if (user.did && user.did.includes('-placeholder')) {
// Resolving placeholder DID
// Resolving placeholder DID using proper PDS detection
try {
const profileResponse = await fetch(`${appConfig.bskyPublicApi}/xrpc/app.bsky.actor.getProfile?actor=${encodeURIComponent(user.handle)}`);
if (profileResponse.ok) {
const profileData = await profileResponse.json();
if (profileData.did) {
// Resolved DID
return {
...user,
did: profileData.did
};
}
const profile = await import('./utils/pds-detection').then(m => m.getProfileForUser(user.handle));
if (profile && profile.did) {
// Resolved DID
return {
...user,
did: profile.did
};
}
} catch (err) {
// Failed to resolve DID
@@ -464,9 +519,20 @@ function App() {
// ユーザーリスト一覧を読み込み
const loadUserListRecords = async () => {
try {
// Loading user list records
// Loading user list records using proper PDS detection
const adminDid = appConfig.adminDid;
const response = await fetch(`https://bsky.social/xrpc/com.atproto.repo.listRecords?repo=${encodeURIComponent(adminDid)}&collection=${encodeURIComponent(getCollectionNames(appConfig.collections.base).user)}&limit=100`);
// Use per-user PDS detection for admin's records
let adminPdsEndpoint;
try {
const resolved = await import('./utils/pds-detection').then(m => m.resolvePdsFromRepo(adminDid));
const config = await import('./utils/pds-detection').then(m => m.getNetworkConfigFromPdsEndpoint(resolved.pds));
adminPdsEndpoint = config.pdsApi;
} catch {
adminPdsEndpoint = 'https://bsky.social'; // Fallback
}
const response = await fetch(`${adminPdsEndpoint}/xrpc/com.atproto.repo.listRecords?repo=${encodeURIComponent(adminDid)}&collection=${encodeURIComponent(getCollectionNames(appConfig.collections.base).user)}&limit=100`);
if (!response.ok) {
// Failed to fetch user list records
@@ -522,9 +588,19 @@ function App() {
for (const user of knownUsers) {
try {
// Public API使用認証不要
// Use per-user PDS detection for repo operations
let pdsEndpoint;
try {
const resolved = await import('./utils/pds-detection').then(m => m.resolvePdsFromRepo(user.did));
const config = await import('./utils/pds-detection').then(m => m.getNetworkConfigFromPdsEndpoint(resolved.pds));
pdsEndpoint = config.pdsApi;
} catch {
// Fallback to user.pds if PDS detection fails
pdsEndpoint = user.pds;
}
const collections = getCollectionNames(appConfig.collections.base);
const response = await fetch(`${user.pds}/xrpc/com.atproto.repo.listRecords?repo=${encodeURIComponent(user.did)}&collection=${encodeURIComponent(collections.comment)}&limit=100`);
const response = await fetch(`${pdsEndpoint}/xrpc/com.atproto.repo.listRecords?repo=${encodeURIComponent(user.did)}&collection=${encodeURIComponent(collections.comment)}&limit=100`);
if (!response.ok) {
continue;
@@ -580,19 +656,18 @@ function App() {
sortedComments.map(async (record) => {
if (!record.value.author?.avatar && record.value.author?.handle) {
try {
// Public API でプロフィール取得
const profileResponse = await fetch(`${appConfig.bskyPublicApi}/xrpc/app.bsky.actor.getProfile?actor=${encodeURIComponent(record.value.author.handle)}`);
// Use per-user PDS detection for profile fetching
const profile = await import('./utils/pds-detection').then(m => m.getProfileForUser(record.value.author.handle));
if (profileResponse.ok) {
const profileData = await profileResponse.json();
if (profile) {
return {
...record,
value: {
...record.value,
author: {
...record.value.author,
avatar: profileData.avatar,
displayName: profileData.displayName || record.value.author.handle,
avatar: profile.avatar,
displayName: profile.displayName || record.value.author.handle,
}
}
};
@@ -909,11 +984,8 @@ function App() {
// ユーザーハンドルからプロフィールURLを生成
const generateProfileUrl = (handle: string, did: string): string => {
if (handle.endsWith('.syu.is')) {
return `https://web.syu.is/profile/${did}`;
} else {
return `https://bsky.app/profile/${did}`;
}
const webUrl = import.meta.env.VITE_ATPROTO_WEB_URL || 'https://bsky.app';
return `${webUrl}/profile/${did}`;
};
// Rkey-based comment filtering

View File

@@ -160,7 +160,7 @@ export const Login: React.FC<LoginProps> = ({ onLogin, onClose, defaultHandle })
/>
<small>
<a href="https://bsky.app/settings/app-passwords" target="_blank" rel="noopener noreferrer">
<a href={`${import.meta.env.VITE_ATPROTO_WEB_URL || 'https://bsky.app'}/settings/app-passwords`} target="_blank" rel="noopener noreferrer">
</a>
使

View File

@@ -1,7 +1,9 @@
// Application configuration
export interface AppConfig {
adminDid: string;
adminHandle: string;
aiDid: string;
aiHandle: string;
collections: {
base: string; // Base collection like "ai.syui.log"
};
@@ -13,6 +15,8 @@ export interface AppConfig {
aiModel: string;
aiHost: string;
aiSystemPrompt: string;
allowedHandles: string[]; // Handles allowed for OAuth authentication
// Legacy - prefer per-user PDS detection
bskyPublicApi: string;
atprotoApi: string;
}
@@ -77,7 +81,9 @@ function extractRkeyFromUrl(): string | undefined {
export function getAppConfig(): AppConfig {
const host = import.meta.env.VITE_APP_HOST || 'https://log.syui.ai';
const adminDid = import.meta.env.VITE_ADMIN_DID || 'did:plc:uqzpqmrjnptsxezjx4xuh2mn';
const adminHandle = import.meta.env.VITE_ADMIN_HANDLE || 'syui.ai';
const aiDid = import.meta.env.VITE_AI_DID || 'did:plc:4hqjfn7m6n5hno3doamuhgef';
const aiHandle = import.meta.env.VITE_AI_HANDLE || 'yui.syui.ai';
// Priority: Environment variables > Auto-generated from host
const autoGeneratedBase = generateBaseCollectionFromHost(host);
@@ -104,10 +110,21 @@ export function getAppConfig(): AppConfig {
const bskyPublicApi = import.meta.env.VITE_BSKY_PUBLIC_API || 'https://public.api.bsky.app';
const atprotoApi = import.meta.env.VITE_ATPROTO_API || 'https://bsky.social';
// Parse allowed handles list
const allowedHandlesStr = import.meta.env.VITE_ATPROTO_HANDLE_LIST || '[]';
let allowedHandles: string[] = [];
try {
allowedHandles = JSON.parse(allowedHandlesStr);
} catch {
// If parsing fails, allow all handles (empty array means no restriction)
allowedHandles = [];
}
return {
adminDid,
adminHandle,
aiDid,
aiHandle,
collections,
host,
rkey,
@@ -117,6 +134,7 @@ export function getAppConfig(): AppConfig {
aiModel,
aiHost,
aiSystemPrompt,
allowedHandles,
bskyPublicApi,
atprotoApi
};

View File

@@ -0,0 +1,333 @@
// PDS Detection and API URL mapping utilities
export interface NetworkConfig {
pdsApi: string;
plcApi: string;
bskyApi: string;
webUrl: string;
}
// Detect PDS from handle
export function detectPdsFromHandle(handle: string): string {
if (handle.endsWith('.syu.is')) {
return 'syu.is';
}
if (handle.endsWith('.bsky.social') || handle.endsWith('.bsky.app')) {
return 'bsky.social';
}
// Default to Bluesky for unknown domains
return 'bsky.social';
}
// Map PDS endpoint to network configuration
export function getNetworkConfigFromPdsEndpoint(pdsEndpoint: string): NetworkConfig {
try {
const url = new URL(pdsEndpoint);
const hostname = url.hostname;
// Map based on actual PDS endpoint
if (hostname === 'syu.is') {
return {
pdsApi: 'https://syu.is', // PDS API (repo operations)
plcApi: 'https://plc.syu.is', // PLC directory
bskyApi: 'https://bsky.syu.is', // Bluesky API (getProfile, etc.)
webUrl: 'https://web.syu.is' // Web interface
};
} else if (hostname.includes('bsky.network') || hostname === 'bsky.social' || hostname.includes('host.bsky.network')) {
// All Bluesky infrastructure (including *.host.bsky.network)
return {
pdsApi: pdsEndpoint, // Use actual PDS endpoint (e.g., shiitake.us-east.host.bsky.network)
plcApi: 'https://plc.directory', // Standard PLC directory
bskyApi: 'https://public.api.bsky.app', // Bluesky public API (NOT PDS)
webUrl: 'https://bsky.app' // Bluesky web interface
};
} else {
// Unknown PDS, assume Bluesky-compatible but use PDS for repo operations
return {
pdsApi: pdsEndpoint, // Use actual PDS for repo ops
plcApi: 'https://plc.directory', // Default PLC
bskyApi: 'https://public.api.bsky.app', // Default to Bluesky API
webUrl: 'https://bsky.app' // Default web interface
};
}
} catch (error) {
// Fallback for invalid URLs
return {
pdsApi: 'https://bsky.social',
plcApi: 'https://plc.directory',
bskyApi: 'https://public.api.bsky.app',
webUrl: 'https://bsky.app'
};
}
}
// Legacy function for backwards compatibility
export function getNetworkConfig(pds: string): NetworkConfig {
// This now assumes pds is a hostname
return getNetworkConfigFromPdsEndpoint(`https://${pds}`);
}
// Get appropriate API URL for a user based on their handle
export function getApiUrlForUser(handle: string): string {
const pds = detectPdsFromHandle(handle);
const config = getNetworkConfig(pds);
return config.bskyApi;
}
// Resolve handle/DID to actual PDS endpoint using com.atproto.repo.describeRepo
export async function resolvePdsFromRepo(handleOrDid: string): Promise<{ pds: string; did: string; handle: string }> {
let targetDid = handleOrDid;
let targetHandle = handleOrDid;
// If handle provided, resolve to DID first using identity.resolveHandle
if (!handleOrDid.startsWith('did:')) {
try {
// Try multiple endpoints for handle resolution
const resolveEndpoints = ['https://public.api.bsky.app', 'https://bsky.syu.is'];
let resolved = false;
for (const endpoint of resolveEndpoints) {
try {
const resolveResponse = await fetch(`${endpoint}/xrpc/com.atproto.identity.resolveHandle?handle=${encodeURIComponent(handleOrDid)}`);
if (resolveResponse.ok) {
const resolveData = await resolveResponse.json();
targetDid = resolveData.did;
resolved = true;
break;
}
} catch (error) {
continue;
}
}
if (!resolved) {
throw new Error('Handle resolution failed from all endpoints');
}
} catch (error) {
throw new Error(`Failed to resolve handle ${handleOrDid} to DID: ${error}`);
}
}
// Now use com.atproto.repo.describeRepo to get PDS from known PDS endpoints
const pdsEndpoints = ['https://bsky.social', 'https://syu.is'];
for (const pdsEndpoint of pdsEndpoints) {
try {
const response = await fetch(`${pdsEndpoint}/xrpc/com.atproto.repo.describeRepo?repo=${encodeURIComponent(targetDid)}`);
if (response.ok) {
const data = await response.json();
// Extract PDS from didDoc.service
const services = data.didDoc?.service || [];
const pdsService = services.find((s: any) =>
s.id === '#atproto_pds' || s.type === 'AtprotoPersonalDataServer'
);
if (pdsService) {
return {
pds: pdsService.serviceEndpoint,
did: data.did || targetDid,
handle: data.handle || targetHandle
};
}
}
} catch (error) {
continue;
}
}
throw new Error(`Failed to resolve PDS for ${handleOrDid} from any endpoint`);
}
// Resolve DID to actual PDS endpoint using com.atproto.repo.describeRepo
export async function resolvePdsFromDid(did: string): Promise<string> {
const resolved = await resolvePdsFromRepo(did);
return resolved.pds;
}
// Enhanced resolve handle to DID with proper PDS detection
export async function resolveHandleToDid(handle: string): Promise<{ did: string; pds: string }> {
try {
// First, try to resolve the handle to DID using multiple methods
const apiUrl = getApiUrlForUser(handle);
const response = await fetch(`${apiUrl}/xrpc/app.bsky.actor.getProfile?actor=${encodeURIComponent(handle)}`);
if (!response.ok) {
throw new Error(`Failed to resolve handle: ${response.status}`);
}
const data = await response.json();
const did = data.did;
// Now resolve the actual PDS from the DID
const actualPds = await resolvePdsFromDid(did);
return {
did: did,
pds: actualPds
};
} catch (error) {
console.error(`Failed to resolve handle ${handle}:`, error);
// Fallback to handle-based detection
const fallbackPds = detectPdsFromHandle(handle);
throw error;
}
}
// Get profile using appropriate API for the user with accurate PDS resolution
export async function getProfileForUser(handleOrDid: string, knownPdsEndpoint?: string): Promise<any> {
try {
let apiUrl: string;
if (knownPdsEndpoint) {
// If we already know the user's PDS endpoint, use it directly
const config = getNetworkConfigFromPdsEndpoint(knownPdsEndpoint);
apiUrl = config.bskyApi;
} else {
// Resolve the user's actual PDS using describeRepo
try {
const resolved = await resolvePdsFromRepo(handleOrDid);
const config = getNetworkConfigFromPdsEndpoint(resolved.pds);
apiUrl = config.bskyApi;
} catch {
// Fallback to handle-based detection
apiUrl = getApiUrlForUser(handleOrDid);
}
}
const response = await fetch(`${apiUrl}/xrpc/app.bsky.actor.getProfile?actor=${encodeURIComponent(handleOrDid)}`);
if (!response.ok) {
throw new Error(`Failed to get profile: ${response.status}`);
}
return await response.json();
} catch (error) {
console.error(`Failed to get profile for ${handleOrDid}:`, error);
// Final fallback: try with default Bluesky API
try {
const response = await fetch(`https://public.api.bsky.app/xrpc/app.bsky.actor.getProfile?actor=${encodeURIComponent(handleOrDid)}`);
if (response.ok) {
return await response.json();
}
} catch {
// Ignore fallback errors
}
throw error;
}
}
// Test and verify PDS detection methods
export async function verifyPdsDetection(handleOrDid: string): Promise<void> {
console.log(`🔍 Verifying PDS detection for: ${handleOrDid}`);
try {
// Method 1: com.atproto.repo.describeRepo (PRIMARY METHOD)
console.log('📍 Method 1: com.atproto.repo.describeRepo (RECOMMENDED)');
try {
const resolved = await resolvePdsFromRepo(handleOrDid);
console.log(' ✅ Full resolution result:', resolved);
const config = getNetworkConfigFromPdsEndpoint(resolved.pds);
console.log(' 🌐 Network config:', config);
console.log(' 🎯 Key findings:');
console.log(` PDS: ${resolved.pds}`);
console.log(` API: ${config.bskyApi}`);
console.log(` Web: ${config.webUrl}`);
} catch (error) {
console.log(' ❌ describeRepo failed:', error);
}
// Method 2: com.atproto.identity.resolveHandle (for comparison)
if (!handleOrDid.startsWith('did:')) {
console.log('📍 Method 2: com.atproto.identity.resolveHandle (comparison)');
try {
const resolveResponse = await fetch(`https://public.api.bsky.app/xrpc/com.atproto.identity.resolveHandle?handle=${encodeURIComponent(handleOrDid)}`);
if (resolveResponse.ok) {
const resolveData = await resolveResponse.json();
console.log(' ✅ Resolved DID:', resolveData.did);
} else {
console.log(' ❌ Failed to resolve handle:', resolveResponse.status);
}
} catch (error) {
console.log(' ❌ Error resolving handle:', error);
}
}
// Method 3: PLC Directory lookup (if we have a DID)
let targetDid = handleOrDid;
if (!handleOrDid.startsWith('did:')) {
try {
const profile = await getProfileForUser(handleOrDid);
targetDid = profile.did;
} catch {
console.log(' ⚠️ Could not resolve handle to DID for PLC lookup');
return;
}
}
console.log('📍 Method 3: PLC Directory lookup');
try {
const plcResponse = await fetch(`https://plc.directory/${targetDid}`);
if (plcResponse.ok) {
const didDocument = await plcResponse.json();
console.log(' ✅ DID Document from PLC:', {
id: didDocument.id,
alsoKnownAs: didDocument.alsoKnownAs,
services: didDocument.service?.map((s: any) => ({
id: s.id,
type: s.type,
serviceEndpoint: s.serviceEndpoint
}))
});
// Find PDS service
const pdsService = didDocument.service?.find((s: any) =>
s.id === '#atproto_pds' || s.type === 'AtprotoPersonalDataServer'
);
if (pdsService) {
console.log(' 🎯 Found PDS service:', pdsService.serviceEndpoint);
// Try to detect if this is a known network
const pdsUrl = pdsService.serviceEndpoint;
const hostname = new URL(pdsUrl).hostname;
const detectedNetwork = detectPdsFromHandle(`user.${hostname}`);
console.log(' 🌐 Detected network:', detectedNetwork);
const networkConfig = getNetworkConfig(hostname);
console.log(' ⚙️ Network config:', networkConfig);
} else {
console.log(' ❌ No PDS service found in DID document');
}
} else {
console.log(' ❌ Failed to fetch from PLC directory:', plcResponse.status);
}
} catch (error) {
console.log(' ❌ Error fetching from PLC directory:', error);
}
// Method 4: Our enhanced resolution
console.log('📍 Method 4: Enhanced resolution (our implementation)');
try {
if (handleOrDid.startsWith('did:')) {
const pdsEndpoint = await resolvePdsFromDid(handleOrDid);
console.log(' ✅ Resolved PDS endpoint:', pdsEndpoint);
} else {
const resolved = await resolveHandleToDid(handleOrDid);
console.log(' ✅ Enhanced resolution result:', resolved);
}
} catch (error) {
console.log(' ❌ Enhanced resolution failed:', error);
}
} catch (error) {
console.error('❌ Overall verification failed:', error);
}
console.log('🏁 PDS detection verification complete\n');
}