From 4f450cd5beb8913bcc98b3c0c192cd4cbda871df Mon Sep 17 00:00:00 2001 From: lwin Date: Fri, 27 Mar 2026 19:22:21 +0800 Subject: [PATCH 1/5] feat: track 'oauthFailed', 'oauthInitiated' event for audit --- .gitignore | 4 ++- package-lock.json | 26 +++++++++++++--- package.json | 2 +- src/login.ts | 73 ++++++++++++++++++++++++++++++++------------ src/utils/helpers.ts | 8 +++++ 5 files changed, 87 insertions(+), 26 deletions(-) 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..9441a2bc 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": "file:./toruslabs-torus.js-17.2.2.tgz", "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" } @@ -3944,8 +3948,8 @@ }, "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==", + "resolved": "file:toruslabs-torus.js-17.2.2.tgz", + "integrity": "sha512-M+68isdHuJJIkYFygUCWvpB1ThJrQJI/DC6UFU7Czcm0AvIzytRYaXasywQv/HZ1Ymr6/SXbisQ60Oa/VYZpfw==", "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..78c8d54b 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": "file:./toruslabs-torus.js-17.2.2.tgz", "bowser": "^2.14.1", "deepmerge": "^4.3.1", "events": "^3.3.0", diff --git a/src/login.ts b/src/login.ts index a0176609..ca632897 100644 --- a/src/login.ts +++ b/src/login.ts @@ -2,14 +2,14 @@ import { BUILD_ENV, BUILD_ENV_TYPE, type INodeDetails, STORAGE_SERVER_MAP, TORUS import { NodeDetailManager } from "@toruslabs/fetch-node-details"; import { type Hex, keccak256, remove0x, utf8ToBytes } from "@toruslabs/metadata-helpers"; import { StorageManager } from "@toruslabs/session-manager"; -import { type KeyType, Torus, TorusKey } from "@toruslabs/torus.js"; +import { type CitadelAuditParams, generateRecordId, 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 { AUTH_CONNECTION_TYPE, SENTRY_TXNS, UX_MODE, UX_MODE_TYPE } from "./utils/enums"; import { serializeError } from "./utils/error"; -import { handleRedirectParameters, isFirefox, padUrlString } from "./utils/helpers"; +import { callCitadelAuditApi, handleRedirectParameters, isFirefox, padUrlString } from "./utils/helpers"; import { CustomAuthArgs, CustomAuthLoginParams, @@ -172,25 +172,52 @@ export class CustomAuth { storageServerUrl: this.config.storageServerUrl, }); + const recordId = args.customState?.recordId || generateRecordId(); + // oAuthUserId is not available yet before the login + const auditPayload: Partial = { + recordId, + web3AuthNetwork: this.config.web3AuthNetwork, + web3AuthClientId: clientId, + authConnection, + authConnectionId, + groupedAuthConnectionId, + }; + + if (!args.customState?.recordId) { + // track the `oauthInitiated` audit if recordId is not provided + auditPayload.oauthInitiated = true; + callCitadelAuditApi(this.config.buildEnv, auditPayload); + } + 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); + + // track the `oauthFailed` audit if the login fails + auditPayload.oauthFailed = true; + + callCitadelAuditApi(this.config.buildEnv, auditPayload); + throw error; } const userInfo = await loginHandler.getUserInfo(loginParams); @@ -201,6 +228,8 @@ export class CustomAuth { idToken: loginParams.idToken || loginParams.accessToken, additionalParams: userInfo.extraConnectionParams, groupedAuthConnectionId, + recordId, + authConnection: userInfo.authConnection, }); return { @@ -218,6 +247,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 +294,8 @@ export class CustomAuth { }, useDkg: this.config.useDkg, checkCommitment: this.config.checkCommitment, + recordId: params.recordId, + authConnection: params.authConnection, }); } ); diff --git a/src/utils/helpers.ts b/src/utils/helpers.ts index a2a9701c..46ad207f 100644 --- a/src/utils/helpers.ts +++ b/src/utils/helpers.ts @@ -1,4 +1,7 @@ +import { BUILD_ENV_TYPE, CITADEL_SERVER_MAP } from "@toruslabs/constants"; +import { put } from "@toruslabs/http-helpers"; import { decodeBase64Url } from "@toruslabs/metadata-helpers"; +import { CitadelAuditParams } from "@toruslabs/torus.js"; import Bowser from "bowser"; import { AUTH_CONNECTION, AUTH_CONNECTION_TYPE, REDIRECT_PARAMS_STORAGE_METHOD_TYPE } from "./enums"; @@ -235,3 +238,8 @@ export function decodeToken(token: string): { header: { alg: string; typ: str payload: JSON.parse(decodeBase64Url(payload)) as T, }; } + +export async function callCitadelAuditApi(buildEnv: BUILD_ENV_TYPE, params: Partial): Promise { + const url = new URL(`${CITADEL_SERVER_MAP[buildEnv]}/v1/user/audit`); + await put(url.toString(), params); +} From d78983b368fb07651f8c40190a8d38e0a34ed28a Mon Sep 17 00:00:00 2001 From: lwin Date: Sun, 29 Mar 2026 13:36:25 +0800 Subject: [PATCH 2/5] chore: addressed cursor comments --- src/login.ts | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/src/login.ts b/src/login.ts index ca632897..5443e070 100644 --- a/src/login.ts +++ b/src/login.ts @@ -177,7 +177,7 @@ export class CustomAuth { const auditPayload: Partial = { recordId, web3AuthNetwork: this.config.web3AuthNetwork, - web3AuthClientId: clientId, + web3AuthClientId: this.config.web3AuthClientId, authConnection, authConnectionId, groupedAuthConnectionId, @@ -185,8 +185,9 @@ export class CustomAuth { if (!args.customState?.recordId) { // track the `oauthInitiated` audit if recordId is not provided - auditPayload.oauthInitiated = true; - callCitadelAuditApi(this.config.buildEnv, auditPayload); + callCitadelAuditApi(this.config.buildEnv, { ...auditPayload, oauthInitiated: true }).catch((error) => { + log.error("Error tracking oauthInitiated audit", error); + }); } let loginParams: LoginWindowResponse; @@ -214,9 +215,9 @@ export class CustomAuth { log.error(error); // track the `oauthFailed` audit if the login fails - auditPayload.oauthFailed = true; - - callCitadelAuditApi(this.config.buildEnv, auditPayload); + callCitadelAuditApi(this.config.buildEnv, { ...auditPayload, oauthFailed: true }).catch((error) => { + log.error("Error tracking oauthFailed audit", error); + }); throw error; } From 63109a675e52a0377ed583f83069f5f8bb476612 Mon Sep 17 00:00:00 2001 From: lwin Date: Sun, 29 Mar 2026 21:44:25 +0800 Subject: [PATCH 3/5] feat: save recordId in session manager for login redirect --- src/login.ts | 11 ++++--- test/unit/login.test.ts | 63 ++++++++++++++++++++++++++++++++++++++++- 2 files changed, 69 insertions(+), 5 deletions(-) diff --git a/src/login.ts b/src/login.ts index 5443e070..02557a22 100644 --- a/src/login.ts +++ b/src/login.ts @@ -153,10 +153,15 @@ 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 hasExistingRecordId = Boolean(args.customState?.recordId); + const recordId = args.customState?.recordId || generateRecordId(); + if (!hasExistingRecordId) { + args.customState = { ...(args.customState || {}), recordId }; + } + const { authConnectionId, authConnection, clientId, jwtParams, hash, queryParameters, customState, groupedAuthConnectionId } = args; const loginHandler: ILoginHandler = createHandler({ authConnection, clientId, @@ -171,8 +176,6 @@ export class CustomAuth { web3AuthNetwork: this.config.web3AuthNetwork, storageServerUrl: this.config.storageServerUrl, }); - - const recordId = args.customState?.recordId || generateRecordId(); // oAuthUserId is not available yet before the login const auditPayload: Partial = { recordId, @@ -183,7 +186,7 @@ export class CustomAuth { groupedAuthConnectionId, }; - if (!args.customState?.recordId) { + if (!hasExistingRecordId) { // track the `oauthInitiated` audit if recordId is not provided callCitadelAuditApi(this.config.buildEnv, { ...auditPayload, oauthInitiated: true }).catch((error) => { log.error("Error tracking oauthInitiated audit", error); diff --git a/test/unit/login.test.ts b/test/unit/login.test.ts index 39f845f2..da9fc8bb 100644 --- a/test/unit/login.test.ts +++ b/test/unit/login.test.ts @@ -1,6 +1,8 @@ import type { TORUS_NETWORK_TYPE } from "@toruslabs/constants"; +import { put } from "@toruslabs/http-helpers"; import { encodeBase64Url } from "@toruslabs/metadata-helpers"; import { StorageManager } from "@toruslabs/session-manager"; +import { generateRecordId } from "@toruslabs/torus.js"; import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import { createHandler } from "../../src/handlers/HandlerFactory"; @@ -14,7 +16,10 @@ vi.mock("@toruslabs/torus.js", () => { }); } Torus.setAPIKey = vi.fn(); - return { Torus }; + return { + Torus, + generateRecordId: vi.fn(() => "generated-record-id"), + }; }); vi.mock("@toruslabs/fetch-node-details", () => ({ @@ -27,6 +32,10 @@ vi.mock("@toruslabs/fetch-node-details", () => ({ }), })); +vi.mock("@toruslabs/http-helpers", () => ({ + put: vi.fn().mockResolvedValue(undefined), +})); + vi.mock("../../src/handlers/HandlerFactory", () => ({ createHandler: vi.fn(), })); @@ -290,6 +299,58 @@ describe("CustomAuth", () => { expect(result.result).toBeDefined(); }); + it("reuses the generated recordId across the redirect round-trip", async () => { + const CustomAuth = await getCustomAuth(); + + const redirectHandler = mockLoginHandler({ + nonce: "redir_nonce", + handleLoginWindow: vi.fn().mockResolvedValue(null), + }); + vi.mocked(createHandler).mockReturnValueOnce(redirectHandler).mockReturnValueOnce(mockLoginHandler()); + + const auth = new CustomAuth({ ...BASE_ARGS, uxMode: UX_MODE.REDIRECT }); + auth.isInitialized = true; + + await auth.triggerLogin({ + authConnection: "google", + authConnectionId: "google-verifier", + clientId: "google-client-id", + }); + + expect(createSessionSpy).toHaveBeenCalledWith({ + args: expect.objectContaining({ + customState: expect.objectContaining({ + recordId: "generated-record-id", + }), + }), + }); + + const storedArgs = createSessionSpy.mock.calls[0][0].args; + const stateObj = { + instanceId: "redir_nonce", + authConnectionId: "google-verifier", + authConnection: "google", + }; + const stateEncoded = encodeURIComponent(encodeBase64Url(JSON.stringify(stateObj))); + const hash = `access_token=mock_access&id_token=mock_id_token&state=${stateEncoded}`; + + setupWindowLocation(hash); + authorizeSessionSpy.mockResolvedValue({ args: storedArgs }); + + const result = await auth.getRedirectResult({ replaceUrl: false }); + + expect(result.error).toBeUndefined(); + expect(vi.mocked(generateRecordId)).toHaveBeenCalledTimes(1); + expect(vi.mocked(put)).toHaveBeenCalledTimes(1); + expect(vi.mocked(put)).toHaveBeenCalledWith( + expect.stringMatching(/\/v1\/user\/audit$/), + expect.objectContaining({ + recordId: "generated-record-id", + oauthInitiated: true, + }) + ); + }); + it("clears storage on triggerLogin error", async () => { const CustomAuth = await getCustomAuth(); From 04a3adb0cc7173dcf22f74698aea6cf5834bae24 Mon Sep 17 00:00:00 2001 From: lwin Date: Mon, 30 Mar 2026 15:36:25 +0800 Subject: [PATCH 4/5] chore: removed audit from CustomAuth --- package-lock.json | 2 +- src/login.ts | 38 ++++-------------- src/utils/error.ts | 9 +++++ src/utils/helpers.ts | 8 ---- test/unit/error.test.ts | 30 ++++++++++++++ test/unit/login.test.ts | 86 ++++++++++++----------------------------- 6 files changed, 72 insertions(+), 101 deletions(-) create mode 100644 test/unit/error.test.ts diff --git a/package-lock.json b/package-lock.json index 9441a2bc..f0ca1d20 100644 --- a/package-lock.json +++ b/package-lock.json @@ -3949,7 +3949,7 @@ "node_modules/@toruslabs/torus.js": { "version": "17.2.2", "resolved": "file:toruslabs-torus.js-17.2.2.tgz", - "integrity": "sha512-M+68isdHuJJIkYFygUCWvpB1ThJrQJI/DC6UFU7Czcm0AvIzytRYaXasywQv/HZ1Ymr6/SXbisQ60Oa/VYZpfw==", + "integrity": "sha512-5SomzIQsGWvkGOoSvPXvv0P1UTFg5ulCsWyaIQUon64h9C9Y4pL6n5X0hM4Q5lmEpk5Q+sLIXot0DBPQxYmydg==", "license": "MIT", "dependencies": { "@toruslabs/constants": "^16.1.1", diff --git a/src/login.ts b/src/login.ts index 02557a22..9bd17297 100644 --- a/src/login.ts +++ b/src/login.ts @@ -2,14 +2,14 @@ import { BUILD_ENV, BUILD_ENV_TYPE, type INodeDetails, STORAGE_SERVER_MAP, TORUS import { NodeDetailManager } from "@toruslabs/fetch-node-details"; import { type Hex, keccak256, remove0x, utf8ToBytes } from "@toruslabs/metadata-helpers"; import { StorageManager } from "@toruslabs/session-manager"; -import { type CitadelAuditParams, generateRecordId, type KeyType, Torus, TorusKey } from "@toruslabs/torus.js"; +import { type KeyType, Torus, TorusKey } from "@toruslabs/torus.js"; import { createHandler } from "./handlers/HandlerFactory"; import { registerServiceWorker } from "./registerServiceWorker"; import SentryHandler from "./sentry"; import { AUTH_CONNECTION_TYPE, SENTRY_TXNS, UX_MODE, UX_MODE_TYPE } from "./utils/enums"; -import { serializeError } from "./utils/error"; -import { callCitadelAuditApi, handleRedirectParameters, isFirefox, padUrlString } from "./utils/helpers"; +import { CustomAuthLoginError, CustomAuthLoginErrorPrefix, serializeError } from "./utils/error"; +import { handleRedirectParameters, isFirefox, padUrlString } from "./utils/helpers"; import { CustomAuthArgs, CustomAuthLoginParams, @@ -156,11 +156,7 @@ export class CustomAuth { if (!this.isInitialized) { throw new Error("Not initialized yet"); } - const hasExistingRecordId = Boolean(args.customState?.recordId); - const recordId = args.customState?.recordId || generateRecordId(); - if (!hasExistingRecordId) { - args.customState = { ...(args.customState || {}), recordId }; - } + const { authConnectionId, authConnection, clientId, jwtParams, hash, queryParameters, customState, groupedAuthConnectionId } = args; const loginHandler: ILoginHandler = createHandler({ authConnection, @@ -176,22 +172,6 @@ export class CustomAuth { web3AuthNetwork: this.config.web3AuthNetwork, storageServerUrl: this.config.storageServerUrl, }); - // oAuthUserId is not available yet before the login - const auditPayload: Partial = { - recordId, - web3AuthNetwork: this.config.web3AuthNetwork, - web3AuthClientId: this.config.web3AuthClientId, - authConnection, - authConnectionId, - groupedAuthConnectionId, - }; - - if (!hasExistingRecordId) { - // track the `oauthInitiated` audit if recordId is not provided - callCitadelAuditApi(this.config.buildEnv, { ...auditPayload, oauthInitiated: true }).catch((error) => { - log.error("Error tracking oauthInitiated audit", error); - }); - } let loginParams: LoginWindowResponse; try { @@ -217,11 +197,9 @@ export class CustomAuth { } catch (error) { log.error(error); - // track the `oauthFailed` audit if the login fails - callCitadelAuditApi(this.config.buildEnv, { ...auditPayload, oauthFailed: true }).catch((error) => { - log.error("Error tracking oauthFailed audit", error); - }); - throw error; + const serializedError = await serializeError(error); + const errorMessage = `${CustomAuthLoginErrorPrefix}${serializedError.message || ""}`; + throw new CustomAuthLoginError(errorMessage); } const userInfo = await loginHandler.getUserInfo(loginParams); @@ -232,7 +210,7 @@ export class CustomAuth { idToken: loginParams.idToken || loginParams.accessToken, additionalParams: userInfo.extraConnectionParams, groupedAuthConnectionId, - recordId, + recordId: args.customState?.recordId, authConnection: userInfo.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/src/utils/helpers.ts b/src/utils/helpers.ts index 46ad207f..a2a9701c 100644 --- a/src/utils/helpers.ts +++ b/src/utils/helpers.ts @@ -1,7 +1,4 @@ -import { BUILD_ENV_TYPE, CITADEL_SERVER_MAP } from "@toruslabs/constants"; -import { put } from "@toruslabs/http-helpers"; import { decodeBase64Url } from "@toruslabs/metadata-helpers"; -import { CitadelAuditParams } from "@toruslabs/torus.js"; import Bowser from "bowser"; import { AUTH_CONNECTION, AUTH_CONNECTION_TYPE, REDIRECT_PARAMS_STORAGE_METHOD_TYPE } from "./enums"; @@ -238,8 +235,3 @@ export function decodeToken(token: string): { header: { alg: string; typ: str payload: JSON.parse(decodeBase64Url(payload)) as T, }; } - -export async function callCitadelAuditApi(buildEnv: BUILD_ENV_TYPE, params: Partial): Promise { - const url = new URL(`${CITADEL_SERVER_MAP[buildEnv]}/v1/user/audit`); - await put(url.toString(), params); -} 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 da9fc8bb..a375c40d 100644 --- a/test/unit/login.test.ts +++ b/test/unit/login.test.ts @@ -1,12 +1,11 @@ import type { TORUS_NETWORK_TYPE } from "@toruslabs/constants"; -import { put } from "@toruslabs/http-helpers"; import { encodeBase64Url } from "@toruslabs/metadata-helpers"; import { StorageManager } from "@toruslabs/session-manager"; -import { generateRecordId } from "@toruslabs/torus.js"; 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", () => { @@ -16,10 +15,7 @@ vi.mock("@toruslabs/torus.js", () => { }); } Torus.setAPIKey = vi.fn(); - return { - Torus, - generateRecordId: vi.fn(() => "generated-record-id"), - }; + return { Torus }; }); vi.mock("@toruslabs/fetch-node-details", () => ({ @@ -32,10 +28,6 @@ vi.mock("@toruslabs/fetch-node-details", () => ({ }), })); -vi.mock("@toruslabs/http-helpers", () => ({ - put: vi.fn().mockResolvedValue(undefined), -})); - vi.mock("../../src/handlers/HandlerFactory", () => ({ createHandler: vi.fn(), })); @@ -195,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", () => { @@ -299,58 +313,6 @@ describe("CustomAuth", () => { expect(result.result).toBeDefined(); }); - it("reuses the generated recordId across the redirect round-trip", async () => { - const CustomAuth = await getCustomAuth(); - - const redirectHandler = mockLoginHandler({ - nonce: "redir_nonce", - handleLoginWindow: vi.fn().mockResolvedValue(null), - }); - vi.mocked(createHandler).mockReturnValueOnce(redirectHandler).mockReturnValueOnce(mockLoginHandler()); - - const auth = new CustomAuth({ ...BASE_ARGS, uxMode: UX_MODE.REDIRECT }); - auth.isInitialized = true; - - await auth.triggerLogin({ - authConnection: "google", - authConnectionId: "google-verifier", - clientId: "google-client-id", - }); - - expect(createSessionSpy).toHaveBeenCalledWith({ - args: expect.objectContaining({ - customState: expect.objectContaining({ - recordId: "generated-record-id", - }), - }), - }); - - const storedArgs = createSessionSpy.mock.calls[0][0].args; - const stateObj = { - instanceId: "redir_nonce", - authConnectionId: "google-verifier", - authConnection: "google", - }; - const stateEncoded = encodeURIComponent(encodeBase64Url(JSON.stringify(stateObj))); - const hash = `access_token=mock_access&id_token=mock_id_token&state=${stateEncoded}`; - - setupWindowLocation(hash); - authorizeSessionSpy.mockResolvedValue({ args: storedArgs }); - - const result = await auth.getRedirectResult({ replaceUrl: false }); - - expect(result.error).toBeUndefined(); - expect(vi.mocked(generateRecordId)).toHaveBeenCalledTimes(1); - expect(vi.mocked(put)).toHaveBeenCalledTimes(1); - expect(vi.mocked(put)).toHaveBeenCalledWith( - expect.stringMatching(/\/v1\/user\/audit$/), - expect.objectContaining({ - recordId: "generated-record-id", - oauthInitiated: true, - }) - ); - }); - it("clears storage on triggerLogin error", async () => { const CustomAuth = await getCustomAuth(); From bfb9f261abb7b4ee30781fafb4f3c1794ce256ab Mon Sep 17 00:00:00 2001 From: lwin Date: Tue, 31 Mar 2026 14:38:07 +0800 Subject: [PATCH 5/5] feat: upgrade torus.js --- package-lock.json | 8 ++++---- package.json | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package-lock.json b/package-lock.json index f0ca1d20..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": "file:./toruslabs-torus.js-17.2.2.tgz", + "@toruslabs/torus.js": "^17.2.3", "bowser": "^2.14.1", "deepmerge": "^4.3.1", "events": "^3.3.0", @@ -3947,9 +3947,9 @@ } }, "node_modules/@toruslabs/torus.js": { - "version": "17.2.2", - "resolved": "file:toruslabs-torus.js-17.2.2.tgz", - "integrity": "sha512-5SomzIQsGWvkGOoSvPXvv0P1UTFg5ulCsWyaIQUon64h9C9Y4pL6n5X0hM4Q5lmEpk5Q+sLIXot0DBPQxYmydg==", + "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", diff --git a/package.json b/package.json index 78c8d54b..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": "file:./toruslabs-torus.js-17.2.2.tgz", + "@toruslabs/torus.js": "^17.2.3", "bowser": "^2.14.1", "deepmerge": "^4.3.1", "events": "^3.3.0",