Автоопределение 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

View File

@ -0,0 +1,16 @@
# Автоопределение SHiNE-сервера по PDA
- краткое описание:
в основном UI и в `SHiNE-browser-plugin-wallet` ручной ввод адреса SHiNE-сервера заменён на ввод логина серверного аккаунта. Клиент читает `server_address` из PDA сервера и сам строит `https://...` и `wss://...`.
- что проверять:
1. В `shine-UI` на экранах настроек входа и серверов в поле SHiNE вводится логин `shineupme`, а статус показывает точный адрес `https://shineup.me`.
2. После сохранения настроек обычный вход и login через другое устройство продолжают работать.
3. В `SHiNE-browser-plugin-wallet` поле сервера принимает логин `shineupme`, а popup показывает `Текущий адрес: https://shineup.me`.
4. В plugin pairing и повторное восстановление wallet-session продолжают работать через авторазрешённый адрес.
- ожидаемый результат:
пользователь больше не вводит вручную `wss://...`; внутренний WS-адрес строится автоматически из PDA серверного аккаунта.
- статус:
pending

View File

@ -11,6 +11,7 @@ Chrome-compatible Manifest V3 plugin for SHiNE wallet-session login.
- сохранить `sessionPriv/sessionKey/sessionId` в локальном хранилище plugin;
- восстанавливать session через `SessionChallenge -> SessionLogin`;
- держать wallet-state в `background service worker`, а popup использовать как UI.
- принимать не адрес сервера, а логин серверного аккаунта SHiNE и находить точный `https://...` / `wss://...` адрес через его PDA.
## Как загрузить локально
@ -35,4 +36,5 @@ Chrome-compatible Manifest V3 plugin for SHiNE wallet-session login.
```bash
npm install
npx esbuild js/lib/vendor/noble-ed25519-entry.js --bundle --format=esm --platform=browser --outfile=js/lib/vendor/noble-ed25519-bundle.js
npx esbuild js/lib/vendor/solana-publickey-entry.js --bundle --format=esm --platform=browser --outfile=js/lib/vendor/solana-publickey-bundle.js
```

View File

@ -1,10 +1,18 @@
import { createRequesterPairingMaterial, decryptPairingPayloadFromEnvelope, deriveEspPairingPasswordHash } from './js/lib/device-pairing.js';
import { loadPluginSettings, loadSessionMaterial, savePluginSettings, saveSessionMaterial, clearSessionMaterial } from './js/lib/session-store.js';
import { ShineApiClient } from './js/lib/shine-api.js';
import {
DEFAULT_SHINE_SERVER_LOGIN,
buildHttpBase,
normalizeServerLogin,
resolveShineServerByServerLogin,
} from './js/lib/shine-server-resolver.js';
const state = {
api: null,
settings: {
serverLogin: DEFAULT_SHINE_SERVER_LOGIN,
serverHttp: buildHttpBase('shineup.me'),
serverUrl: 'wss://shineup.me/ws',
login: '',
},
@ -50,19 +58,36 @@ function ensureApi(serverUrl = state.settings.serverUrl) {
return state.api;
}
async function resolveSettingsServer(nextSettings = {}) {
const serverLogin = normalizeServerLogin(nextSettings?.serverLogin || state.settings.serverLogin || DEFAULT_SHINE_SERVER_LOGIN)
|| DEFAULT_SHINE_SERVER_LOGIN;
const resolved = await resolveShineServerByServerLogin(serverLogin);
return {
serverLogin: resolved.serverLogin,
serverHttp: resolved.serverHttp,
serverUrl: resolved.serverUrl,
};
}
async function loadStateFromStorage() {
const settings = await loadPluginSettings();
const storedServerLogin = normalizeServerLogin(settings?.serverLogin || state.settings.serverLogin || DEFAULT_SHINE_SERVER_LOGIN)
|| DEFAULT_SHINE_SERVER_LOGIN;
state.settings = {
serverUrl: String(settings?.serverUrl || 'wss://shineup.me/ws').trim() || 'wss://shineup.me/ws',
serverLogin: storedServerLogin,
serverHttp: String(settings?.serverHttp || state.settings.serverHttp || buildHttpBase('shineup.me')).trim() || buildHttpBase('shineup.me'),
serverUrl: String(settings?.serverUrl || state.settings.serverUrl || 'wss://shineup.me/ws').trim() || 'wss://shineup.me/ws',
login: String(settings?.login || '').trim(),
};
state.activeSession = await loadSessionMaterial();
}
async function persistSettings(nextSettings = {}) {
const resolved = await resolveSettingsServer(nextSettings);
state.settings = {
...state.settings,
...nextSettings,
...resolved,
};
await savePluginSettings(state.settings);
return state.settings;
@ -79,6 +104,8 @@ async function resumeActiveSession() {
try {
await persistSettings({
serverLogin: String(sessionRecord?.serverLogin || state.settings.serverLogin || DEFAULT_SHINE_SERVER_LOGIN).trim(),
serverHttp: String(sessionRecord?.serverHttp || state.settings.serverHttp || buildHttpBase('shineup.me')).trim(),
serverUrl: String(sessionRecord?.serverUrl || state.settings.serverUrl || 'wss://shineup.me/ws').trim(),
login: String(sessionRecord?.login || state.settings.login || '').trim(),
});
@ -106,6 +133,8 @@ async function attachApprovedSession(payload) {
sessionKey: state.requesterMaterial?.sessionKey || '',
sessionPrivPkcs8: state.requesterMaterial?.sessionPrivPkcs8 || '',
sessionType: Number(approvedSession?.sessionType || 50) || 50,
serverLogin: state.settings.serverLogin,
serverHttp: state.settings.serverHttp,
serverUrl: state.settings.serverUrl,
};
if (!sessionRecord.login || !sessionRecord.sessionId || !sessionRecord.sessionKey || !sessionRecord.sessionPrivPkcs8) {
@ -159,14 +188,14 @@ async function pollPairingStatus() {
}
}
async function startPairing({ login, usePassword, password, serverUrl }) {
async function startPairing({ login, usePassword, password, serverLogin }) {
const cleanLogin = String(login || '').trim();
if (!cleanLogin) {
throw new Error('Введите логин.');
}
await persistSettings({
serverUrl: String(serverUrl || state.settings.serverUrl || 'wss://shineup.me/ws').trim() || 'wss://shineup.me/ws',
serverLogin: String(serverLogin || state.settings.serverLogin || DEFAULT_SHINE_SERVER_LOGIN).trim(),
login: cleanLogin,
});
clearPairingState();

View File

@ -0,0 +1,186 @@
import { base64ToBytes } from './crypto-utils.js';
import { PublicKey } from './vendor/solana-publickey-bundle.js';
const SOLANA_ENDPOINT_DEFAULT = 'https://api.devnet.solana.com';
const SHINE_USERS_PROGRAM_ID = 'FZS1YctoeEhCkZ5VTjsysUFAXR8CqxYztcLboXcg2Rpm';
const SHINE_USERS_USER_PDA_SEED_PREFIX = 'user_login=';
const DEFAULT_SHINE_SERVER_LOGIN = 'shineupme';
const DEFAULT_SHINE_SERVER_ADDRESS = 'shineup.me';
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();
}
}
function normalizeServerLogin(value) {
return String(value || '').trim().toLowerCase();
}
function buildHttpBase(address) {
const host = normalizeHostLike(address) || DEFAULT_SHINE_SERVER_ADDRESS;
return `https://${host}`;
}
function buildWsUrl(address) {
const host = normalizeHostLike(address) || DEFAULT_SHINE_SERVER_ADDRESS;
return `wss://${host}/ws`;
}
function readU8(bytes, cursorRef) {
if (cursorRef.value >= bytes.length) throw new Error('Повреждённый формат PDA');
return bytes[cursorRef.value++];
}
function readBytes(bytes, cursorRef, length) {
if (cursorRef.value + length > bytes.length) throw new Error('Повреждённый формат PDA');
const out = bytes.slice(cursorRef.value, cursorRef.value + length);
cursorRef.value += length;
return out;
}
function readStrU8(bytes, cursorRef) {
const length = readU8(bytes, cursorRef);
return new TextDecoder().decode(readBytes(bytes, cursorRef, length));
}
function parseServerFieldsFromUserPda(dataBytes) {
const bytes = dataBytes instanceof Uint8Array ? dataBytes : new Uint8Array(dataBytes || []);
if (bytes.length < 5) throw new Error('Некорректный формат PDA');
const cursorRef = { value: 0 };
const magic = new TextDecoder().decode(readBytes(bytes, cursorRef, 5));
if (magic !== 'SHiNE') throw new Error('Некорректный формат PDA');
cursorRef.value += 1; // format_major
cursorRef.value += 1; // format_minor
cursorRef.value += 2; // record_len
cursorRef.value += 8; // created_at_ms
cursorRef.value += 8; // updated_at_ms
cursorRef.value += 4; // record_number
cursorRef.value += 32; // prev_record_hash
readStrU8(bytes, cursorRef); // login
const blocksCount = readU8(bytes, cursorRef);
let isServer = false;
let serverAddress = '';
let accessServers = [];
for (let i = 0; i < blocksCount; i += 1) {
const blockType = readU8(bytes, cursorRef);
cursorRef.value += 1; // block_version
if (blockType === 1 || blockType === 2) {
cursorRef.value += 32;
continue;
}
if (blockType === 3) {
const count = readU8(bytes, cursorRef);
for (let j = 0; j < count; j += 1) {
cursorRef.value += 1;
readStrU8(bytes, cursorRef);
cursorRef.value += 32;
cursorRef.value += 8 + 8 + 4 + 32 + 64;
const arPresent = readU8(bytes, cursorRef);
if (arPresent === 1) readStrU8(bytes, cursorRef);
}
continue;
}
if (blockType === 30) {
isServer = readU8(bytes, cursorRef) === 1;
if (isServer) {
cursorRef.value += 1; // address_format_type
cursorRef.value += 1; // address_format_version
serverAddress = readStrU8(bytes, cursorRef);
const syncCount = readU8(bytes, cursorRef);
for (let j = 0; j < syncCount; j += 1) readStrU8(bytes, cursorRef);
}
continue;
}
if (blockType === 40) {
const accessCount = readU8(bytes, cursorRef);
accessServers = [];
for (let j = 0; j < accessCount; j += 1) accessServers.push(readStrU8(bytes, cursorRef));
continue;
}
if (blockType === 50) {
cursorRef.value += 1;
const sessionsCount = readU8(bytes, cursorRef);
for (let j = 0; j < sessionsCount; j += 1) {
cursorRef.value += 1 + 1;
readStrU8(bytes, cursorRef);
cursorRef.value += 32;
}
continue;
}
if (blockType === 70) {
cursorRef.value += 1;
continue;
}
throw new Error(`Неизвестный блок PDA: ${blockType}`);
}
return {
isServer,
serverAddress: normalizeHostLike(serverAddress),
accessServers: accessServers.map((value) => normalizeServerLogin(value)).filter(Boolean),
};
}
async function fetchUserPda(login, solanaEndpoint = SOLANA_ENDPOINT_DEFAULT) {
const cleanLogin = normalizeServerLogin(login);
if (!cleanLogin) throw new Error('Не указан логин для чтения PDA.');
const usersProgram = new PublicKey(SHINE_USERS_PROGRAM_ID);
const enc = new TextEncoder();
const [userPda] = PublicKey.findProgramAddressSync(
[enc.encode(SHINE_USERS_USER_PDA_SEED_PREFIX), enc.encode(cleanLogin)],
usersProgram,
);
const response = await fetch(String(solanaEndpoint || SOLANA_ENDPOINT_DEFAULT), {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
cache: 'no-store',
body: JSON.stringify({
jsonrpc: '2.0',
id: 1,
method: 'getAccountInfo',
params: [userPda.toBase58(), { encoding: 'base64', commitment: 'confirmed' }],
}),
});
if (!response.ok) throw new Error('Не удалось прочитать Solana RPC.');
const json = await response.json();
const dataB64 = json?.result?.value?.data?.[0];
if (!dataB64) throw new Error(`PDA не найдена для логина @${cleanLogin}.`);
return parseServerFieldsFromUserPda(base64ToBytes(dataB64));
}
export async function resolveShineServerByServerLogin(serverLogin, solanaEndpoint = SOLANA_ENDPOINT_DEFAULT) {
const cleanServerLogin = normalizeServerLogin(serverLogin) || DEFAULT_SHINE_SERVER_LOGIN;
const parsed = await fetchUserPda(cleanServerLogin, solanaEndpoint);
if (!parsed.isServer) {
throw new Error(`Логин @${cleanServerLogin} не опубликован как сервер SHiNE.`);
}
if (!parsed.serverAddress) {
throw new Error(`У server PDA пользователя @${cleanServerLogin} не задан server_address.`);
}
return {
serverLogin: cleanServerLogin,
serverAddress: parsed.serverAddress,
serverHttp: buildHttpBase(parsed.serverAddress),
serverUrl: buildWsUrl(parsed.serverAddress),
};
}
export {
DEFAULT_SHINE_SERVER_ADDRESS,
DEFAULT_SHINE_SERVER_LOGIN,
SOLANA_ENDPOINT_DEFAULT,
buildHttpBase,
buildWsUrl,
normalizeServerLogin,
};

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,3 @@
import { PublicKey } from '@solana/web3.js';
export { PublicKey };

View File

@ -9,12 +9,22 @@
"version": "1.0.0",
"license": "ISC",
"dependencies": {
"@noble/curves": "^1.5.0"
"@noble/curves": "^1.5.0",
"@solana/web3.js": "^1.98.4"
},
"devDependencies": {
"esbuild": "^0.28.1"
}
},
"node_modules/@babel/runtime": {
"version": "7.29.7",
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.29.7.tgz",
"integrity": "sha512-Nq8OhGWiZIZGV6hLHoyAKLLcJihP/xFeBMGJoUrxTX2psI8dCifzLhZISFb+VWS3wFMRDmCGw5R+dOySCqPLhw==",
"license": "MIT",
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@esbuild/aix-ppc64": {
"version": "0.28.1",
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.1.tgz",
@ -481,6 +491,295 @@
"url": "https://paulmillr.com/funding/"
}
},
"node_modules/@noble/hashes": {
"version": "1.8.0",
"resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.8.0.tgz",
"integrity": "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==",
"license": "MIT",
"engines": {
"node": "^14.21.3 || >=16"
},
"funding": {
"url": "https://paulmillr.com/funding/"
}
},
"node_modules/@solana/buffer-layout": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/@solana/buffer-layout/-/buffer-layout-4.0.1.tgz",
"integrity": "sha512-E1ImOIAD1tBZFRdjeM4/pzTiTApC0AOBGwyAMS4fwIodCWArzJ3DWdoh8cKxeFM2fElkxBh2Aqts1BPC373rHA==",
"license": "MIT",
"dependencies": {
"buffer": "~6.0.3"
},
"engines": {
"node": ">=5.10"
}
},
"node_modules/@solana/codecs-core": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/@solana/codecs-core/-/codecs-core-2.3.0.tgz",
"integrity": "sha512-oG+VZzN6YhBHIoSKgS5ESM9VIGzhWjEHEGNPSibiDTxFhsFWxNaz8LbMDPjBUE69r9wmdGLkrQ+wVPbnJcZPvw==",
"license": "MIT",
"dependencies": {
"@solana/errors": "2.3.0"
},
"engines": {
"node": ">=20.18.0"
},
"peerDependencies": {
"typescript": ">=5.3.3"
}
},
"node_modules/@solana/codecs-numbers": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/@solana/codecs-numbers/-/codecs-numbers-2.3.0.tgz",
"integrity": "sha512-jFvvwKJKffvG7Iz9dmN51OGB7JBcy2CJ6Xf3NqD/VP90xak66m/Lg48T01u5IQ/hc15mChVHiBm+HHuOFDUrQg==",
"license": "MIT",
"dependencies": {
"@solana/codecs-core": "2.3.0",
"@solana/errors": "2.3.0"
},
"engines": {
"node": ">=20.18.0"
},
"peerDependencies": {
"typescript": ">=5.3.3"
}
},
"node_modules/@solana/errors": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/@solana/errors/-/errors-2.3.0.tgz",
"integrity": "sha512-66RI9MAbwYV0UtP7kGcTBVLxJgUxoZGm8Fbc0ah+lGiAw17Gugco6+9GrJCV83VyF2mDWyYnYM9qdI3yjgpnaQ==",
"license": "MIT",
"dependencies": {
"chalk": "^5.4.1",
"commander": "^14.0.0"
},
"bin": {
"errors": "bin/cli.mjs"
},
"engines": {
"node": ">=20.18.0"
},
"peerDependencies": {
"typescript": ">=5.3.3"
}
},
"node_modules/@solana/web3.js": {
"version": "1.98.4",
"resolved": "https://registry.npmjs.org/@solana/web3.js/-/web3.js-1.98.4.tgz",
"integrity": "sha512-vv9lfnvjUsRiq//+j5pBdXig0IQdtzA0BRZ3bXEP4KaIyF1CcaydWqgyzQgfZMNIsWNWmG+AUHwPy4AHOD6gpw==",
"license": "MIT",
"dependencies": {
"@babel/runtime": "^7.25.0",
"@noble/curves": "^1.4.2",
"@noble/hashes": "^1.4.0",
"@solana/buffer-layout": "^4.0.1",
"@solana/codecs-numbers": "^2.1.0",
"agentkeepalive": "^4.5.0",
"bn.js": "^5.2.1",
"borsh": "^0.7.0",
"bs58": "^4.0.1",
"buffer": "6.0.3",
"fast-stable-stringify": "^1.0.0",
"jayson": "^4.1.1",
"node-fetch": "^2.7.0",
"rpc-websockets": "^9.0.2",
"superstruct": "^2.0.2"
}
},
"node_modules/@swc/helpers": {
"version": "0.5.23",
"resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.23.tgz",
"integrity": "sha512-5lSsMOTXURePglDfvuAQUqkGek9Hg2kksOYay2m0+XR++b2NWYL/4sWyuvVBIs8oKnJaxkdi9whaL/sqN13afw==",
"license": "Apache-2.0",
"dependencies": {
"tslib": "^2.8.0"
}
},
"node_modules/@types/connect": {
"version": "3.4.38",
"resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz",
"integrity": "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==",
"license": "MIT",
"dependencies": {
"@types/node": "*"
}
},
"node_modules/@types/node": {
"version": "12.20.55",
"resolved": "https://registry.npmjs.org/@types/node/-/node-12.20.55.tgz",
"integrity": "sha512-J8xLz7q2OFulZ2cyGTLE1TbbZcjpno7FaN6zdJNrgAdrJ+DZzh/uFR6YrTb4C+nXakvud8Q4+rbhoIWlYQbUFQ==",
"license": "MIT"
},
"node_modules/@types/uuid": {
"version": "10.0.0",
"resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-10.0.0.tgz",
"integrity": "sha512-7gqG38EyHgyP1S+7+xomFtL+ZNHcKv6DwNaCZmJmo1vgMugyF3TCnXVg4t1uk89mLNwnLtnY3TpOpCOyp1/xHQ==",
"license": "MIT"
},
"node_modules/@types/ws": {
"version": "7.4.7",
"resolved": "https://registry.npmjs.org/@types/ws/-/ws-7.4.7.tgz",
"integrity": "sha512-JQbbmxZTZehdc2iszGKs5oC3NFnjeay7mtAWrdt7qNtAVK0g19muApzAy4bm9byz79xa2ZnO/BOBC2R8RC5Lww==",
"license": "MIT",
"dependencies": {
"@types/node": "*"
}
},
"node_modules/agentkeepalive": {
"version": "4.6.0",
"resolved": "https://registry.npmjs.org/agentkeepalive/-/agentkeepalive-4.6.0.tgz",
"integrity": "sha512-kja8j7PjmncONqaTsB8fQ+wE2mSU2DJ9D4XKoJ5PFWIdRMa6SLSN1ff4mOr4jCbfRSsxR4keIiySJU0N9T5hIQ==",
"license": "MIT",
"dependencies": {
"humanize-ms": "^1.2.1"
},
"engines": {
"node": ">= 8.0.0"
}
},
"node_modules/base-x": {
"version": "3.0.11",
"resolved": "https://registry.npmjs.org/base-x/-/base-x-3.0.11.tgz",
"integrity": "sha512-xz7wQ8xDhdyP7tQxwdteLYeFfS68tSMNCZ/Y37WJ4bhGfKPpqEIlmIyueQHqOyoPhE6xNUqjzRr8ra0eF9VRvA==",
"license": "MIT",
"dependencies": {
"safe-buffer": "^5.0.1"
}
},
"node_modules/base64-js": {
"version": "1.5.1",
"resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz",
"integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/feross"
},
{
"type": "patreon",
"url": "https://www.patreon.com/feross"
},
{
"type": "consulting",
"url": "https://feross.org/support"
}
],
"license": "MIT"
},
"node_modules/bn.js": {
"version": "5.2.3",
"resolved": "https://registry.npmjs.org/bn.js/-/bn.js-5.2.3.tgz",
"integrity": "sha512-EAcmnPkxpntVL+DS7bO1zhcZNvCkxqtkd0ZY53h06GNQ3DEkkGZ/gKgmDv6DdZQGj9BgfSPKtJJ7Dp1GPP8f7w==",
"license": "MIT"
},
"node_modules/borsh": {
"version": "0.7.0",
"resolved": "https://registry.npmjs.org/borsh/-/borsh-0.7.0.tgz",
"integrity": "sha512-CLCsZGIBCFnPtkNnieW/a8wmreDmfUtjU2m9yHrzPXIlNbqVs0AQrSatSG6vdNYUqdc83tkQi2eHfF98ubzQLA==",
"license": "Apache-2.0",
"dependencies": {
"bn.js": "^5.2.0",
"bs58": "^4.0.0",
"text-encoding-utf-8": "^1.0.2"
}
},
"node_modules/bs58": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/bs58/-/bs58-4.0.1.tgz",
"integrity": "sha512-Ok3Wdf5vOIlBrgCvTq96gBkJw+JUEzdBgyaza5HLtPm7yTHkjRy8+JzNyHF7BHa0bNWOQIp3m5YF0nnFcOIKLw==",
"license": "MIT",
"dependencies": {
"base-x": "^3.0.2"
}
},
"node_modules/buffer": {
"version": "6.0.3",
"resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz",
"integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/feross"
},
{
"type": "patreon",
"url": "https://www.patreon.com/feross"
},
{
"type": "consulting",
"url": "https://feross.org/support"
}
],
"license": "MIT",
"dependencies": {
"base64-js": "^1.3.1",
"ieee754": "^1.2.1"
}
},
"node_modules/bufferutil": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/bufferutil/-/bufferutil-4.1.0.tgz",
"integrity": "sha512-ZMANVnAixE6AWWnPzlW2KpUrxhm9woycYvPOo67jWHyFowASTEd9s+QN1EIMsSDtwhIxN4sWE1jotpuDUIgyIw==",
"hasInstallScript": true,
"license": "MIT",
"optional": true,
"dependencies": {
"node-gyp-build": "^4.3.0"
},
"engines": {
"node": ">=6.14.2"
}
},
"node_modules/chalk": {
"version": "5.6.2",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.2.tgz",
"integrity": "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==",
"license": "MIT",
"engines": {
"node": "^12.17.0 || ^14.13 || >=16.0.0"
},
"funding": {
"url": "https://github.com/chalk/chalk?sponsor=1"
}
},
"node_modules/commander": {
"version": "14.0.3",
"resolved": "https://registry.npmjs.org/commander/-/commander-14.0.3.tgz",
"integrity": "sha512-H+y0Jo/T1RZ9qPP4Eh1pkcQcLRglraJaSLoyOtHxu6AapkjWVCy2Sit1QQ4x3Dng8qDlSsZEet7g5Pq06MvTgw==",
"license": "MIT",
"engines": {
"node": ">=20"
}
},
"node_modules/delay": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/delay/-/delay-5.0.0.tgz",
"integrity": "sha512-ReEBKkIfe4ya47wlPYf/gu5ib6yUG0/Aez0JQZQz94kiWtRQvZIQbTiehsnwHvLSWJnQdhVeqYue7Id1dKr0qw==",
"license": "MIT",
"engines": {
"node": ">=10"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/es6-promise": {
"version": "4.2.8",
"resolved": "https://registry.npmjs.org/es6-promise/-/es6-promise-4.2.8.tgz",
"integrity": "sha512-HJDGx5daxeIvxdBxvG2cb9g4tEvwIk3i8+nhX0yGrYmZUzbkdg8QbDevheDB8gd0//uPj4c1EQua8Q+MViT0/w==",
"license": "MIT"
},
"node_modules/es6-promisify": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/es6-promisify/-/es6-promisify-5.0.0.tgz",
"integrity": "sha512-C+d6UdsYDk0lMebHNR4S2NybQMMngAOnOwYBQjTOiv0MkoJMP0Myw2mgpDLBcpfCmRLxyFqYhS/CfOENq4SJhQ==",
"license": "MIT",
"dependencies": {
"es6-promise": "^4.0.3"
}
},
"node_modules/esbuild": {
"version": "0.28.1",
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.1.tgz",
@ -522,6 +821,357 @@
"@esbuild/win32-ia32": "0.28.1",
"@esbuild/win32-x64": "0.28.1"
}
},
"node_modules/eventemitter3": {
"version": "5.0.4",
"resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.4.tgz",
"integrity": "sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw==",
"license": "MIT"
},
"node_modules/eyes": {
"version": "0.1.8",
"resolved": "https://registry.npmjs.org/eyes/-/eyes-0.1.8.tgz",
"integrity": "sha512-GipyPsXO1anza0AOZdy69Im7hGFCNB7Y/NGjDlZGJ3GJJLtwNSb2vrzYrTYJRrRloVx7pl+bhUaTB8yiccPvFQ==",
"engines": {
"node": "> 0.1.90"
}
},
"node_modules/fast-stable-stringify": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/fast-stable-stringify/-/fast-stable-stringify-1.0.0.tgz",
"integrity": "sha512-wpYMUmFu5f00Sm0cj2pfivpmawLZ0NKdviQ4w9zJeR8JVtOpOxHmLaJuj0vxvGqMJQWyP/COUkF75/57OKyRag==",
"license": "MIT"
},
"node_modules/humanize-ms": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/humanize-ms/-/humanize-ms-1.2.1.tgz",
"integrity": "sha512-Fl70vYtsAFb/C06PTS9dZBo7ihau+Tu/DNCk/OyHhea07S+aeMWpFFkUaXRa8fI+ScZbEI8dfSxwY7gxZ9SAVQ==",
"license": "MIT",
"dependencies": {
"ms": "^2.0.0"
}
},
"node_modules/ieee754": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz",
"integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/feross"
},
{
"type": "patreon",
"url": "https://www.patreon.com/feross"
},
{
"type": "consulting",
"url": "https://feross.org/support"
}
],
"license": "BSD-3-Clause"
},
"node_modules/isomorphic-ws": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/isomorphic-ws/-/isomorphic-ws-4.0.1.tgz",
"integrity": "sha512-BhBvN2MBpWTaSHdWRb/bwdZJ1WaehQ2L1KngkCkfLUGF0mAWAT1sQUQacEmQ0jXkFw/czDXPNQSL5u2/Krsz1w==",
"license": "MIT",
"peerDependencies": {
"ws": "*"
}
},
"node_modules/jayson": {
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/jayson/-/jayson-4.3.0.tgz",
"integrity": "sha512-AauzHcUcqs8OBnCHOkJY280VaTiCm57AbuO7lqzcw7JapGj50BisE3xhksye4zlTSR1+1tAz67wLTl8tEH1obQ==",
"license": "MIT",
"dependencies": {
"@types/connect": "^3.4.33",
"@types/node": "^12.12.54",
"@types/ws": "^7.4.4",
"commander": "^2.20.3",
"delay": "^5.0.0",
"es6-promisify": "^5.0.0",
"eyes": "^0.1.8",
"isomorphic-ws": "^4.0.1",
"json-stringify-safe": "^5.0.1",
"stream-json": "^1.9.1",
"uuid": "^8.3.2",
"ws": "^7.5.10"
},
"bin": {
"jayson": "bin/jayson.js"
},
"engines": {
"node": ">=8"
}
},
"node_modules/jayson/node_modules/commander": {
"version": "2.20.3",
"resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz",
"integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==",
"license": "MIT"
},
"node_modules/json-stringify-safe": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz",
"integrity": "sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==",
"license": "ISC"
},
"node_modules/ms": {
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
"license": "MIT"
},
"node_modules/node-fetch": {
"version": "2.7.0",
"resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz",
"integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==",
"license": "MIT",
"dependencies": {
"whatwg-url": "^5.0.0"
},
"engines": {
"node": "4.x || >=6.0.0"
},
"peerDependencies": {
"encoding": "^0.1.0"
},
"peerDependenciesMeta": {
"encoding": {
"optional": true
}
}
},
"node_modules/node-gyp-build": {
"version": "4.8.4",
"resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.8.4.tgz",
"integrity": "sha512-LA4ZjwlnUblHVgq0oBF3Jl/6h/Nvs5fzBLwdEF4nuxnFdsfajde4WfxtJr3CaiH+F6ewcIB/q4jQ4UzPyid+CQ==",
"license": "MIT",
"optional": true,
"bin": {
"node-gyp-build": "bin.js",
"node-gyp-build-optional": "optional.js",
"node-gyp-build-test": "build-test.js"
}
},
"node_modules/rpc-websockets": {
"version": "9.3.9",
"resolved": "https://registry.npmjs.org/rpc-websockets/-/rpc-websockets-9.3.9.tgz",
"integrity": "sha512-2iQDaTB4g5fDB2ihrTFSJSibCEuxaRi1q7qTW7ZO9/M5/TC+ToHA4D9/ffNLEbAoHNNrcdeP05oATNk44SKZXA==",
"license": "LGPL-3.0-only",
"dependencies": {
"@swc/helpers": "^0.5.11",
"@types/uuid": "^10.0.0",
"@types/ws": "^8.2.2",
"buffer": "^6.0.3",
"eventemitter3": "^5.0.1",
"uuid": "^14.0.0",
"ws": "^8.5.0"
},
"funding": {
"type": "paypal",
"url": "https://paypal.me/kozjak"
},
"optionalDependencies": {
"bufferutil": "^4.0.1",
"utf-8-validate": "^6.0.0"
}
},
"node_modules/rpc-websockets/node_modules/@types/ws": {
"version": "8.18.1",
"resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.1.tgz",
"integrity": "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==",
"license": "MIT",
"dependencies": {
"@types/node": "*"
}
},
"node_modules/rpc-websockets/node_modules/utf-8-validate": {
"version": "6.0.6",
"resolved": "https://registry.npmjs.org/utf-8-validate/-/utf-8-validate-6.0.6.tgz",
"integrity": "sha512-q3l3P9UtEEiAHcsgsqTgf9PPjctrDWoIXW3NpOHFdRDbLvu4DLIcxHangJ4RLrWkBcKjmcs/6NkerI8T/rE4LA==",
"hasInstallScript": true,
"license": "MIT",
"optional": true,
"dependencies": {
"node-gyp-build": "^4.3.0"
},
"engines": {
"node": ">=6.14.2"
}
},
"node_modules/rpc-websockets/node_modules/uuid": {
"version": "14.0.0",
"resolved": "https://registry.npmjs.org/uuid/-/uuid-14.0.0.tgz",
"integrity": "sha512-Qo+uWgilfSmAhXCMav1uYFynlQO7fMFiMVZsQqZRMIXp0O7rR7qjkj+cPvBHLgBqi960QCoo/PH2/6ZtVqKvrg==",
"funding": [
"https://github.com/sponsors/broofa",
"https://github.com/sponsors/ctavan"
],
"license": "MIT",
"bin": {
"uuid": "dist-node/bin/uuid"
}
},
"node_modules/rpc-websockets/node_modules/ws": {
"version": "8.21.0",
"resolved": "https://registry.npmjs.org/ws/-/ws-8.21.0.tgz",
"integrity": "sha512-Vsp28b7DRcimFQvrqu2Wek3z1iYxDCWqHYB8Qsnk/S4RfaCQzPGPyBNuVjJV3cd6UiKtUtp6sNM77gWvzcCH+g==",
"license": "MIT",
"engines": {
"node": ">=10.0.0"
},
"peerDependencies": {
"bufferutil": "^4.0.1",
"utf-8-validate": ">=5.0.2"
},
"peerDependenciesMeta": {
"bufferutil": {
"optional": true
},
"utf-8-validate": {
"optional": true
}
}
},
"node_modules/safe-buffer": {
"version": "5.2.1",
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
"integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/feross"
},
{
"type": "patreon",
"url": "https://www.patreon.com/feross"
},
{
"type": "consulting",
"url": "https://feross.org/support"
}
],
"license": "MIT"
},
"node_modules/stream-chain": {
"version": "2.2.5",
"resolved": "https://registry.npmjs.org/stream-chain/-/stream-chain-2.2.5.tgz",
"integrity": "sha512-1TJmBx6aSWqZ4tx7aTpBDXK0/e2hhcNSTV8+CbFJtDjbb+I1mZ8lHit0Grw9GRT+6JbIrrDd8esncgBi8aBXGA==",
"license": "BSD-3-Clause"
},
"node_modules/stream-json": {
"version": "1.9.1",
"resolved": "https://registry.npmjs.org/stream-json/-/stream-json-1.9.1.tgz",
"integrity": "sha512-uWkjJ+2Nt/LO9Z/JyKZbMusL8Dkh97uUBTv3AJQ74y07lVahLY4eEFsPsE97pxYBwr8nnjMAIch5eqI0gPShyw==",
"license": "BSD-3-Clause",
"dependencies": {
"stream-chain": "^2.2.5"
}
},
"node_modules/superstruct": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/superstruct/-/superstruct-2.0.2.tgz",
"integrity": "sha512-uV+TFRZdXsqXTL2pRvujROjdZQ4RAlBUS5BTh9IGm+jTqQntYThciG/qu57Gs69yjnVUSqdxF9YLmSnpupBW9A==",
"license": "MIT",
"engines": {
"node": ">=14.0.0"
}
},
"node_modules/text-encoding-utf-8": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/text-encoding-utf-8/-/text-encoding-utf-8-1.0.2.tgz",
"integrity": "sha512-8bw4MY9WjdsD2aMtO0OzOCY3pXGYNx2d2FfHRVUKkiCPDWjKuOlhLVASS+pD7VkLTVjW268LYJHwsnPFlBpbAg=="
},
"node_modules/tr46": {
"version": "0.0.3",
"resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz",
"integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==",
"license": "MIT"
},
"node_modules/tslib": {
"version": "2.8.1",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
"license": "0BSD"
},
"node_modules/typescript": {
"version": "6.0.3",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-6.0.3.tgz",
"integrity": "sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==",
"license": "Apache-2.0",
"peer": true,
"bin": {
"tsc": "bin/tsc",
"tsserver": "bin/tsserver"
},
"engines": {
"node": ">=14.17"
}
},
"node_modules/utf-8-validate": {
"version": "5.0.10",
"resolved": "https://registry.npmjs.org/utf-8-validate/-/utf-8-validate-5.0.10.tgz",
"integrity": "sha512-Z6czzLq4u8fPOyx7TU6X3dvUZVvoJmxSQ+IcrlmagKhilxlhZgxPK6C5Jqbkw1IDUmFTM+cz9QDnnLTwDz/2gQ==",
"hasInstallScript": true,
"license": "MIT",
"optional": true,
"peer": true,
"dependencies": {
"node-gyp-build": "^4.3.0"
},
"engines": {
"node": ">=6.14.2"
}
},
"node_modules/uuid": {
"version": "8.3.2",
"resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz",
"integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==",
"deprecated": "uuid@10 and below is no longer supported. For ESM codebases, update to uuid@latest. For CommonJS codebases, use uuid@11 (but be aware this version will likely be deprecated in 2028).",
"license": "MIT",
"bin": {
"uuid": "dist/bin/uuid"
}
},
"node_modules/webidl-conversions": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz",
"integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==",
"license": "BSD-2-Clause"
},
"node_modules/whatwg-url": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz",
"integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==",
"license": "MIT",
"dependencies": {
"tr46": "~0.0.3",
"webidl-conversions": "^3.0.0"
}
},
"node_modules/ws": {
"version": "7.5.11",
"resolved": "https://registry.npmjs.org/ws/-/ws-7.5.11.tgz",
"integrity": "sha512-zS54Oen9bITtp7kp2XM3AydrCIq1D+HwJOuH+c+e4LfpL/lotP5osijd+UoMnxwAam1GN8R4KtLAyIrIcBNpiA==",
"license": "MIT",
"engines": {
"node": ">=8.3.0"
},
"peerDependencies": {
"bufferutil": "^4.0.1",
"utf-8-validate": "^5.0.2"
},
"peerDependenciesMeta": {
"bufferutil": {
"optional": true
},
"utf-8-validate": {
"optional": true
}
}
}
}
}

View File

@ -10,7 +10,8 @@
"author": "",
"license": "ISC",
"dependencies": {
"@noble/curves": "^1.5.0"
"@noble/curves": "^1.5.0",
"@solana/web3.js": "^1.98.4"
},
"devDependencies": {
"esbuild": "^0.28.1"

View File

@ -18,9 +18,10 @@
</div>
<label class="field">
<span>Shine server</span>
<input id="server-url" type="text" placeholder="wss://shineup.me/ws" />
<span>Логин сервера SHiNE</span>
<input id="server-url" type="text" placeholder="shineupme" />
</label>
<p id="server-address" class="muted small">Текущий адрес: https://shineup.me</p>
<div id="session-card" class="card hidden">
<div class="card-title">Подключённая wallet-session</div>

View File

@ -1,5 +1,6 @@
const els = {
serverUrl: document.querySelector('#server-url'),
serverAddress: document.querySelector('#server-address'),
loginInput: document.querySelector('#login-input'),
usePassword: document.querySelector('#use-password'),
passwordField: document.querySelector('#password-field'),
@ -22,6 +23,8 @@ const els = {
let state = {
settings: {
serverLogin: 'shineupme',
serverHttp: 'https://shineup.me',
serverUrl: 'wss://shineup.me/ws',
login: '',
},
@ -61,11 +64,13 @@ function formatRemaining(ms) {
function applyState(nextState) {
state = nextState || state;
const serverValue = String(state?.settings?.serverUrl || 'wss://shineup.me/ws');
const serverValue = String(state?.settings?.serverLogin || 'shineupme');
const serverAddressValue = String(state?.settings?.serverHttp || 'https://shineup.me');
const loginValue = String(state?.settings?.login || '');
if (document.activeElement !== els.serverUrl) {
els.serverUrl.value = serverValue;
}
els.serverAddress.textContent = `Текущий адрес: ${serverAddressValue}`;
if (document.activeElement !== els.loginInput) {
els.loginInput.value = loginValue;
}
@ -134,7 +139,7 @@ async function refreshState() {
async function saveSettings() {
await sendMessage('wallet:saveSettings', {
serverUrl: String(els.serverUrl.value || '').trim(),
serverLogin: String(els.serverUrl.value || '').trim(),
login: String(els.loginInput.value || '').trim(),
});
}
@ -162,7 +167,7 @@ async function startPairing() {
login,
usePassword: !!els.usePassword.checked,
password: String(els.passwordInput.value || ''),
serverUrl: String(els.serverUrl.value || '').trim(),
serverLogin: String(els.serverUrl.value || '').trim(),
});
applyState(response.state);
} catch (error) {

View File

@ -1,2 +1,2 @@
client.version=1.2.205
server.version=1.2.194
client.version=1.2.206
server.version=1.2.195

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>

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';

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} сек.`);

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 {
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);
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-канала автоматически.',
);
});

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 {
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);
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-канала автоматически.',
);
});

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';
}

View File

@ -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,
};
}

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() {