Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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

Expand Down
1 change: 1 addition & 0 deletions lib/build/framework/custom/framework.d.ts

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

19 changes: 14 additions & 5 deletions lib/build/framework/custom/framework.js

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

11 changes: 10 additions & 1 deletion lib/build/recipe/oauth2provider/constants.js

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

16 changes: 15 additions & 1 deletion lib/build/recipe/session/constants.js

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion lib/build/version.d.ts

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion lib/build/version.js

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

20 changes: 15 additions & 5 deletions lib/ts/framework/custom/framework.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand All @@ -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) => {
Expand Down Expand Up @@ -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;
}
};
}

Expand Down
2 changes: 1 addition & 1 deletion lib/ts/version.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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"];

Expand Down
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -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": {
Expand Down
116 changes: 116 additions & 0 deletions test/framework/custom.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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("<html>First</html>");

// Second call - should be ignored
resp.sendHTMLResponse("<html>Second</html>");

// Verify only the first response is set
assert.strictEqual(resp.body, "<html>First</html>");
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>HTML</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>HTML</html>");

// Try to send JSON response - should be ignored
resp.sendJSONResponse({ status: "JSON" });

// Verify HTML response is preserved
assert.strictEqual(resp.body, "<html>HTML</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",
})
);
});
});
Loading