From 6b20b4b1efb0a5bc41c838854adfb75bcdc4b68d Mon Sep 17 00:00:00 2001 From: Alexandru Ciobanu Date: Tue, 2 Jan 2024 13:11:12 +0000 Subject: [PATCH 1/3] 2.1.8 (#90) --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index f9629ad6..1bd72779 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@bucketco/tracking-sdk", - "version": "2.1.8-0", + "version": "2.1.8", "license": "MIT", "private": false, "repository": { From bb79b376e4f61092a17c8e744c79dea683fe0599 Mon Sep 17 00:00:00 2001 From: Alexandru Ciobanu Date: Tue, 2 Jan 2024 15:21:32 +0000 Subject: [PATCH 2/3] chore: update webpack (#91) --- package.json | 2 +- yarn.lock | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package.json b/package.json index 1bd72779..2072f33d 100644 --- a/package.json +++ b/package.json @@ -64,7 +64,7 @@ "vite": "^4.4.12", "vite-plugin-dts": "^3.5.2", "vitest": "^0.33.0", - "webpack": "^5.88.2", + "webpack": "^5.89.0", "webpack-cli": "^5.1.4", "webpack-node-externals": "^3.0.0" }, diff --git a/yarn.lock b/yarn.lock index 8c536055..31c7a0ad 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5039,10 +5039,10 @@ webpack@^5: watchpack "^2.3.1" webpack-sources "^3.2.3" -webpack@^5.88.2: - version "5.88.2" - resolved "https://registry.yarnpkg.com/webpack/-/webpack-5.88.2.tgz#f62b4b842f1c6ff580f3fcb2ed4f0b579f4c210e" - integrity sha512-JmcgNZ1iKj+aiR0OvTYtWQqJwq37Pf683dY9bVORwVbUrDhLhdn/PlO2sHsFHPkj7sHNQF3JwaAkp49V+Sq1tQ== +webpack@^5.89.0: + version "5.89.0" + resolved "https://registry.yarnpkg.com/webpack/-/webpack-5.89.0.tgz#56b8bf9a34356e93a6625770006490bf3a7f32dc" + integrity sha512-qyfIC10pOr70V+jkmud8tMfajraGCZMBWJtrmuBymQKCrLTRejBI8STDp1MCyZu/QTdZSeacCQYpYNQVOzX5kw== dependencies: "@types/eslint-scope" "^3.7.3" "@types/estree" "^1.0.0" From 60be1896263c0525589b358ed278c23cb20a7032 Mon Sep 17 00:00:00 2001 From: Matus Vacula Date: Fri, 12 Jan 2024 15:41:38 +0100 Subject: [PATCH 3/3] fix: auth cookie serialization/deserialization (#93) * fix: auth cookie serialization/deserialization * format * add suffix * format * fix connection loop * chore: add test coverage to the new issues * chore: bump version * chore: PR comments * feat: use real cookies for testing * chore: restore timer --------- Co-authored-by: Alexandru Ciobanu --- package.json | 3 +- src/prompt-storage.ts | 24 ++-- src/sse.ts | 11 +- test/prompt-storage.test.ts | 242 +++++++++++++++++++++++++++++------- test/sse.test.ts | 16 ++- yarn.lock | 16 ++- 6 files changed, 252 insertions(+), 60 deletions(-) diff --git a/package.json b/package.json index 2072f33d..1f269c27 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@bucketco/tracking-sdk", - "version": "2.1.8", + "version": "2.1.9", "license": "MIT", "private": false, "repository": { @@ -34,6 +34,7 @@ "@preact/preset-vite": "^2.5.0", "@types/js-cookie": "^3.0.3", "@types/node": "^20.4.4", + "@types/jsdom": "^21.1.6", "@types/webpack": "^5.28.2", "@types/webpack-node-externals": "^3.0.0", "@typescript-eslint/eslint-plugin": "^6.4.0", diff --git a/src/prompt-storage.ts b/src/prompt-storage.ts index 4263f80e..bea0ed9d 100644 --- a/src/prompt-storage.ts +++ b/src/prompt-storage.ts @@ -26,7 +26,7 @@ export const rememberAuthToken = ( token: string, expiresAt: Date, ) => { - Cookies.set(`bucket-token-${userId}`, `${channel}:${token}`, { + Cookies.set(`bucket-token-${userId}`, JSON.stringify({ channel, token }), { expires: expiresAt, sameSite: "strict", secure: true, @@ -39,13 +39,23 @@ export const getAuthToken = (userId: string) => { return undefined; } - const [channel, token] = val.split(":"); - if (!channel?.length || !token?.length) { + try { + const { channel, token } = JSON.parse(val) as { + channel: string; + token: string; + }; + if (!channel?.length || !token?.length) { + return undefined; + } + return { + channel, + token, + }; + } catch (e) { return undefined; } +}; - return { - channel, - token, - }; +export const forgetAuthToken = (userId: string) => { + Cookies.remove(`bucket-token-${userId}`); }; diff --git a/src/sse.ts b/src/sse.ts index 47061de9..633915a4 100644 --- a/src/sse.ts +++ b/src/sse.ts @@ -1,7 +1,11 @@ import fetch from "cross-fetch"; import { SSE_REALTIME_HOST } from "./config"; -import { getAuthToken, rememberAuthToken } from "./prompt-storage"; +import { + forgetAuthToken, + getAuthToken, + rememberAuthToken, +} from "./prompt-storage"; interface AblyTokenDetails { token: string; @@ -12,8 +16,8 @@ interface AblyTokenRequest { keyName: string; } -const ABLY_TOKEN_ERROR_MIN = 40140; -const ABLY_TOKEN_ERROR_MAX = 40149; +const ABLY_TOKEN_ERROR_MIN = 40000; +const ABLY_TOKEN_ERROR_MAX = 49999; export class AblySSEChannel { private isOpen: boolean = false; @@ -130,6 +134,7 @@ export class AblySSEChannel { errorCode <= ABLY_TOKEN_ERROR_MAX ) { this.log("event source token expired, refresh required"); + forgetAuthToken(this.userId); } } else { const connectionState = (e as any)?.target?.readyState; diff --git a/test/prompt-storage.test.ts b/test/prompt-storage.test.ts index 66aac2ff..172ef5b8 100644 --- a/test/prompt-storage.test.ts +++ b/test/prompt-storage.test.ts @@ -1,82 +1,236 @@ -import Cookies from "js-cookie"; -import { describe, expect, test, vi } from "vitest"; +import { + afterAll, + afterEach, + beforeAll, + describe, + expect, + test, + vi, +} from "vitest"; import { checkPromptMessageCompleted, + forgetAuthToken, getAuthToken, markPromptMessageCompleted, rememberAuthToken, } from "../src/prompt-storage"; -vi.mock("js-cookie"); - describe("prompt-storage", () => { - test("markPromptMessageCompleted", async () => { - const spy = vi.spyOn(Cookies, "set"); + beforeAll(() => { + const cookies: Record = {}; + + Object.defineProperty(document, "cookie", { + set: (val: string) => { + if (!val) { + Object.keys(cookies).forEach((k) => delete cookies[k]); + return; + } + const i = val.indexOf("="); + cookies[val.slice(0, i)] = val.slice(i + 1); + }, + get: () => + Object.entries(cookies) + .map(([k, v]) => `${k}=${v}`) + .join("; "), + }); - markPromptMessageCompleted("user", "prompt", new Date("2021-01-01")); + vi.setSystemTime(new Date("2024-01-11T09:55:37.000Z")); + }); - expect(spy).toHaveBeenCalledWith("bucket-prompt-user", "prompt", { - expires: new Date("2021-01-01"), - sameSite: "strict", - secure: true, - }); + afterEach(() => { + document.cookie = undefined!; + vi.clearAllMocks(); }); - test("checkPromptMessageCompleted with positive result", async () => { - const spy = vi.spyOn(Cookies, "get").mockReturnValue("prompt" as any); + afterAll(() => { + vi.useRealTimers(); + }); - expect(checkPromptMessageCompleted("user", "prompt")).toBe(true); + describe("markPromptMessageCompleted", () => { + test("adds new cookie", async () => { + markPromptMessageCompleted( + "user", + "prompt2", + new Date("2024-01-04T14:01:20.000Z"), + ); + + expect(document.cookie).toBe( + "bucket-prompt-user=prompt2; path=/; expires=Thu, 04 Jan 2024 14:01:20 GMT; sameSite=strict; secure", + ); + }); + + test("rewrites existing cookie", async () => { + document.cookie = + "bucket-prompt-user=prompt1; path=/; expires=Thu, 04 Jan 2021 14:01:20 GMT; sameSite=strict; secure"; + + markPromptMessageCompleted( + "user", + "prompt2", + new Date("2024-01-04T14:01:20.000Z"), + ); - expect(spy).toHaveBeenCalledWith("bucket-prompt-user"); + expect(document.cookie).toBe( + "bucket-prompt-user=prompt2; path=/; expires=Thu, 04 Jan 2024 14:01:20 GMT; sameSite=strict; secure", + ); + }); }); - test("checkPromptMessageCompleted with negative result", async () => { - const spy = vi.spyOn(Cookies, "get").mockReturnValue("other" as any); + describe("checkPromptMessageCompleted", () => { + test("cookie with same use and prompt results in true", async () => { + document.cookie = + "bucket-prompt-user=prompt; path=/; expires=Thu, 04 Jan 2024 14:01:20 GMT; sameSite=strict; secure"; - expect(checkPromptMessageCompleted("user", "prompt")).toBe(false); + expect(checkPromptMessageCompleted("user", "prompt")).toBe(true); - expect(spy).toHaveBeenCalledWith("bucket-prompt-user"); - }); + expect(document.cookie).toBe( + "bucket-prompt-user=prompt; path=/; expires=Thu, 04 Jan 2024 14:01:20 GMT; sameSite=strict; secure", + ); + }); + + test("cookie with different prompt results in false", async () => { + document.cookie = + "bucket-prompt-user=prompt1; path=/; expires=Thu, 04 Jan 2024 14:01:20 GMT; sameSite=strict; secure"; - test("rememberAuthToken", async () => { - const spy = vi.spyOn(Cookies, "set"); + expect(checkPromptMessageCompleted("user", "prompt2")).toBe(false); + }); - rememberAuthToken("user", "channel", "token", new Date("2021-01-01")); + test("cookie with different user results in false", async () => { + document.cookie = + "bucket-prompt-user1=prompt1; path=/; expires=Thu, 04 Jan 2024 14:01:20 GMT; sameSite=strict; secure"; + + expect(checkPromptMessageCompleted("user2", "prompt1")).toBe(false); + }); - expect(spy).toHaveBeenCalledWith("bucket-token-user", "channel:token", { - expires: new Date("2021-01-01"), - sameSite: "strict", - secure: true, + test("no cookie results in false", async () => { + expect(checkPromptMessageCompleted("user", "prompt2")).toBe(false); }); }); - test("getAuthToken with positive result", async () => { - const spy = vi - .spyOn(Cookies, "get") - .mockReturnValue("channel:token" as any); + describe("rememberAuthToken", () => { + test("adds new cookie if none was there", async () => { + expect(document.cookie).toBe(""); - expect(getAuthToken("user")).toStrictEqual({ - channel: "channel", - token: "token", + rememberAuthToken( + 'user1"%%', + "channel:suffix", + "secret$%", + new Date("2024-01-02T15:02:20.000Z"), + ); + + expect(document.cookie).toBe( + "bucket-token-user1%22%25%25={%22channel%22:%22channel:suffix%22%2C%22token%22:%22secret$%25%22}; path=/; expires=Tue, 02 Jan 2024 15:02:20 GMT; sameSite=strict; secure", + ); }); - expect(spy).toHaveBeenCalledWith("bucket-token-user"); + test("replaces existing cookie for same user", async () => { + document.cookie = + "bucket-token-user1%22%25%25={%22channel%22:%22channel:suffix%22%2C%22token%22:%22secret$%25%22}; path=/; expires=Tue, 02 Jan 2024 15:02:20 GMT; sameSite=strict; secure"; + + rememberAuthToken( + 'user1"%%', + "channel2:suffix2", + "secret2$%", + new Date("2023-01-02T15:02:20.000Z"), + ); + + expect(document.cookie).toBe( + "bucket-token-user1%22%25%25={%22channel%22:%22channel2:suffix2%22%2C%22token%22:%22secret2$%25%22}; path=/; expires=Mon, 02 Jan 2023 15:02:20 GMT; sameSite=strict; secure", + ); + }); }); - test("getAuthToken with error", async () => { - const spy = vi.spyOn(Cookies, "get").mockReturnValue("token" as any); + describe("forgetAuthToken", () => { + test("clears the user's cookie if even if there was nothing before", async () => { + forgetAuthToken("user"); - expect(getAuthToken("user")).toBeUndefined(); + expect(document.cookie).toBe( + "bucket-token-user=; path=/; expires=Wed, 10 Jan 2024 09:55:37 GMT", + ); + }); - expect(spy).toHaveBeenCalledWith("bucket-token-user"); + test("clears the user's cookie", async () => { + document.cookie = + "bucket-token-user1={%22channel%22:%22channel:suffix%22%2C%22token%22:%22secret$%25%22}; path=/; expires=Tue, 02 Jan 2024 15:02:20 GMT; sameSite=strict; secure"; + + forgetAuthToken("user1"); + + expect(document.cookie).toBe( + "bucket-token-user1=; path=/; expires=Wed, 10 Jan 2024 09:55:37 GMT", + ); + }); + + test("does nothing if there is a cookie for a different user", async () => { + document.cookie = + "bucket-token-user1={%22channel%22:%22channel:suffix%22%2C%22token%22:%22secret$%25%22}; path=/; expires=Tue, 02 Jan 2028 15:02:20 GMT; sameSite=strict; secure"; + + forgetAuthToken("user2"); + + expect(document.cookie).toBe( + "bucket-token-user1={%22channel%22:%22channel:suffix%22%2C%22token%22:%22secret$%25%22}; path=/; expires=Tue, 02 Jan 2028 15:02:20 GMT; sameSite=strict; secure; bucket-token-user2=; path=/; expires=Wed, 10 Jan 2024 09:55:37 GMT", + ); + }); }); - test("getAuthToken with negative result", async () => { - const spy = vi.spyOn(Cookies, "get").mockReturnValue(undefined as any); + describe("getAuthToken", () => { + test("returns the auth token if it's available for the user", async () => { + document.cookie = + "bucket-token-user1%22%25%25={%22channel%22:%22channel:suffix%22%2C%22token%22:%22secret$%25%22}; path=/; expires=Tue, 02 Jan 2024 15:02:20 GMT; sameSite=strict; secure"; + + expect(getAuthToken('user1"%%')).toStrictEqual({ + channel: "channel:suffix", + token: "secret$%", + }); + }); + + test("return undefined if no cookie for user", async () => { + document.cookie = + "bucket-token-user1={%22channel%22:%22channel:suffix%22%2C%22token%22:%22secret$%25%22}; path=/; expires=Tue, 02 Jan 2024 15:02:20 GMT; sameSite=strict; secure"; - expect(getAuthToken("user")).toBeUndefined(); + expect(getAuthToken("user2")).toBeUndefined(); + }); + + test("returns undefined if no cookie", async () => { + expect(getAuthToken("user")).toBeUndefined(); + }); + + test("return undefined if corrupted cookie", async () => { + document.cookie = + "bucket-token-user={channel%22:%22channel:suffix%22%2C%22token%22:%22secret$%25%22}; path=/; expires=Tue, 02 Jan 2024 15:02:20 GMT; sameSite=strict; secure"; + + expect(getAuthToken("user")).toBeUndefined(); + }); + + test("return undefined if a field is missing", async () => { + document.cookie = + "bucket-token-user={%2C%22token%22:%22secret$%25%22}; path=/; expires=Tue, 02 Jan 2024 15:02:20 GMT; sameSite=strict; secure"; + + expect(getAuthToken("user")).toBeUndefined(); + }); + }); - expect(spy).toHaveBeenCalledWith("bucket-token-user"); + test("manages all cookies for the user", () => { + rememberAuthToken( + "user1", + "channel:suffix", + "secret$%", + new Date("2024-01-02T15:02:20.000Z"), + ); + + markPromptMessageCompleted( + "user1", + "alex-prompt", + new Date("2024-01-02T15:03:20.000Z"), + ); + + expect(document.cookie).toBe( + "bucket-token-user1={%22channel%22:%22channel:suffix%22%2C%22token%22:%22secret$%25%22}; path=/; expires=Tue, 02 Jan 2024 15:02:20 GMT; sameSite=strict; secure; bucket-prompt-user1=alex-prompt; path=/; expires=Tue, 02 Jan 2024 15:03:20 GMT; sameSite=strict; secure", + ); + + forgetAuthToken("user1"); + + expect(document.cookie).toBe( + "bucket-token-user1=; path=/; expires=Wed, 10 Jan 2024 09:55:37 GMT; bucket-prompt-user1=alex-prompt; path=/; expires=Tue, 02 Jan 2024 15:03:20 GMT; sameSite=strict; secure", + ); }); }); diff --git a/test/sse.test.ts b/test/sse.test.ts index 1f707159..7c3891e8 100644 --- a/test/sse.test.ts +++ b/test/sse.test.ts @@ -2,7 +2,11 @@ import flushPromises from "flush-promises"; import nock from "nock"; import { afterEach, beforeEach, describe, expect, test, vi } from "vitest"; -import { getAuthToken, rememberAuthToken } from "../src/prompt-storage"; +import { + forgetAuthToken, + getAuthToken, + rememberAuthToken, +} from "../src/prompt-storage"; import { AblySSEChannel, closeAblySSEChannel, @@ -30,6 +34,7 @@ Object.defineProperty(window, "EventSource", { vi.mock("../src/prompt-storage", () => { return { rememberAuthToken: vi.fn(), + forgetAuthToken: vi.fn(), getAuthToken: vi.fn(), }; }); @@ -387,6 +392,8 @@ describe("message handling", () => { expect(errorCallback).toBeDefined(); await errorCallback!({} as any); + + expect(forgetAuthToken).not.toHaveBeenCalled(); expect(close).toHaveBeenCalledTimes(1); }); @@ -425,7 +432,7 @@ describe("message handling", () => { expect(close).toHaveBeenCalledTimes(1); }); - test("disconnects when ably reports token is expired", async () => { + test("disconnects when ably reports token errors", async () => { const sse = new AblySSEChannel( userId, channel, @@ -451,10 +458,11 @@ describe("message handling", () => { await errorCallback!( new MessageEvent("error", { - data: JSON.stringify({ code: 40140 }), + data: JSON.stringify({ code: 40110 }), }), ); + expect(forgetAuthToken).toHaveBeenCalledTimes(1); expect(close).toHaveBeenCalled(); }); }); @@ -566,7 +574,7 @@ describe("automatic retries", () => { vi.useRealTimers(); }); - test("resets retry count on successfull connect", async () => { + test("resets retry count on successful connect", async () => { const sse = new AblySSEChannel( userId, channel, diff --git a/yarn.lock b/yarn.lock index 31c7a0ad..4473be53 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1025,6 +1025,15 @@ resolved "https://registry.yarnpkg.com/@types/js-cookie/-/js-cookie-3.0.3.tgz#d6bfbbdd0c187354ca555213d1962f6d0691ff4e" integrity sha512-Xe7IImK09HP1sv2M/aI+48a20VX+TdRJucfq4vfRVy6nWN8PYPOEnlMRSgxJAgYQIXJVL8dZ4/ilAM7dWNaOww== +"@types/jsdom@^21.1.6": + version "21.1.6" + resolved "https://registry.yarnpkg.com/@types/jsdom/-/jsdom-21.1.6.tgz#bcbc7b245787ea863f3da1ef19aa1dcfb9271a1b" + integrity sha512-/7kkMsC+/kMs7gAYmmBR9P0vGTnOoLhQhyhQJSlXGI5bzTHp6xdo0TtKWQAsz6pmSAeVqKSbqeyP6hytqr9FDw== + dependencies: + "@types/node" "*" + "@types/tough-cookie" "*" + parse5 "^7.0.0" + "@types/json-schema@*", "@types/json-schema@^7.0.8": version "7.0.9" resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.9.tgz#97edc9037ea0c38585320b28964dde3b39e4660d" @@ -1055,6 +1064,11 @@ resolved "https://registry.yarnpkg.com/@types/semver/-/semver-7.5.0.tgz#591c1ce3a702c45ee15f47a42ade72c2fd78978a" integrity sha512-G8hZ6XJiHnuhQKR7ZmysCeJWE08o8T0AXtk5darsCaTVsYZhhgUrq53jizaR2FvsoeCwJhlmwTjkXBY5Pn/ZHw== +"@types/tough-cookie@*": + version "4.0.5" + resolved "https://registry.yarnpkg.com/@types/tough-cookie/-/tough-cookie-4.0.5.tgz#cb6e2a691b70cb177c6e3ae9c1d2e8b2ea8cd304" + integrity sha512-/Ad8+nIOV7Rl++6f1BdKxFSMgmoqEoYbHRpPcx3JEfv8VRsQe9Z4mCXeJBzxs7mbHY/XOZZuXlRNfhpVPbs6ZA== + "@types/webpack-node-externals@^3.0.0": version "3.0.0" resolved "https://registry.yarnpkg.com/@types/webpack-node-externals/-/webpack-node-externals-3.0.0.tgz#4b85b93cde99a16779c4b1faebaa23c3ef46ec0b" @@ -3733,7 +3747,7 @@ parse-json@^5.0.0: json-parse-even-better-errors "^2.3.0" lines-and-columns "^1.1.6" -parse5@^7.1.2: +parse5@^7.0.0, parse5@^7.1.2: version "7.1.2" resolved "https://registry.yarnpkg.com/parse5/-/parse5-7.1.2.tgz#0736bebbfd77793823240a23b7fc5e010b7f8e32" integrity sha512-Czj1WaSVpaoj0wbhMzLmWD69anp2WH7FXMB9n1Sy8/ZFF9jolSQVMu1Ij5WIyGmcBmhk7EOndpO4mIpihVqAXw==