Автоопределение SHiNE-сервера по логину через PDA

This commit is contained in:
AidarKC
2026-06-16 16:32:33 +04:00
parent 3efa8bb7ee
commit f8a76bcd7f
19 changed files with 17426 additions and 48 deletions
+1 -1
View File
@@ -7,7 +7,7 @@
<link rel="manifest" href="./manifest.webmanifest" />
<title>Shine UI Demo</title>
<script>
window.__SHINE_BUILD_HASH__ = '20260616091500';
window.__SHINE_BUILD_HASH__ = '20260616124000';
window.__SHINE_CLIENT_VERSION__ = '1.2.8';
</script>
<script>
+2 -2
View File
@@ -33,7 +33,7 @@ import {
} from './state.js';
import * as startView from './pages/start-view.js?v=202606142105';
import * as entrySettingsView from './pages/entry-settings-view.js';
import * as entrySettingsView from './pages/entry-settings-view.js?v=202606161240';
import * as registerView from './pages/register-view.js';
import * as registrationPaymentView from './pages/registration-payment-view.js';
import * as registrationKeysView from './pages/registration-keys-view.js';
@@ -51,7 +51,7 @@ import * as profileEditView from './pages/profile-edit-view.js';
import * as walletView from './pages/wallet-view.js?v=202605300007';
import * as settingsView from './pages/settings-view.js';
import * as developerSettingsView from './pages/developer-settings-view.js';
import * as serverSettingsView from './pages/server-settings-view.js';
import * as serverSettingsView from './pages/server-settings-view.js?v=202606161240';
import * as toolsSettingsView from './pages/tools-settings-view.js';
import * as deviceView from './pages/device-view.js?v=202606131435';
import * as connectDeviceView from './pages/connect-device-view.js?v=202606142055';
+3 -3
View File
@@ -173,7 +173,7 @@ export function render({ navigate }) {
let cleanupEvent = () => {};
let disposed = false;
let settingsBusy = false;
let pairingPasswordConfigured = loadLocalPairingPasswordState(state.session.login, state.entrySettings.shineServer);
let pairingPasswordConfigured = loadLocalPairingPasswordState(state.session.login, state.entrySettings.shineServerLogin || state.entrySettings.shineServerHttp);
let dialogMode = '';
screen.append(
@@ -357,7 +357,7 @@ export function render({ navigate }) {
ttlSeconds: 180,
}));
pairingPasswordConfigured = false;
saveLocalPairingPasswordState(state.session.login, state.entrySettings.shineServer, false);
saveLocalPairingPasswordState(state.session.login, state.entrySettings.shineServerLogin || state.entrySettings.shineServerHttp, false);
setAuthInfo(`Подключение по коду без дополнительного пароля включено. TTL: ${payload?.ttlSeconds || 180} сек.`);
setStatus(status, 'Дополнительный пароль убран. Подключение по коду теперь работает без него.', 'info');
};
@@ -598,7 +598,7 @@ export function render({ navigate }) {
ttlSeconds: 180,
}));
pairingPasswordConfigured = true;
saveLocalPairingPasswordState(state.session.login, state.entrySettings.shineServer, true);
saveLocalPairingPasswordState(state.session.login, state.entrySettings.shineServerLogin || state.entrySettings.shineServerHttp, true);
closePasswordDialog();
renderSettingsCard();
setAuthInfo(`Подключение по коду включено с дополнительным паролем. TTL: ${payload?.ttlSeconds || 180} сек.`);
+28 -12
View File
@@ -1,12 +1,12 @@
import { renderHeader } from '../components/header.js';
import { saveEntrySettings, state } from '../state.js';
import { checkServerAvailabilityByKey } from '../services/server-health-service.js';
import { checkServerAvailabilityByKey, resolveAndCheckShineServerLogin } from '../services/server-health-service.js';
export const pageMeta = { id: 'entry-settings-view', title: 'Настройки входа', showAppChrome: false };
const SERVER_FIELDS = [
{ key: 'solanaServer', label: 'Адрес Solana сервера' },
{ key: 'shineServer', label: 'Адрес сервера Сияние' },
{ key: 'shineServerLogin', label: 'Логин сервера Сияние' },
{ key: 'arweaveServer', label: 'Адрес сервера Arweave' },
];
@@ -17,7 +17,8 @@ export function render({ navigate }) {
const draft = {
language: state.entrySettings.language,
solanaServer: state.entrySettings.solanaServer,
shineServer: state.entrySettings.shineServer,
shineServerLogin: state.entrySettings.shineServerLogin,
shineServerHttp: state.entrySettings.shineServerHttp,
arweaveServer: state.entrySettings.arweaveServer,
statuses: { ...state.entrySettings.statuses },
};
@@ -69,13 +70,15 @@ export function render({ navigate }) {
const status = document.createElement('span');
status.className = 'status-line';
const applyStatus = (value) => {
const applyStatus = (value, exactAddress = '') => {
draft.statuses[field.key] = value;
checkButton.classList.remove('is-available', 'is-unavailable');
status.classList.remove('is-available', 'is-unavailable');
if (value === 'available') {
status.textContent = 'Доступен';
status.textContent = field.key === 'shineServerLogin' && exactAddress
? `Доступен: ${exactAddress}`
: 'Доступен';
checkButton.classList.add('is-available');
status.classList.add('is-available');
} else if (value === 'unavailable') {
@@ -83,7 +86,9 @@ export function render({ navigate }) {
checkButton.classList.add('is-unavailable');
status.classList.add('is-unavailable');
} else {
status.textContent = 'Статус не проверен';
status.textContent = field.key === 'shineServerLogin' && draft.shineServerHttp
? `Текущий адрес: ${draft.shineServerHttp}`
: 'Статус не проверен';
}
};
@@ -92,8 +97,15 @@ export function render({ navigate }) {
checkButton.disabled = true;
checkButton.textContent = 'Проверка...';
try {
const next = await checkServerAvailabilityByKey(field.key, input.value);
applyStatus(next);
if (field.key === 'shineServerLogin') {
const resolved = await resolveAndCheckShineServerLogin(input.value, draft.solanaServer);
draft.shineServerHttp = resolved.httpBase;
draft.shineServer = resolved.wsUrl;
applyStatus(resolved.status, resolved.httpBase);
} else {
const next = await checkServerAvailabilityByKey(field.key, input.value);
applyStatus(next);
}
} finally {
checkButton.disabled = false;
checkButton.textContent = 'Проверить';
@@ -148,9 +160,13 @@ export function render({ navigate }) {
saveButton.className = 'primary-btn';
saveButton.type = 'button';
saveButton.textContent = 'Сохранить';
saveButton.addEventListener('click', () => {
saveEntrySettings(draft);
navigate('start-view');
saveButton.addEventListener('click', async () => {
try {
await saveEntrySettings(draft);
navigate('start-view');
} catch (error) {
window.alert(error?.message || 'Не удалось сохранить настройки входа.');
}
});
actions.append(serverUiButton, cancelButton, saveButton);
@@ -161,7 +177,7 @@ export function render({ navigate }) {
help.textContent = '?';
help.addEventListener('click', () => {
window.alert(
'Текст для разработчиков: после ввода адреса любого сервера автопроверка запускается по кнопке "Проверить", после перехода в другое поле или если подождать больше 3 секунд. Зелёный статус означает доступность, красный — недоступность.',
'Текст для разработчиков: для SHiNE вводится логин серверного аккаунта. Клиент читает его PDA, берёт server_address, показывает точный https-адрес и проверяет доступность WS-канала автоматически.',
);
});
+28 -12
View File
@@ -1,12 +1,12 @@
import { renderHeader } from '../components/header.js';
import { saveEntrySettings, state } from '../state.js';
import { checkServerAvailabilityByKey } from '../services/server-health-service.js';
import { checkServerAvailabilityByKey, resolveAndCheckShineServerLogin } from '../services/server-health-service.js';
export const pageMeta = { id: 'server-settings-view', title: 'Настройки серверов' };
const SERVER_FIELDS = [
{ key: 'solanaServer', label: 'Адрес Solana сервера' },
{ key: 'shineServer', label: 'Адрес сервера Сияние' },
{ key: 'shineServerLogin', label: 'Логин сервера Сияние' },
{ key: 'arweaveServer', label: 'Адрес сервера Arweave' },
];
@@ -17,7 +17,8 @@ export function render({ navigate }) {
const draft = {
language: state.entrySettings.language,
solanaServer: state.entrySettings.solanaServer,
shineServer: state.entrySettings.shineServer,
shineServerLogin: state.entrySettings.shineServerLogin,
shineServerHttp: state.entrySettings.shineServerHttp,
arweaveServer: state.entrySettings.arweaveServer,
callPreflightTimeoutMs: Number(state.entrySettings.callPreflightTimeoutMs || 6000),
statuses: { ...state.entrySettings.statuses },
@@ -52,13 +53,15 @@ export function render({ navigate }) {
const status = document.createElement('span');
status.className = 'status-line';
const applyStatus = (value) => {
const applyStatus = (value, exactAddress = '') => {
draft.statuses[field.key] = value;
checkButton.classList.remove('is-available', 'is-unavailable');
status.classList.remove('is-available', 'is-unavailable');
if (value === 'available') {
status.textContent = 'Доступен';
status.textContent = field.key === 'shineServerLogin' && exactAddress
? `Доступен: ${exactAddress}`
: 'Доступен';
checkButton.classList.add('is-available');
status.classList.add('is-available');
} else if (value === 'unavailable') {
@@ -66,7 +69,9 @@ export function render({ navigate }) {
checkButton.classList.add('is-unavailable');
status.classList.add('is-unavailable');
} else {
status.textContent = 'Статус не проверен';
status.textContent = field.key === 'shineServerLogin' && draft.shineServerHttp
? `Текущий адрес: ${draft.shineServerHttp}`
: 'Статус не проверен';
}
};
@@ -75,8 +80,15 @@ export function render({ navigate }) {
checkButton.disabled = true;
checkButton.textContent = 'Проверка...';
try {
const next = await checkServerAvailabilityByKey(field.key, input.value);
applyStatus(next);
if (field.key === 'shineServerLogin') {
const resolved = await resolveAndCheckShineServerLogin(input.value, draft.solanaServer);
draft.shineServerHttp = resolved.httpBase;
draft.shineServer = resolved.wsUrl;
applyStatus(resolved.status, resolved.httpBase);
} else {
const next = await checkServerAvailabilityByKey(field.key, input.value);
applyStatus(next);
}
} finally {
checkButton.disabled = false;
checkButton.textContent = 'Проверить';
@@ -149,9 +161,13 @@ export function render({ navigate }) {
saveButton.className = 'primary-btn';
saveButton.type = 'button';
saveButton.textContent = 'Сохранить';
saveButton.addEventListener('click', () => {
saveEntrySettings(draft);
navigate('settings-view');
saveButton.addEventListener('click', async () => {
try {
await saveEntrySettings(draft);
navigate('settings-view');
} catch (error) {
window.alert(error?.message || 'Не удалось сохранить настройки серверов.');
}
});
actions.append(cancelButton, saveButton);
@@ -162,7 +178,7 @@ export function render({ navigate }) {
help.textContent = '?';
help.addEventListener('click', () => {
window.alert(
'Текст для разработчиков: после ввода адреса любого сервера автопроверка запускается по кнопке "Проверить", после перехода в другое поле или если подождать больше 3 секунд. Зелёный статус означает доступность, красный — недоступность.',
'Текст для разработчиков: для SHiNE вводится логин серверного аккаунта. Клиент читает его PDA, берёт server_address, показывает точный https-адрес и проверяет доступность WS-канала автоматически.',
);
});
+21 -1
View File
@@ -1,3 +1,6 @@
import { resolveShineServerByServerLogin } from './shine-server-resolver.js';
import { SOLANA_ENDPOINT_DEFAULT } from '../solana-programs.js';
function normalizeUrl(value) {
return String(value || '').trim();
}
@@ -103,10 +106,28 @@ function checkShineWs(url, timeoutMs = 7000) {
});
}
export async function resolveAndCheckShineServerLogin(serverLogin, solanaEndpoint) {
const resolved = await resolveShineServerByServerLogin({
serverLogin,
solanaEndpoint,
});
const available = await checkShineWs(resolved.wsUrl);
return {
status: available ? 'available' : 'unavailable',
serverLogin: resolved.serverLogin,
serverAddress: resolved.serverAddress,
httpBase: resolved.httpBase,
wsUrl: resolved.wsUrl,
};
}
export async function checkServerAvailabilityByKey(key, url) {
if (key === 'solanaServer') {
return (await checkSolanaRpc(url)) ? 'available' : 'unavailable';
}
if (key === 'shineServerLogin') {
return (await resolveAndCheckShineServerLogin(url, SOLANA_ENDPOINT_DEFAULT)).status;
}
if (key === 'shineServer') {
return (await checkShineWs(url)) ? 'available' : 'unavailable';
}
@@ -115,4 +136,3 @@ export async function checkServerAvailabilityByKey(key, url) {
}
return 'unavailable';
}
@@ -0,0 +1,81 @@
import { readShineUserPda } from './shine-user-pda-service.js';
export const DEFAULT_SHINE_SERVER_LOGIN = 'shineupme';
export const DEFAULT_SHINE_SERVER_ADDRESS = 'shineup.me';
export const DEFAULT_SHINE_SERVER_HTTP = `https://${DEFAULT_SHINE_SERVER_ADDRESS}`;
export const DEFAULT_SHINE_SERVER_WS = `wss://${DEFAULT_SHINE_SERVER_ADDRESS}/ws`;
function normalizeHostLike(value) {
const raw = String(value || '').trim();
if (!raw) return '';
try {
const withScheme = /^[a-z]+:\/\//i.test(raw) ? raw : `https://${raw}`;
const parsed = new URL(withScheme);
return String(parsed.host || '').trim().toLowerCase();
} catch {
return raw.replace(/^https?:\/\//i, '').replace(/^wss?:\/\//i, '').replace(/\/.*$/, '').trim().toLowerCase();
}
}
export function normalizeShineServerLogin(value) {
return String(value || '').trim().toLowerCase();
}
export function buildShineHttpUrlFromAddress(address) {
const host = normalizeHostLike(address);
if (!host) return DEFAULT_SHINE_SERVER_HTTP;
return `https://${host}`;
}
export function buildShineWsUrlFromAddress(address) {
const host = normalizeHostLike(address);
if (!host) return DEFAULT_SHINE_SERVER_WS;
return `wss://${host}/ws`;
}
export async function resolveShineServerByServerLogin({ serverLogin, solanaEndpoint }) {
const cleanServerLogin = normalizeShineServerLogin(serverLogin) || DEFAULT_SHINE_SERVER_LOGIN;
const parsed = await readShineUserPda({
login: cleanServerLogin,
solanaEndpoint,
});
if (!parsed?.isServer) {
throw new Error(`Логин @${cleanServerLogin} не опубликован как сервер SHiNE.`);
}
const serverAddress = normalizeHostLike(parsed?.serverAddress || '');
if (!serverAddress) {
throw new Error(`У server PDA пользователя @${cleanServerLogin} не задан server_address.`);
}
return {
serverLogin: cleanServerLogin,
serverAddress,
httpBase: buildShineHttpUrlFromAddress(serverAddress),
wsUrl: buildShineWsUrlFromAddress(serverAddress),
userPda: parsed,
};
}
export async function resolveShineServerForUserLogin({ login, solanaEndpoint, fallbackServerLogin = '' }) {
const cleanLogin = normalizeShineServerLogin(login);
if (!cleanLogin) throw new Error('Не указан логин пользователя для поиска сервера доступа.');
const parsedUser = await readShineUserPda({
login: cleanLogin,
solanaEndpoint,
});
const accessServers = Array.isArray(parsedUser?.accessServers)
? parsedUser.accessServers.map((value) => normalizeShineServerLogin(value)).filter(Boolean)
: [];
const pickedServerLogin = accessServers[0] || normalizeShineServerLogin(fallbackServerLogin);
if (!pickedServerLogin) {
throw new Error(`У пользователя @${cleanLogin} в PDA не указан access server.`);
}
const resolved = await resolveShineServerByServerLogin({
serverLogin: pickedServerLogin,
solanaEndpoint,
});
return {
...resolved,
accessServerLogin: pickedServerLogin,
userPda: parsedUser,
};
}
+34 -5
View File
@@ -1,6 +1,12 @@
import { AuthService } from './services/auth-service.js';
import { listStoredMessages, putStoredMessage, clearStoredMessages } from './services/message-store.js';
import { SOLANA_ENDPOINT_DEFAULT } from './solana-programs.js';
import {
DEFAULT_SHINE_SERVER_HTTP,
DEFAULT_SHINE_SERVER_LOGIN,
DEFAULT_SHINE_SERVER_WS,
resolveShineServerByServerLogin,
} from './services/shine-server-resolver.js';
const clone = (value) => JSON.parse(JSON.stringify(value));
const SESSION_STORAGE_KEY = 'shine-ui-current-session-v1';
@@ -78,7 +84,9 @@ function inferTunnelWsUrl() {
const LOCAL_WS_OVERRIDE_URL = readLocalWsOverrideUrl() || inferTunnelWsUrl();
const DEFAULT_SOLANA_SERVER = SOLANA_ENDPOINT_DEFAULT;
const DEFAULT_SHINE_SERVER = 'wss://shineup.me/ws';
const DEFAULT_SHINE_SERVER = DEFAULT_SHINE_SERVER_WS;
const DEFAULT_SHINE_SERVER_LOGIN_VALUE = DEFAULT_SHINE_SERVER_LOGIN;
const DEFAULT_SHINE_SERVER_HTTP_VALUE = DEFAULT_SHINE_SERVER_HTTP;
const DEFAULT_ARWEAVE_SERVER = 'https://arweave.net';
const DEFAULT_CALL_PREFLIGHT_TIMEOUT_MS = 6000;
const DEFAULT_OPENAI_BASE_URL = 'https://api.openai.com/v1';
@@ -171,11 +179,13 @@ function persistEntrySettings(settings) {
language: String(settings?.language || 'ru'),
solanaServer: String(settings?.solanaServer || DEFAULT_SOLANA_SERVER),
shineServer: String(settings?.shineServer || DEFAULT_SHINE_SERVER),
shineServerLogin: String(settings?.shineServerLogin || DEFAULT_SHINE_SERVER_LOGIN_VALUE),
shineServerHttp: String(settings?.shineServerHttp || DEFAULT_SHINE_SERVER_HTTP_VALUE),
arweaveServer: String(settings?.arweaveServer || DEFAULT_ARWEAVE_SERVER),
callPreflightTimeoutMs: Math.max(1000, Math.min(20000, Number(settings?.callPreflightTimeoutMs || DEFAULT_CALL_PREFLIGHT_TIMEOUT_MS) || DEFAULT_CALL_PREFLIGHT_TIMEOUT_MS)),
statuses: {
solanaServer: String(settings?.statuses?.solanaServer || 'idle'),
shineServer: String(settings?.statuses?.shineServer || 'idle'),
shineServerLogin: String(settings?.statuses?.shineServerLogin || settings?.statuses?.shineServer || 'idle'),
arweaveServer: String(settings?.statuses?.arweaveServer || 'idle'),
},
tools: normalizeToolsSettings(settings?.tools),
@@ -235,11 +245,13 @@ function createInitialState({ withStoredSession = true } = {}) {
language: String(storedEntrySettings?.language || 'ru'),
solanaServer: String(storedEntrySettings?.solanaServer || DEFAULT_SOLANA_SERVER),
shineServer: String(LOCAL_WS_OVERRIDE_URL || storedEntrySettings?.shineServer || initialShineServer),
shineServerLogin: String(storedEntrySettings?.shineServerLogin || DEFAULT_SHINE_SERVER_LOGIN_VALUE),
shineServerHttp: String(storedEntrySettings?.shineServerHttp || DEFAULT_SHINE_SERVER_HTTP_VALUE),
arweaveServer: String(storedEntrySettings?.arweaveServer || DEFAULT_ARWEAVE_SERVER),
callPreflightTimeoutMs: Math.max(1000, Math.min(20000, Number(storedEntrySettings?.callPreflightTimeoutMs || DEFAULT_CALL_PREFLIGHT_TIMEOUT_MS) || DEFAULT_CALL_PREFLIGHT_TIMEOUT_MS)),
statuses: {
solanaServer: String(storedEntrySettings?.statuses?.solanaServer || 'idle'),
shineServer: String(storedEntrySettings?.statuses?.shineServer || 'idle'),
shineServerLogin: String(storedEntrySettings?.statuses?.shineServerLogin || storedEntrySettings?.statuses?.shineServer || 'idle'),
arweaveServer: String(storedEntrySettings?.statuses?.arweaveServer || 'idle'),
},
tools: normalizeToolsSettings(storedEntrySettings?.tools),
@@ -649,11 +661,28 @@ export function checkServerAvailability(address) {
}
export async function saveEntrySettings(nextSettings) {
const forcedShineServer = LOCAL_WS_OVERRIDE_URL || nextSettings.shineServer;
const nextSolanaServer = String(nextSettings?.solanaServer || state.entrySettings.solanaServer || DEFAULT_SOLANA_SERVER);
const nextShineServerLogin = String(nextSettings?.shineServerLogin || state.entrySettings.shineServerLogin || DEFAULT_SHINE_SERVER_LOGIN_VALUE).trim().toLowerCase()
|| DEFAULT_SHINE_SERVER_LOGIN_VALUE;
let forcedShineServer = LOCAL_WS_OVERRIDE_URL || '';
let resolvedShineServerHttp = String(state.entrySettings.shineServerHttp || DEFAULT_SHINE_SERVER_HTTP_VALUE).trim() || DEFAULT_SHINE_SERVER_HTTP_VALUE;
if (!forcedShineServer) {
const resolved = await resolveShineServerByServerLogin({
serverLogin: nextShineServerLogin,
solanaEndpoint: nextSolanaServer,
});
forcedShineServer = resolved.wsUrl;
resolvedShineServerHttp = resolved.httpBase;
}
state.entrySettings = {
...state.entrySettings,
...nextSettings,
solanaServer: nextSolanaServer,
shineServer: forcedShineServer,
shineServerLogin: nextShineServerLogin,
shineServerHttp: resolvedShineServerHttp,
statuses: {
...state.entrySettings.statuses,
...(nextSettings.statuses || {}),
@@ -662,7 +691,7 @@ export async function saveEntrySettings(nextSettings) {
};
persistEntrySettings(state.entrySettings);
await authService.reconnect(state.entrySettings.shineServer);
state.startHint = 'Настройки входа сохранены, адреса серверов обновлены.';
state.startHint = `Настройки входа сохранены. SHiNE: ${state.entrySettings.shineServerHttp}`;
}
export function clearStartHint() {