/** * VrilsoftApi integration: JWT login, premium check, server-side secret protection. * @module ellyPremium */ const K = { apiBase: 'ellyVrilApiBaseUrl', jwt: 'ellyVrilsoftJwt', /** Last successful login email (for Premium UI greeting only). */ lastLoginEmail: 'ellyVrilsoftLastLoginEmail', encApiKey: 'ellyProtectedApiKey', encSecret: 'ellyProtectedApiSecret' }; const DEFAULT_API_BASE = 'https://vrilsoftapi.com'; /** Relative to API origin: plan picker, then cart checkout. */ export const ELLY_PREMIUM_PLANS_PATH = 'Client/VrilPremium'; /** * @returns {Promise} */ export async function getApiBase() { const x = await chrome.storage.local.get([K.apiBase]); const b = x[K.apiBase]; const raw = typeof b === 'string' && b.trim() !== '' ? b : DEFAULT_API_BASE; return raw.replace(/\/$/, ''); } /** * @param {string} base * @returns {Promise} */ export async function setApiBase(base) { const t = (base || '').trim().replace(/\/$/, ''); await chrome.storage.local.set({ [K.apiBase]: t || DEFAULT_API_BASE }); } /** * Vril One premium plans (user picks a tier, then proceeds to cart). * @returns {Promise} */ export async function getPremiumPlansPageUrl() { const base = await getApiBase(); return `${base}/${ELLY_PREMIUM_PLANS_PATH}`; } /** * Public site base for /Legal/Terms etc. (matches VrilWebHosting Register links). * @returns {Promise} */ export async function getLegalPagesBaseUrl() { const api = await getApiBase(); try { const u = new URL(api); if (u.hostname === 'vrilsoftapi.com' || u.hostname.endsWith('.vrilsoftapi.com')) { return 'https://vrilsoft.com'; } return `${u.protocol}//${u.hostname}`; } catch { return 'https://vrilsoft.com'; } } /** * Canonical Elly proprietary software license page. * @returns {Promise} */ export async function getEllyLicenseUrl() { const base = await getLegalPagesBaseUrl(); return `${base}/Legal/Elly`; } /** * @returns {Promise} */ export async function getJwt() { const x = await chrome.storage.local.get([K.jwt]); const j = x[K.jwt]; return typeof j === 'string' && j.length > 0 ? j : null; } /** * @param {string|null} token * @returns {Promise} */ export async function setJwt(token) { if (!token) { await chrome.storage.local.remove([K.jwt, K.lastLoginEmail]); return; } await chrome.storage.local.set({ [K.jwt]: token }); } /** * Email from the last successful login (Premium greeting). Cleared when JWT is cleared. * @returns {Promise} */ export async function getLastLoginEmail() { const x = await chrome.storage.local.get([K.lastLoginEmail]); const e = x[K.lastLoginEmail]; return typeof e === 'string' && e.trim() !== '' ? e.trim() : null; } /** * Whether server-encrypted API key / secret blobs exist in extension storage. * @returns {Promise<{ hasApiKey: boolean, hasSecret: boolean }>} */ export async function getProtectedCredentialPresence() { const x = await chrome.storage.local.get([K.encApiKey, K.encSecret]); const hasApiKey = typeof x[K.encApiKey] === 'string' && x[K.encApiKey].trim().length > 0; const hasSecret = typeof x[K.encSecret] === 'string' && x[K.encSecret].trim().length > 0; return { hasApiKey, hasSecret }; } /** * @param {string} email * @param {string} password * @returns {Promise<{ ok: boolean, error?: string }>} */ /** * @param {{ * realName: string, * userName: string, * email: string, * mobile: string, * password: string, * confirmPassword: string, * acceptTos: boolean, * acceptRefund: boolean, * acceptPrivacy: boolean * }} body * @returns {Promise<{ ok: boolean, message?: string, error?: string }>} */ export async function registerVrilsoftAccount(body) { const base = await getApiBase(); try { const res = await fetch(`${base}/Api/Auth/Register`, { method: 'POST', headers: { 'Content-Type': 'application/json', Accept: 'application/json' }, credentials: 'omit', body: JSON.stringify(body) }); const data = await res.json().catch(() => ({})); if (!res.ok) { return { ok: false, error: data.message || `Registration failed (${res.status})` }; } return { ok: true, message: data.message || 'Account created.' }; } catch (e) { return { ok: false, error: e instanceof Error ? e.message : 'Network error' }; } } /** * @param {string} email * @param {string} password * @returns {Promise<{ * ok: boolean, * error?: string, * requiresMfa?: boolean, * mfaTicket?: string, * requiresMfaEnrollment?: boolean, * message?: string * }>} */ export async function loginVrilsoft(email, password) { const base = await getApiBase(); try { const res = await fetch(`${base}/Api/Auth/Login`, { method: 'POST', headers: { 'Content-Type': 'application/json', Accept: 'application/json' }, credentials: 'omit', body: JSON.stringify({ emailOrUserName: email, password }) }); const data = await res.json().catch(() => ({})); const requiresMfa = !!(data.requiresMfa ?? data.RequiresMfa); const mfaTicket = data.mfaTicket ?? data.MfaTicket ?? null; const requiresMfaEnrollment = !!(data.requiresMfaEnrollment ?? data.RequiresMfaEnrollment); const token = data.data?.token ?? data.Data?.token ?? data.data?.Token ?? null; const success = !!(data.success ?? data.Success); if (requiresMfaEnrollment) { return { ok: false, requiresMfaEnrollment: true, message: data.message || data.Message || 'Authenticator MFA enrollment required.', error: data.message || data.Message || 'MFA enrollment required on your Vrilsoft / Vril One account before signing in here.' }; } if (success && requiresMfa && mfaTicket) { return { ok: false, requiresMfa: true, mfaTicket: String(mfaTicket), message: data.message || data.Message || 'MFA required.' }; } if (!res.ok || !success || !token) { return { ok: false, error: data.message || data.Message || `Login failed (${res.status})` }; } await setJwt(token); await chrome.storage.local.set({ [K.lastLoginEmail]: email.trim() }); return { ok: true }; } catch (e) { return { ok: false, error: e instanceof Error ? e.message : 'Network error' }; } } /** * Completes login after authenticator MFA. * @param {string} mfaTicket * @param {string} code * @param {string} [emailForGreeting] * @returns {Promise<{ ok: boolean, error?: string }>} */ export async function verifyVrilsoftMfa(mfaTicket, code, emailForGreeting) { const base = await getApiBase(); try { const res = await fetch(`${base}/Api/Auth/MfaVerify`, { method: 'POST', headers: { 'Content-Type': 'application/json', Accept: 'application/json' }, credentials: 'omit', body: JSON.stringify({ mfaTicket, code }) }); const data = await res.json().catch(() => ({})); const success = !!(data.success ?? data.Success); const token = data.data?.token ?? data.Data?.token ?? data.data?.Token ?? null; if (!res.ok || !success || !token) { return { ok: false, error: data.message || data.Message || `MFA verify failed (${res.status})` }; } await setJwt(token); if (emailForGreeting && String(emailForGreeting).trim()) { await chrome.storage.local.set({ [K.lastLoginEmail]: String(emailForGreeting).trim() }); } return { ok: true }; } catch (e) { return { ok: false, error: e instanceof Error ? e.message : 'Network error' }; } } /** * @returns {Promise<{ isPremium: boolean, raw?: unknown, error?: string }>} */ export async function fetchAmIPremium() { const jwt = await getJwt(); if (!jwt) { return { isPremium: false, error: 'not_logged_in' }; } const base = await getApiBase(); try { const res = await fetch(`${base}/ellyext/am-i-premium`, { method: 'POST', headers: { Authorization: `Bearer ${jwt}`, Accept: 'application/json' } }); const raw = await res.json().catch(() => ({})); if (!res.ok) { if (res.status === 401) { await setJwt(null); } return { isPremium: false, error: (raw && raw.error) || `HTTP ${res.status}`, raw }; } return { isPremium: !!raw.isPremium, raw }; } catch (e) { return { isPremium: false, error: e instanceof Error ? e.message : 'network' }; } } /** * @param {string} plaintext * @returns {Promise<{ ok: boolean, protectedPayload?: string, error?: string }>} */ export async function protectPlaintext(plaintext) { const jwt = await getJwt(); if (!jwt) { return { ok: false, error: 'not_logged_in' }; } const base = await getApiBase(); try { const res = await fetch(`${base}/ellyext/protect-secret`, { method: 'POST', headers: { Authorization: `Bearer ${jwt}`, 'Content-Type': 'application/json', Accept: 'application/json' }, body: JSON.stringify({ plaintext }) }); const raw = await res.json().catch(() => ({})); if (!res.ok) { return { ok: false, error: raw.error || `HTTP ${res.status}` }; } if (!raw.protectedPayload) { return { ok: false, error: 'invalid_response' }; } return { ok: true, protectedPayload: raw.protectedPayload }; } catch (e) { return { ok: false, error: e instanceof Error ? e.message : 'network' }; } } export { K as EllyStorageKeys };