diff --git a/.gitignore b/.gitignore index d88c89b8..992c3ff2 100644 --- a/.gitignore +++ b/.gitignore @@ -106,4 +106,6 @@ dist toruslabs-customauth-* bundle.min.js types2/ -.DS_Store \ No newline at end of file +.DS_Store + +.npmrc \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 22fb61aa..01559c7f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -17,7 +17,7 @@ "@toruslabs/http-helpers": "^9.0.0", "@toruslabs/metadata-helpers": "^8.2.0", "@toruslabs/session-manager": "^5.6.0", - "@toruslabs/torus.js": "^17.2.2", + "@toruslabs/torus.js": "^17.2.3", "bowser": "^2.14.1", "deepmerge": "^4.3.1", "events": "^3.3.0", @@ -83,6 +83,7 @@ "integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@babel/code-frame": "^7.29.0", "@babel/generator": "^7.29.0", @@ -1643,6 +1644,7 @@ "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.6.tgz", "integrity": "sha512-05WQkdpL9COIMz4LjTxGpPNCdlpyimKppYNoJ5Di5EUObifl8t4tuLuUBBZEpoLYOmfvIWrsp9fCl0HoPRVTdA==", "license": "MIT", + "peer": true, "engines": { "node": ">=6.9.0" } @@ -2614,6 +2616,7 @@ "integrity": "sha512-DhGl4xMVFGVIyMwswXeyzdL4uXD5OGILGX5N8Y+f6W7LhC1Ze2poSNrkF/fedpVDHEEZ+PHFW0vL14I+mm8K3Q==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@octokit/auth-token": "^6.0.0", "@octokit/graphql": "^9.0.3", @@ -3683,6 +3686,7 @@ "integrity": "sha512-s69UXxvefeQxuZ5nY7/THtTrIEvJxNVCp3ns4kwoCw1qMpgpvn/296WCKVmM7MiwnaAdzEKnAvLAwaxZc2nM7Q==", "devOptional": true, "license": "MIT", + "peer": true, "engines": { "node": ">=18" } @@ -3943,9 +3947,9 @@ } }, "node_modules/@toruslabs/torus.js": { - "version": "17.2.2", - "resolved": "https://registry.npmjs.org/@toruslabs/torus.js/-/torus.js-17.2.2.tgz", - "integrity": "sha512-u1r3DYwW7Bt/APlwcBw1jVBuKRfhmYXwY0iA9DDIhHTZ6IW6RTb7Q7nfoy7IXPrPKoj70Iq2DmfNhVV9kFfI5Q==", + "version": "17.2.3", + "resolved": "https://registry.npmjs.org/@toruslabs/torus.js/-/torus.js-17.2.3.tgz", + "integrity": "sha512-CmE2xm3LRZh36UySDXNZOeO/pqf7hbvuT5KRUzmgJI4r+Q//HrJ1xOW2zJ9UZl9bym1Il0031r+Mv+N1krbWpQ==", "license": "MIT", "dependencies": { "@toruslabs/constants": "^16.1.1", @@ -4020,6 +4024,7 @@ "integrity": "sha512-DpzbrH7wIcBaJibpKo9nnSQL0MTRdnWttGyE5haGwK86xgMOkFLp7vEyfQPGLOJh5wNYiJ3V9PmUMDhV9u8kkQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "undici-types": "~7.18.0" } @@ -4083,6 +4088,7 @@ "integrity": "sha512-klQbnPAAiGYFyI02+znpBRLyjL4/BrBd0nyWkdC0s/6xFLkXYQ8OoRrSkqacS1ddVxf/LDyODIKbQ5TgKAf/Fg==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "8.56.1", "@typescript-eslint/types": "8.56.1", @@ -4706,6 +4712,7 @@ "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", "dev": true, "license": "MIT", + "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -5223,6 +5230,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "baseline-browser-mapping": "^2.9.0", "caniuse-lite": "^1.0.30001759", @@ -6239,6 +6247,7 @@ "integrity": "sha512-VmQ+sifHUbI/IcSopBCF/HO3YiHQx/AVd3UVyYL6weuwW+HvON9VYn5l6Zl1WZzPWXPNZrSQpxwkkZ/VuvJZzg==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", @@ -6299,6 +6308,7 @@ "integrity": "sha512-82GZUjRS0p/jganf6q1rEO25VSoHH0hKPCTrgillPjdI/3bgBhAE1QzHrHTizjpRvy6pGAvKjDJtk2pF9NDq8w==", "dev": true, "license": "MIT", + "peer": true, "bin": { "eslint-config-prettier": "bin/cli.js" }, @@ -6425,6 +6435,7 @@ "integrity": "sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@rtsao/scc": "^1.1.0", "array-includes": "^3.1.9", @@ -9867,6 +9878,7 @@ "integrity": "sha512-UOnG6LftzbdaHZcKoPFtOcCKztrQ57WkHDeRD9t/PTQtmT0NHSeWWepj6pS0z/N7+08BHFDQVUrfmfMRcZwbMg==", "dev": true, "license": "MIT", + "peer": true, "bin": { "prettier": "bin/prettier.cjs" }, @@ -10372,6 +10384,7 @@ "integrity": "sha512-2oMpl67a3zCH9H79LeMcbDhXW/UmWG/y2zuqnF2jQq5uq9TbM9TVyXvA4+t+ne2IIkBdrLpAaRQAvo7YI/Yyeg==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@types/estree": "1.0.8" }, @@ -11225,7 +11238,8 @@ "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", "dev": true, - "license": "0BSD" + "license": "0BSD", + "peer": true }, "node_modules/type-check": { "version": "0.4.0", @@ -11347,6 +11361,7 @@ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "dev": true, "license": "Apache-2.0", + "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -11473,6 +11488,7 @@ "dev": true, "hasInstallScript": true, "license": "MIT", + "peer": true, "dependencies": { "napi-postinstall": "^0.3.0" }, @@ -11596,6 +11612,7 @@ "integrity": "sha512-1gFhNi+bHhRE/qKZOJXACm6tX4bA3Isy9KuKF15AgSRuRazNBOJfdDemPBU16/mpMxApDPrWvZ08DcLPEoRnuA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "lightningcss": "^1.32.0", "picomatch": "^4.0.3", @@ -11674,6 +11691,7 @@ "integrity": "sha512-yF+o4POL41rpAzj5KVILUxm1GCjKnELvaqmU9TLLUbMfDzuN0UpUR9uaDs+mCtjPe+uYPksXDRLQGGPvj1cTmA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@vitest/expect": "4.1.1", "@vitest/mocker": "4.1.1", diff --git a/package.json b/package.json index c4731646..1cb529db 100644 --- a/package.json +++ b/package.json @@ -39,7 +39,7 @@ "@toruslabs/http-helpers": "^9.0.0", "@toruslabs/metadata-helpers": "^8.2.0", "@toruslabs/session-manager": "^5.6.0", - "@toruslabs/torus.js": "^17.2.2", + "@toruslabs/torus.js": "^17.2.3", "bowser": "^2.14.1", "deepmerge": "^4.3.1", "events": "^3.3.0", diff --git a/src/login.ts b/src/login.ts index a0176609..9bd17297 100644 --- a/src/login.ts +++ b/src/login.ts @@ -7,8 +7,8 @@ import { type KeyType, Torus, TorusKey } from "@toruslabs/torus.js"; import { createHandler } from "./handlers/HandlerFactory"; import { registerServiceWorker } from "./registerServiceWorker"; import SentryHandler from "./sentry"; -import { SENTRY_TXNS, UX_MODE, UX_MODE_TYPE } from "./utils/enums"; -import { serializeError } from "./utils/error"; +import { AUTH_CONNECTION_TYPE, SENTRY_TXNS, UX_MODE, UX_MODE_TYPE } from "./utils/enums"; +import { CustomAuthLoginError, CustomAuthLoginErrorPrefix, serializeError } from "./utils/error"; import { handleRedirectParameters, isFirefox, padUrlString } from "./utils/helpers"; import { CustomAuthArgs, @@ -153,10 +153,11 @@ export class CustomAuth { } async triggerLogin(args: CustomAuthLoginParams): Promise { - const { authConnectionId, authConnection, clientId, jwtParams, hash, queryParameters, customState, groupedAuthConnectionId } = args; if (!this.isInitialized) { throw new Error("Not initialized yet"); } + + const { authConnectionId, authConnection, clientId, jwtParams, hash, queryParameters, customState, groupedAuthConnectionId } = args; const loginHandler: ILoginHandler = createHandler({ authConnection, clientId, @@ -173,24 +174,32 @@ export class CustomAuth { }); let loginParams: LoginWindowResponse; - if (hash && queryParameters) { - const { error, hashParameters, instanceParameters } = handleRedirectParameters(hash, queryParameters); - if (error) throw new Error(error); - const { access_token: accessToken, id_token: idToken, tgAuthResult, ...rest } = hashParameters; - // State has to be last here otherwise it will be overwritten - loginParams = { accessToken, idToken: idToken || tgAuthResult || "", ...rest, state: instanceParameters }; - } else { - this.sessionManager.clearOrphanedData(); - if (this.config.uxMode === UX_MODE.REDIRECT) { - const sessionId = this.getSessionId(`torus_login_${loginHandler.nonce}`); - this.sessionManager.setSessionId(sessionId); - await this.sessionManager.createSession({ args }); + try { + if (hash && queryParameters) { + const { error, hashParameters, instanceParameters } = handleRedirectParameters(hash, queryParameters); + if (error) throw new Error(error); + const { access_token: accessToken, id_token: idToken, tgAuthResult, ...rest } = hashParameters; + // State has to be last here otherwise it will be overwritten + loginParams = { accessToken, idToken: idToken || tgAuthResult || "", ...rest, state: instanceParameters }; + } else { + this.sessionManager.clearOrphanedData(); + if (this.config.uxMode === UX_MODE.REDIRECT) { + const sessionId = this.getSessionId(`torus_login_${loginHandler.nonce}`); + this.sessionManager.setSessionId(sessionId); + await this.sessionManager.createSession({ args }); + } + loginParams = await loginHandler.handleLoginWindow({ + locationReplaceOnRedirect: this.config.locationReplaceOnRedirect, + popupFeatures: this.config.popupFeatures, + }); + if (this.config.uxMode === UX_MODE.REDIRECT) return null; } - loginParams = await loginHandler.handleLoginWindow({ - locationReplaceOnRedirect: this.config.locationReplaceOnRedirect, - popupFeatures: this.config.popupFeatures, - }); - if (this.config.uxMode === UX_MODE.REDIRECT) return null; + } catch (error) { + log.error(error); + + const serializedError = await serializeError(error); + const errorMessage = `${CustomAuthLoginErrorPrefix}${serializedError.message || ""}`; + throw new CustomAuthLoginError(errorMessage); } const userInfo = await loginHandler.getUserInfo(loginParams); @@ -201,6 +210,8 @@ export class CustomAuth { idToken: loginParams.idToken || loginParams.accessToken, additionalParams: userInfo.extraConnectionParams, groupedAuthConnectionId, + recordId: args.customState?.recordId, + authConnection: userInfo.authConnection, }); return { @@ -218,6 +229,8 @@ export class CustomAuth { idToken: string; additionalParams?: ExtraParams; groupedAuthConnectionId?: string; + recordId?: string; + authConnection: AUTH_CONNECTION_TYPE; }): Promise { const { authConnectionId, userId, idToken, additionalParams, groupedAuthConnectionId } = params; const verifier = groupedAuthConnectionId || authConnectionId; @@ -263,6 +276,8 @@ export class CustomAuth { }, useDkg: this.config.useDkg, checkCommitment: this.config.checkCommitment, + recordId: params.recordId, + authConnection: params.authConnection, }); } ); diff --git a/src/utils/error.ts b/src/utils/error.ts index 9d56d798..6031b2b9 100644 --- a/src/utils/error.ts +++ b/src/utils/error.ts @@ -24,3 +24,12 @@ export const serializeError = async (error: unknown): Promise => { } return err; }; + +export const CustomAuthLoginErrorPrefix = "CustomAuthLoginError: login failure."; + +export class CustomAuthLoginError extends Error { + constructor(message: string) { + super(message); + this.name = "CustomAuthLoginError"; + } +} diff --git a/test/unit/error.test.ts b/test/unit/error.test.ts new file mode 100644 index 00000000..40228ce9 --- /dev/null +++ b/test/unit/error.test.ts @@ -0,0 +1,30 @@ +import { describe, expect, it } from "vitest"; + +import { CustomAuthLoginError, CustomAuthLoginErrorPrefix, serializeError } from "../../src/utils/error"; + +describe("error utils", () => { + describe("serializeError", () => { + it("returns the original Error instance", async () => { + const originalError = new Error("popup blocked"); + + const serializedError = await serializeError(originalError); + + expect(serializedError).toBe(originalError); + }); + }); + + describe("CustomAuthLoginError", () => { + it("exports the expected login error prefix", () => { + expect(CustomAuthLoginErrorPrefix).toBe("CustomAuthLoginError: login failure."); + }); + + it("creates a named error that preserves the message", () => { + const error = new CustomAuthLoginError(`${CustomAuthLoginErrorPrefix} popup blocked`); + + expect(error).toBeInstanceOf(Error); + expect(error).toBeInstanceOf(CustomAuthLoginError); + expect(error.name).toBe("CustomAuthLoginError"); + expect(error.message).toBe("CustomAuthLoginError: login failure. popup blocked"); + }); + }); +}); diff --git a/test/unit/login.test.ts b/test/unit/login.test.ts index 39f845f2..a375c40d 100644 --- a/test/unit/login.test.ts +++ b/test/unit/login.test.ts @@ -5,6 +5,7 @@ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import { createHandler } from "../../src/handlers/HandlerFactory"; import { UX_MODE } from "../../src/utils/enums"; +import { CustomAuthLoginError, CustomAuthLoginErrorPrefix } from "../../src/utils/error"; import type { CustomAuthArgs, ILoginHandler, LoginWindowResponse, TorusConnectionResponse } from "../../src/utils/interfaces"; vi.mock("@toruslabs/torus.js", () => { @@ -186,6 +187,28 @@ describe("CustomAuth", () => { expect(setSessionIdSpy.mock.calls[0][0]).toBe(firstSessionId); }); + + it("wraps login window failures in CustomAuthLoginError", async () => { + const CustomAuth = await getCustomAuth(); + const handler = mockLoginHandler({ + handleLoginWindow: vi.fn().mockRejectedValue(new Error("Popup blocked")), + }); + vi.mocked(createHandler).mockReturnValue(handler); + + const auth = new CustomAuth({ ...BASE_ARGS, uxMode: UX_MODE.REDIRECT }); + auth.isInitialized = true; + + const error = await auth + .triggerLogin({ + authConnection: "google", + authConnectionId: "google-verifier", + clientId: "google-client-id", + }) + .catch((err) => err); + + expect(error).toBeInstanceOf(CustomAuthLoginError); + expect(error.message).toBe(`${CustomAuthLoginErrorPrefix}Popup blocked`); + }); }); describe("triggerLogin – popup mode with hash/queryParameters", () => {