v3 · 37 files

ProjectElly/options/ellyPremium.js 10 KB Raw
/**
 * 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<string>}
 */
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<void>}
 */
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<string>}
 */
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<string>}
 */
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<string>}
 */
export async function getEllyLicenseUrl() {
    const base = await getLegalPagesBaseUrl();
    return `${base}/Legal/Elly`;
}

/**
 * @returns {Promise<string|null>}
 */
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<void>}
 */
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<string|null>}
 */
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 };