Browse Source

bug-fixes

Nostr-Signature: 9a1ba983e0b0db8cff3675a078a376df5c9ad351c3988ea893f3e8084a65a1e6 573634b648634cbad10f2451776089ea21090d9407f715e83c577b4611ae6edc 724a326cbd6a33f1ff6a2c37b242c7571e35149281609e9eb1c6a197422a13834d9ac2f5d0719026bc66126bd0022df49adf50aa08af93dd95076f407b0f0456
main
Silberengel 3 weeks ago
parent
commit
ab607f21d3
  1. 1
      nostr/commit-signatures.jsonl
  2. 21
      src/app.css
  3. 100
      src/lib/services/nostr/persistent-event-cache.ts
  4. 183
      src/routes/api-docs/+page.svelte
  5. 79
      src/routes/api/openapi.json/+server.ts
  6. 1
      static/swagger-ui/swagger-ui.css

1
nostr/commit-signatures.jsonl

@ -17,3 +17,4 @@ @@ -17,3 +17,4 @@
{"kind":1640,"pubkey":"573634b648634cbad10f2451776089ea21090d9407f715e83c577b4611ae6edc","created_at":1771581869,"tags":[["author","Silberengel","silberengel7@protonmail.com"],["message","gix build and publish CLI to npm"]],"content":"Signed commit: gix build and publish CLI to npm","id":"7515d5ecd835df785a5e896062818b469bcad83a22efa84499d1736e73ae4844","sig":"b4bb7849515c545a609df14939a0a2ddfcd08ee2160cdc01c932a4b0b55668a54fa3fe1d15ad55fe74cfdb23e6c357cf581ab0aaef44da8c64dc098202a7383f"}
{"kind":1640,"pubkey":"573634b648634cbad10f2451776089ea21090d9407f715e83c577b4611ae6edc","created_at":1771584107,"tags":[["author","Silberengel","silberengel7@protonmail.com"],["message","pubkey lookup for maintainer\ninclude all tags in the r.a. preset\nupdate client tags on publish\nadd verification/correction step"]],"content":"Signed commit: pubkey lookup for maintainer\ninclude all tags in the r.a. preset\nupdate client tags on publish\nadd verification/correction step","id":"cc27d54e23cecca7e126e7a1b9e0881ee9c9addf39a97841992ac35422221e5d","sig":"7c5e7173e4bfc17a71cec49c8ac2fad15ecab3a84ef53ac90ba7ab6f1c051e2e6d108cecfa075917b6be8a9d1d54d3995595a0b95c004995ec89fe8a621315cd"}
{"kind":1640,"pubkey":"573634b648634cbad10f2451776089ea21090d9407f715e83c577b4611ae6edc","created_at":1771584611,"tags":[["author","Silberengel","silberengel7@protonmail.com"],["message","fix login persistence"]],"content":"Signed commit: fix login persistence","id":"e02d4dbaf56fb0498ca6871ae25bd5da1061eeca1d28c88d54ff5f6549982f11","sig":"647fa0385224b33546c55c786b3c2cf3b2cfab5de9f9748ce814e40e8c6819131ebb9e86d7682bffa327e3b690297f17bcfb2f6b2d5fb6b65e1d9474d66659b1"}
{"kind":1640,"pubkey":"573634b648634cbad10f2451776089ea21090d9407f715e83c577b4611ae6edc","created_at":1771587832,"tags":[["author","Silberengel","silberengel7@protonmail.com"],["message","implemented IndexedDB to organize the persistent event cache\nbackground deletion removal\ncorrected and expanded search and added cancel button\nshow maintainers on the search result cards\nremove code search\nremoved hard-coded theme classes"]],"content":"Signed commit: implemented IndexedDB to organize the persistent event cache\nbackground deletion removal\ncorrected and expanded search and added cancel button\nshow maintainers on the search result cards\nremove code search\nremoved hard-coded theme classes","id":"8080f3cad9abacfc9a5fe08bc26744ff8444d0228ea8a6e8a449c8c2704885d6","sig":"70120c99f5e8a1e9df6d74af756a51641c4998265b9233d5a7d187d9e21302dc6377ae274b07be4d6515af1dabfada43fa9af1a087a34e2879b028ac34e551ca"}

21
src/app.css

@ -1905,3 +1905,24 @@ label.filter-checkbox > span, @@ -1905,3 +1905,24 @@ label.filter-checkbox > span,
/* Code blocks use consistent dark-gray background in both themes */
/* All syntax highlighting colors are optimized for #1e1e1e background */
/* Swagger UI: Make response body backgrounds darker in light mode for better contrast */
/* This ensures light green/blue/yellow syntax highlighting is readable */
html[data-theme="light"] .swagger-ui .response pre,
html[data-theme="light"] .swagger-ui .response .highlight-code,
html[data-theme="light"] .swagger-ui .response .highlight-code pre,
html[data-theme="light"] .swagger-ui .response-body pre,
html[data-theme="light"] .swagger-ui .response-body .highlight-code,
html[data-theme="light"] .swagger-ui .response-body .highlight-code pre,
html[data-theme="light"] .swagger-ui .responses-wrapper pre,
html[data-theme="light"] .swagger-ui .responses-wrapper .highlight-code,
html[data-theme="light"] .swagger-ui .responses-wrapper .highlight-code pre,
html[data-theme="light"] .swagger-ui .responses-inner pre,
html[data-theme="light"] .swagger-ui .responses-inner .highlight-code,
html[data-theme="light"] .swagger-ui .responses-inner .highlight-code pre,
html[data-theme="light"] .swagger-ui .response pre.microlight,
html[data-theme="light"] .swagger-ui .response-body pre.microlight,
html[data-theme="light"] .swagger-ui .responses-wrapper pre.microlight {
background: #0f172a !important; /* Darker slate background for light mode (slate-900) */
color: #e2e8f0 !important; /* Light text for dark background */
}

100
src/lib/services/nostr/persistent-event-cache.ts

@ -145,6 +145,8 @@ export class PersistentEventCache { @@ -145,6 +145,8 @@ export class PersistentEventCache {
private defaultTTL: number = 5 * 60 * 1000; // 5 minutes
private profileTTL: number = 30 * 60 * 1000; // 30 minutes for profiles
private maxCacheAge: number = 7 * 24 * 60 * 60 * 1000; // 7 days max age
private writeQueue: Array<() => Promise<void>> = [];
private isProcessingQueue: boolean = false;
constructor() {
this.init();
@ -298,6 +300,31 @@ export class PersistentEventCache { @@ -298,6 +300,31 @@ export class PersistentEventCache {
}
}
/**
* Process write queue to prevent concurrent IndexedDB transactions
*/
private async processWriteQueue(): Promise<void> {
if (this.isProcessingQueue || this.writeQueue.length === 0) {
return;
}
this.isProcessingQueue = true;
while (this.writeQueue.length > 0) {
const writeFn = this.writeQueue.shift();
if (writeFn) {
try {
await writeFn();
} catch (error) {
// Log but continue processing queue
logger.debug({ error }, 'Error in write queue item');
}
}
}
this.isProcessingQueue = false;
}
/**
* Store events in cache, merging with existing events
*/
@ -308,6 +335,45 @@ export class PersistentEventCache { @@ -308,6 +335,45 @@ export class PersistentEventCache {
return;
}
// Queue the write operation to prevent concurrent transactions
return new Promise<void>((resolve, reject) => {
let resolved = false;
this.writeQueue.push(async () => {
try {
await this._setInternal(filters, events, ttl);
if (!resolved) {
resolved = true;
resolve();
}
} catch (error) {
if (!resolved) {
resolved = true;
reject(error);
}
}
});
// Process queue asynchronously
this.processWriteQueue().catch(err => {
if (!resolved) {
resolved = true;
reject(err);
} else {
logger.debug({ error: err }, 'Error processing write queue');
}
});
});
}
/**
* Internal set method that does the actual work
*/
private async _setInternal(filters: NostrFilter[], events: NostrEvent[], ttl?: number): Promise<void> {
if (!this.db) {
return;
}
try {
const filterKey = generateMultiFilterKey(filters);
const now = Date.now();
@ -321,14 +387,19 @@ export class PersistentEventCache { @@ -321,14 +387,19 @@ export class PersistentEventCache {
// Use longer TTL for profile events
const effectiveTTL = isProfileQuery ? this.profileTTL : cacheTTL;
// Get existing filter entry
// Get existing filter entry (outside transaction)
const existingEntry = await this.getFilterEntry(filterKey);
const existingEventIds = new Set(existingEntry?.eventIds || []);
// Store/update events
const eventStore = this.db.transaction([STORE_EVENTS], 'readwrite').objectStore(STORE_EVENTS);
// Use a single transaction for all operations
const transaction = this.db.transaction([STORE_EVENTS, STORE_PROFILES, STORE_FILTERS], 'readwrite');
const eventStore = transaction.objectStore(STORE_EVENTS);
const profileStore = transaction.objectStore(STORE_PROFILES);
const filterStore = transaction.objectStore(STORE_FILTERS);
const newEventIds: string[] = [];
// Process all events in the transaction
for (const event of events) {
// For replaceable events, check if we have a newer version for this pubkey
if (REPLACEABLE_KINDS.includes(event.kind)) {
@ -364,9 +435,8 @@ export class PersistentEventCache { @@ -364,9 +435,8 @@ export class PersistentEventCache {
newEventIds.push(event.id);
// Also store in profiles store if it's a profile event
// Also store in profiles store if it's a profile event (using same transaction)
if (event.kind === 0) {
const profileStore = this.db.transaction([STORE_PROFILES], 'readwrite').objectStore(STORE_PROFILES);
const existingProfile = await new Promise<CachedEvent | undefined>((resolve) => {
const req = profileStore.get(event.pubkey);
req.onsuccess = () => resolve(req.result);
@ -386,8 +456,7 @@ export class PersistentEventCache { @@ -386,8 +456,7 @@ export class PersistentEventCache {
// Merge with existing event IDs (don't delete valid events)
const mergedEventIds = Array.from(new Set([...existingEntry?.eventIds || [], ...newEventIds]));
// Update filter cache entry
const filterStore = this.db.transaction([STORE_FILTERS], 'readwrite').objectStore(STORE_FILTERS);
// Update filter cache entry (using same transaction)
const filterEntry: FilterCacheEntry = {
filterKey,
eventIds: mergedEventIds,
@ -401,6 +470,12 @@ export class PersistentEventCache { @@ -401,6 +470,12 @@ export class PersistentEventCache {
request.onerror = () => reject(request.error);
});
// Wait for transaction to complete
await new Promise<void>((resolve, reject) => {
transaction.oncomplete = () => resolve();
transaction.onerror = () => reject(transaction.error);
});
logger.debug({
filterKey,
eventCount: events.length,
@ -408,7 +483,18 @@ export class PersistentEventCache { @@ -408,7 +483,18 @@ export class PersistentEventCache {
ttl: effectiveTTL
}, 'Cached events in IndexedDB');
} catch (error) {
// Check if it's a quota exceeded error or other recoverable error
if (error instanceof DOMException) {
if (error.name === 'QuotaExceededError') {
logger.warn({ error, filters }, 'IndexedDB quota exceeded, skipping cache write');
return; // Don't throw, just skip this write
} else if (error.name === 'TransactionInactiveError' || error.name === 'InvalidStateError') {
logger.debug({ error, filters }, 'IndexedDB transaction error, likely concurrent write, skipping');
return; // Don't throw, just skip this write
}
}
logger.error({ error, filters }, 'Error writing to event cache');
throw error; // Re-throw other errors
}
}

183
src/routes/api-docs/+page.svelte

@ -1,5 +1,7 @@ @@ -1,5 +1,7 @@
<script lang="ts">
import { onMount } from 'svelte';
import { isNIP07Available, getPublicKeyWithNIP07, signEventWithNIP07 } from '$lib/services/nostr/nip07-signer.js';
import type { NostrEvent } from '$lib/types/nostr.js';
const browserExample = `// Get user's pubkey (hex format) from NIP-07 extension
const userPubkey = await window.nostr.getPublicKey();
@ -53,7 +55,7 @@ const response = await fetch('https://gitrepublic.com/api/repos/list', { @@ -53,7 +55,7 @@ const response = await fetch('https://gitrepublic.com/api/repos/list', {
// Then load the bundle
bundleScript = document.createElement('script');
bundleScript.src = '/swagger-ui/swagger-ui-bundle.js';
bundleScript.onload = () => {
bundleScript.onload = async () => {
// @ts-ignore - SwaggerUIBundle is loaded from static files
const ui = window.SwaggerUIBundle({
url: '/api/openapi.json',
@ -74,8 +76,33 @@ const response = await fetch('https://gitrepublic.com/api/repos/list', { @@ -74,8 +76,33 @@ const response = await fetch('https://gitrepublic.com/api/repos/list', {
defaultModelsExpandDepth: 1,
defaultModelExpandDepth: 1,
showExtensions: true,
showCommonExtensions: true
showCommonExtensions: true,
// Custom request interceptor to add NIP-98 auth
requestInterceptor: (request: any) => {
// Check if we have stored auth
const authData = localStorage.getItem('swagger-ui-nip98-auth');
if (authData) {
try {
const { token } = JSON.parse(authData);
if (token) {
request.headers['Authorization'] = `Nostr ${token}`;
}
} catch (e) {
console.warn('Failed to parse stored auth data', e);
}
}
return request;
}
});
// Wait for Swagger UI to render, then hook into the authorize button
setTimeout(() => {
const observer = setupNIP07Authorization(ui);
if (observer) {
// Store observer for cleanup
(window as any).__swaggerAuthObserver = observer;
}
}, 1000);
};
document.head.appendChild(bundleScript);
};
@ -86,8 +113,151 @@ const response = await fetch('https://gitrepublic.com/api/repos/list', { @@ -86,8 +113,151 @@ const response = await fetch('https://gitrepublic.com/api/repos/list', {
if (link.parentNode) document.head.removeChild(link);
if (presetScript.parentNode) document.head.removeChild(presetScript);
if (bundleScript?.parentNode) document.head.removeChild(bundleScript);
if ((window as any).__swaggerAuthObserver) {
(window as any).__swaggerAuthObserver.disconnect();
delete (window as any).__swaggerAuthObserver;
}
};
});
function setupNIP07Authorization(ui: any): MutationObserver | null {
// Check if NIP-07 is available
if (!isNIP07Available()) {
console.warn('NIP-07 extension not available');
return null;
}
// Function to handle authorization
const handleAuthorize = async () => {
try {
// Get public key from NIP-07
const npub = await getPublicKeyWithNIP07();
// Create NIP-98 auth event template
const currentUrl = window.location.origin;
const authEventTemplate: Omit<NostrEvent, 'sig' | 'id'> = {
kind: 27235,
created_at: Math.floor(Date.now() / 1000),
tags: [
['u', currentUrl],
['method', 'GET']
],
content: '',
pubkey: '' // Will be filled by NIP-07
};
// Sign the event with NIP-07
const signedEvent = await signEventWithNIP07(authEventTemplate);
// Encode to base64
const base64Event = btoa(JSON.stringify(signedEvent));
// Store in localStorage for request interceptor
localStorage.setItem('swagger-ui-nip98-auth', JSON.stringify({ token: base64Event }));
// Update Swagger UI's auth state
if (ui.authActions) {
ui.authActions.authorize({
NIP98: {
name: 'NIP-98',
schema: {
type: 'http',
scheme: 'Nostr'
},
value: base64Event
}
});
}
// Close the modal if it's open
const modal = document.querySelector('.auth-container');
if (modal) {
const closeBtn = modal.querySelector('.auth-btn-wrapper .btn-done, .auth-btn-wrapper button');
if (closeBtn) {
(closeBtn as HTMLElement).click();
}
}
// Show success message
const successMsg = document.createElement('div');
successMsg.style.cssText = 'position: fixed; top: 20px; right: 20px; background: var(--success-bg); color: var(--success-text); padding: 1rem; border-radius: 0.5rem; z-index: 10000; box-shadow: 0 4px 12px var(--shadow-color);';
successMsg.textContent = 'Successfully authorized with NIP-07!';
document.body.appendChild(successMsg);
setTimeout(() => successMsg.remove(), 3000);
} catch (error) {
console.error('NIP-07 authorization failed:', error);
const errorMsg = document.createElement('div');
errorMsg.style.cssText = 'position: fixed; top: 20px; right: 20px; background: var(--error-bg); color: var(--error-text); padding: 1rem; border-radius: 0.5rem; z-index: 10000; box-shadow: 0 4px 12px var(--shadow-color);';
errorMsg.textContent = `Authorization failed: ${error instanceof Error ? error.message : String(error)}`;
document.body.appendChild(errorMsg);
setTimeout(() => errorMsg.remove(), 5000);
}
};
// Hook into Swagger UI's authorization flow
// Watch for the authorization modal to appear and intercept the authorize button
const setupAuthModalHandler = () => {
// Watch for authorization modal
const observer = new MutationObserver((mutations) => {
mutations.forEach((mutation) => {
mutation.addedNodes.forEach((node) => {
if (node.nodeType === Node.ELEMENT_NODE) {
const element = node as Element;
// Check if this is the auth modal
if (element.classList?.contains('auth-container') || element.querySelector?.('.auth-container')) {
const modal = element.classList?.contains('auth-container') ? element : element.querySelector('.auth-container');
if (modal) {
// Find the authorize/done button in the modal
const authorizeBtn = modal.querySelector('.auth-btn-wrapper .btn-done, .auth-btn-wrapper button, button.btn-done');
if (authorizeBtn && !(authorizeBtn as any).__nip07HandlerAdded) {
// Mark as handled
(authorizeBtn as any).__nip07HandlerAdded = true;
// Add click handler
authorizeBtn.addEventListener('click', async (e: Event) => {
e.preventDefault();
e.stopPropagation();
// Call our authorization handler
await handleAuthorize();
}, { capture: true });
}
}
}
}
});
});
});
// Observe the entire document for changes
observer.observe(document.body, {
childList: true,
subtree: true
});
// Also check immediately
const modal = document.querySelector('.auth-container');
if (modal) {
const authorizeBtn = modal.querySelector('.auth-btn-wrapper .btn-done, .auth-btn-wrapper button, button.btn-done');
if (authorizeBtn && !(authorizeBtn as any).__nip07HandlerAdded) {
(authorizeBtn as any).__nip07HandlerAdded = true;
authorizeBtn.addEventListener('click', async (e: Event) => {
e.preventDefault();
e.stopPropagation();
await handleAuthorize();
}, { capture: true });
}
}
return observer;
};
// Set up the handler and return observer for cleanup
return setupAuthModalHandler();
}
</script>
<div class="api-docs-container">
@ -701,6 +871,15 @@ const response = await fetch('https://gitrepublic.com/api/repos/list', { @@ -701,6 +871,15 @@ const response = await fetch('https://gitrepublic.com/api/repos/list', {
padding: 0;
}
/* Response body code blocks */
:global(.swagger-ui .response pre),
:global(.swagger-ui .response-body pre),
:global(.swagger-ui .response-content-type + div pre) {
color: var(--text-primary) !important;
}
/* Response body code blocks - styles moved to app.css for better priority and to ensure they apply */
/* Model definitions */
:global(.swagger-ui .model-container),
:global(.swagger-ui .model-box) {

79
src/routes/api/openapi.json/+server.ts

@ -1,21 +1,76 @@ @@ -1,21 +1,76 @@
import { json } from '@sveltejs/kit';
import { json, error } from '@sveltejs/kit';
import type { RequestHandler } from './$types';
import { readFileSync } from 'fs';
import { join } from 'path';
import { fileURLToPath } from 'url';
// Read the file dynamically to avoid Vite caching issues
const __dirname = fileURLToPath(new URL('.', import.meta.url));
const specPath = join(__dirname, 'openapi.json');
// Cache the spec in production
let cachedSpec: any = null;
export const GET: RequestHandler = async () => {
// Read file fresh on each request to avoid cache issues during development
const openApiSpec = JSON.parse(readFileSync(specPath, 'utf-8'));
return json(openApiSpec, {
headers: {
'Content-Type': 'application/json',
'Cache-Control': 'public, max-age=3600'
try {
// Use cached spec if available (in production)
if (cachedSpec && typeof process !== 'undefined' && process.env.NODE_ENV === 'production') {
return json(cachedSpec, {
headers: {
'Content-Type': 'application/json',
'Cache-Control': 'public, max-age=3600'
}
});
}
});
// Try to read the file using different path resolution methods
let openApiSpec: any = null;
let lastError: Error | null = null;
// Method 1: Try using fileURLToPath (works in most cases)
try {
const __dirname = fileURLToPath(new URL('.', import.meta.url));
const specPath = join(__dirname, 'openapi.json');
openApiSpec = JSON.parse(readFileSync(specPath, 'utf-8'));
} catch (err) {
lastError = err instanceof Error ? err : new Error(String(err));
// Method 2: Try using process.cwd() (fallback for build environments)
try {
if (typeof process !== 'undefined' && process.cwd) {
const specPath = join(process.cwd(), 'src/routes/api/openapi.json/openapi.json');
openApiSpec = JSON.parse(readFileSync(specPath, 'utf-8'));
}
} catch (err2) {
lastError = err2 instanceof Error ? err2 : new Error(String(err2));
// Method 3: Try relative to import.meta.url parent
try {
const __filename = fileURLToPath(import.meta.url);
const __dirname = join(__filename, '..');
const specPath = join(__dirname, 'openapi.json');
openApiSpec = JSON.parse(readFileSync(specPath, 'utf-8'));
} catch (err3) {
lastError = err3 instanceof Error ? err3 : new Error(String(err3));
throw new Error(`Failed to locate openapi.json file. Tried multiple paths. Last error: ${lastError.message}`);
}
}
}
if (!openApiSpec) {
throw new Error('Failed to load OpenAPI specification');
}
// Cache for production
if (typeof process !== 'undefined' && process.env.NODE_ENV === 'production') {
cachedSpec = openApiSpec;
}
return json(openApiSpec, {
headers: {
'Content-Type': 'application/json',
'Cache-Control': 'public, max-age=3600'
}
});
} catch (err) {
const errorMessage = err instanceof Error ? err.message : String(err);
console.error('Error reading openapi.json:', errorMessage);
return error(500, `Failed to load OpenAPI specification: ${errorMessage}`);
}
};

1
static/swagger-ui/swagger-ui.css

File diff suppressed because one or more lines are too long
Loading…
Cancel
Save