diff --git a/SECURITY.md b/SECURITY.md
index 628a7a6f..7046075e 100644
--- a/SECURITY.md
+++ b/SECURITY.md
@@ -21,7 +21,11 @@ We enforce a strict CSP through both Nginx and React-level meta tags.
- **Restrictions**: `'unsafe-inline'` is prohibited in production.
- **Allowed Sources**:
- Scripts/Styles: `'self'`
- - API Connections: `https://*.stellar.org`, `https://api.coingecko.com`
+ - API Connections/Wallets: `https://*.stellar.org`, `wss://*.stellar.org`, `https://*.sorobanrpc.com`, `https://api.coingecko.com`, `wss://*.walletconnect.com`, `https://*.walletconnect.com`, `https://*.walletconnect.org`, `https://albedo.link`, `https://*.albedo.link`
+ - Inline Scripts: Allowed via strict SHA-256 hash validation for the theme initialization script.
+
+### Adding New Wallet or API Endpoints
+To add a new endpoint or wallet integration, update the `Content-Security-Policy` header in `nginx.conf` and the corresponding meta tag in `index.html`. Add the domains to `connect-src` (for APIs/WebSocket) or `frame-src` (for iframes).
### 2. Automated Guardrails
- **Dependabot**: Monitors `npm` and `github-actions` ecosystems daily for updates.
diff --git a/index.html b/index.html
index 0fe4149a..5bf921ec 100644
--- a/index.html
+++ b/index.html
@@ -2,6 +2,7 @@
+
Stellar Developer Dashboard
diff --git a/nginx.conf b/nginx.conf
index b0094f36..beab4a61 100644
--- a/nginx.conf
+++ b/nginx.conf
@@ -9,7 +9,7 @@ server {
add_header X-Content-Type-Options "nosniff" always;
add_header X-XSS-Protection "1; mode=block" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
- add_header Content-Security-Policy "default-src 'self'; script-src 'self'; style-src 'self'; connect-src 'self' https://*.stellar.org https://api.coingecko.com; img-src 'self' data: https:; font-src 'self'; object-src 'none'; base-uri 'self'; upgrade-insecure-requests;" always;
+ add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'sha256-2cZtek/4Mta9v4mtLkqsK+tSR0rUJ9LWlEhKKvL5/Ts='; style-src 'self' https://fonts.googleapis.com; connect-src 'self' https://*.stellar.org wss://*.stellar.org https://*.sorobanrpc.com https://api.coingecko.com wss://*.walletconnect.com https://*.walletconnect.com https://*.walletconnect.org; img-src 'self' data: https:; font-src 'self' https://fonts.gstatic.com; frame-src 'self' https://albedo.link https://*.albedo.link; object-src 'none'; base-uri 'self'; upgrade-insecure-requests;" always;
# Health check endpoint
location /health {
diff --git a/src/components/dashboard/Transactions.tsx b/src/components/dashboard/Transactions.tsx
index fc481ec4..09879482 100644
--- a/src/components/dashboard/Transactions.tsx
+++ b/src/components/dashboard/Transactions.tsx
@@ -123,7 +123,7 @@ import { useAddressLabels } from '../../hooks/useAddressLabels'
import { generateTransactionDescription } from '../../lib/aiTransactionDescription'
const VIRTUAL_SCROLL_THRESHOLD = 200
-const PAGE_SIZE = 100
+const PAGE_SIZE = 50
function normalizeSearch(value) {
return String(value || '').toLowerCase().trim()
@@ -168,6 +168,38 @@ function flattenOperation(op) {
}
}
+function InfiniteScrollSentinel({ onIntersect, hasMore, loading, label = 'items' }: { onIntersect: () => void, hasMore: boolean, loading: boolean, label?: string }) {
+ const sentinelRef = React.useRef(null)
+
+ React.useEffect(() => {
+ if (!hasMore || loading) return
+
+ const observer = new IntersectionObserver((entries) => {
+ if (entries[0].isIntersecting) {
+ onIntersect()
+ }
+ }, { rootMargin: '200px' })
+
+ if (sentinelRef.current) {
+ observer.observe(sentinelRef.current)
+ }
+
+ return () => observer.disconnect()
+ }, [onIntersect, hasMore, loading])
+
+ return (
+
+ {loading ? (
+ Loading more...
+ ) : hasMore ? (
+ Scroll for more
+ ) : (
+ No more {label}
+ )}
+
+ )
+}
+
function LoadingRows({ count, height }) {
return (
@@ -596,29 +628,12 @@ export default function Transactions() {
))}
-
- {txHasMore || txPagingLoading ? (
-
- {txPagingLoading ? 'Loading...' : 'Load More'}
-
- ) : (
- No more transactions
- )}
-
+
>
)}
@@ -897,29 +912,12 @@ export default function Transactions() {
))}
-
- {opsHasMore || opsPagingLoading ? (
-
- {opsPagingLoading ? 'Loading...' : 'Load More'}
-
- ) : (
- No more operations
- )}
-
+
>
)}
diff --git a/tests/csp.test.js b/tests/csp.test.js
new file mode 100644
index 00000000..75899c20
--- /dev/null
+++ b/tests/csp.test.js
@@ -0,0 +1,55 @@
+import fs from 'fs';
+import path from 'path';
+import { fileURLToPath } from 'url';
+import assert from 'assert';
+
+const __filename = fileURLToPath(import.meta.url);
+const __dirname = path.dirname(__filename);
+
+const nginxConfPath = path.resolve(__dirname, '../nginx.conf');
+const indexHtmlPath = path.resolve(__dirname, '../index.html');
+
+console.log('Running CSP tests...');
+
+try {
+ // Primary flow: nginx.conf should have a valid CSP header
+ const nginxContent = fs.readFileSync(nginxConfPath, 'utf8');
+ const cspRegex = /add_header Content-Security-Policy "(.*)" always;/;
+ const match = nginxContent.match(cspRegex);
+ assert(match, 'CSP header missing in nginx.conf');
+ const csp = match[1];
+
+ assert(csp.includes("default-src 'self'"), "CSP missing default-src 'self'");
+ assert(csp.includes("script-src 'self'"), "CSP missing script-src 'self'");
+ assert(csp.includes("connect-src"), "CSP missing connect-src");
+ console.log('✅ Primary flow: nginx.conf valid');
+
+ // Primary flow: index.html should have a valid CSP meta tag
+ const htmlContent = fs.readFileSync(indexHtmlPath, 'utf8');
+ const htmlCspRegex = / /;
+ const htmlMatch = htmlContent.match(htmlCspRegex);
+ assert(htmlMatch, 'CSP meta tag missing in index.html');
+ const htmlCsp = htmlMatch[1];
+
+ assert(htmlCsp.includes("default-src 'self'"), "HTML CSP missing default-src 'self'");
+ assert(htmlCsp.includes("script-src 'self'"), "HTML CSP missing script-src 'self'");
+ assert(htmlCsp.includes("connect-src"), "HTML CSP missing connect-src");
+ console.log('✅ Primary flow: index.html valid');
+
+ // Boundary case: should allow required Stellar endpoints but not wildcard everything
+ assert(csp.includes("https://*.stellar.org"), "Missing stellar.org");
+ assert(csp.includes("wss://*.walletconnect.com"), "Missing walletconnect.com");
+ assert(!csp.match(/connect-src [^;]*\s\*(?:\s|;)/), "connect-src is too permissive with wildcard");
+ console.log('✅ Boundary case: Required endpoints allowed securely');
+
+ // Failure case: should not allow arbitrary domains like http://evil.com
+ assert(!csp.includes("evil.com"), "CSP should not allow evil.com");
+ assert(!csp.includes("http://"), "CSP should not allow http://");
+ console.log('✅ Failure case: Malicious endpoints blocked');
+
+ console.log('All tests passed!');
+ process.exit(0);
+} catch (error) {
+ console.error('Test failed:', error.message);
+ process.exit(1);
+}