diff --git a/services/apple/.gitignore b/services/apple/.gitignore index 911edee..a09cd82 100644 --- a/services/apple/.gitignore +++ b/services/apple/.gitignore @@ -90,3 +90,5 @@ Shared (Extension)/Resources/images/ Shared (Extension)/Resources/background-bundle.js Shared (Extension)/Resources/content-bundle.js Shared (Extension)/Resources/manifest.json +Shared (Extension)/Resources/preauth.html +Shared (Extension)/Resources/preauth.js diff --git a/services/apple/HotPocket.xcodeproj/project.pbxproj b/services/apple/HotPocket.xcodeproj/project.pbxproj index cd9d48b..ff99e18 100644 --- a/services/apple/HotPocket.xcodeproj/project.pbxproj +++ b/services/apple/HotPocket.xcodeproj/project.pbxproj @@ -114,6 +114,8 @@ "Resources/content-bundle.js", Resources/images, Resources/manifest.json, + Resources/preauth.html, + Resources/preauth.js, SafariWebExtensionHandler.m, ); target = 4CABCAD42E56F0C900D8A354 /* HotPocket Extension (iOS) */; @@ -126,6 +128,8 @@ "Resources/content-bundle.js", Resources/images, Resources/manifest.json, + Resources/preauth.html, + Resources/preauth.js, SafariWebExtensionHandler.m, ); target = 4CABCADE2E56F0C900D8A354 /* HotPocket Extension (macOS) */; diff --git a/services/extension/rollup.config.js b/services/extension/rollup.config.js index 95ea0b3..628cddd 100644 --- a/services/extension/rollup.config.js +++ b/services/extension/rollup.config.js @@ -40,7 +40,9 @@ if (BASE_URL === null) { const PLUGINS = [ string({ - include: /\.html$/, + include: [ + 'src/content/templates/*.html', + ], }), replace({ include: /\.js$/, @@ -115,6 +117,13 @@ export default [ src: 'assets/images', dest: OUTPUT_PATH, }, + { + src: [ + 'src/content/preauth.html', + 'src/content/preauth.js', + ], + dest: OUTPUT_PATH, + }, ], }), ], diff --git a/services/extension/src/background/main.js b/services/extension/src/background/main.js index 9397bea..e27ccb6 100644 --- a/services/extension/src/background/main.js +++ b/services/extension/src/background/main.js @@ -1,8 +1,23 @@ import HotPocketExtension from '../common'; -const AUTH_URL = (new URL('/integrations/extension/authenticate/', HotPocketExtension.base_url)).toString(); -const POST_AUTH_URL = (new URL('/integrations/extension/post-authenticate/', HotPocketExtension.base_url)).toString(); -const RPC_URL = (new URL('/rpc/', HotPocketExtension.base_url)).toString(); +const POST_AUTH_PATH = '/integrations/extension/post-authenticate/'; +const RPC_PATH = '/rpc/'; + +let authSessionToken = null; +let rpcURL = null; + +const updateRpcURL = () => { + rpcURL = null; + if (HotPocketExtension.base_url !== null) { + rpcURL = (new URL(RPC_PATH, HotPocketExtension.base_url)).toString(); + } + + HotPocketExtension.LOGGER.debug( + 'HotPocketExtension.background.updateRpcURL()', + HotPocketExtension.base_url, + rpcURL, + ); +}; const makeJSONRPCCall = (method, params) => { return { @@ -43,7 +58,7 @@ const executeJSONRPCCall = async (url, call, {accessToken}) => { }, ); HotPocketExtension.LOGGER.debug( - 'HotPocketExtension.content.executeJSONRPCCall()', response, + 'HotPocketExtension.background.executeJSONRPCCall()', response, ); if (response.status !== 200) { @@ -56,7 +71,7 @@ const executeJSONRPCCall = async (url, call, {accessToken}) => { const callResult = await response.json(); if (callResult.error) { HotPocketExtension.LOGGER.error( - 'HotPocketExtension.content.executeJSONRPCCall(): RPC error', + 'HotPocketExtension.background.executeJSONRPCCall(): RPC error', callResult.error.code, callResult.error.message, callResult.error.data, @@ -69,7 +84,7 @@ const executeJSONRPCCall = async (url, call, {accessToken}) => { } } catch (exception) { HotPocketExtension.LOGGER.error( - 'HotPocketExtension.content.executeJSONRPCCall(): Fetch error', exception, + 'HotPocketExtension.background.executeJSONRPCCall(): Fetch error', exception, ); error = { code: -32000, @@ -90,9 +105,9 @@ const getAccessTokenMeta = () => { const doSave = async (accessToken, tab) => { const call = makeJSONRPCCall('saves.create', [tab.url]); - const [result, error] = await executeJSONRPCCall(RPC_URL, call, {accessToken}); + const [result, error] = await executeJSONRPCCall(rpcURL, call, {accessToken}); HotPocketExtension.LOGGER.debug( - 'HotPocketExtension.content.doSave():', result, error, + 'HotPocketExtension.background.doSave():', result, error, ); if (error !== null) { @@ -112,7 +127,7 @@ const doCreateAndStoreAccessToken = async (authKey) => { ); const [accessToken, error] = await executeJSONRPCCall( - RPC_URL, accessTokenCall, {accessToken: null}, + rpcURL, accessTokenCall, {accessToken: null}, ); if (error === null) { @@ -125,15 +140,34 @@ const doCreateAndStoreAccessToken = async (authKey) => { }; const doHandleAuthFlow = (authTab) => { + HotPocketExtension.LOGGER.debug( + 'HotPocketExtension.background.doHandleAuthFlow()', authTab, + ); + let currentAuthTabId = authTab.id; + return new Promise((resolve, reject) => { const onTabsUpdated = (tabId, changeInfo, updatedTab) => { - if (tabId === authTab.id) { - HotPocketExtension.LOGGER.debug( - 'HotPocketExtension.content.doHandleAuthFlow.onTabsUpdated()', updatedTab, changeInfo, - ); + const changedURL = changeInfo.url; - const changedURL = changeInfo.url; - if (changedURL && changedURL.startsWith(POST_AUTH_URL)) { + HotPocketExtension.LOGGER.debug( + 'HotPocketExtension.background.doHandleAuthFlow.onTabsUpdated()', + updatedTab, + changedURL, + (changedURL && changedURL.includes(POST_AUTH_PATH)), + changeInfo, + ); + + const expectedSessionTabQuery = `?authSessionToken=${authSessionToken}`; + if (tabId !== currentAuthTabId && changedURL.includes(expectedSessionTabQuery)) { + // When redirecting from the preauth page to the HotPocket instance, + // Safari "replaces" the auth tab with a new one. This nasty hack will + // allow the extension to keep track of it. + // I hate computers. + currentAuthTabId = tabId; + } + + if (tabId === currentAuthTabId) { + if (changedURL && changedURL.includes(POST_AUTH_PATH)) { const parsedChangedURL = new URL(changedURL); const authKey = parsedChangedURL.searchParams.get('auth_key'); @@ -154,8 +188,9 @@ const doHandleAuthFlow = (authTab) => { reject(error); }). finally(() => { + authSessionToken = null; HotPocketExtension.api.tabs.onUpdated.removeListener(onTabsUpdated); - HotPocketExtension.api.tabs.remove(authTab.id); + HotPocketExtension.api.tabs.remove(currentAuthTabId); }); } } @@ -175,7 +210,7 @@ const doCheckAuth = async (accessToken) => { [accessToken, getAccessTokenMeta()], ); - const [result, error] = await executeJSONRPCCall(RPC_URL, call, { + const [result, error] = await executeJSONRPCCall(rpcURL, call, { accessToken, }); @@ -196,15 +231,28 @@ const doCheckAuth = async (accessToken) => { return accessToken; }; -const doGetAccessToken = async () => { - let storageResult = await HotPocketExtension.api.storage.local.get('accessToken'); - let accessToken = await doCheckAuth( - storageResult.accessToken || null, +const doSetupRPC = async () => { + let storageResult = await HotPocketExtension.api.storage.local.get( + ['accessToken', 'baseURL'], ); + let accessToken = null; + if (storageResult.baseURL) { + HotPocketExtension.base_url = storageResult.baseURL; + updateRpcURL(); + + accessToken = await doCheckAuth( + storageResult.accessToken || null, + ); + } + if (accessToken === null) { + authSessionToken = crypto.randomUUID(); + const authTab = await HotPocketExtension.api.tabs.create({ - url: AUTH_URL, + url: HotPocketExtension.api.runtime.getURL( + `preauth.html?authSessionToken=${authSessionToken}`, + ), }); accessToken = await doHandleAuthFlow(authTab); @@ -217,24 +265,53 @@ const doSendTabMessage = (tab, message) => { HotPocketExtension.api.tabs.sendMessage(tab.id, message). then((result) => { HotPocketExtension.LOGGER.debug( - 'HotPocketExtension.content.doSendTabMessage(): message sent', message, result, + 'HotPocketExtension.background.doSendTabMessage(): message sent', + message, + result, ); }). catch((error) => { HotPocketExtension.LOGGER.error( - 'HotPocketExtension.content.doSendTabMessage(): could not send message', error, + 'HotPocketExtension.background.doSendTabMessage(): could not send message', + error, + ); + }); +}; + +const doUpdateBaseURL = (nextBaseURL) => { + HotPocketExtension.base_url = nextBaseURL; + updateRpcURL(); + + HotPocketExtension.api.storage.local. + set({ + baseURL: nextBaseURL, + }). + then(() => { + HotPocketExtension.LOGGER.debug( + 'HotPocketExtension.background.doUpdateBaseURL()', 'Base URL saved', + ); + }). + catch((error) => { + HotPocketExtension.LOGGER.error( + 'HotPocketExtension.background.doUpdateBaseURL()', error, ); }); }; const onTabCreated = (tab) => { - HotPocketExtension.LOGGER.debug('HotPocketExtension.onTabCreated()', tab); - HotPocketExtension.api.action.enable(tab.id); + HotPocketExtension.LOGGER.debug( + 'HotPocketExtension.background.onTabCreated()', tab, + ); + HotPocketExtension.api.action.enable(tab.id).catch((error) => { + HotPocketExtension.LOGGER.error( + 'HotPocketExtension.background.onTabCreated()', tab.id, error, + ); + }); }; const onBrowserActionClicked = async (tab) => { HotPocketExtension.LOGGER.debug( - 'HotPocketExtension.onBrowserActionClicked()', tab.url, + 'HotPocketExtension.background.onBrowserActionClicked()', tab.url, ); if (!tab.url) { @@ -245,11 +322,11 @@ const onBrowserActionClicked = async (tab) => { let error = null; try { - let accessToken = await doGetAccessToken(); + let accessToken = await doSetupRPC(); result = await doSave(accessToken, tab); HotPocketExtension.LOGGER.debug( - 'HotPocketExtension.onBrowserActionClicked()', result, + 'HotPocketExtension.background.onBrowserActionClicked()', result, ); } catch (exception) { HotPocketExtension.LOGGER.error( @@ -276,10 +353,11 @@ const onMessage = (message, sender, sendResponse) => { let response = {ok: true}; try { if (message.type === 'HotPocket:Extension:ping') { - HotPocketExtension.LOGGER.debug(sender.tab.id); doSendTabMessage(sender.tab, { type: 'HotPocket:Extension:pong', }); + } else if (message.type === 'HotPocket:Extension:setBaseURL') { + doUpdateBaseURL(message.result); } } catch (exception) { HotPocketExtension.LOGGER.error( @@ -300,6 +378,8 @@ export default ({...configuration}) => { background: true, }); + updateRpcURL(); + HotPocketExtension.api.tabs.onCreated.addListener(onTabCreated); HotPocketExtension.api.action.onClicked.addListener(onBrowserActionClicked); diff --git a/services/extension/src/common.js b/services/extension/src/common.js index e029f2a..4be8ab4 100644 --- a/services/extension/src/common.js +++ b/services/extension/src/common.js @@ -8,7 +8,7 @@ const HotPocketExtension = { version: __HOTPOCKET_EXTENSION_VERSION__, debug: DEBUG, api: null, - base_url: __HOTPOCKET_EXTENSION_BASE_URL__, + base_url: null, LOGGER: { // eslint-disable-next-line no-console debug: (DEBUG === true) ? console.log : noop, diff --git a/services/extension/src/content/preauth.html b/services/extension/src/content/preauth.html new file mode 100644 index 0000000..dbde3b2 --- /dev/null +++ b/services/extension/src/content/preauth.html @@ -0,0 +1,91 @@ + + + +
+ + + + +HotPocket by BTHLabs
+
+
+ HotPocket by BTHLabs v25.9.12
+
+
+ Copyright © 2025-present by BTHLabs. All rights reserved.
+