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:
@@ -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)
|
||||
},
|
||||
}
|
||||
Reference in New Issue
Block a user