diff --git a/html/seed-display.html b/html/seed-display.html index 682b66d9..ae7c49f7 100644 --- a/html/seed-display.html +++ b/html/seed-display.html @@ -111,7 +111,7 @@

-
+
@@ -231,7 +231,7 @@

} function showPassword() { - if (seedInput.type === "password") { + if (uiSigninPassphraseText.type === "password") { uiSigninPassphraseText.type = "text"; uiShowSeedText.textContent = "Hide passphrase"; uiShowSeedOn.classList.add("hidden"); @@ -245,7 +245,7 @@

} function confirmStoredPassphrase() { - if (seedConfirm.checked) { + if (uiSeedConfirm.checked) { uiSignupSubmit.removeAttribute("disabled") } else { uiSignupSubmit.setAttribute("disabled", "disabled") diff --git a/html/signin-connect.html b/html/signin-connect.html index ec4e308b..ab8a2fa8 100644 --- a/html/signin-connect.html +++ b/html/signin-connect.html @@ -54,7 +54,7 @@

+ + diff --git a/package-lock.json b/package-lock.json index 287457ed..0b70aa6d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,13 +11,14 @@ "dependencies": { "buffer": "^6.0.3", "confusables": "^1.1.0", + "core-js": "^3.20.2", "crypto-browserify": "^3.12.0", "idb-keyval": "^6.1.0", "mustache": "^4.2.0", "post-me": "^0.4.5", "punycode": "^2.1.1", "randombytes": "^2.1.0", - "skynet-js": "^4.0.21-beta", + "skynet-js": "^4.0.22-beta", "skynet-mysky-utils": "^0.3.0", "stream-browserify": "^3.0.0", "tweetnacl": "^1.0.3", @@ -2278,25 +2279,6 @@ "node": ">=4.0.0" } }, - "node_modules/@skynetlabs/tus-js-client": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/@skynetlabs/tus-js-client/-/tus-js-client-2.3.0.tgz", - "integrity": "sha512-piGvPlJh+Bu3Qf08bDlc/TnFLXE81KnFoPgvnsddNwTSLyyspxPFxJmHO5ki6SYyOl3HmUtGPoix+r2M2UpFEA==", - "dependencies": { - "buffer-from": "^0.1.1", - "combine-errors": "^3.0.3", - "is-stream": "^2.0.0", - "js-base64": "^2.6.1", - "lodash.throttle": "^4.1.1", - "proper-lockfile": "^2.0.1", - "url-parse": "^1.4.3" - } - }, - "node_modules/@skynetlabs/tus-js-client/node_modules/buffer-from": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-0.1.2.tgz", - "integrity": "sha512-RiWIenusJsmI2KcvqQABB83tLxCByE3upSP8QU3rJDMVFGPWLvPQJt/O1Su9moRWeH7d+Q2HYb68f6+v+tw2vg==" - }, "node_modules/@tailwindcss/forms": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/@tailwindcss/forms/-/forms-0.4.0.tgz", @@ -3128,19 +3110,6 @@ "node": ">=8" } }, - "node_modules/async-mutex": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/async-mutex/-/async-mutex-0.3.2.tgz", - "integrity": "sha512-HuTK7E7MT7jZEh1P9GtRW9+aTWiDWWi9InbZ5hjxrnRa39KS4BW04+xLBhYNS2aXhHUIKZSw3gj4Pn1pj+qGAA==", - "dependencies": { - "tslib": "^2.3.1" - } - }, - "node_modules/async-mutex/node_modules/tslib": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.1.tgz", - "integrity": "sha512-77EbyPPpMz+FRFRuAFlWMtmgUWGe9UOG2Z25NqCwiIjRhOf5iKGuzSe5P2w1laq+FkRy4p+PCuVkJSGkzTEKVw==" - }, "node_modules/asynckit": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", @@ -3924,6 +3893,16 @@ "safe-buffer": "~5.1.1" } }, + "node_modules/core-js": { + "version": "3.21.0", + "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.21.0.tgz", + "integrity": "sha512-YUdI3fFu4TF/2WykQ2xzSiTQdldLB4KVuL9WeAy5XONZYt5Cun/fpQvctoKbCgvPhmzADeesTk/j2Rdx77AcKQ==", + "hasInstallScript": true, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/core-js" + } + }, "node_modules/core-js-compat": { "version": "3.20.2", "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.20.2.tgz", @@ -8596,12 +8575,10 @@ } }, "node_modules/skynet-js": { - "version": "4.0.21-beta", - "resolved": "https://registry.npmjs.org/skynet-js/-/skynet-js-4.0.21-beta.tgz", - "integrity": "sha512-Urnuacp+3Ps7P7I3GH6kH8Ggj3poRURYhZdqTEvjLNYCvVJdPOJzJhtWst/r5oTDyIfnmKdh4x+KAiM9veY8Sg==", + "version": "4.0.22-beta", + "resolved": "https://registry.npmjs.org/skynet-js/-/skynet-js-4.0.22-beta.tgz", + "integrity": "sha512-DpTCuMIT5JSBTwJX3nb/qkHVAFyCeEA8fanBGzZPj4QviYK3Mmk+lZKLETd5RzxavkBtgW7VlSogmxdTDjjbyg==", "dependencies": { - "@skynetlabs/tus-js-client": "^2.3.0", - "async-mutex": "^0.3.2", "axios": "^0.24.0", "base32-decode": "^1.0.0", "base32-encode": "^1.1.1", @@ -8614,6 +8591,7 @@ "randombytes": "^2.1.0", "sjcl": "^1.0.8", "skynet-mysky-utils": "^0.3.0", + "tus-js-client": "^2.2.0", "tweetnacl": "^1.0.3", "url-join": "^4.0.1", "url-parse": "^1.5.1" @@ -9225,7 +9203,6 @@ "version": "2.3.0", "resolved": "https://registry.npmjs.org/tus-js-client/-/tus-js-client-2.3.0.tgz", "integrity": "sha512-I4cSwm6N5qxqCmBqenvutwSHe9ntf81lLrtf6BmLpG2v4wTl89atCQKqGgqvkodE6Lx+iKIjMbaXmfvStTg01g==", - "dev": true, "dependencies": { "buffer-from": "^0.1.1", "combine-errors": "^3.0.3", @@ -9239,8 +9216,7 @@ "node_modules/tus-js-client/node_modules/buffer-from": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-0.1.2.tgz", - "integrity": "sha512-RiWIenusJsmI2KcvqQABB83tLxCByE3upSP8QU3rJDMVFGPWLvPQJt/O1Su9moRWeH7d+Q2HYb68f6+v+tw2vg==", - "dev": true + "integrity": "sha512-RiWIenusJsmI2KcvqQABB83tLxCByE3upSP8QU3rJDMVFGPWLvPQJt/O1Su9moRWeH7d+Q2HYb68f6+v+tw2vg==" }, "node_modules/tweetnacl": { "version": "1.0.3", @@ -11443,27 +11419,6 @@ } } }, - "@skynetlabs/tus-js-client": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/@skynetlabs/tus-js-client/-/tus-js-client-2.3.0.tgz", - "integrity": "sha512-piGvPlJh+Bu3Qf08bDlc/TnFLXE81KnFoPgvnsddNwTSLyyspxPFxJmHO5ki6SYyOl3HmUtGPoix+r2M2UpFEA==", - "requires": { - "buffer-from": "^0.1.1", - "combine-errors": "^3.0.3", - "is-stream": "^2.0.0", - "js-base64": "^2.6.1", - "lodash.throttle": "^4.1.1", - "proper-lockfile": "^2.0.1", - "url-parse": "^1.4.3" - }, - "dependencies": { - "buffer-from": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-0.1.2.tgz", - "integrity": "sha512-RiWIenusJsmI2KcvqQABB83tLxCByE3upSP8QU3rJDMVFGPWLvPQJt/O1Su9moRWeH7d+Q2HYb68f6+v+tw2vg==" - } - } - }, "@tailwindcss/forms": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/@tailwindcss/forms/-/forms-0.4.0.tgz", @@ -12130,21 +12085,6 @@ "integrity": "sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ==", "dev": true }, - "async-mutex": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/async-mutex/-/async-mutex-0.3.2.tgz", - "integrity": "sha512-HuTK7E7MT7jZEh1P9GtRW9+aTWiDWWi9InbZ5hjxrnRa39KS4BW04+xLBhYNS2aXhHUIKZSw3gj4Pn1pj+qGAA==", - "requires": { - "tslib": "^2.3.1" - }, - "dependencies": { - "tslib": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.1.tgz", - "integrity": "sha512-77EbyPPpMz+FRFRuAFlWMtmgUWGe9UOG2Z25NqCwiIjRhOf5iKGuzSe5P2w1laq+FkRy4p+PCuVkJSGkzTEKVw==" - } - } - }, "asynckit": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", @@ -12730,6 +12670,11 @@ "safe-buffer": "~5.1.1" } }, + "core-js": { + "version": "3.21.0", + "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.21.0.tgz", + "integrity": "sha512-YUdI3fFu4TF/2WykQ2xzSiTQdldLB4KVuL9WeAy5XONZYt5Cun/fpQvctoKbCgvPhmzADeesTk/j2Rdx77AcKQ==" + }, "core-js-compat": { "version": "3.20.2", "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.20.2.tgz", @@ -16254,12 +16199,10 @@ "integrity": "sha512-LzIjEQ0S0DpIgnxMEayM1rq9aGwGRG4OnZhCdjx7glTaJtf4zRfpg87ImfjSJjoW9vKpagd82McDOwbRT5kQKQ==" }, "skynet-js": { - "version": "4.0.21-beta", - "resolved": "https://registry.npmjs.org/skynet-js/-/skynet-js-4.0.21-beta.tgz", - "integrity": "sha512-Urnuacp+3Ps7P7I3GH6kH8Ggj3poRURYhZdqTEvjLNYCvVJdPOJzJhtWst/r5oTDyIfnmKdh4x+KAiM9veY8Sg==", + "version": "4.0.22-beta", + "resolved": "https://registry.npmjs.org/skynet-js/-/skynet-js-4.0.22-beta.tgz", + "integrity": "sha512-DpTCuMIT5JSBTwJX3nb/qkHVAFyCeEA8fanBGzZPj4QviYK3Mmk+lZKLETd5RzxavkBtgW7VlSogmxdTDjjbyg==", "requires": { - "@skynetlabs/tus-js-client": "^2.3.0", - "async-mutex": "^0.3.2", "axios": "^0.24.0", "base32-decode": "^1.0.0", "base32-encode": "^1.1.1", @@ -16272,6 +16215,7 @@ "randombytes": "^2.1.0", "sjcl": "^1.0.8", "skynet-mysky-utils": "^0.3.0", + "tus-js-client": "^2.2.0", "tweetnacl": "^1.0.3", "url-join": "^4.0.1", "url-parse": "^1.5.1" @@ -16728,7 +16672,6 @@ "version": "2.3.0", "resolved": "https://registry.npmjs.org/tus-js-client/-/tus-js-client-2.3.0.tgz", "integrity": "sha512-I4cSwm6N5qxqCmBqenvutwSHe9ntf81lLrtf6BmLpG2v4wTl89atCQKqGgqvkodE6Lx+iKIjMbaXmfvStTg01g==", - "dev": true, "requires": { "buffer-from": "^0.1.1", "combine-errors": "^3.0.3", @@ -16742,8 +16685,7 @@ "buffer-from": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-0.1.2.tgz", - "integrity": "sha512-RiWIenusJsmI2KcvqQABB83tLxCByE3upSP8QU3rJDMVFGPWLvPQJt/O1Su9moRWeH7d+Q2HYb68f6+v+tw2vg==", - "dev": true + "integrity": "sha512-RiWIenusJsmI2KcvqQABB83tLxCByE3upSP8QU3rJDMVFGPWLvPQJt/O1Su9moRWeH7d+Q2HYb68f6+v+tw2vg==" } } }, diff --git a/package.json b/package.json index d14a31fe..a5ccbe3c 100644 --- a/package.json +++ b/package.json @@ -45,13 +45,14 @@ "dependencies": { "buffer": "^6.0.3", "confusables": "^1.1.0", + "core-js": "^3.20.2", "crypto-browserify": "^3.12.0", "idb-keyval": "^6.1.0", "mustache": "^4.2.0", "post-me": "^0.4.5", "punycode": "^2.1.1", "randombytes": "^2.1.0", - "skynet-js": "^4.0.21-beta", + "skynet-js": "^4.0.22-beta", "skynet-mysky-utils": "^0.3.0", "stream-browserify": "^3.0.0", "tweetnacl": "^1.0.3", diff --git a/scripts/permissions-display.ts b/scripts/permissions-display.ts index 83cb1ab6..3a2d2562 100644 --- a/scripts/permissions-display.ts +++ b/scripts/permissions-display.ts @@ -19,7 +19,7 @@ let parentConnection: Connection | null = null; // ====== window.onerror = function (error: any) { - console.log(error); + console.warn(error); if (parentConnection) { if (typeof error === "string") { void parentConnection.remoteHandle().call("catchError", error); diff --git a/scripts/permissions.ts b/scripts/permissions.ts index 90bc7fb2..dc1d0ba0 100644 --- a/scripts/permissions.ts +++ b/scripts/permissions.ts @@ -34,7 +34,7 @@ const methods = { // ====== self.onerror = function (error: any) { - console.log(error); + console.warn(error); if (parentConnection) { if (typeof error === "string") { void parentConnection.remoteHandle().call("catchError", error); diff --git a/scripts/seed-display.ts b/scripts/seed-display.ts index 7af4e06e..8ad80b56 100644 --- a/scripts/seed-display.ts +++ b/scripts/seed-display.ts @@ -30,7 +30,7 @@ let parentConnection: Connection | null = null; // ====== window.onerror = function (error: any) { - console.log(error); + console.warn(error); if (parentConnection) { if (typeof error === "string") { void parentConnection.remoteHandle().call("catchError", error); @@ -71,6 +71,7 @@ window.onload = async () => { }; (window as any).signIn = (event: Event) => { + // Prevent making unnecessary request. event.preventDefault(); const phraseValue = uiSigninPassphraseText.value; @@ -86,7 +87,10 @@ window.onload = async () => { handleResponse({ seed, email: null, action: "signin" }); }; -(window as any).signUp = () => { +(window as any).signUp = (event: Event) => { + // Prevent making unnecessary request. + event.preventDefault(); + if (uiSeedConfirm.checked === false) return; const seed = phraseToSeed(uiSignupPassphraseText.value); diff --git a/scripts/seed-selection.ts b/scripts/seed-selection.ts index 39dd0ead..e8b6f595 100644 --- a/scripts/seed-selection.ts +++ b/scripts/seed-selection.ts @@ -8,7 +8,7 @@ let parentConnection: Connection | null = null; // ====== window.onerror = function (error: any) { - console.log(error); + console.warn(error); if (parentConnection) { if (typeof error === "string") { void parentConnection.remoteHandle().call("catchError", error); diff --git a/scripts/signin-connect.ts b/scripts/signin-connect.ts index 1724f35f..8f2d01ae 100644 --- a/scripts/signin-connect.ts +++ b/scripts/signin-connect.ts @@ -1,5 +1,7 @@ import { ChildHandshake, Connection, WindowMessenger } from "post-me"; +import { log } from "../src/util"; + const uiConnectEmailText = document.getElementById("connect-email-text")!; let readyEmail: string | null = null; @@ -11,7 +13,7 @@ let parentConnection: Connection | null = null; // ====== window.onerror = function (error: any) { - console.log(error); + console.warn(error); if (parentConnection) { if (typeof error === "string") { void parentConnection.remoteHandle().call("catchError", error); @@ -37,7 +39,7 @@ window.onload = async () => { }; (window as any).continue = () => { - handleEmail(null); + handleEmail(""); }; // ========== @@ -48,6 +50,8 @@ window.onload = async () => { * Initialize the communication with the UI. */ async function init(): Promise { + log("Entered init"); + // Establish handshake with parent window. const messenger = new WindowMessenger({ @@ -67,11 +71,13 @@ async function init(): Promise { * @returns - The email, if set. */ async function getEmail(): Promise { + log("Entered getEmail"); + const checkInterval = 100; return new Promise((resolve) => { const checkFunc = () => { - if (readyEmail) { + if (readyEmail !== null) { resolve(readyEmail); } }; @@ -85,7 +91,7 @@ async function getEmail(): Promise { * * @param email - The email. */ -function handleEmail(email: string | null): void { +function handleEmail(email: string): void { // Set `readyEmail`, triggering `getEmail`. readyEmail = email; } diff --git a/scripts/ui.ts b/scripts/ui.ts index 67fcc365..8d5bb79f 100644 --- a/scripts/ui.ts +++ b/scripts/ui.ts @@ -5,6 +5,7 @@ import { defaultHandshakeAttemptsInterval, defaultHandshakeMaxAttempts, dispatchedErrorEvent, + ensureUrl, errorWindowClosed, monitorWindowError, Permission, @@ -12,8 +13,15 @@ import { import { MySky, SkynetClient } from "skynet-js"; import { hashWithSalt } from "../src/crypto"; -import { login, register } from "../src/portal-account"; -import { checkStoredSeed, EMAIL_STORAGE_KEY, SEED_STORAGE_KEY } from "../src/mysky"; +import { + checkStoredSeed, + EMAIL_STORAGE_KEY, + getCurrentAndReferrerDomains, + INITIAL_PORTAL, + PORTAL_LOGIN_COMPLETE_SENTINEL_KEY, + PORTAL_LOGIN_COMPLETE_SUCCESS_VALUE, + SEED_STORAGE_KEY, +} from "../src/mysky"; import { getPermissionsProviderUrl, relativePermissionsDisplayUrl, @@ -23,15 +31,22 @@ import { SeedProviderResponse, } from "../src/provider"; import { log } from "../src/util"; +import { getUserSettings } from "../src/user_settings"; const RELATIVE_SEED_SELECTION_DISPLAY_URL = "seed-selection.html"; const RELATIVE_SIGNIN_CONNECT_DISPLAY_URL = "signin-connect.html"; +const MYSKY_PORTAL_LOGIN_TIMEOUT = 30000; + +// Create a client on the current portal. const client = new SkynetClient(); let parentConnection: Connection | null = null; // Set value of dev on load. let dev = false; +/// #if ENV == 'dev' +dev = true; +/// #endif // ====== // Events @@ -52,7 +67,7 @@ window.addEventListener("beforeunload", function (event) { }); window.onerror = function (error: any) { - console.log(error); + console.warn(error); if (parentConnection) { if (typeof error === "string") { void parentConnection.remoteHandle().call("catchError", error); @@ -65,10 +80,6 @@ window.onerror = function (error: any) { // TODO: Wrap in a try-catch block? Does onerror handler catch thrown errors? // Code that runs on page load. window.onload = () => { - /// #if ENV == 'dev' - dev = true; - /// #endif - void init(); }; @@ -117,6 +128,9 @@ async function requestLoginAccess(permissions: Permission[]): Promise<[boolean, // Save the seed and email in local storage. saveSeedAndEmail(seed, email); + // Wait for Main MySky to login successfully. + await resolveOnMySkyPortalLogin(); + // Pass in any request permissions and get a permissions response. const permissionsResponse = await getPermissions(seed, permissions); @@ -156,12 +170,7 @@ async function checkBrowserSupported(): Promise { * then we display the signin-connect page where the user may connect his email * on signin. * - * 5. If we got the email, then we register/login to set the JWT cookie. - * - * 6. If the user provided a new email at some point, then we save it in user - * settings, after having successfully connected to a portal account. - * - * (7. We return the seed and email and save them in storage in another + * (5. We return the seed and email and save them in storage in another * function, which triggers Main MySky's storage listener.) * * @returns - The seed and email. @@ -191,7 +200,10 @@ async function getSeedAndEmail(): Promise<[Uint8Array, string | null]> { // We're signing in, try to get the email from saved settings. let savedEmailFound = false; if (!email) { - email = await getEmailFromSettings(); + const siaskyClient = new SkynetClient(INITIAL_PORTAL); + const { currentDomain } = await getCurrentAndReferrerDomains(); + const { email: receivedEmail } = await getUserSettings(siaskyClient, seed, currentDomain); + email = receivedEmail; savedEmailFound = email !== null; } @@ -208,16 +220,6 @@ async function getSeedAndEmail(): Promise<[Uint8Array, string | null]> { } } - // Register/login. - if (email) { - await connectToPortalAccount(seed, email); - } - - // TODO: Save the new provided email in user settings. - if (emailProvidedByUser) { - await saveEmailInSettings(); - } - return [seed, email]; } @@ -228,23 +230,13 @@ async function getSeedAndEmail(): Promise<[Uint8Array, string | null]> { */ async function getSeedAndEmailFromProvider(): Promise { // Show seed provider chooser. - const seedProviderDisplayUrl = await getSeedProviderDisplayUrl(); + const seedProviderDisplayUrl = ensureUrl(await getSeedProviderDisplayUrl()); // User has chosen seed provider, open seed provider display. log("Calling runSeedProviderDisplay"); return await runSeedProviderDisplay(seedProviderDisplayUrl); } -// TODO -/** - * Tries to get the email from the saved user settings. - * - * @returns - The email if found. - */ -async function getEmailFromSettings(): Promise { - return null; -} - /** * Launch the permissions provider and get any ungranted permissions. Show * permissions to user and get their response. @@ -254,6 +246,8 @@ async function getEmailFromSettings(): Promise { * @returns - The permissions response. */ async function getPermissions(seed: Uint8Array, permissions: Permission[]): Promise { + log("Entered getPermissions"); + // Open the permissions provider. log("Calling launchPermissionsProvider"); const permissionsProvider = await launchPermissionsProvider(seed); @@ -280,39 +274,6 @@ async function getPermissions(seed: Uint8Array, permissions: Permission[]): Prom return permissionsResponse; } -/** - * Connects to a portal account by either registering or logging in to an - * existing account. The resulting cookie will be set on the MySky domain and - * takes effect in Main MySky immediate. - * - * NOTE: Main MySky will register "auto re-login"; we don't have to do that - * here. - * - * @param seed - The user seed. - * @param email - The user email. - */ -async function connectToPortalAccount(seed: Uint8Array, email: string): Promise { - // Register and get the JWT cookie. - // - // Make requests to login and register in parallel. At most one can succeed, - // and this saves a lot of time. - try { - await Promise.any([register(client, seed, email), login(client, seed, email)]); - } catch (e) { - throw new Error(`Could not register or login: ${e}`); - } -} - -// TODO -/** - * If the email was provided by the user, save it in user settings. - * - * @returns - An empty promise. - */ -async function saveEmailInSettings(): Promise { - return; -} - /** * Gets the user's seed provider display URL if set, or the default. * @@ -339,7 +300,7 @@ async function getSeedProviderDisplayUrl(): Promise { * @returns - The URL. */ async function getSigninConnectDisplayUrl(): Promise { - return `${window.location.hostname}/${RELATIVE_SIGNIN_CONNECT_DISPLAY_URL}`; + return ensureUrl(`${window.location.hostname}/${RELATIVE_SIGNIN_CONNECT_DISPLAY_URL}`); } /** @@ -354,7 +315,7 @@ async function runPermissionsProviderDisplay( pendingPermissions: Permission[] ): Promise { const permissionsProviderUrl = await getPermissionsProviderUrl(seed); - const permissionsProviderDisplayUrl = `${permissionsProviderUrl}/${relativePermissionsDisplayUrl}`; + const permissionsProviderDisplayUrl = ensureUrl(`${permissionsProviderUrl}/${relativePermissionsDisplayUrl}`); return setupAndRunDisplay(permissionsProviderDisplayUrl, "getPermissions", pendingPermissions, document.referrer); } @@ -377,7 +338,7 @@ async function runSeedProviderDisplay(seedProviderDisplayUrl: string): Promise { // Get the display URL. - const seedSelectionDisplayUrl = `${window.location.hostname}/${RELATIVE_SEED_SELECTION_DISPLAY_URL}`; + const seedSelectionDisplayUrl = ensureUrl(`${window.location.hostname}/${RELATIVE_SEED_SELECTION_DISPLAY_URL}`); return setupAndRunDisplay(seedSelectionDisplayUrl, "getSeedProvider"); } @@ -390,7 +351,13 @@ async function _runSeedSelectionDisplay(): Promise { async function runSigninConnectDisplay(): Promise { const signinConnectDisplayUrl = await getSigninConnectDisplayUrl(); - return setupAndRunDisplay(signinConnectDisplayUrl, "getEmail"); + return setupAndRunDisplay(signinConnectDisplayUrl, "getEmail").then( + // Convert "" to null. In signin-connect.ts we use "" to signify no email + // was given. + (email: string) => { + return email === "" ? null : email; + } + ); } /** @@ -445,8 +412,12 @@ async function connectDisplayProvider(childFrame: HTMLIFrameElement): Promise(displayUrl: string, methodName: string, ...methodParams: unknown[]): Promise { - // Add error listener. + // Add debug parameter to the URL. + const displayUrlObject = new URL(displayUrl); + displayUrlObject.search = window.location.search; + displayUrl = displayUrlObject.toString(); + // Add error listener. const { promise: promiseError, controller: controllerError } = monitorWindowError(); let frame: HTMLIFrameElement; @@ -461,13 +432,13 @@ async function setupAndRunDisplay(displayUrl: string, methodName: string, ... try { // Launch the full-screen iframe and connection. - frame = launchDisplay(displayUrl); connection = await connectDisplayProvider(frame); // Get the response. - + // // TODO: This should be a dual-promise that also calls ping() on an interval and rejects if no response was found in a given amount of time. + log(`Calling method ${methodName} in iframe`); const response = await connection.remoteHandle().call(methodName, ...methodParams); resolve(response); @@ -494,6 +465,68 @@ async function setupAndRunDisplay(displayUrl: string, methodName: string, ... }); } +/** + * Resolves when portal login on Main MySky completes successfully. + * + * We register a storage event listener inside a promise that resolves the + * promise when we detect a successful portal login. The successful login is + * signaled via local storage. Any value other than "1" is considered to be an + * error message. If a successful login is not detected within a given timeout, + * then we reject the promise. + * + * @returns - An empty promise. + */ +async function resolveOnMySkyPortalLogin(): Promise { + log("Entered resolveOnMySkyPortalLogin"); + + const abortController = new AbortController(); + + // Set up a promise that succeeds on successful login in main MySky, and fails + // when the login attempt returns an error. + const promise1 = new Promise((resolve, reject) => { + const handleEvent = async ({ key, newValue }: StorageEvent) => { + // We only want the promise to resolve or reject when the right storage + // key is encountered. Any other storage key shouldn't trigger a `resolve`. + if (key !== PORTAL_LOGIN_COMPLETE_SENTINEL_KEY) { + return; + } + // We only want the promise to resolve or reject when the right storage + // key is set, and not removed. + // + // NOTE: We do remove the storage key before setting it in Main MySky, + // because otherwise, setting an already-set key has no effect. + if (!newValue) { + // Key was removed. + return; + } + + // Check for errors from Main MySky. + if (newValue !== PORTAL_LOGIN_COMPLETE_SUCCESS_VALUE) { + reject(newValue); + } + + // We got the value signaling a successful login, resolve the promise. + resolve(); + }; + + // Set up a storage event listener. + window.addEventListener("storage", handleEvent, { + signal: abortController.signal, + }); + }); + + // Set up promise that rejects on timeout. + const promise2 = new Promise((_, reject) => setTimeout(reject, MYSKY_PORTAL_LOGIN_TIMEOUT)); + + // Return when either promise finishes. Promise 1 returns when a login either + // fails or succeeds. Promise 2 returns when the execution time surpasses the + // timeout window. + return Promise.race([promise1, promise2]).finally(() => { + // Unregister the event listener. + abortController.abort(); + }); +} + // ======= // Helpers // ======= @@ -510,8 +543,9 @@ async function catchError(errorMsg: string): Promise { /** * Stores the root seed and email in local storage. This triggers the storage - * event listener in the main invisible MySky frame. Main MySky needs the email - * so that it can login again when the JWT cookie expires. + * event listener in the main invisible MySky frame. This switches to the + * preferred portal, registers or logs in to the portal account and sets up + * login again when the JWT cookie expires. See `setUpStorageEventListener`. * * NOTE: If ENV == 'dev' the seed is salted before storage. * @@ -519,9 +553,10 @@ async function catchError(errorMsg: string): Promise { * @param email - The email. */ export function saveSeedAndEmail(seed: Uint8Array, email: string | null): void { - log("Called saveSeedAndEmail"); + log("Entered saveSeedAndEmail"); + if (!localStorage) { - console.log("WARNING: localStorage disabled, seed not stored"); + console.warn("WARNING: localStorage disabled, seed not stored"); return; } @@ -535,6 +570,10 @@ export function saveSeedAndEmail(seed: Uint8Array, email: string | null): void { localStorage.setItem(EMAIL_STORAGE_KEY, email); } + // Clear the seed, or if we set it to a value that's already set it will not + // trigger the even listener. + localStorage.removeItem(SEED_STORAGE_KEY); + // Set the seed, triggering the storage event. localStorage.setItem(SEED_STORAGE_KEY, JSON.stringify(Array.from(seed))); } diff --git a/src/crypto.ts b/src/crypto.ts index 56768f3d..9e8f82a4 100644 --- a/src/crypto.ts +++ b/src/crypto.ts @@ -2,9 +2,26 @@ import { hash, sign } from "tweetnacl"; import { KeyPair, stringToUint8ArrayUtf8 } from "skynet-js"; import { toHexString } from "./util"; +import { ENCRYPTION_ROOT_PATH_SEED_BYTES_LENGTH } from "./encrypted_files"; + +// Descriptive salt that should not be changed. +const SALT_ENCRYPTED_PATH_SEED = "encrypted filesystem path seed"; const SALT_ROOT_DISCOVERABLE_KEY = "root discoverable key"; +/** + * Derives the root path seed. + * + * @param seed - The user seed. + * @returns - The root path seed. + */ +export function deriveRootPathSeed(seed: Uint8Array): Uint8Array { + const bytes = new Uint8Array([...sha512(SALT_ENCRYPTED_PATH_SEED), ...sha512(seed)]); + // NOTE: Truncate to 32 bytes instead of the 64 bytes for a directory path + // seed. This is a historical artifact left for backwards compatibility. + return sha512(bytes).slice(0, ENCRYPTION_ROOT_PATH_SEED_BYTES_LENGTH); +} + /** * Hashes the given message with the given salt applied. * diff --git a/src/index.ts b/src/index.ts index 6055afe7..9d93678d 100644 --- a/src/index.ts +++ b/src/index.ts @@ -13,8 +13,8 @@ import { log } from "./util"; try { await MySky.initialize(); } catch (err) { - console.log(err); + console.warn(err); } })().catch((err) => { - console.log(err); + console.warn(err); }); diff --git a/src/mysky.ts b/src/mysky.ts index c2226082..4f63b3c5 100644 --- a/src/mysky.ts +++ b/src/mysky.ts @@ -8,22 +8,27 @@ import { SkynetClient, PUBLIC_KEY_LENGTH, PRIVATE_KEY_LENGTH, + ExecuteRequestError, } from "skynet-js"; - import { CheckPermissionsResponse, PermCategory, Permission, PermType } from "skynet-mysky-utils"; import { sign } from "tweetnacl"; -import { genKeyPairFromSeed, hashWithSalt, sha512 } from "./crypto"; -import { deriveEncryptedPathSeedForRoot, ENCRYPTION_ROOT_PATH_SEED_BYTES_LENGTH } from "./encrypted_files"; -import { logout } from "./portal-account"; + +import { deriveRootPathSeed, genKeyPairFromSeed, hashWithSalt, sha512 } from "./crypto"; +import { deriveEncryptedPathSeedForRoot } from "./encrypted_files"; +import { login, logout, register } from "./portal_account"; import { launchPermissionsProvider } from "./provider"; import { SEED_LENGTH } from "./seed"; -import { hexToUint8Array, log, readablePermission } from "./util"; +import { getUserSettings, setUserSettings } from "./user_settings"; +import { ALPHA_ENABLED, DEV_ENABLED, hexToUint8Array, log, readablePermission } from "./util"; -export const SEED_STORAGE_KEY = "seed"; export const EMAIL_STORAGE_KEY = "email"; +export const PORTAL_STORAGE_KEY = "portal"; +export const SEED_STORAGE_KEY = "seed"; -// Descriptive salt that should not be changed. -const SALT_ENCRYPTED_PATH_SEED = "encrypted filesystem path seed"; +export const PORTAL_LOGIN_COMPLETE_SENTINEL_KEY = "portal-login-complete"; +export const PORTAL_LOGIN_COMPLETE_SUCCESS_VALUE = "1"; + +export const INITIAL_PORTAL = "https://siasky.net"; // SALT_MESSAGE_SIGNING is the prefix with which we salt the data that MySky // signs in order to be able to prove ownership of the MySky id. @@ -58,17 +63,22 @@ export class PermissionsProvider { } } +// TODO: Rename to differentiate from `MySky` in SDK? Perhaps `MainMySky`. /** * The class responsible for holding MySky-related data and connections and for * communicating with skapps and with the permissions provider. + * + * @property client - The associated SkynetClient. + * @property mySkyDomain - The current domain of this MySky instance. + * @property referrerDomain - The domain of the parent skapp. + * @property parentConnection - The handshake connection with the parent window. + * @property permissionsProvider - The permissions provider, if it has been loaded. */ export class MySky { - /* The handshake connection with the skapp. */ - protected parentConnection: Promise; - /* The permissions provider handle. */ - protected permissionsProvider: Promise | null = null; - /* The user's JWT token. */ - protected jwt: Promise | null = null; + protected parentConnection: Promise | null = null; + + protected email: string | null = null; + protected preferredPortal: string | null = null; // ============ // Constructors @@ -77,44 +87,27 @@ export class MySky { /** * Creates the `MySky` instance. * - * @param client - The Skynet Client. + * @param client - The Skynet client. + * @param mySkyDomain - The current domain of this MySky instance. * @param referrerDomain - The domain that referred us here (i.e. of the host skapp). - * @param seed - The user seed, if found. + * @param permissionsProvider - The permissions provider, if it has been loaded. */ - constructor(protected client: SkynetClient, protected referrerDomain: string, seed: Uint8Array | null) { - // Set child methods. - - const methods = { - checkLogin: this.checkLogin.bind(this), - getEncryptedFileSeed: this.getEncryptedPathSeed.bind(this), - getEncryptedPathSeed: this.getEncryptedPathSeed.bind(this), - logout: this.logout.bind(this), - signMessage: this.signMessage.bind(this), - signRegistryEntry: this.signRegistryEntry.bind(this), - signEncryptedRegistryEntry: this.signEncryptedRegistryEntry.bind(this), - userID: this.userID.bind(this), - verifyMessageSignature: this.verifyMessageSignature.bind(this), - }; - - // Enable communication with connector in parent skapp. - - log("Making handshake"); - const messenger = new WindowMessenger({ - localWindow: window, - remoteWindow: window.parent, - remoteOrigin: "*", - }); - this.parentConnection = ChildHandshake(messenger, methods); - - // Launch the permissions provider if the seed was given. - - if (seed) { - this.permissionsProvider = launchPermissionsProvider(seed); - } - } + constructor( + protected client: SkynetClient, + protected mySkyDomain: string, + protected referrerDomain: string, + protected permissionsProvider: Promise | null + ) {} /** - * Initializes MySky and returns a handle to the `MySky` instance. + * Do the asynchronous parts of initialization here before calling the + * constructor. + * + * NOTE: async is not allowed in constructors, which is why the work is split + * up like this. + * + * For the preferred portal flow, see "Load MySky redirect flow" on + * `redirectIfNotOnPreferredPortal` in the SDK. * * @returns - The `MySky` instance. * @throws - Will throw if the browser does not support web strorage. @@ -125,27 +118,60 @@ export class MySky { if (typeof Storage == "undefined") { throw new Error("Browser does not support web storage"); } + if (!localStorage) { + throw new Error("localStorage disabled"); + } - // Check for stored seed in localstorage. - + // Check for the stored seed in localstorage. const seed = checkStoredSeed(); - // Initialize the Skynet client. + // Launch the permissions provider if the seed was given. + let permissionsProvider = null; + if (seed) { + permissionsProvider = launchPermissionsProvider(seed); + } - const client = new SkynetClient(); + // Check for the preferred portal in localstorage. + let preferredPortal = checkStoredPreferredPortal(); - // Get the referrer. + const initialClient = getLoginClient(seed, preferredPortal); - const referrerDomain = await client.extractDomain(document.referrer); + const { currentDomain, referrerDomain } = await getCurrentAndReferrerDomains(); + if (!referrerDomain) { + throw new Error("Referrer not found"); + } // Create MySky object. - log("Calling new MySky()"); - const mySky = new MySky(client, referrerDomain, seed); + const mySky = new MySky(initialClient, currentDomain, referrerDomain, permissionsProvider); + + // Login to portal. + { + // Get email from local storage. + let storedEmail = checkStoredEmail(); + + // Get the preferred portal and stored email from user settings. + if (seed && !preferredPortal) { + const { portal, email } = await getUserSettings(initialClient, seed, currentDomain); + preferredPortal = portal; + storedEmail = email; + } + + // Set the portal. + mySky.setPortal(preferredPortal); + + // Set up auto-relogin if the email was found. + if (seed && storedEmail) { + mySky.email = storedEmail; + mySky.setupAutoRelogin(seed, storedEmail); + } + } // Set up the storage event listener. + mySky.setupStorageEventListener(); - mySky.setUpStorageEventListener(); + // We are ready to accept requests. Set up the handshake connection. + mySky.connectToParent(); return mySky; } @@ -188,6 +214,18 @@ export class MySky { return [true, permissionsResponse]; } + /** + * Checks whether the user can be automatically logged in to the portal (the + * user email was found). + * + * @returns - Whether the email was found. + */ + async checkPortalLogin(): Promise { + log("Entered checkPortalLogin"); + + return this.email !== null; + } + /** * Gets the encrypted path seed for the given path. * @@ -199,46 +237,100 @@ export class MySky { log("Entered getEncryptedPathSeed"); // Check with the permissions provider that we have permission for this request. - await this.checkPermission(path, PermCategory.Hidden, PermType.Read); // Get the seed. - const seed = checkStoredSeed(); if (!seed) { throw new Error("User seed not found"); } // Compute the root path seed. - - const bytes = new Uint8Array([...sha512(SALT_ENCRYPTED_PATH_SEED), ...sha512(seed)]); - // NOTE: Truncate to 32 bytes instead of the 64 bytes for a directory path - // seed. This is a historical artifact left for backwards compatibility. - const rootPathSeedBytes = sha512(bytes).slice(0, ENCRYPTION_ROOT_PATH_SEED_BYTES_LENGTH); + const rootPathSeedBytes = deriveRootPathSeed(seed); // Compute the child path seed. - return deriveEncryptedPathSeedForRoot(rootPathSeedBytes, path, isDirectory); } - // TODO + /** + * Gets the user's preferred portal, if set. + * + * @returns - The preferred portal, if set. + */ + async getPreferredPortal(): Promise { + log("Entered getPreferredPortal"); + + return this.preferredPortal; + } + + // TODO: Logout from all tabs. /** * Logs out of MySky. */ async logout(): Promise { - // Clear the stored seed. + const errors = []; - clearStoredSeed(); + // Check if user is logged in. + const seed = checkStoredSeed(); + + if (seed) { + // Clear the stored seed. + clearStoredSeed(); + } else { + errors.push(new Error("MySky user is already logged out")); + } + + // Clear other stored values. + clearStoredEmail(); + clearStoredPreferredPortal(); + + // Restore original `executeRequest`. + this.client.customOptions.loginFn = undefined; // Clear the JWT cookie. + // + // NOTE: We do this even if we could not find a seed above. The local + // storage might have been cleared with the JWT token still being active. + // + // NOTE: This will not auto-relogin on an expired JWT, just to logout again. + // If we get a 401 error, we just return silently without throwing. + try { + log("Calling logout"); + await logout(this.client); + } catch (e) { + if ((e as ExecuteRequestError).responseStatus !== 401) { + errors.push(e); + } + } + + // Throw all encountered errors. + if (errors.length > 0) { + throw new Error(`Error${errors.length > 1 ? "s" : ""} logging out: ${errors}`); + } + } + + /** + * Tries to log in to the portal through MySky. Should be called by the SDK + * whenever it detects an expired JWT. + */ + async portalLogin(): Promise { + // Get the seed. + const seed = checkStoredSeed(); + if (!seed) { + throw new Error("User seed not found"); + } + + // Get the email. + const email = this.email; + if (!email) { + throw new Error("Email not found"); + } - // TODO: When auto re-login is implemented, this should not auto-login on an - // expired JWT just to logout again. - await logout(this.client); + await login(this.client, seed, email); } /** - * signs the given data using the MySky user's private key. This method can be + * Signs the given data using the MySky user's private key. This method can be * used for MySky user verification as the signature may be verified against * the user's public key, which is the MySky user id. * @@ -246,7 +338,7 @@ export class MySky { * verifies an original message against the signature and the user's public * key * - * NOTE: this function (internally) adds a salt to the given data array to + * NOTE: This function (internally) adds a salt to the given data array to * ensure there's no potential overlap with anything else, like registry * entries. * @@ -328,14 +420,12 @@ export class MySky { */ async userID(): Promise { // Get the seed. - const seed = checkStoredSeed(); if (!seed) { throw new Error("User seed not found"); } // Get the public key. - const { publicKey } = genKeyPairFromSeed(seed); return publicKey; } @@ -374,18 +464,241 @@ export class MySky { // ================ /** - * Set up a listener for the storage event. If the seed is set in the UI, it - * should trigger a load of the permissions provider. + * Sets up the handshake connection with the parent. + */ + protected connectToParent(): void { + // Set child methods. + + const methods = { + checkLogin: this.checkLogin.bind(this), + checkPortalLogin: this.checkPortalLogin.bind(this), + // NOTE: `getEncryptedFileSeed` was renamed to `getEncryptedPathSeed`, but + // we still expose `getEncryptedFileSeed` in the API for backwards + // compatibility. + getEncryptedFileSeed: this.getEncryptedPathSeed.bind(this), + getEncryptedPathSeed: this.getEncryptedPathSeed.bind(this), + getPreferredPortal: this.getPreferredPortal.bind(this), + logout: this.logout.bind(this), + portalLogin: this.portalLogin.bind(this), + signMessage: this.signMessage.bind(this), + signRegistryEntry: this.signRegistryEntry.bind(this), + signEncryptedRegistryEntry: this.signEncryptedRegistryEntry.bind(this), + userID: this.userID.bind(this), + verifyMessageSignature: this.verifyMessageSignature.bind(this), + }; + + // Enable communication with connector in parent skapp. + + log("Performing handshake"); + const messenger = new WindowMessenger({ + localWindow: window, + remoteWindow: window.parent, + remoteOrigin: "*", + }); + this.parentConnection = ChildHandshake(messenger, methods); + } + + /** + * Connects to a portal account by either registering or logging in to an + * existing account. The resulting cookie will be set on the MySky domain. + * + * NOTE: We will register "auto re-login" in a separate function. + * + * @param seed - The user seed. + * @param email - The user email. + */ + protected async connectToPortalAccount(seed: Uint8Array, email: string): Promise { + log("Entered connectToPortalAccount"); + + // Try to connect to the portal account and set the JWT cookie. + // + // Make requests to login and register in parallel. At most one can succeed, + // and this saves a lot of time. + try { + await Promise.any([register(this.client, seed, email), login(this.client, seed, email)]); + } catch (err) { + const errors = (err as AggregateError).errors; + throw new Error(`Could not register or login: [${errors}]`); + } + } + + /** + * Logs in from MySky UI. + * + * Flow: + * + * 0. Unload the permissions provider and seed if stored. (Done in + * `setupStorageEventListener`.) + * + * 1. Always use siasky.net first. + * + * 2. Get the preferred portal and switch to it (`setPortal`), or if not found + * switch to current portal. + * + * 3. If we got the email, then we register/login to set the JWT cookie + * (`connectToPortalAccount`). + * + * 4. If the email is set, it should set up automatic re-login on JWT + * cookie expiry. + * + * 5. Save the email in user settings. + * + * 6. Trigger a load of the permissions provider. + * + * @param seed - The user seed. + */ + protected async loginFromUi(seed: Uint8Array): Promise { + log("Entered loginFromUi"); + + // Connect to siasky.net first. + // + // NOTE: Don't use the stored preferred portal here because we are just + // logging into a new account and need to get the user settings for the + // first time. Always use siasky.net. + this.client = getLoginClient(seed, null); + + // Login to portal. + { + // Get the preferred portal and switch to it. + const { portal: preferredPortal, email } = await getUserSettings(this.client, seed, this.mySkyDomain); + let storedEmail = email; + + // Set the portal + this.setPortal(preferredPortal); + + // The email wasn't in the user settings but the user might have just + // signed up with it -- check local storage. We don't need to do this if + // the email was already found. + // TODO: Add dedicated flow(s) for changing the email after it's set. + let isEmailProvidedByUser = false; + if (!storedEmail) { + storedEmail = checkStoredEmail(); + isEmailProvidedByUser = storedEmail !== null; + } + + if (storedEmail) { + // If an email was found, try to connect to a portal account and save + // the email if it was valid. + await this.loginHandleEmail(seed, storedEmail, isEmailProvidedByUser); + } + } + + // Launch the new permissions provider. + this.permissionsProvider = launchPermissionsProvider(seed); + } + + /** + * Handling for when the email is found or provided while logging in. + * + * @param seed - The user seed. + * @param storedEmail - The email, either found in user settings or set in browser storage. + * @param isEmailProvidedByUser - Indicates whether the user provided the email (it was found in browser storage). */ - setUpStorageEventListener(): void { - window.addEventListener("storage", ({ key, newValue }: StorageEvent) => { + protected async loginHandleEmail( + seed: Uint8Array, + storedEmail: string, + isEmailProvidedByUser: boolean + ): Promise { + try { + // Register/login to ensure the email is valid and get the JWT (in case + // we don't redirect to a preferred portal). + await this.connectToPortalAccount(seed, storedEmail); + } catch (e) { + // We don't want to make MySky initialization fail just because the + // user entered an invalid email. He'd never be able to log in and + // change it again. + // + // TODO: Maybe this should return a warning to the skapp? We don't + // have the ifrastructure in place for that yet. + console.warn(e); + + return; + } + + this.email = storedEmail; + + // Set up auto re-login on JWT expiry. + this.setupAutoRelogin(seed, storedEmail); + + // Save the email in user settings. Do this after we've connected to + // the portal account so we know that the email is valid. + if (isEmailProvidedByUser) { + await setUserSettings(this.client, seed, this.mySkyDomain, { portal: this.preferredPortal, email: storedEmail }); + } + } + + /** + * Sets the portal, either the preferred portal if given or the current portal + * otherwise. + * + * @param preferredPortal - The user's preferred portal + */ + protected setPortal(preferredPortal: string | null): void { + log(`Entered setPortal with portal: ${preferredPortal}`); + + if (preferredPortal) { + // Connect to the preferred portal if it was found. + this.client = new SkynetClient(preferredPortal); + this.preferredPortal = preferredPortal; + } else { + // Else, connect to the current portal as opposed to siasky.net. + this.client = new SkynetClient(); + } + } + + /** + * Sets up auto re-login. It modifies the client's `executeRequest` method to + * check if the request failed with `401 Unauthorized Response`. If so, it + * will try to login and make the request again. + * + * NOTE: If the request was a portal account logout, we will not login again + * just to logout. We also will not throw an error on 401, instead returning + * silently. There is no way for the client to know whether the cookie is set + * ahead of time, and an error would not be actionable. + * + * NOTE: We restore the original `executeRequest` on logout. We do not try to + * modify `executeRequest` if it is already modified and throw an error + * instead. + * + * @param seed - The user seed. + * @param email - The user email. + * @throws - Will throw if auto-login is already set up. + */ + protected setupAutoRelogin(seed: Uint8Array, email: string): void { + log("Entered setupAutoRelogin"); + + if (this.client.customOptions.loginFn) { + throw new Error("Tried to setup auto re-login with it already being set up"); + } + + this.client.customOptions.loginFn = async () => { + await login(this.client, seed, email); + }; + } + + /** + * Set up a listener for the storage event. Triggered when the seed is set. + * Unloads the permission provider to disable MySky functionality until the + * permission provider is loaded again at the end. + * + * For the preferred portal flow, see "Load MySky redirect flow" on + * `redirectIfNotOnPreferredPortal` in the SDK. + */ + protected setupStorageEventListener(): void { + log("Entered setupStorageEventListener"); + + window.addEventListener("storage", async ({ key, newValue }: StorageEvent) => { if (key !== SEED_STORAGE_KEY) { return; } + log("Entered storage event listener with seed storage key"); + if (this.permissionsProvider) { - // Unload the old permissions provider. No need to await on this. - void this.permissionsProvider.then((provider) => provider.close()); + // Unload the old permissions provider first. This makes sure that MySky + // can't respond to more requests until the new permissions provider is + // loaded at the end of this function. + await this.permissionsProvider.then((provider) => provider.close()); this.permissionsProvider = null; } @@ -394,26 +707,23 @@ export class MySky { return; } - // Parse the seed. - const seed = new Uint8Array(JSON.parse(newValue)); - - // Launch the new permissions provider. - this.permissionsProvider = launchPermissionsProvider(seed); - - // If the email is found, then set up auto-login on Main MySky. - const email = localStorage.getItem(EMAIL_STORAGE_KEY); - if (email) { - // Clear the stored email. - // - // The email can be cleared here because `localStorage` is only used to - // marshal the email from MySky UI over to the invisible MySky iframe. - // We don't clear the seed because we need it in storage so that users - // are automatically logged-in, when possible. But for the email, it - // should be stored on MySky, as the local storage can get cleared, - // users can move across browsers etc. - localStorage.removeItem(EMAIL_STORAGE_KEY); - - // TODO: Set up auto-login. + // Clear any existing value to make sure the storage event is triggered + // when we set the key. + localStorage.removeItem(PORTAL_LOGIN_COMPLETE_SENTINEL_KEY); + + try { + // Parse the seed. + const seed = new Uint8Array(JSON.parse(newValue)); + + await this.loginFromUi(seed); + + // Signal to MySky UI that we are done. + localStorage.setItem(PORTAL_LOGIN_COMPLETE_SENTINEL_KEY, PORTAL_LOGIN_COMPLETE_SUCCESS_VALUE); + } catch (e) { + log(`Error in storage event listener: ${e}`); + + // Send error to MySky UI. + localStorage.setItem(PORTAL_LOGIN_COMPLETE_SENTINEL_KEY, (e as Error).message); } }); } @@ -427,27 +737,28 @@ export class MySky { * @param category - The permission category. * @returns - The signature. */ - async signRegistryEntryHelper(entry: RegistryEntry, path: string, category: PermCategory): Promise { - log("Entered signRegistryEntry"); + protected async signRegistryEntryHelper( + entry: RegistryEntry, + path: string, + category: PermCategory + ): Promise { + log("Entered signRegistryEntryHelper"); // Check with the permissions provider that we have permission for this request. - await this.checkPermission(path, category, PermType.Write); // Get the seed. - const seed = checkStoredSeed(); if (!seed) { throw new Error("User seed not found"); } // Get the private key. - const { privateKey } = genKeyPairFromSeed(seed); // Sign the entry. - const signature = await signEntry(privateKey, entry, true); + return signature; } @@ -460,9 +771,8 @@ export class MySky { * @param permType - The permission type. * @throws - Will throw if the user doesn't have the required permission. */ - async checkPermission(path: string, category: PermCategory, permType: PermType): Promise { + protected async checkPermission(path: string, category: PermCategory, permType: PermType): Promise { // Check for the permissions provider. - if (!this.permissionsProvider) { throw new Error("Permissions provider not loaded"); } @@ -480,6 +790,34 @@ export class MySky { } } +// ======= +// Helpers +// ======= + +/** + * Checks for email stored in local storage. + * + * @returns - The email, or null if not found. + */ +export function checkStoredEmail(): string | null { + log("Entered checkStoredEmail"); + + const email = localStorage.getItem(EMAIL_STORAGE_KEY); + return email || null; +} + +/** + * Checks for preferred portal stored in local storage. + * + * @returns - The preferred portal, or null if not found. + */ +export function checkStoredPreferredPortal(): string | null { + log("Entered checkStoredPreferredPortal"); + + const portal = localStorage.getItem(PORTAL_STORAGE_KEY); + return portal || null; +} + /** * Checks for seed stored in local storage from previous sessions. * @@ -488,11 +826,6 @@ export class MySky { export function checkStoredSeed(): Uint8Array | null { log("Entered checkStoredSeed"); - if (!localStorage) { - console.log("WARNING: localStorage disabled"); - return null; - } - const seedStr = localStorage.getItem(SEED_STORAGE_KEY); if (!seedStr) { return null; @@ -507,7 +840,7 @@ export function checkStoredSeed(): Uint8Array | null { throw new Error("Bad seed length"); } } catch (err) { - log(err as string); + log(`Error getting stored seed: ${err as string}`); clearStoredSeed(); return null; } @@ -515,16 +848,96 @@ export function checkStoredSeed(): Uint8Array | null { return seed; } +/** + * Clears the stored email from local storage. + */ +function clearStoredEmail(): void { + log("Entered clearStoredEmail"); + + localStorage.removeItem(EMAIL_STORAGE_KEY); +} + +/** + * Clears the stored preferred portal from local storage. + */ +function clearStoredPreferredPortal(): void { + log("Entered clearStoredPreferredPortal"); + + localStorage.removeItem(PORTAL_STORAGE_KEY); +} + /** * Clears the seed stored in local storage. */ export function clearStoredSeed(): void { log("Entered clearStoredSeed"); - if (!localStorage) { - console.log("WARNING: localStorage disabled"); - return; + localStorage.removeItem(SEED_STORAGE_KEY); +} + +/** + * Gets the current and referrer domains. + * + * @returns - The current and referrer domains. + */ +export async function getCurrentAndReferrerDomains(): Promise<{ + currentDomain: string; + referrerDomain: string | null; +}> { + // Get the MySky domain (i.e. `skynet-mysky.hns or sandbridge.hns`). Use + // hard-coded values since we don't expect official MySky to be hosted + // anywhere else for now. + let currentDomain; + if (ALPHA_ENABLED && DEV_ENABLED) { + throw new Error("Alpha and dev modes cannot both be enabled"); + } else if (ALPHA_ENABLED) { + currentDomain = "sandbridge.hns"; + } else if (DEV_ENABLED) { + currentDomain = "skynet-mysky-dev.hns"; + } else { + currentDomain = "skynet-mysky.hns"; + } + + // Get the referrer and MySky domains. + // Extract skapp domain from actual portal. + // NOTE: The skapp should have opened MySky on the same portal as itself. + let referrerDomain = null; + if (document.referrer) { + const referrerClient = new SkynetClient(document.referrer); + const referrerUrlObj = new URL(document.referrer); + referrerDomain = await referrerClient.extractDomain(referrerUrlObj.hostname); } - localStorage.removeItem(SEED_STORAGE_KEY); + // Sanity check that the current domain as extracted from the URL is + // equivalent to the hard-coded domain we got above. + { + const actualPortalClient = new SkynetClient(); + // Extract the MySky domain from the current URL. + const currentDomainExtracted = await actualPortalClient.extractDomain(window.location.hostname); + if (currentDomainExtracted !== currentDomain) { + throw new Error( + `Extracted current domain '${currentDomainExtracted}' is different from the expected domain '${currentDomain}'` + ); + } + } + + return { currentDomain, referrerDomain }; +} + +/** + * Initialize the Skynet client. + * + * Connect to the preferred portal if it was found, otherwise connect to + * siasky.net if the seed was found, otherwise connect to the current + * portal. + * + * @param seed - The user seed, if given. + * @param preferredPortal - The user's preferred portal, if found. + * @returns - The Skynet client to be used for logging in to the portal. + */ +function getLoginClient(seed: Uint8Array | null, preferredPortal: string | null): SkynetClient { + log("Entered getLoginClient"); + + const initialPortal = seed ? INITIAL_PORTAL : undefined; + return new SkynetClient(preferredPortal || initialPortal); } diff --git a/src/portal-account.ts b/src/portal_account.ts similarity index 94% rename from src/portal-account.ts rename to src/portal_account.ts index 6742f920..376c25b8 100644 --- a/src/portal-account.ts +++ b/src/portal_account.ts @@ -4,11 +4,7 @@ import { sign } from "tweetnacl"; import { genKeyPairFromHash, hashWithSalt } from "./crypto"; import { hexToUint8Array, stringToUint8ArrayUtf8, toHexString, validateHexString, validateUint8ArrayLen } from "./util"; - -/** - * The name of the response header containing the JWT token. - */ -export const JWT_HEADER_NAME = "skynet-token"; +import { ensureUrl } from "skynet-mysky-utils"; /** * The size of the expected signature. @@ -53,6 +49,7 @@ export type CustomLoginOptions = CustomClientOptions & { * Custom logout options. * * @property [endpointLogout] - The relative URL path of the portal endpoint to contact for large uploads. + * @property [executeRequest] - A function to override the client's existing `executeRequest`. */ export type CustomLogoutOptions = CustomClientOptions & { endpointLogout?: string; @@ -87,6 +84,8 @@ export const DEFAULT_LOGOUT_OPTIONS = { ...DEFAULT_CUSTOM_CLIENT_OPTIONS, endpointLogout: "/api/logout", + + executeRequest: undefined, }; /** @@ -257,14 +256,14 @@ function genPortalLoginKeypair(seed: Uint8Array, email: string): KeyPair { } /** - * Gets the portal recipient string from the portal URL, e.g. siasky.net => - * siasky.net, dev1.siasky.dev => siasky.dev. + * Gets the portal recipient string from the portal URL, e.g. https://siasky.net + * => https://siasky.net, https://dev1.siasky.dev => https://siasky.dev. * * @param portalUrl - The full portal URL. * @returns - The shortened portal recipient URL. */ -function getPortalRecipient(portalUrl: string): string { - const url = new URL(portalUrl); +export function getPortalRecipient(portalUrl: string): string { + const url = new URL(ensureUrl(portalUrl)); // Get last two portions of the hostname. url.hostname = url.hostname.split(".").slice(-2).join("."); diff --git a/src/provider.ts b/src/provider.ts index f7d59ffd..d46ec1da 100644 --- a/src/provider.ts +++ b/src/provider.ts @@ -1,6 +1,7 @@ import { Connection, ParentHandshake, WorkerMessenger } from "post-me"; import { defaultHandshakeAttemptsInterval, defaultHandshakeMaxAttempts, ensureUrl } from "skynet-mysky-utils"; import { PermissionsProvider } from "./mysky"; +import { log } from "./util"; export const relativePermissionsWorkerUrl = "permissions.js"; export const relativePermissionsDisplayUrl = "permissions-display.html"; @@ -23,6 +24,10 @@ export type SeedProviderResponse = { action: SeedProviderAction; }; +// TODO: Either remove, or fully implement if we still want to have custom +// permissions providers. This is from when we decided that users can choose +// their own permissions providers, but we didn't yet have a way to access/save +// user settings. /** * Tries to get the saved permissions provider preference, returning the default provider if not found. * @@ -48,7 +53,7 @@ export async function getPermissionsProviderUrl(_seed: Uint8Array): Promise { - console.log("Entered launchPermissionsProvider"); + log("Entered launchPermissionsProvider"); const permissionsProviderUrl = await getPermissionsProviderUrl(seed); diff --git a/src/skydb_internal.ts b/src/skydb_internal.ts new file mode 100644 index 00000000..41cf5c67 --- /dev/null +++ b/src/skydb_internal.ts @@ -0,0 +1,194 @@ +import { + deriveEncryptedFileKeyEntropy, + deriveEncryptedFileTweak, + decryptJSONFile, + ENCRYPTED_JSON_RESPONSE_VERSION, + EncryptedJSONResponse, + getOrCreateSkyDBRegistryEntry, + JsonData, + SkynetClient, + encryptJSONFile, + MAX_REVISION, + RegistryEntry, + signEntry, +} from "skynet-js"; + +import { deriveRootPathSeed, genKeyPairFromSeed } from "./crypto"; +import { deriveEncryptedPathSeedForRoot } from "./encrypted_files"; +import { log, validateObject, validateString } from "./util"; + +/** + * Gets Encrypted JSON at the given path through MySky. + * + * @param client - The Skynet client. + * @param seed - The root MySky user seed. + * @param path - The data path. + * @returns - An object containing the decrypted json data. + * @throws - Will throw if the user does not have Hidden Read permission on the path. + */ +export async function getJSONEncryptedInternal( + client: SkynetClient, + seed: Uint8Array, + path: string +): Promise { + log("Entered getJSONEncryptedInternal"); + + validateString("path", path, "parameter"); + + const { publicKey } = genKeyPairFromSeed(seed); + const pathSeed = await getEncryptedPathSeedInternal(seed, path, false); + + // Fetch the raw encrypted JSON data. + const dataKey = deriveEncryptedFileTweak(pathSeed); + const opts = { hashedDataKeyHex: true }; + log("Calling getRawBytes"); + const { data } = await client.db.getRawBytes(publicKey, dataKey, opts); + if (data === null) { + return { data: null }; + } + + const encryptionKey = deriveEncryptedFileKeyEntropy(pathSeed); + const json = decryptJSONFile(data, encryptionKey); + + return { data: json }; +} + +/** + * Sets Encrypted JSON at the given path through MySky. + * + * @param client - The Skynet client. + * @param seed - The root MySky user seed. + * @param path - The data path. + * @param json - The json to encrypt and set. + * @returns - An object containing the original json data. + * @throws - Will throw if the user does not have Hidden Write permission on the path. + */ +export async function setJSONEncryptedInternal( + client: SkynetClient, + seed: Uint8Array, + path: string, + json: JsonData +): Promise { + log("Entered setJSONEncryptedInternal"); + + validateString("path", path, "parameter"); + validateObject("json", json, "parameter"); + + const { publicKey } = genKeyPairFromSeed(seed); + const pathSeed = await getEncryptedPathSeedInternal(seed, path, false); + const dataKey = deriveEncryptedFileTweak(pathSeed); + const opts = { hashedDataKeyHex: true }; + + // Immediately fail if the mutex is not available. + return await client.db.revisionNumberCache.withCachedEntryLock( + publicKey, + dataKey, + async (cachedRevisionEntry: { revision: bigint }) => { + // Get the cached revision number before doing anything else. + const newRevision = incrementRevision(cachedRevisionEntry.revision); + + // Derive the key. + const encryptionKey = deriveEncryptedFileKeyEntropy(pathSeed); + + // Pad and encrypt json file. + log("Calling encryptJSONFile"); + const data = encryptJSONFile(json, { version: ENCRYPTED_JSON_RESPONSE_VERSION }, encryptionKey); + + log("Calling getOrCreateSkyDBRegistryEntry"); + const [entry] = await getOrCreateSkyDBRegistryEntry(client, dataKey, data, newRevision, opts); + + // Sign the entry. + log("Calling signEncryptedRegistryEntryInternal"); + const signature = await signEncryptedRegistryEntryInternal(seed, entry, path); + + log("Calling postSignedEntry"); + await client.registry.postSignedEntry(publicKey, entry, signature, opts); + + return { data: json }; + } + ); +} + +/** + * Gets the encrypted path seed for the given path without requiring + * permissions. This should NOT be exported - for internal use only. + * + * @param seed - The root MySky user seed. + * @param path - The given file or directory path. + * @param isDirectory - Whether the path corresponds to a directory. + * @returns - The hex-encoded encrypted path seed. + */ +async function getEncryptedPathSeedInternal(seed: Uint8Array, path: string, isDirectory: boolean): Promise { + log("Entered getEncryptedPathSeedInternal"); + + // Compute the root path seed. + const rootPathSeedBytes = deriveRootPathSeed(seed); + + // Compute the child path seed. + return deriveEncryptedPathSeedForRoot(rootPathSeedBytes, path, isDirectory); +} + +/** + * Increments the given revision number and checks to make sure it is not + * greater than the maximum revision. + * + * @param revision - The given revision number. + * @returns - The incremented revision number. + * @throws - Will throw if the incremented revision number is greater than the maximum revision. + */ +function incrementRevision(revision: bigint): bigint { + revision = revision + BigInt(1); + + // Throw if the revision is already the maximum value. + if (revision > MAX_REVISION) { + throw new Error("Current entry already has maximum allowed revision, could not update the entry"); + } + + return revision; +} + +/** + * Signs the encrypted registry entry without requiring permissions. For + * internal use only. + * + * @param seed - The root MySky user seed. + * @param entry - The encrypted registry entry. + * @param path - The MySky path. + * @returns - The signature. + */ +async function signEncryptedRegistryEntryInternal( + seed: Uint8Array, + entry: RegistryEntry, + path: string +): Promise { + log("Entered signEncryptedRegistryEntryInternal"); + + // Check that the entry data key corresponds to the right path. + // + // Use `isDirectory: false` because registry entries can only correspond to files right now. + const pathSeed = await getEncryptedPathSeedInternal(seed, path, false); + const dataKey = deriveEncryptedFileTweak(pathSeed); + if (entry.dataKey !== dataKey) { + throw new Error("Path does not match the data key in the encrypted registry entry."); + } + + return signRegistryEntryHelperInternal(seed, entry); +} + +/** + * Internal version of `signRegistryEntryHelper` that does not check for + * permissions. + * + * @param seed - The root MySky user seed. + * @param entry - The registry entry. + * @returns - The signature. + */ +async function signRegistryEntryHelperInternal(seed: Uint8Array, entry: RegistryEntry): Promise { + log("Entered signRegistryEntryHelperInternal"); + + // Get the private key. + const { privateKey } = genKeyPairFromSeed(seed); + + // Sign the entry. + return await signEntry(privateKey, entry, true); +} diff --git a/src/user_settings.ts b/src/user_settings.ts new file mode 100644 index 00000000..a4cab487 --- /dev/null +++ b/src/user_settings.ts @@ -0,0 +1,77 @@ +import { SkynetClient } from "skynet-js"; + +import { getJSONEncryptedInternal, setJSONEncryptedInternal } from "./skydb_internal"; +import { log } from "./util"; + +/** + * Settings associated with a user's MySky account. + * + * @property portal - The user's preferred portal. We redirect a skapp to this portal, if it is set. + * @property email - The user's portal account email. We connect to their portal account if this is set. + */ +type UserSettings = { portal: string | null; email: string | null }; + +/** + * Checks for the preferred portal and stored email in user settings, and sets + * them if found. + * + * @param client - The Skynet client. + * @param seed - The root MySky user seed. + * @param mySkyDomain - The domain of the current MySky instance. + * @returns - The portal and email, if found. + */ +export async function getUserSettings( + client: SkynetClient, + seed: Uint8Array, + mySkyDomain: string +): Promise { + log("Entered getUserSettings"); + + let email = null, + portal = null; + + // Get the settings path for the MySky domain. + const path = getUserSettingsPath(mySkyDomain); + + // Check for stored portal and email in user settings. + const { data: userSettings } = await getJSONEncryptedInternal(client, seed, path); + if (userSettings) { + email = (userSettings.email as string) || null; + portal = (userSettings.portal as string) || null; + } + + return { portal, email }; +} + +/** + * Sets the user settings. + * + * @param client - The Skynet client. + * @param seed - The root MySky user seed. + * @param mySkyDomain - The domain of the current MySky instance. + * @param settings - The given user settings. + */ +export async function setUserSettings( + client: SkynetClient, + seed: Uint8Array, + mySkyDomain: string, + settings: UserSettings +): Promise { + log("Entered setUserSettings"); + + // Get the settings path for the MySky domain. + const path = getUserSettingsPath(mySkyDomain); + + // Set preferred portal and email in user settings. + await setJSONEncryptedInternal(client, seed, path, settings); +} + +/** + * Get the path to the user settings stored in the root MySky domain. + * + * @param mySkyDomain - The domain of the current MySky instance. + * @returns - The user settings path. + */ +function getUserSettingsPath(mySkyDomain: string): string { + return `${mySkyDomain}/settings.json`; +} diff --git a/src/util.ts b/src/util.ts index 28a28105..b9395a98 100644 --- a/src/util.ts +++ b/src/util.ts @@ -5,7 +5,9 @@ import { Buffer } from "buffer"; import { permCategoryToString, Permission, permTypeToString } from "skynet-mysky-utils"; const urlParams = new URLSearchParams(window.location.search); -const DEBUG_ENABLED = urlParams.get("debug") === "true"; +export const ALPHA_ENABLED = urlParams.get("alpha") === "true"; +export const DEBUG_ENABLED = urlParams.get("debug") === "true"; +export const DEV_ENABLED = urlParams.get("dev") === "true"; /** * Converts a hex encoded string to a uint8 array. @@ -93,6 +95,23 @@ export function toHexString(byteArray: Uint8Array): string { // Validation Functions // ==================== +/** + * Validates the given value as an object. + * + * @param name - The name of the value. + * @param value - The actual value. + * @param valueKind - The kind of value that is being checked (e.g. "parameter", "response field", etc.) + * @throws - Will throw if not a valid object. + */ +export function validateObject(name: string, value: unknown, valueKind: string): void { + if (typeof value !== "object") { + throwValidationError(name, value, valueKind, "type 'object'"); + } + if (value === null) { + throwValidationError(name, value, valueKind, "non-null"); + } +} + /** * Validates the given value as a string. * diff --git a/tests/integration/portal-account.test.ts b/tests/integration/portal-account.test.ts index ce0f469c..78f7550c 100644 --- a/tests/integration/portal-account.test.ts +++ b/tests/integration/portal-account.test.ts @@ -2,7 +2,7 @@ import { DEFAULT_SKYNET_PORTAL_URL, SkynetClient } from "skynet-js"; import { randomAsciiString } from "../utils"; import { generatePhrase, phraseToSeed } from "../../src/seed"; -import { login, register } from "../../src/portal-account"; +import { login, register } from "../../src/portal_account"; // const portalUrl = DEFAULT_SKYNET_PORTAL_URL; const portalUrl = "https://siasky.xyz"; diff --git a/tests/unit/portal-account.test.ts b/tests/unit/portal-account.test.ts index a207fb2a..a997ebb8 100644 --- a/tests/unit/portal-account.test.ts +++ b/tests/unit/portal-account.test.ts @@ -1,7 +1,7 @@ import { DEFAULT_SKYNET_PORTAL_URL, SkynetClient } from "skynet-js"; import { phraseToSeed } from "../../src/seed"; -import { login, register } from "../../src/portal-account"; +import { getPortalRecipient, login, register } from "../../src/portal_account"; const portalUrl = DEFAULT_SKYNET_PORTAL_URL; const client = new SkynetClient(portalUrl); @@ -74,3 +74,15 @@ describe("Unit tests for registration and login", () => { ); }); }); + +describe("getPortalRecipient", () => { + const cases = [ + ["https://siasky.net", "https://siasky.net"], + ["https://dev1.siasky.dev", "https://siasky.dev"], + ]; + + it.each(cases)("(%s) should return '%s'", (portalUrl, expectedRecipient) => { + const recipient = getPortalRecipient(portalUrl); + expect(recipient).toEqual(expectedRecipient); + }); +}); diff --git a/tsconfig.json b/tsconfig.json index ecdca3ff..b4b24574 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -11,6 +11,9 @@ "strict": true, "strictNullChecks": true, + "lib": ["dom", "es2021"], + "target": "es6", + "moduleResolution": "node", "types": ["node", "jest"], "typeRoots": ["./types", "./node_modules/@types"] }, diff --git a/tsconfig.scripts.json b/tsconfig.scripts.json index 3774e482..7e4ddfbb 100644 --- a/tsconfig.scripts.json +++ b/tsconfig.scripts.json @@ -6,7 +6,9 @@ "strict": true, "skipLibCheck": true, - "lib": ["dom", "esnext", "webworker"], + "lib": ["dom", "es2021", "webworker"], + "target": "es6", + "moduleResolution": "node", "outDir": "dist", "types": ["node"], "typeRoots": ["./types", "./node_modules/@types"] diff --git a/webpack.config.js b/webpack.config.js index a5941272..7bebe0f8 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -1,4 +1,5 @@ const path = require("path"); +const process = require("process"); // define preprocessor variables const opts = { @@ -6,7 +7,13 @@ const opts = { }; module.exports = { - entry: "./src/index.ts", + entry: [ + // Provide polyfill for Promise.any for Opera. + // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/any#browser_compatibility + "core-js/stable/promise/any", + "core-js/stable/aggregate-error", + "./src/index.ts", + ], mode: "production", devtool: "inline-source-map", @@ -17,8 +24,8 @@ module.exports = { exclude: /node_modules/, // prettier-ignore use: [ - { loader: "ifdef-loader", options: opts }, { loader: "ts-loader" }, + { loader: "ifdef-loader", options: opts }, ], }, ], diff --git a/webpack.config.permissions-display.js b/webpack.config.permissions-display.js index 04193a95..113994a7 100644 --- a/webpack.config.permissions-display.js +++ b/webpack.config.permissions-display.js @@ -17,8 +17,8 @@ module.exports = { test: /\.tsx?$/, exclude: /node_modules/, use: [ - { loader: "ifdef-loader", options: opts }, { loader: "ts-loader", options: { configFile: "tsconfig.scripts.json" } }, + { loader: "ifdef-loader", options: opts }, ], include: [path.resolve(__dirname, `scripts/${name}.ts`)], }, diff --git a/webpack.config.permissions.js b/webpack.config.permissions.js index 8c18b602..c1bb0a09 100644 --- a/webpack.config.permissions.js +++ b/webpack.config.permissions.js @@ -17,8 +17,8 @@ module.exports = { test: /\.tsx?$/, exclude: /node_modules/, use: [ - { loader: "ifdef-loader", options: opts }, { loader: "ts-loader", options: { configFile: "tsconfig.scripts.json" } }, + { loader: "ifdef-loader", options: opts }, ], include: [path.resolve(__dirname, `scripts/${name}.ts`)], }, diff --git a/webpack.config.seed-display.js b/webpack.config.seed-display.js index 0b6a2ead..a049ed55 100644 --- a/webpack.config.seed-display.js +++ b/webpack.config.seed-display.js @@ -17,8 +17,8 @@ module.exports = { test: /\.tsx?$/, exclude: /node_modules/, use: [ - { loader: "ifdef-loader", options: opts }, { loader: "ts-loader", options: { configFile: "tsconfig.scripts.json" } }, + { loader: "ifdef-loader", options: opts }, ], include: [path.resolve(__dirname, "src"), path.resolve(__dirname, `scripts/${name}.ts`)], }, diff --git a/webpack.config.seed-selection.js b/webpack.config.seed-selection.js index 8ca1d438..d0cf06a3 100644 --- a/webpack.config.seed-selection.js +++ b/webpack.config.seed-selection.js @@ -17,8 +17,8 @@ module.exports = { test: /\.tsx?$/, exclude: /node_modules/, use: [ - { loader: "ifdef-loader", options: opts }, { loader: "ts-loader", options: { configFile: "tsconfig.scripts.json" } }, + { loader: "ifdef-loader", options: opts }, ], include: [path.resolve(__dirname, `scripts/${name}.ts`)], }, diff --git a/webpack.config.signin-connect.js b/webpack.config.signin-connect.js index dfb20ca1..127a1d1f 100644 --- a/webpack.config.signin-connect.js +++ b/webpack.config.signin-connect.js @@ -17,8 +17,8 @@ module.exports = { test: /\.tsx?$/, exclude: /node_modules/, use: [ - { loader: "ifdef-loader", options: opts }, { loader: "ts-loader", options: { configFile: "tsconfig.scripts.json" } }, + { loader: "ifdef-loader", options: opts }, ], include: [path.resolve(__dirname, "src"), path.resolve(__dirname, `scripts/${name}.ts`)], }, diff --git a/webpack.config.ui.js b/webpack.config.ui.js index 7f6aede7..4299c5b6 100644 --- a/webpack.config.ui.js +++ b/webpack.config.ui.js @@ -17,8 +17,8 @@ module.exports = { test: /\.tsx?$/, exclude: /node_modules/, use: [ - { loader: "ifdef-loader", options: opts }, { loader: "ts-loader", options: { configFile: "tsconfig.scripts.json" } }, + { loader: "ifdef-loader", options: opts }, ], include: [path.resolve(__dirname, "src"), path.resolve(__dirname, `scripts/${name}.ts`)], },