9 changed files with 379 additions and 54 deletions
@ -0,0 +1,23 @@
@@ -0,0 +1,23 @@
|
||||
versions: |
||||
'0.3.1': |
||||
- 'Media attachments rendering in all feeds and views' |
||||
- 'NIP-92/NIP-94 image tags support' |
||||
- 'Blossom support for media attachments' |
||||
- 'OP-only view added to feed' |
||||
- 'Text to Speech (TTS) support added, for the languages: Czech, German, British English, American English, Spanish, French, Dutch, Polish, Russian, Turkish, and Chinese' |
||||
'0.3.0': |
||||
- 'Version history modal added to event menu' |
||||
- 'Event and npub deletion/reporting added' |
||||
- 'User and event reporting added' |
||||
- 'Profile settings added' |
||||
- 'Finished implementing themes' |
||||
- 'Add Edit/Clone of all events' |
||||
'0.2.1': |
||||
- 'Themes added: Fog, Forum, Socialmedia, and Terminal' |
||||
- 'Find page added: search for content by hashtags, users, and content, full-text search' |
||||
'0.2.0': |
||||
- 'Version management and update system' |
||||
- 'About page with product information' |
||||
- 'Improved user experience with automatic routing to About page on first visit and after updates' |
||||
- 'Enhanced settings page with About button' |
||||
- 'Better version tracking and display' |
||||
@ -0,0 +1,92 @@
@@ -0,0 +1,92 @@
|
||||
/** |
||||
* Changelog service - loads and provides access to version changelog |
||||
*/ |
||||
|
||||
import yaml from 'js-yaml'; |
||||
|
||||
export interface ChangelogData { |
||||
versions: Record<string, string[]>; |
||||
} |
||||
|
||||
let cachedChangelog: ChangelogData | null = null; |
||||
|
||||
/** |
||||
* Load changelog from YAML file |
||||
*/ |
||||
export async function loadChangelog(): Promise<ChangelogData> { |
||||
if (cachedChangelog) { |
||||
return cachedChangelog; |
||||
} |
||||
|
||||
try { |
||||
const response = await fetch('/changelog.yaml', { cache: 'no-store' }); |
||||
if (!response.ok) { |
||||
throw new Error(`Failed to fetch changelog: ${response.statusText}`); |
||||
} |
||||
|
||||
const yamlText = await response.text(); |
||||
const parsed = yaml.load(yamlText) as ChangelogData; |
||||
|
||||
// Validate structure
|
||||
if (!parsed || !parsed.versions || typeof parsed.versions !== 'object') { |
||||
throw new Error('Invalid changelog format'); |
||||
} |
||||
|
||||
cachedChangelog = parsed; |
||||
return cachedChangelog; |
||||
} catch (error) { |
||||
console.error('Failed to load changelog:', error); |
||||
// Return empty changelog on error
|
||||
return { versions: {} }; |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* Get changelog for a specific version |
||||
*/ |
||||
export async function getChangelogForVersion(version: string): Promise<string[]> { |
||||
const changelog = await loadChangelog(); |
||||
return changelog.versions[version] || []; |
||||
} |
||||
|
||||
/** |
||||
* Get all versions in the changelog, sorted by version number (newest first) |
||||
*/ |
||||
export async function getAllVersions(): Promise<string[]> { |
||||
const changelog = await loadChangelog(); |
||||
const versions = Object.keys(changelog.versions); |
||||
|
||||
// Sort versions in descending order (newest first)
|
||||
return versions.sort((a, b) => { |
||||
const partsA = a.split('.').map(Number); |
||||
const partsB = b.split('.').map(Number); |
||||
|
||||
for (let i = 0; i < Math.max(partsA.length, partsB.length); i++) { |
||||
const partA = partsA[i] || 0; |
||||
const partB = partsB[i] || 0; |
||||
if (partA !== partB) { |
||||
return partB - partA; // Descending order
|
||||
} |
||||
} |
||||
return 0; |
||||
}); |
||||
} |
||||
|
||||
/** |
||||
* Get the newest version from the changelog |
||||
*/ |
||||
export async function getNewestVersion(): Promise<string | null> { |
||||
const versions = await getAllVersions(); |
||||
return versions.length > 0 ? versions[0] : null; |
||||
} |
||||
|
||||
/** |
||||
* Get changelog for the newest version |
||||
*/ |
||||
export async function getNewestChangelog(): Promise<string[]> { |
||||
const newestVersion = await getNewestVersion(); |
||||
if (!newestVersion) { |
||||
return []; |
||||
} |
||||
return getChangelogForVersion(newestVersion); |
||||
} |
||||
@ -0,0 +1,137 @@
@@ -0,0 +1,137 @@
|
||||
/** |
||||
* GitHub API helper with rate limit detection and token fallback |
||||
*/ |
||||
|
||||
import { sessionManager } from './auth/session-manager.js'; |
||||
import { loadEncryptedApiKey } from './security/api-key-storage.js'; |
||||
|
||||
/** |
||||
* Check if a GitHub API response indicates rate limiting |
||||
*/ |
||||
function isRateLimited(response: Response): boolean { |
||||
// Check status code
|
||||
if (response.status === 403 || response.status === 429) { |
||||
return true; |
||||
} |
||||
|
||||
// Check rate limit headers
|
||||
const remaining = response.headers.get('X-RateLimit-Remaining'); |
||||
if (remaining !== null && parseInt(remaining, 10) === 0) { |
||||
return true; |
||||
} |
||||
|
||||
return false; |
||||
} |
||||
|
||||
/** |
||||
* Get GitHub token from settings if available |
||||
* Returns null if token is not available or cannot be decrypted |
||||
*/ |
||||
async function getGitHubToken(): Promise<string | null> { |
||||
try { |
||||
const session = sessionManager.getSession(); |
||||
if (!session) { |
||||
// No session, can't decrypt token
|
||||
return null; |
||||
} |
||||
|
||||
// Try to get password from session (for nsec/anonymous methods)
|
||||
let password: string | undefined = session.password; |
||||
|
||||
// If no password in session, try to get from sessionStorage (for nsec/anonymous)
|
||||
if (!password && typeof window !== 'undefined') { |
||||
try { |
||||
const encryptedPassword = sessionStorage.getItem(`aitherboard_password_${session.pubkey}`); |
||||
if (encryptedPassword) { |
||||
// Decrypt password from sessionStorage
|
||||
const key = session.pubkey.slice(0, 16); |
||||
const encryptedBytes = atob(encryptedPassword); |
||||
let decrypted = ''; |
||||
for (let i = 0; i < encryptedBytes.length; i++) { |
||||
const keyChar = key.charCodeAt(i % key.length); |
||||
const encChar = encryptedBytes.charCodeAt(i); |
||||
decrypted += String.fromCharCode(encChar ^ keyChar); |
||||
} |
||||
password = decrypted; |
||||
} |
||||
} catch { |
||||
// Failed to decrypt password from sessionStorage
|
||||
} |
||||
} |
||||
|
||||
// If still no password, we can't decrypt the token
|
||||
if (!password) { |
||||
return null; |
||||
} |
||||
|
||||
// Try to load the GitHub token
|
||||
const token = await loadEncryptedApiKey('github.token', password); |
||||
return token; |
||||
} catch (error) { |
||||
console.warn('Failed to get GitHub token:', error); |
||||
return null; |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* Make a GitHub API request with automatic token fallback on rate limiting |
||||
* @param url - GitHub API URL |
||||
* @param options - Fetch options (headers will be merged) |
||||
* @returns Response object |
||||
*/ |
||||
export async function fetchGitHubApi( |
||||
url: string, |
||||
options: RequestInit = {} |
||||
): Promise<Response> { |
||||
// First attempt without token
|
||||
let response = await fetch(url, { |
||||
...options, |
||||
headers: { |
||||
'Accept': 'application/vnd.github.v3+json', |
||||
...options.headers |
||||
} |
||||
}); |
||||
|
||||
// Check if rate limited
|
||||
if (isRateLimited(response)) { |
||||
console.log('GitHub API rate limit detected, attempting to use saved token...'); |
||||
|
||||
// Try to get token from settings
|
||||
const token = await getGitHubToken(); |
||||
|
||||
if (token) { |
||||
console.log('Using saved GitHub token for authenticated request'); |
||||
|
||||
// Retry with token (use Bearer for compatibility with both classic and fine-grained tokens)
|
||||
response = await fetch(url, { |
||||
...options, |
||||
headers: { |
||||
'Accept': 'application/vnd.github.v3+json', |
||||
'Authorization': `Bearer ${token}`, |
||||
...options.headers |
||||
} |
||||
}); |
||||
|
||||
// Check if still rate limited (token might also be exhausted)
|
||||
if (isRateLimited(response)) { |
||||
const resetTime = response.headers.get('X-RateLimit-Reset'); |
||||
if (resetTime) { |
||||
const resetDate = new Date(parseInt(resetTime, 10) * 1000); |
||||
console.warn(`GitHub API rate limit still exceeded even with token. Resets at: ${resetDate.toISOString()}`); |
||||
} else { |
||||
console.warn('GitHub API rate limit exceeded even with token'); |
||||
} |
||||
} |
||||
} else { |
||||
const resetTime = response.headers.get('X-RateLimit-Reset'); |
||||
if (resetTime) { |
||||
const resetDate = new Date(parseInt(resetTime, 10) * 1000); |
||||
console.warn(`GitHub API rate limit exceeded. No token available. Resets at: ${resetDate.toISOString()}`); |
||||
} else { |
||||
console.warn('GitHub API rate limit exceeded. No token available in settings.'); |
||||
} |
||||
} |
||||
} |
||||
|
||||
return response; |
||||
} |
||||
Loading…
Reference in new issue