test pds oauth did
This commit is contained in:
@ -5,6 +5,7 @@ import { authService, User } from './services/auth';
|
||||
import { atprotoOAuthService } from './services/atproto-oauth';
|
||||
import { appConfig, getCollectionNames } from './config/app';
|
||||
import { getProfileForUser, detectPdsFromHandle, getApiUrlForUser, verifyPdsDetection, getNetworkConfigFromPdsEndpoint, getNetworkConfig } from './utils/pds-detection';
|
||||
import { isValidDid } from './utils/validation';
|
||||
import './App.css';
|
||||
|
||||
function App() {
|
||||
@ -338,16 +339,23 @@ function App() {
|
||||
const currentAdminDid = adminDid || appConfig.adminDid;
|
||||
|
||||
// Don't proceed if we don't have a valid DID
|
||||
if (!currentAdminDid) {
|
||||
if (!currentAdminDid || !isValidDid(currentAdminDid)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Use admin's PDS from config
|
||||
const adminConfig = getNetworkConfig(appConfig.atprotoPds);
|
||||
const collections = getCollectionNames(appConfig.collections.base);
|
||||
// Resolve admin's actual PDS from their DID
|
||||
let adminPdsEndpoint;
|
||||
try {
|
||||
const resolved = await import('./utils/pds-detection').then(m => m.resolvePdsFromRepo(currentAdminDid));
|
||||
const config = await import('./utils/pds-detection').then(m => m.getNetworkConfigFromPdsEndpoint(resolved.pds));
|
||||
adminPdsEndpoint = config.pdsApi;
|
||||
} catch {
|
||||
// Fallback to configured PDS
|
||||
const adminConfig = getNetworkConfig(appConfig.atprotoPds);
|
||||
adminPdsEndpoint = adminConfig.pdsApi;
|
||||
}
|
||||
|
||||
// First, get user list from admin using their proper PDS
|
||||
const adminPdsEndpoint = adminConfig.pdsApi;
|
||||
const collections = getCollectionNames(appConfig.collections.base);
|
||||
|
||||
const userListResponse = await fetch(`${adminPdsEndpoint}/xrpc/com.atproto.repo.listRecords?repo=${encodeURIComponent(currentAdminDid)}&collection=${encodeURIComponent(collections.user)}&limit=100`);
|
||||
|
||||
@ -383,6 +391,10 @@ function App() {
|
||||
// Use per-user PDS detection for each user's chat records
|
||||
let userPdsEndpoint;
|
||||
try {
|
||||
// Validate DID format before making API calls
|
||||
if (!userDid || !userDid.startsWith('did:')) {
|
||||
continue;
|
||||
}
|
||||
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;
|
||||
@ -396,6 +408,9 @@ function App() {
|
||||
const chatData = await chatResponse.json();
|
||||
const records = chatData.records || [];
|
||||
allChatRecords.push(...records);
|
||||
} else if (chatResponse.status === 400) {
|
||||
// Skip 400 errors (repo not found, etc)
|
||||
continue;
|
||||
}
|
||||
} catch (err) {
|
||||
continue;
|
||||
@ -443,13 +458,21 @@ function App() {
|
||||
const currentAdminDid = adminDid || appConfig.adminDid;
|
||||
|
||||
// Don't proceed if we don't have a valid DID
|
||||
if (!currentAdminDid) {
|
||||
if (!currentAdminDid || !isValidDid(currentAdminDid)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Use admin's PDS for collection access (from config)
|
||||
const adminConfig = getNetworkConfig(appConfig.atprotoPds);
|
||||
const atprotoApi = adminConfig.pdsApi;
|
||||
// Resolve admin's actual PDS from their DID
|
||||
let atprotoApi;
|
||||
try {
|
||||
const resolved = await import('./utils/pds-detection').then(m => m.resolvePdsFromRepo(currentAdminDid));
|
||||
const config = await import('./utils/pds-detection').then(m => m.getNetworkConfigFromPdsEndpoint(resolved.pds));
|
||||
atprotoApi = config.pdsApi;
|
||||
} catch {
|
||||
// Fallback to configured PDS
|
||||
const adminConfig = getNetworkConfig(appConfig.atprotoPds);
|
||||
atprotoApi = adminConfig.pdsApi;
|
||||
}
|
||||
const collections = getCollectionNames(appConfig.collections.base);
|
||||
|
||||
// Load lang:en records
|
||||
|
@ -204,25 +204,47 @@ class AtprotoOAuthService {
|
||||
return `${origin}/client-metadata.json`;
|
||||
}
|
||||
|
||||
private detectPDSFromHandle(handle: string): string {
|
||||
|
||||
private async detectPDSFromHandle(handle: string): Promise<string> {
|
||||
// Handle detection for OAuth PDS routing
|
||||
|
||||
// Supported PDS hosts and their corresponding handles
|
||||
// Check if handle ends with known PDS domains first
|
||||
const pdsMapping = {
|
||||
'syu.is': 'https://syu.is',
|
||||
'bsky.social': 'https://bsky.social',
|
||||
};
|
||||
|
||||
// Check if handle ends with known PDS domains
|
||||
for (const [domain, pdsUrl] of Object.entries(pdsMapping)) {
|
||||
if (handle.endsWith(`.${domain}`)) {
|
||||
|
||||
// Using PDS for domain match
|
||||
return pdsUrl;
|
||||
}
|
||||
}
|
||||
|
||||
// For handles that don't match domain patterns, resolve via API
|
||||
try {
|
||||
// Try to resolve handle to get the actual PDS
|
||||
const endpoints = ['https://syu.is', 'https://bsky.social'];
|
||||
|
||||
for (const endpoint of endpoints) {
|
||||
try {
|
||||
const response = await fetch(`${endpoint}/xrpc/com.atproto.identity.resolveHandle?handle=${encodeURIComponent(handle)}`);
|
||||
if (response.ok) {
|
||||
const data = await response.json();
|
||||
if (data.did) {
|
||||
console.log('[OAuth Debug] Resolved handle via', endpoint, '- using that PDS');
|
||||
return endpoint;
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.log('[OAuth Debug] Handle resolution failed, using default');
|
||||
}
|
||||
|
||||
// Default to bsky.social
|
||||
|
||||
// Using default bsky.social
|
||||
return 'https://bsky.social';
|
||||
}
|
||||
|
||||
@ -250,41 +272,53 @@ class AtprotoOAuthService {
|
||||
|
||||
|
||||
// Detect PDS based on handle
|
||||
const pdsUrl = this.detectPDSFromHandle(handle);
|
||||
const pdsUrl = await this.detectPDSFromHandle(handle);
|
||||
// Starting OAuth flow
|
||||
|
||||
|
||||
// Re-initialize OAuth client with correct PDS if needed
|
||||
if (pdsUrl !== 'https://bsky.social') {
|
||||
|
||||
this.oauthClient = await BrowserOAuthClient.load({
|
||||
clientId: this.getClientId(),
|
||||
handleResolver: pdsUrl,
|
||||
});
|
||||
}
|
||||
// Always re-initialize OAuth client with detected PDS
|
||||
// Re-initializing OAuth client
|
||||
|
||||
// Clear existing client to force fresh initialization
|
||||
this.oauthClient = null;
|
||||
this.initializePromise = null;
|
||||
|
||||
this.oauthClient = await BrowserOAuthClient.load({
|
||||
clientId: this.getClientId(),
|
||||
handleResolver: pdsUrl,
|
||||
});
|
||||
|
||||
// OAuth client initialized
|
||||
|
||||
// Start OAuth authorization flow
|
||||
|
||||
|
||||
try {
|
||||
const authUrl = await this.oauthClient.authorize(handle, {
|
||||
// Starting OAuth authorization
|
||||
|
||||
// Try to authorize with DID instead of handle for syu.is PDS only
|
||||
let authTarget = handle;
|
||||
if (pdsUrl === 'https://syu.is') {
|
||||
try {
|
||||
const resolveResponse = await fetch(`${pdsUrl}/xrpc/com.atproto.identity.resolveHandle?handle=${encodeURIComponent(handle)}`);
|
||||
if (resolveResponse.ok) {
|
||||
const resolveData = await resolveResponse.json();
|
||||
authTarget = resolveData.did;
|
||||
// Using DID for syu.is OAuth workaround
|
||||
}
|
||||
} catch (e) {
|
||||
// Could not resolve to DID, using handle
|
||||
}
|
||||
}
|
||||
|
||||
const authUrl = await this.oauthClient.authorize(authTarget, {
|
||||
scope: 'atproto transition:generic',
|
||||
});
|
||||
|
||||
|
||||
|
||||
// Store some debug info before redirect
|
||||
sessionStorage.setItem('oauth_debug_pre_redirect', JSON.stringify({
|
||||
timestamp: new Date().toISOString(),
|
||||
handle: handle,
|
||||
authUrl: authUrl.toString(),
|
||||
currentUrl: window.location.href
|
||||
}));
|
||||
|
||||
// Redirect to authorization server
|
||||
|
||||
window.location.href = authUrl.toString();
|
||||
} catch (authorizeError) {
|
||||
|
||||
// Authorization failed
|
||||
throw authorizeError;
|
||||
}
|
||||
|
||||
|
135
oauth/src/tests/console-test.ts
Normal file
135
oauth/src/tests/console-test.ts
Normal file
@ -0,0 +1,135 @@
|
||||
// Simple console test for OAuth app
|
||||
// This runs before 'npm run preview' to display test results
|
||||
|
||||
// Mock import.meta.env for Node.js environment
|
||||
(global as any).import = {
|
||||
meta: {
|
||||
env: {
|
||||
VITE_ATPROTO_PDS: process.env.VITE_ATPROTO_PDS || 'syu.is',
|
||||
VITE_ADMIN_HANDLE: process.env.VITE_ADMIN_HANDLE || 'ai.syui.ai',
|
||||
VITE_AI_HANDLE: process.env.VITE_AI_HANDLE || 'ai.syui.ai',
|
||||
VITE_OAUTH_COLLECTION: process.env.VITE_OAUTH_COLLECTION || 'ai.syui.log',
|
||||
VITE_ATPROTO_HANDLE_LIST: process.env.VITE_ATPROTO_HANDLE_LIST || '["syui.ai", "ai.syui.ai", "yui.syui.ai"]',
|
||||
VITE_APP_HOST: process.env.VITE_APP_HOST || 'https://log.syui.ai'
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Simple implementation of functions for testing
|
||||
function detectPdsFromHandle(handle: string): string {
|
||||
if (handle.endsWith('.syu.is') || handle.endsWith('.syui.ai')) {
|
||||
return 'syu.is';
|
||||
}
|
||||
if (handle.endsWith('.bsky.social')) {
|
||||
return 'bsky.social';
|
||||
}
|
||||
// Default case - check if it's in the allowed list
|
||||
const allowedHandles = JSON.parse((global as any).import.meta.env.VITE_ATPROTO_HANDLE_LIST || '[]');
|
||||
if (allowedHandles.includes(handle)) {
|
||||
return (global as any).import.meta.env.VITE_ATPROTO_PDS || 'syu.is';
|
||||
}
|
||||
return 'bsky.social';
|
||||
}
|
||||
|
||||
function getNetworkConfig(pds: string) {
|
||||
switch (pds) {
|
||||
case 'bsky.social':
|
||||
case 'bsky.app':
|
||||
return {
|
||||
pdsApi: `https://${pds}`,
|
||||
plcApi: 'https://plc.directory',
|
||||
bskyApi: 'https://public.api.bsky.app',
|
||||
webUrl: 'https://bsky.app'
|
||||
};
|
||||
case 'syu.is':
|
||||
return {
|
||||
pdsApi: 'https://syu.is',
|
||||
plcApi: 'https://plc.syu.is',
|
||||
bskyApi: 'https://bsky.syu.is',
|
||||
webUrl: 'https://web.syu.is'
|
||||
};
|
||||
default:
|
||||
return {
|
||||
pdsApi: `https://${pds}`,
|
||||
plcApi: 'https://plc.directory',
|
||||
bskyApi: 'https://public.api.bsky.app',
|
||||
webUrl: 'https://bsky.app'
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// Main test execution
|
||||
console.log('\n=== OAuth App Configuration Tests ===\n');
|
||||
|
||||
// Test 1: Handle input behavior
|
||||
console.log('1. Handle Input → PDS Detection:');
|
||||
const testHandles = [
|
||||
'syui.ai',
|
||||
'syui.syu.is',
|
||||
'syui.syui.ai',
|
||||
'test.bsky.social',
|
||||
'unknown.handle'
|
||||
];
|
||||
|
||||
testHandles.forEach(handle => {
|
||||
const pds = detectPdsFromHandle(handle);
|
||||
const config = getNetworkConfig(pds);
|
||||
console.log(` ${handle.padEnd(20)} → PDS: ${pds.padEnd(12)} → API: ${config.pdsApi}`);
|
||||
});
|
||||
|
||||
// Test 2: Environment variable impact
|
||||
console.log('\n2. Current Environment Configuration:');
|
||||
const env = (global as any).import.meta.env;
|
||||
console.log(` VITE_ATPROTO_PDS: ${env.VITE_ATPROTO_PDS}`);
|
||||
console.log(` VITE_ADMIN_HANDLE: ${env.VITE_ADMIN_HANDLE}`);
|
||||
console.log(` VITE_AI_HANDLE: ${env.VITE_AI_HANDLE}`);
|
||||
console.log(` VITE_OAUTH_COLLECTION: ${env.VITE_OAUTH_COLLECTION}`);
|
||||
console.log(` VITE_ATPROTO_HANDLE_LIST: ${env.VITE_ATPROTO_HANDLE_LIST}`);
|
||||
|
||||
// Test 3: API endpoint generation
|
||||
console.log('\n3. Generated API Endpoints:');
|
||||
const adminPds = detectPdsFromHandle(env.VITE_ADMIN_HANDLE);
|
||||
const adminConfig = getNetworkConfig(adminPds);
|
||||
console.log(` Admin PDS detection: ${env.VITE_ADMIN_HANDLE} → ${adminPds}`);
|
||||
console.log(` Admin API endpoints:`);
|
||||
console.log(` - PDS API: ${adminConfig.pdsApi}`);
|
||||
console.log(` - Bsky API: ${adminConfig.bskyApi}`);
|
||||
console.log(` - Web URL: ${adminConfig.webUrl}`);
|
||||
|
||||
// Test 4: Collection URLs
|
||||
console.log('\n4. Collection API URLs:');
|
||||
const baseCollection = env.VITE_OAUTH_COLLECTION;
|
||||
console.log(` User list: ${adminConfig.pdsApi}/xrpc/com.atproto.repo.listRecords?repo=${env.VITE_ADMIN_HANDLE}&collection=${baseCollection}.user`);
|
||||
console.log(` Chat: ${adminConfig.pdsApi}/xrpc/com.atproto.repo.listRecords?repo=${env.VITE_ADMIN_HANDLE}&collection=${baseCollection}.chat`);
|
||||
console.log(` Lang: ${adminConfig.pdsApi}/xrpc/com.atproto.repo.listRecords?repo=${env.VITE_ADMIN_HANDLE}&collection=${baseCollection}.chat.lang`);
|
||||
console.log(` Comment: ${adminConfig.pdsApi}/xrpc/com.atproto.repo.listRecords?repo=${env.VITE_ADMIN_HANDLE}&collection=${baseCollection}.chat.comment`);
|
||||
|
||||
// Test 5: OAuth routing logic
|
||||
console.log('\n5. OAuth Authorization Logic:');
|
||||
const allowedHandles = JSON.parse(env.VITE_ATPROTO_HANDLE_LIST || '[]');
|
||||
console.log(` Allowed handles: ${JSON.stringify(allowedHandles)}`);
|
||||
console.log(` OAuth scenarios:`);
|
||||
|
||||
const oauthTestCases = [
|
||||
'syui.ai', // Should use syu.is (in allowed list)
|
||||
'test.syu.is', // Should use syu.is (*.syu.is pattern)
|
||||
'user.bsky.social' // Should use bsky.social (default)
|
||||
];
|
||||
|
||||
oauthTestCases.forEach(handle => {
|
||||
const pds = detectPdsFromHandle(handle);
|
||||
const isAllowed = allowedHandles.includes(handle);
|
||||
const reason = handle.endsWith('.syu.is') ? '*.syu.is pattern' :
|
||||
isAllowed ? 'in allowed list' :
|
||||
'default';
|
||||
console.log(` ${handle.padEnd(20)} → https://${pds}/oauth/authorize (${reason})`);
|
||||
});
|
||||
|
||||
// Test 6: AI Profile Resolution
|
||||
console.log('\n6. AI Profile Resolution:');
|
||||
const aiPds = detectPdsFromHandle(env.VITE_AI_HANDLE);
|
||||
const aiConfig = getNetworkConfig(aiPds);
|
||||
console.log(` AI Handle: ${env.VITE_AI_HANDLE} → PDS: ${aiPds}`);
|
||||
console.log(` AI Profile API: ${aiConfig.bskyApi}/xrpc/app.bsky.actor.getProfile?actor=${env.VITE_AI_HANDLE}`);
|
||||
|
||||
console.log('\n=== Tests Complete ===\n');
|
141
oauth/src/tests/oauth.test.ts
Normal file
141
oauth/src/tests/oauth.test.ts
Normal file
@ -0,0 +1,141 @@
|
||||
import { describe, it, expect, beforeEach } from 'vitest';
|
||||
import { getAppConfig } from '../config/app';
|
||||
import { detectPdsFromHandle, getNetworkConfig } from '../App';
|
||||
|
||||
// Test helper to mock environment variables
|
||||
const mockEnv = (vars: Record<string, string>) => {
|
||||
Object.keys(vars).forEach(key => {
|
||||
(import.meta.env as any)[key] = vars[key];
|
||||
});
|
||||
};
|
||||
|
||||
describe('OAuth App Tests', () => {
|
||||
describe('Handle Input Behavior', () => {
|
||||
it('should detect PDS for syui.ai (Bluesky)', () => {
|
||||
const pds = detectPdsFromHandle('syui.ai');
|
||||
expect(pds).toBe('bsky.social');
|
||||
});
|
||||
|
||||
it('should detect PDS for syui.syu.is (syu.is)', () => {
|
||||
const pds = detectPdsFromHandle('syui.syu.is');
|
||||
expect(pds).toBe('syu.is');
|
||||
});
|
||||
|
||||
it('should detect PDS for syui.syui.ai (syu.is)', () => {
|
||||
const pds = detectPdsFromHandle('syui.syui.ai');
|
||||
expect(pds).toBe('syu.is');
|
||||
});
|
||||
|
||||
it('should use network config for different PDS', () => {
|
||||
const bskyConfig = getNetworkConfig('bsky.social');
|
||||
expect(bskyConfig.pdsApi).toBe('https://bsky.social');
|
||||
expect(bskyConfig.bskyApi).toBe('https://public.api.bsky.app');
|
||||
expect(bskyConfig.webUrl).toBe('https://bsky.app');
|
||||
|
||||
const syuisConfig = getNetworkConfig('syu.is');
|
||||
expect(syuisConfig.pdsApi).toBe('https://syu.is');
|
||||
expect(syuisConfig.bskyApi).toBe('https://bsky.syu.is');
|
||||
expect(syuisConfig.webUrl).toBe('https://web.syu.is');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Environment Variable Changes', () => {
|
||||
beforeEach(() => {
|
||||
// Reset environment variables
|
||||
delete (import.meta.env as any).VITE_ATPROTO_PDS;
|
||||
delete (import.meta.env as any).VITE_ADMIN_HANDLE;
|
||||
delete (import.meta.env as any).VITE_AI_HANDLE;
|
||||
});
|
||||
|
||||
it('should use correct PDS for AI profile', () => {
|
||||
mockEnv({
|
||||
VITE_ATPROTO_PDS: 'syu.is',
|
||||
VITE_ADMIN_HANDLE: 'ai.syui.ai',
|
||||
VITE_AI_HANDLE: 'ai.syui.ai'
|
||||
});
|
||||
|
||||
const config = getAppConfig();
|
||||
expect(config.atprotoPds).toBe('syu.is');
|
||||
expect(config.adminHandle).toBe('ai.syui.ai');
|
||||
expect(config.aiHandle).toBe('ai.syui.ai');
|
||||
|
||||
// Network config should use syu.is endpoints
|
||||
const networkConfig = getNetworkConfig(config.atprotoPds);
|
||||
expect(networkConfig.bskyApi).toBe('https://bsky.syu.is');
|
||||
});
|
||||
|
||||
it('should construct correct API requests for admin userlist', () => {
|
||||
mockEnv({
|
||||
VITE_ATPROTO_PDS: 'syu.is',
|
||||
VITE_ADMIN_HANDLE: 'ai.syui.ai',
|
||||
VITE_OAUTH_COLLECTION: 'ai.syui.log'
|
||||
});
|
||||
|
||||
const config = getAppConfig();
|
||||
const networkConfig = getNetworkConfig(config.atprotoPds);
|
||||
const userListUrl = `${networkConfig.pdsApi}/xrpc/com.atproto.repo.listRecords?repo=${config.adminHandle}&collection=${config.collections.base}.user`;
|
||||
|
||||
expect(userListUrl).toBe('https://syu.is/xrpc/com.atproto.repo.listRecords?repo=ai.syui.ai&collection=ai.syui.log.user');
|
||||
});
|
||||
});
|
||||
|
||||
describe('OAuth Login Flow', () => {
|
||||
it('should use syu.is OAuth for handles in VITE_ATPROTO_HANDLE_LIST', () => {
|
||||
mockEnv({
|
||||
VITE_ATPROTO_HANDLE_LIST: '["syui.ai", "ai.syui.ai", "yui.syui.ai"]',
|
||||
VITE_ATPROTO_PDS: 'syu.is'
|
||||
});
|
||||
|
||||
const config = getAppConfig();
|
||||
const handle = 'syui.ai';
|
||||
|
||||
// Check if handle is in allowed list
|
||||
expect(config.allowedHandles).toContain(handle);
|
||||
|
||||
// Should use configured PDS for OAuth
|
||||
const expectedAuthUrl = `https://${config.atprotoPds}/oauth/authorize`;
|
||||
expect(expectedAuthUrl).toContain('syu.is');
|
||||
});
|
||||
|
||||
it('should use syu.is OAuth for *.syu.is handles', () => {
|
||||
const handle = 'test.syu.is';
|
||||
const pds = detectPdsFromHandle(handle);
|
||||
expect(pds).toBe('syu.is');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// Terminal display test output
|
||||
export function runTerminalTests() {
|
||||
console.log('\n=== OAuth App Tests ===\n');
|
||||
|
||||
// Test 1: Handle input behavior
|
||||
console.log('1. Handle Input Detection:');
|
||||
const handles = ['syui.ai', 'syui.syu.is', 'syui.syui.ai'];
|
||||
handles.forEach(handle => {
|
||||
const pds = detectPdsFromHandle(handle);
|
||||
console.log(` ${handle} → PDS: ${pds}`);
|
||||
});
|
||||
|
||||
// Test 2: Environment variable impact
|
||||
console.log('\n2. Environment Variables:');
|
||||
const config = getAppConfig();
|
||||
console.log(` VITE_ATPROTO_PDS: ${config.atprotoPds}`);
|
||||
console.log(` VITE_ADMIN_HANDLE: ${config.adminHandle}`);
|
||||
console.log(` VITE_AI_HANDLE: ${config.aiHandle}`);
|
||||
console.log(` VITE_OAUTH_COLLECTION: ${config.collections.base}`);
|
||||
|
||||
// Test 3: API endpoints
|
||||
console.log('\n3. API Endpoints:');
|
||||
const networkConfig = getNetworkConfig(config.atprotoPds);
|
||||
console.log(` Admin PDS API: ${networkConfig.pdsApi}`);
|
||||
console.log(` Admin Bsky API: ${networkConfig.bskyApi}`);
|
||||
console.log(` User list URL: ${networkConfig.pdsApi}/xrpc/com.atproto.repo.listRecords?repo=${config.adminHandle}&collection=${config.collections.base}.user`);
|
||||
|
||||
// Test 4: OAuth routing
|
||||
console.log('\n4. OAuth Routing:');
|
||||
console.log(` Allowed handles: ${JSON.stringify(config.allowedHandles)}`);
|
||||
console.log(` OAuth endpoint: https://${config.atprotoPds}/oauth/authorize`);
|
||||
|
||||
console.log('\n=== End Tests ===\n');
|
||||
}
|
@ -1,5 +1,7 @@
|
||||
// PDS Detection and API URL mapping utilities
|
||||
|
||||
import { isValidDid, isValidHandle } from './validation';
|
||||
|
||||
export interface NetworkConfig {
|
||||
pdsApi: string;
|
||||
plcApi: string;
|
||||
@ -9,12 +11,33 @@ export interface NetworkConfig {
|
||||
|
||||
// Detect PDS from handle
|
||||
export function detectPdsFromHandle(handle: string): string {
|
||||
if (handle.endsWith('.syu.is')) {
|
||||
// Get allowed handles from environment
|
||||
const allowedHandlesStr = import.meta.env.VITE_ATPROTO_HANDLE_LIST || '[]';
|
||||
let allowedHandles: string[] = [];
|
||||
try {
|
||||
allowedHandles = JSON.parse(allowedHandlesStr);
|
||||
} catch {
|
||||
allowedHandles = [];
|
||||
}
|
||||
|
||||
// Get configured PDS from environment
|
||||
const configuredPds = import.meta.env.VITE_ATPROTO_PDS || 'syu.is';
|
||||
|
||||
// Check if handle is in allowed list
|
||||
if (allowedHandles.includes(handle)) {
|
||||
return configuredPds;
|
||||
}
|
||||
|
||||
// Check if handle ends with .syu.is or .syui.ai
|
||||
if (handle.endsWith('.syu.is') || handle.endsWith('.syui.ai')) {
|
||||
return 'syu.is';
|
||||
}
|
||||
|
||||
// Check if handle ends with .bsky.social or .bsky.app
|
||||
if (handle.endsWith('.bsky.social') || handle.endsWith('.bsky.app')) {
|
||||
return 'bsky.social';
|
||||
}
|
||||
|
||||
// Default to Bluesky for unknown domains
|
||||
return 'bsky.social';
|
||||
}
|
||||
@ -74,8 +97,13 @@ export function getApiUrlForUser(handle: string): string {
|
||||
return config.bskyApi;
|
||||
}
|
||||
|
||||
// Resolve handle/DID to actual PDS endpoint using com.atproto.repo.describeRepo
|
||||
// Resolve handle/DID to actual PDS endpoint using PLC API first
|
||||
export async function resolvePdsFromRepo(handleOrDid: string): Promise<{ pds: string; did: string; handle: string }> {
|
||||
// Validate input
|
||||
if (!handleOrDid || (!isValidDid(handleOrDid) && !isValidHandle(handleOrDid))) {
|
||||
throw new Error(`Invalid identifier: ${handleOrDid}`);
|
||||
}
|
||||
|
||||
let targetDid = handleOrDid;
|
||||
let targetHandle = handleOrDid;
|
||||
|
||||
@ -83,7 +111,7 @@ export async function resolvePdsFromRepo(handleOrDid: string): Promise<{ pds: st
|
||||
if (!handleOrDid.startsWith('did:')) {
|
||||
try {
|
||||
// Try multiple endpoints for handle resolution
|
||||
const resolveEndpoints = ['https://public.api.bsky.app', 'https://bsky.syu.is'];
|
||||
const resolveEndpoints = ['https://public.api.bsky.app', 'https://bsky.syu.is', 'https://syu.is'];
|
||||
let resolved = false;
|
||||
|
||||
for (const endpoint of resolveEndpoints) {
|
||||
@ -108,7 +136,34 @@ export async function resolvePdsFromRepo(handleOrDid: string): Promise<{ pds: st
|
||||
}
|
||||
}
|
||||
|
||||
// Now use com.atproto.repo.describeRepo to get PDS from known PDS endpoints
|
||||
// First, try PLC API to get the authoritative DID document
|
||||
const plcApis = ['https://plc.directory', 'https://plc.syu.is'];
|
||||
|
||||
for (const plcApi of plcApis) {
|
||||
try {
|
||||
const plcResponse = await fetch(`${plcApi}/${targetDid}`);
|
||||
if (plcResponse.ok) {
|
||||
const didDocument = await plcResponse.json();
|
||||
|
||||
// Find PDS service in DID document
|
||||
const pdsService = didDocument.service?.find((s: any) =>
|
||||
s.id === '#atproto_pds' || s.type === 'AtprotoPersonalDataServer'
|
||||
);
|
||||
|
||||
if (pdsService && pdsService.serviceEndpoint) {
|
||||
return {
|
||||
pds: pdsService.serviceEndpoint,
|
||||
did: targetDid,
|
||||
handle: targetHandle
|
||||
};
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback: 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) {
|
||||
|
21
oauth/src/utils/validation.ts
Normal file
21
oauth/src/utils/validation.ts
Normal file
@ -0,0 +1,21 @@
|
||||
// Validation utilities for atproto identifiers
|
||||
|
||||
export function isValidDid(did: string): boolean {
|
||||
if (!did || typeof did !== 'string') return false;
|
||||
|
||||
// Basic DID format: did:method:identifier
|
||||
const didRegex = /^did:[a-z]+:[a-zA-Z0-9._%-]+$/;
|
||||
return didRegex.test(did);
|
||||
}
|
||||
|
||||
export function isValidHandle(handle: string): boolean {
|
||||
if (!handle || typeof handle !== 'string') return false;
|
||||
|
||||
// Basic handle format: subdomain.domain.tld
|
||||
const handleRegex = /^([a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?\.)+[a-zA-Z]{2,}$/;
|
||||
return handleRegex.test(handle);
|
||||
}
|
||||
|
||||
export function isValidAtprotoIdentifier(identifier: string): boolean {
|
||||
return isValidDid(identifier) || isValidHandle(identifier);
|
||||
}
|
Reference in New Issue
Block a user