test oauth pds
This commit is contained in:
@@ -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
|
||||
|
@@ -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
|
||||
|
@@ -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>
|
||||
を使用してください
|
||||
|
@@ -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
|
||||
};
|
||||
|
333
oauth/src/utils/pds-detection.ts
Normal file
333
oauth/src/utils/pds-detection.ts
Normal 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');
|
||||
}
|
Reference in New Issue
Block a user