diff --git a/CHANGELOG.md b/CHANGELOG.md index b38bf0c72..45bf3456b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## Unreleased +## [23.1.0] - 2025-11-25 + +- Fix the OAuth2Provider `tokenExchange` error message when the refresh token is expired +- Updates custom framework to following correct CollectingResponse flow order (to ensure that subsequent calls to the `sendJSONResponse` doesn't overwrite the response). + ## [23.0.1] - 2025-07-31 - Updated FDI support to 4.2 @@ -15,7 +20,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Fix WebAuthn credential listing and removal to work even when the WebAuthn user is not the primary user and when there are multiple WebAuthn users linked - Prevent removal of WebAuthn credentials unless all session claims are satisfied - Change how sessions are fetched before listing, removing and adding WebAuthn credentials -- Fix the OAuth2Provider `tokenExchange` error message when the refresh token is expired ## [23.0.0] - 2025-07-21 diff --git a/lib/build/framework/custom/framework.d.ts b/lib/build/framework/custom/framework.d.ts index 02374da39..c5d34113c 100644 --- a/lib/build/framework/custom/framework.d.ts +++ b/lib/build/framework/custom/framework.d.ts @@ -42,6 +42,7 @@ export declare class CollectingResponse extends BaseResponse { readonly headers: Headers; readonly cookies: CookieInfo[]; body?: string; + private responseSet; constructor(); sendHTMLResponse: (html: string) => void; setHeader: (key: string, value: string, allowDuplicateKey: boolean) => void; diff --git a/lib/build/framework/custom/framework.js b/lib/build/framework/custom/framework.js index 672f9ab6c..30a140da2 100644 --- a/lib/build/framework/custom/framework.js +++ b/lib/build/framework/custom/framework.js @@ -80,8 +80,11 @@ class CollectingResponse extends response_1.BaseResponse { constructor() { super(); this.sendHTMLResponse = (html) => { - this.headers.set("Content-Type", "text/html"); - this.body = html; + if (!this.responseSet) { + this.headers.set("Content-Type", "text/html"); + this.body = html; + this.responseSet = true; + } }; this.setHeader = (key, value, allowDuplicateKey) => { var _a; @@ -108,11 +111,16 @@ class CollectingResponse extends response_1.BaseResponse { * @param {number} statusCode */ this.setStatusCode = (statusCode) => { - this.statusCode = statusCode; + if (!this.responseSet) { + this.statusCode = statusCode; + } }; this.sendJSONResponse = (content) => { - this.headers.set("Content-Type", "application/json"); - this.body = JSON.stringify(content); + if (!this.responseSet) { + this.headers.set("Content-Type", "application/json"); + this.body = JSON.stringify(content); + this.responseSet = true; + } }; // In node16 the Headers class is only supported behind an experimental flag, so we sometimes need to add an implementation for it // Still, if available we are using the built-in (node 18+) @@ -123,6 +131,7 @@ class CollectingResponse extends response_1.BaseResponse { } this.statusCode = 200; this.cookies = []; + this.responseSet = false; } } exports.CollectingResponse = CollectingResponse; diff --git a/lib/build/recipe/oauth2provider/constants.js b/lib/build/recipe/oauth2provider/constants.js index 260e9eb50..e3550b35f 100644 --- a/lib/build/recipe/oauth2provider/constants.js +++ b/lib/build/recipe/oauth2provider/constants.js @@ -14,7 +14,16 @@ * under the License. */ Object.defineProperty(exports, "__esModule", { value: true }); -exports.LOGOUT_PATH = exports.END_SESSION_PATH = exports.INTROSPECT_TOKEN_PATH = exports.REVOKE_TOKEN_PATH = exports.USER_INFO_PATH = exports.LOGIN_INFO_PATH = exports.TOKEN_PATH = exports.AUTH_PATH = exports.LOGIN_PATH = void 0; +exports.LOGOUT_PATH = + exports.END_SESSION_PATH = + exports.INTROSPECT_TOKEN_PATH = + exports.REVOKE_TOKEN_PATH = + exports.USER_INFO_PATH = + exports.LOGIN_INFO_PATH = + exports.TOKEN_PATH = + exports.AUTH_PATH = + exports.LOGIN_PATH = + void 0; exports.LOGIN_PATH = "/oauth/login"; exports.AUTH_PATH = "/oauth/auth"; exports.TOKEN_PATH = "/oauth/token"; diff --git a/lib/build/recipe/session/constants.js b/lib/build/recipe/session/constants.js index e196684e5..82fdcadde 100644 --- a/lib/build/recipe/session/constants.js +++ b/lib/build/recipe/session/constants.js @@ -14,7 +14,21 @@ * under the License. */ Object.defineProperty(exports, "__esModule", { value: true }); -exports.authModeHeaderKey = exports.frontTokenHeaderKey = exports.antiCsrfHeaderKey = exports.refreshTokenHeaderKey = exports.refreshTokenCookieKey = exports.accessTokenCookieKey = exports.accessTokenHeaderKey = exports.authorizationHeaderKey = exports.protectedProps = exports.JWKCacheCooldownInMs = exports.oneYearInMs = exports.availableTokenTransferMethods = exports.SIGNOUT_API_PATH = exports.REFRESH_API_PATH = void 0; +exports.authModeHeaderKey = + exports.frontTokenHeaderKey = + exports.antiCsrfHeaderKey = + exports.refreshTokenHeaderKey = + exports.refreshTokenCookieKey = + exports.accessTokenCookieKey = + exports.accessTokenHeaderKey = + exports.authorizationHeaderKey = + exports.protectedProps = + exports.JWKCacheCooldownInMs = + exports.oneYearInMs = + exports.availableTokenTransferMethods = + exports.SIGNOUT_API_PATH = + exports.REFRESH_API_PATH = + void 0; exports.REFRESH_API_PATH = "/session/refresh"; exports.SIGNOUT_API_PATH = "/signout"; exports.availableTokenTransferMethods = ["cookie", "header"]; diff --git a/lib/build/version.d.ts b/lib/build/version.d.ts index 39bb79109..8002bb7af 100644 --- a/lib/build/version.d.ts +++ b/lib/build/version.d.ts @@ -1,4 +1,4 @@ // @ts-nocheck -export declare const version = "23.0.1"; +export declare const version = "23.0.2"; export declare const cdiSupported: string[]; export declare const dashboardVersion = "0.15"; diff --git a/lib/build/version.js b/lib/build/version.js index 159cb09df..3d2f88d77 100644 --- a/lib/build/version.js +++ b/lib/build/version.js @@ -15,7 +15,7 @@ exports.dashboardVersion = exports.cdiSupported = exports.version = void 0; * License for the specific language governing permissions and limitations * under the License. */ -exports.version = "23.0.1"; +exports.version = "23.0.2"; exports.cdiSupported = ["5.3"]; // Note: The actual script import for dashboard uses v{DASHBOARD_VERSION} exports.dashboardVersion = "0.15"; diff --git a/lib/ts/framework/custom/framework.ts b/lib/ts/framework/custom/framework.ts index c98062c94..7e10d9b38 100644 --- a/lib/ts/framework/custom/framework.ts +++ b/lib/ts/framework/custom/framework.ts @@ -103,6 +103,7 @@ export class CollectingResponse extends BaseResponse { public readonly headers: Headers; public readonly cookies: CookieInfo[]; public body?: string; + private responseSet: boolean; constructor() { super(); @@ -115,11 +116,15 @@ export class CollectingResponse extends BaseResponse { } this.statusCode = 200; this.cookies = []; + this.responseSet = false; } sendHTMLResponse = (html: string) => { - this.headers.set("Content-Type", "text/html"); - this.body = html; + if (!this.responseSet) { + this.headers.set("Content-Type", "text/html"); + this.body = html; + this.responseSet = true; + } }; setHeader = (key: string, value: string, allowDuplicateKey: boolean) => { @@ -158,12 +163,17 @@ export class CollectingResponse extends BaseResponse { * @param {number} statusCode */ setStatusCode = (statusCode: number) => { - this.statusCode = statusCode; + if (!this.responseSet) { + this.statusCode = statusCode; + } }; sendJSONResponse = (content: any) => { - this.headers.set("Content-Type", "application/json"); - this.body = JSON.stringify(content); + if (!this.responseSet) { + this.headers.set("Content-Type", "application/json"); + this.body = JSON.stringify(content); + this.responseSet = true; + } }; } diff --git a/lib/ts/version.ts b/lib/ts/version.ts index 3c133145a..a75987b91 100644 --- a/lib/ts/version.ts +++ b/lib/ts/version.ts @@ -12,7 +12,7 @@ * License for the specific language governing permissions and limitations * under the License. */ -export const version = "23.0.1"; +export const version = "23.0.2"; export const cdiSupported = ["5.3"]; diff --git a/package-lock.json b/package-lock.json index fc6333007..2aefaf756 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "supertokens-node", - "version": "23.0.1", + "version": "23.0.2", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "supertokens-node", - "version": "23.0.1", + "version": "23.0.2", "license": "Apache-2.0", "dependencies": { "buffer": "^6.0.3", diff --git a/package.json b/package.json index 23a15c530..2031ab819 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "supertokens-node", - "version": "23.0.1", + "version": "23.0.2", "description": "NodeJS driver for SuperTokens core", "main": "index.js", "scripts": { diff --git a/test/framework/custom.test.js b/test/framework/custom.test.js index 8733fe309..6d224a65a 100644 --- a/test/framework/custom.test.js +++ b/test/framework/custom.test.js @@ -233,3 +233,119 @@ describe(`PreParsedRequest`, function () { assert(JSON.stringify(formData2) === JSON.stringify(mockFormData)); }); }); + +describe(`CollectingResponse`, function () { + it("sendJSONResponse should ignore subsequent calls after first call", function () { + const resp = new CustomFramework.CollectingResponse(); + + // First call - should set the response + resp.sendJSONResponse({ status: "FIRST", message: "This should be preserved" }); + + // Second call - should be ignored + resp.sendJSONResponse({ status: "SECOND", message: "This should be ignored" }); + + // Verify only the first response is set + assert.strictEqual(resp.body, JSON.stringify({ status: "FIRST", message: "This should be preserved" })); + assert.strictEqual(resp.headers.get("Content-Type"), "application/json"); + }); + + it("sendHTMLResponse should ignore subsequent calls after first call", function () { + const resp = new CustomFramework.CollectingResponse(); + + // First call - should set the response + resp.sendHTMLResponse("First"); + + // Second call - should be ignored + resp.sendHTMLResponse("Second"); + + // Verify only the first response is set + assert.strictEqual(resp.body, "First"); + assert.strictEqual(resp.headers.get("Content-Type"), "text/html"); + }); + + it("setStatusCode should ignore subsequent calls after response is set", function () { + const resp = new CustomFramework.CollectingResponse(); + + // Set status code and send response + resp.setStatusCode(400); + resp.sendJSONResponse({ status: "ERROR" }); + + // Try to change status code after response is sent - should be ignored + resp.setStatusCode(200); + + // Verify status code remains 400 + assert.strictEqual(resp.statusCode, 400); + assert.strictEqual(resp.body, JSON.stringify({ status: "ERROR" })); + }); + + it("sendJSONResponse should not allow sendHTMLResponse to overwrite", function () { + const resp = new CustomFramework.CollectingResponse(); + + // Set JSON response first + resp.sendJSONResponse({ status: "JSON" }); + + // Try to send HTML response - should be ignored + resp.sendHTMLResponse("HTML"); + + // Verify JSON response is preserved + assert.strictEqual(resp.body, JSON.stringify({ status: "JSON" })); + assert.strictEqual(resp.headers.get("Content-Type"), "application/json"); + }); + + it("sendHTMLResponse should not allow sendJSONResponse to overwrite", function () { + const resp = new CustomFramework.CollectingResponse(); + + // Set HTML response first + resp.sendHTMLResponse("HTML"); + + // Try to send JSON response - should be ignored + resp.sendJSONResponse({ status: "JSON" }); + + // Verify HTML response is preserved + assert.strictEqual(resp.body, "HTML"); + assert.strictEqual(resp.headers.get("Content-Type"), "text/html"); + }); + + it("should allow setting status code before sending response", function () { + const resp = new CustomFramework.CollectingResponse(); + + // Set status code first + resp.setStatusCode(400); + + // Then send response + resp.sendJSONResponse({ status: "ERROR" }); + + // Verify both are set correctly + assert.strictEqual(resp.statusCode, 400); + assert.strictEqual(resp.body, JSON.stringify({ status: "ERROR" })); + }); + + it("should simulate API override behavior - custom response should be preserved", function () { + const resp = new CustomFramework.CollectingResponse(); + + // Simulate what happens in an API override: + // 1. User calls setStatusCode and sendJSONResponse with custom response + resp.setStatusCode(400); + resp.sendJSONResponse({ + status: "CUSTOM_ERROR", + message: "Custom error from override", + }); + + // 2. SDK later tries to send the returned value (should be ignored) + resp.setStatusCode(200); + resp.sendJSONResponse({ + status: "GENERAL_ERROR", + message: "This should not appear", + }); + + // Verify the custom response is preserved + assert.strictEqual(resp.statusCode, 400); + assert.strictEqual( + resp.body, + JSON.stringify({ + status: "CUSTOM_ERROR", + message: "Custom error from override", + }) + ); + }); +});