refactor: rename SolaceScanScout to Solace and update related configurations

- Updated branding from "SolaceScanScout" to "Solace" across various files including deployment scripts, API responses, and documentation.
- Changed default base URL for Playwright tests and updated security headers to reflect the new branding.
- Enhanced README and API documentation to include new authentication endpoints and product access details.

This refactor aligns the project branding and improves clarity in the API documentation.
This commit is contained in:
defiQUG
2026-04-10 12:52:17 -07:00
parent 6eef6b07f6
commit 0972178cc5
160 changed files with 13274 additions and 1061 deletions
+321
View File
@@ -0,0 +1,321 @@
import { getExplorerApiBase } from './blockscout'
declare global {
interface Window {
ethereum?: {
request: (args: { method: string; params?: unknown[] | object }) => Promise<unknown>
}
}
}
export interface AccessUser {
id: string
email: string
username: string
is_admin?: boolean
}
export interface AccessSession {
user: AccessUser
token: string
expires_at: string
}
export interface WalletAccessSession {
token: string
expiresAt: string
track: string
permissions: string[]
address: string
}
export interface AccessProduct {
slug: string
name: string
provider: string
vmid: number
http_url: string
ws_url?: string
default_tier: string
requires_approval: boolean
billing_model: string
description: string
use_cases: string[]
management_features: string[]
}
export interface AccessAPIKeyRecord {
id: string
name: string
tier: string
productSlug: string
scopes: string[]
monthlyQuota: number
requestsUsed: number
approved: boolean
approvedAt?: string | null
rateLimitPerSecond: number
rateLimitPerMinute: number
lastUsedAt?: string | null
expiresAt?: string | null
revoked: boolean
createdAt: string
}
export interface CreateAccessAPIKeyRequest {
name: string
tier: string
productSlug: string
expiresDays?: number
monthlyQuota?: number
scopes?: string[]
}
export interface AccessSubscription {
id: string
productSlug: string
tier: string
status: string
monthlyQuota: number
requestsUsed: number
requiresApproval: boolean
approvedAt?: string | null
approvedBy?: string | null
notes?: string | null
createdAt: string
}
export interface AccessUsageSummary {
product_slug: string
active_keys: number
requests_used: number
monthly_quota: number
}
export interface AccessAuditEntry {
id: number
apiKeyId: string
keyName: string
productSlug: string
methodName: string
requestCount: number
lastIp?: string | null
createdAt: string
}
const ACCESS_TOKEN_STORAGE_KEY = 'explorer_access_token'
const WALLET_SESSION_STORAGE_KEY = 'explorer_wallet_session'
const ACCESS_SESSION_EVENT = 'explorer-access-session-changed'
const ACCESS_API_PREFIX = '/explorer-api/v1'
function getStoredAccessToken(): string {
if (typeof window === 'undefined') return ''
return window.localStorage.getItem(ACCESS_TOKEN_STORAGE_KEY) || getStoredWalletSession()?.token || ''
}
function setStoredAccessToken(token: string) {
if (typeof window === 'undefined') return
if (token) {
window.localStorage.setItem(ACCESS_TOKEN_STORAGE_KEY, token)
} else {
window.localStorage.removeItem(ACCESS_TOKEN_STORAGE_KEY)
}
window.dispatchEvent(new Event(ACCESS_SESSION_EVENT))
}
function getStoredWalletSession(): WalletAccessSession | null {
if (typeof window === 'undefined') return null
const raw = window.localStorage.getItem(WALLET_SESSION_STORAGE_KEY)
if (!raw) return null
try {
return JSON.parse(raw) as WalletAccessSession
} catch {
window.localStorage.removeItem(WALLET_SESSION_STORAGE_KEY)
return null
}
}
function setStoredWalletSession(session: WalletAccessSession | null) {
if (typeof window === 'undefined') return
if (session) {
window.localStorage.setItem(WALLET_SESSION_STORAGE_KEY, JSON.stringify(session))
} else {
window.localStorage.removeItem(WALLET_SESSION_STORAGE_KEY)
}
window.dispatchEvent(new Event(ACCESS_SESSION_EVENT))
}
function buildWalletMessage(nonce: string) {
return `Sign this message to authenticate with SolaceScan.\n\nNonce: ${nonce}`
}
async function fetchWalletJson<T>(path: string, init?: RequestInit): Promise<T> {
const headers = new Headers(init?.headers || {})
headers.set('Content-Type', 'application/json')
const response = await fetch(`${getExplorerApiBase()}${path}`, {
...init,
headers,
})
const payload = await response.json().catch(() => null)
if (!response.ok) {
throw new Error(payload?.error?.message || `HTTP ${response.status}`)
}
return payload as T
}
async function fetchJson<T>(path: string, init?: RequestInit, includeAuth = false): Promise<T> {
const headers = new Headers(init?.headers || {})
headers.set('Content-Type', 'application/json')
if (includeAuth) {
const token = getStoredAccessToken()
if (token) {
headers.set('Authorization', `Bearer ${token}`)
}
}
const response = await fetch(`${getExplorerApiBase()}${path}`, {
...init,
headers,
})
const payload = await response.json().catch(() => null)
if (!response.ok) {
throw new Error(payload?.error?.message || `HTTP ${response.status}`)
}
return payload as T
}
export const accessApi = {
getStoredAccessToken,
getStoredWalletSession,
clearSession() {
setStoredAccessToken('')
},
clearWalletSession() {
setStoredWalletSession(null)
},
async register(email: string, username: string, password: string): Promise<AccessSession> {
const response = await fetchJson<AccessSession>(`${ACCESS_API_PREFIX}/auth/register`, {
method: 'POST',
body: JSON.stringify({ email, username, password }),
})
setStoredAccessToken(response.token)
return response
},
async login(email: string, password: string): Promise<AccessSession> {
const response = await fetchJson<AccessSession>(`${ACCESS_API_PREFIX}/auth/login`, {
method: 'POST',
body: JSON.stringify({ email, password }),
})
setStoredAccessToken(response.token)
return response
},
async createWalletNonce(address: string): Promise<{ nonce: string; address: string }> {
return fetchWalletJson<{ nonce: string; address: string }>(`${ACCESS_API_PREFIX}/auth/nonce`, {
method: 'POST',
body: JSON.stringify({ address }),
})
},
async authenticateWallet(address: string, signature: string, nonce: string): Promise<WalletAccessSession> {
const response = await fetchWalletJson<{
token: string
expires_at: string
track: string
permissions: string[]
}>(`${ACCESS_API_PREFIX}/auth/wallet`, {
method: 'POST',
body: JSON.stringify({ address, signature, nonce }),
})
const session: WalletAccessSession = {
token: response.token,
expiresAt: response.expires_at,
track: response.track,
permissions: response.permissions || [],
address,
}
setStoredWalletSession(session)
return session
},
async connectWalletSession(): Promise<WalletAccessSession> {
if (typeof window === 'undefined' || typeof window.ethereum === 'undefined') {
throw new Error('No EOA wallet detected. Please open the explorer with a browser wallet installed.')
}
const accounts = (await window.ethereum.request({
method: 'eth_requestAccounts',
})) as string[]
const address = accounts?.[0]
if (!address) {
throw new Error('Wallet connection was cancelled.')
}
const nonceResponse = await accessApi.createWalletNonce(address)
const message = buildWalletMessage(nonceResponse.nonce)
const signature = (await window.ethereum.request({
method: 'personal_sign',
params: [message, address],
})) as string
return accessApi.authenticateWallet(address, signature, nonceResponse.nonce)
},
async getMe(): Promise<{ user: AccessUser; subscriptions?: AccessSubscription[] }> {
return fetchJson<{ user: AccessUser; subscriptions?: AccessSubscription[] }>(`${ACCESS_API_PREFIX}/access/me`, undefined, true)
},
async listProducts(): Promise<{ products: AccessProduct[]; note?: string }> {
return fetchJson<{ products: AccessProduct[]; note?: string }>(`${ACCESS_API_PREFIX}/access/products`)
},
async listAPIKeys(): Promise<{ api_keys: AccessAPIKeyRecord[] }> {
return fetchJson<{ api_keys: AccessAPIKeyRecord[] }>(`${ACCESS_API_PREFIX}/access/api-keys`, undefined, true)
},
async createAPIKey(request: CreateAccessAPIKeyRequest): Promise<{ api_key: string; record?: AccessAPIKeyRecord }> {
return fetchJson<{ api_key: string; record?: AccessAPIKeyRecord }>(`${ACCESS_API_PREFIX}/access/api-keys`, {
method: 'POST',
body: JSON.stringify({
name: request.name,
tier: request.tier,
product_slug: request.productSlug,
expires_days: request.expiresDays,
monthly_quota: request.monthlyQuota,
scopes: request.scopes,
}),
}, true)
},
async revokeAPIKey(id: string): Promise<{ revoked: boolean; api_key_id: string }> {
return fetchJson<{ revoked: boolean; api_key_id: string }>(`${ACCESS_API_PREFIX}/access/api-keys/${id}`, {
method: 'POST',
}, true)
},
async listSubscriptions(): Promise<{ subscriptions: AccessSubscription[] }> {
return fetchJson<{ subscriptions: AccessSubscription[] }>(`${ACCESS_API_PREFIX}/access/subscriptions`, undefined, true)
},
async requestSubscription(productSlug: string, tier: string): Promise<{ subscription: AccessSubscription }> {
return fetchJson<{ subscription: AccessSubscription }>(`${ACCESS_API_PREFIX}/access/subscriptions`, {
method: 'POST',
body: JSON.stringify({ product_slug: productSlug, tier }),
}, true)
},
async getUsage(): Promise<{ usage: AccessUsageSummary[] }> {
return fetchJson<{ usage: AccessUsageSummary[] }>(`${ACCESS_API_PREFIX}/access/usage`, undefined, true)
},
async listAudit(limit = 20): Promise<{ entries: AccessAuditEntry[] }> {
return fetchJson<{ entries: AccessAuditEntry[] }>(`${ACCESS_API_PREFIX}/access/audit?limit=${encodeURIComponent(limit)}`, undefined, true)
},
async listAdminSubscriptions(status = 'pending'): Promise<{ subscriptions: AccessSubscription[] }> {
const suffix = status ? `?status=${encodeURIComponent(status)}` : ''
return fetchJson<{ subscriptions: AccessSubscription[] }>(`${ACCESS_API_PREFIX}/access/admin/subscriptions${suffix}`, undefined, true)
},
async listAdminAudit(limit = 50, productSlug = ''): Promise<{ entries: AccessAuditEntry[] }> {
const params = new URLSearchParams()
params.set('limit', String(limit))
if (productSlug) params.set('product', productSlug)
return fetchJson<{ entries: AccessAuditEntry[] }>(`${ACCESS_API_PREFIX}/access/admin/audit?${params.toString()}`, undefined, true)
},
async updateAdminSubscription(subscriptionId: string, status: string, notes = ''): Promise<{ subscription: AccessSubscription }> {
return fetchJson<{ subscription: AccessSubscription }>(`${ACCESS_API_PREFIX}/access/admin/subscriptions`, {
method: 'POST',
body: JSON.stringify({
subscription_id: subscriptionId,
status,
notes,
}),
}, true)
},
}