/* global FIREFOX getActiveTab waitForTabUrl URLS */// toolbox.js /* global chromeLocal */// storage-util.js 'use strict'; /* exported tokenMan */ const tokenMan = (() => { const AUTH = { dropbox: { flow: 'token', clientId: 'zg52vphuapvpng9', authURL: 'https://www.dropbox.com/oauth2/authorize', tokenURL: 'https://api.dropboxapi.com/oauth2/token', revoke: token => fetch('https://api.dropboxapi.com/2/auth/token/revoke', { method: 'POST', headers: { 'Authorization': `Bearer ${token}`, }, }), }, google: { flow: 'code', clientId: '283762574871-d4u58s4arra5jdan2gr00heasjlttt1e.apps.googleusercontent.com', clientSecret: 'J0nc5TlR_0V_ex9-sZk-5faf', authURL: 'https://accounts.google.com/o/oauth2/v2/auth', authQuery: { // NOTE: Google needs 'prompt' parameter to deliver multiple refresh // tokens for multiple machines. // https://stackoverflow.com/q/18519185 access_type: 'offline', prompt: 'consent', }, tokenURL: 'https://oauth2.googleapis.com/token', scopes: ['https://www.googleapis.com/auth/drive.appdata'], // FIXME: https://github.com/openstyles/stylus/issues/1248 // revoke: token => { // const params = {token}; // return postQuery(`https://accounts.google.com/o/oauth2/revoke?${new URLSearchParams(params)}`); // }, }, onedrive: { flow: 'code', clientId: '3864ce03-867c-4ad8-9856-371a097d47b1', clientSecret: '9Pj=TpsrStq8K@1BiwB9PIWLppM:@s=w', authURL: 'https://login.microsoftonline.com/common/oauth2/v2.0/authorize', tokenURL: 'https://login.microsoftonline.com/common/oauth2/v2.0/token', scopes: ['Files.ReadWrite.AppFolder', 'offline_access'], }, userstylesworld: { flow: 'code', clientId: 'zeDmKhJIfJqULtcrGMsWaxRtWHEimKgS', clientSecret: 'wqHsvTuThQmXmDiVvOpZxPwSIbyycNFImpAOTxjaIRqDbsXcTOqrymMJKsOMuibFaij' + 'ZZAkVYTDbLkQuYFKqgpMsMlFlgwQOYHvHFbgxQHDTwwdOroYhOwFuekCwXUlk', authURL: URLS.usw + 'api/oauth/style/link', tokenURL: URLS.usw + 'api/oauth/token', redirect_uri: 'https://gusted.xyz/callback_helper/', }, }; const NETWORK_LATENCY = 30; // seconds const DEFAULT_REDIRECT_URI = 'https://clngdbkpkpeebahjckkjfobafhncgmne.chromiumapp.org/'; let alwaysUseTab = FIREFOX ? false : null; class TokenError extends Error { constructor(provider, message) { super(`[${provider}] ${message}`); this.name = 'TokenError'; this.provider = provider; if (Error.captureStackTrace) { Error.captureStackTrace(this, TokenError); } } } return { buildKeys(name, hooks) { const prefix = `secure/token/${hooks ? hooks.keyName(name) : name}/`; const k = { TOKEN: `${prefix}token`, EXPIRE: `${prefix}expire`, REFRESH: `${prefix}refresh`, }; k.LIST = Object.values(k); return k; }, getClientId(name) { return AUTH[name].clientId; }, async getToken(name, interactive, hooks) { const k = tokenMan.buildKeys(name, hooks); const obj = await chromeLocal.get(k.LIST); if (obj[k.TOKEN]) { if (!obj[k.EXPIRE] || Date.now() < obj[k.EXPIRE]) { return obj[k.TOKEN]; } if (obj[k.REFRESH]) { return refreshToken(name, k, obj); } } if (!interactive) { throw new TokenError(name, 'Token is missing'); } return authUser(k, name, interactive, hooks); }, async revokeToken(name, hooks) { const provider = AUTH[name]; const k = tokenMan.buildKeys(name, hooks); if (provider.revoke) { try { const token = await chromeLocal.getValue(k.TOKEN); if (token) await provider.revoke(token); } catch (e) { console.error(e); } } await chromeLocal.remove(k.LIST); }, }; async function refreshToken(name, k, obj) { if (!obj[k.REFRESH]) { throw new TokenError(name, 'No refresh token'); } const provider = AUTH[name]; const body = { client_id: provider.clientId, refresh_token: obj[k.REFRESH], grant_type: 'refresh_token', scope: provider.scopes.join(' '), }; if (provider.clientSecret) { body.client_secret = provider.clientSecret; } const result = await postQuery(provider.tokenURL, body); if (!result.refresh_token) { // reuse old refresh token result.refresh_token = obj[k.REFRESH]; } return handleTokenResult(result, k); } async function authUser(keys, name, interactive = false, hooks = null) { await require(['/vendor/webext-launch-web-auth-flow/webext-launch-web-auth-flow']); /* global webextLaunchWebAuthFlow */ const provider = AUTH[name]; const state = Math.random().toFixed(8).slice(2); const query = { response_type: provider.flow, client_id: provider.clientId, redirect_uri: provider.redirect_uri || DEFAULT_REDIRECT_URI, state, }; if (provider.scopes) { query.scope = provider.scopes.join(' '); } if (provider.authQuery) { Object.assign(query, provider.authQuery); } if (alwaysUseTab == null) { alwaysUseTab = await detectVivaldiWebRequestBug(); } if (hooks) hooks.query(query); const url = `${provider.authURL}?${new URLSearchParams(query)}`; const width = Math.min(screen.availWidth - 100, 800); const height = Math.min(screen.availHeight - 100, 800); const wnd = await browser.windows.getLastFocused(); const finalUrl = await webextLaunchWebAuthFlow({ url, alwaysUseTab, interactive, redirect_uri: query.redirect_uri, windowOptions: Object.assign({ state: 'normal', width, height, }, wnd.state !== 'minimized' && { // Center the popup to the current window top: Math.ceil(wnd.top + (wnd.height - width) / 2), left: Math.ceil(wnd.left + (wnd.width - width) / 2), }), }); const params = new URLSearchParams( provider.flow === 'token' ? new URL(finalUrl).hash.slice(1) : new URL(finalUrl).search.slice(1) ); if (params.get('state') !== state) { throw new TokenError(name, `Unexpected state: ${params.get('state')}, expected: ${state}`); } let result; if (provider.flow === 'token') { const obj = {}; for (const [key, value] of params) { obj[key] = value; } result = obj; } else { const code = params.get('code'); const body = { code, grant_type: 'authorization_code', client_id: provider.clientId, redirect_uri: query.redirect_uri, state, }; if (provider.clientSecret) { body.client_secret = provider.clientSecret; } result = await postQuery(provider.tokenURL, body); } return handleTokenResult(result, keys); } async function handleTokenResult(result, k) { await chromeLocal.set({ [k.TOKEN]: result.access_token, [k.EXPIRE]: result.expires_in ? Date.now() + (result.expires_in - NETWORK_LATENCY) * 1000 : undefined, [k.REFRESH]: result.refresh_token, }); return result.access_token; } async function postQuery(url, body) { const options = { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded', }, body: body ? new URLSearchParams(body) : null, }; const r = await fetch(url, options); if (r.ok) { return r.json(); } const text = await r.text(); const err = new Error(`Failed to fetch (${r.status}): ${text}`); err.code = r.status; throw err; } async function detectVivaldiWebRequestBug() { // Workaround for https://github.com/openstyles/stylus/issues/1182 // Note that modern Vivaldi isn't exposed in `navigator.userAgent` but it adds `extData` to tabs const anyTab = await getActiveTab() || (await browser.tabs.query({}))[0]; if (anyTab && !(anyTab.extData || anyTab.vivExtData)) { return false; } let bugged = true; const TEST_URL = chrome.runtime.getURL('manifest.json'); const check = ({url}) => { bugged = url !== TEST_URL; }; chrome.webRequest.onBeforeRequest.addListener(check, {urls: [TEST_URL], types: ['main_frame']}); const {tabs: [tab]} = await browser.windows.create({ type: 'popup', state: 'minimized', url: TEST_URL, }); await waitForTabUrl(tab); chrome.windows.remove(tab.windowId); chrome.webRequest.onBeforeRequest.removeListener(check); return bugged; } })();