Skip to content

Commit

Permalink
Merge pull request #1 from kinde-oss/feat/memory-storage-manager
Browse files Browse the repository at this point in the history
feat: Add memory session manager
  • Loading branch information
DanielRivers authored Aug 30, 2024
2 parents 6300e66 + 2d462e4 commit 7d906a5
Show file tree
Hide file tree
Showing 11 changed files with 1,083 additions and 732 deletions.
3 changes: 2 additions & 1 deletion lib/index.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
export type * from "./types";
export * from "./main";
export * from "./types";
export * from "./sessionManager";
9 changes: 5 additions & 4 deletions lib/main.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,8 @@ import {
sanitizeRedirect,
mapLoginMethodParamsForUrl,
generateAuthUrl,
Scopes,
IssuerRouteTypes,
} from "./index";
import { IssuerRouteTypes, Scopes } from "./types";
import type { LoginMethodParams, LoginOptions } from "./index";

describe("base64UrlEncode", () => {
Expand Down Expand Up @@ -75,9 +74,10 @@ describe("generateAuthUrl", () => {
redirectURL: "https://example.com",
audience: "audience123",
prompt: "login",
state: "state123",
};
const expectedUrl =
"https://auth.example.com/oauth2/auth?client_id=client123&response_type=code&start_page=login&login_hint=user%40example.com&is_create_org=true&connection_id=conn123&redirect_uri=https%3A%2F%2Fexample.com&audience=audience123&scope=openid+profile&prompt=login";
"https://auth.example.com/oauth2/auth?client_id=client123&response_type=code&start_page=login&login_hint=user%40example.com&is_create_org=true&connection_id=conn123&redirect_uri=https%3A%2F%2Fexample.com&audience=audience123&scope=openid+profile&prompt=login&state=state123";

const result = generateAuthUrl(domain, IssuerRouteTypes.login, options);
expect(result.toString()).toBe(expectedUrl);
Expand Down Expand Up @@ -109,9 +109,10 @@ describe("generateAuthUrl", () => {
scope: [Scopes.openid, Scopes.profile, Scopes.offline_access],
redirectURL: "https://example2.com",
prompt: "create",
state: "state123",
};
const expectedUrl =
"https://auth.example.com/oauth2/auth?client_id=client123&response_type=code&start_page=login&redirect_uri=https%3A%2F%2Fexample2.com&scope=openid+profile+offline_access&prompt=create";
"https://auth.example.com/oauth2/auth?client_id=client123&response_type=code&start_page=login&redirect_uri=https%3A%2F%2Fexample2.com&scope=openid+profile+offline_access&prompt=create&state=state123";

const result = generateAuthUrl(domain, IssuerRouteTypes.login, options);
expect(result.toString()).toBe(expectedUrl);
Expand Down
21 changes: 21 additions & 0 deletions lib/sessionManager/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { StorageSettingsType } from "./types.ts";

export const storageSettings: StorageSettingsType = {
/**
* The password to encrypt the store. (cookies only)
*/
storePassword: "",
/**
* The prefix to use for the storage keys.
*/
keyPrefix: "kinde-",
/**
* The maximum length of the storage.
*
* If the length is exceeded the items will be split into multiple storage items.
*/
maxLength: 2000,
};

export { MemoryStorage } from "./stores/memory.js";
export * from "./types.ts";
112 changes: 112 additions & 0 deletions lib/sessionManager/stores/memory.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
import { describe, it, expect, beforeEach } from "vitest";
import { MemoryStorage } from "./memory";
import { StorageKeys } from "../types";

enum ExtraKeys {
testKey = "testKey2",
}

describe("MemoryStorage standard keys", () => {
let sessionManager: MemoryStorage;

beforeEach(() => {
sessionManager = new MemoryStorage();
});

it("should set and get an item in session storage", async () => {
await sessionManager.setSessionItem(StorageKeys.accessToken, "testValue");
expect(await sessionManager.getSessionItem(StorageKeys.accessToken)).toBe(
"testValue",
);
});

it("should remove an item from session storage", async () => {
await sessionManager.setSessionItem(StorageKeys.accessToken, "testValue");
expect(await sessionManager.getSessionItem(StorageKeys.accessToken)).toBe(
"testValue",
);

await sessionManager.removeSessionItem(StorageKeys.accessToken);
expect(
await sessionManager.getSessionItem(StorageKeys.accessToken),
).toBeNull();
});

it("should clear all items from session storage", async () => {
await sessionManager.setSessionItem(StorageKeys.accessToken, "testValue");
expect(await sessionManager.getSessionItem(StorageKeys.accessToken)).toBe(
"testValue",
);

sessionManager.destroySession();
expect(
await sessionManager.getSessionItem(StorageKeys.accessToken),
).toBeNull();
});
});

describe("MemoryStorage keys: storageKeys", () => {
let sessionManager: MemoryStorage<ExtraKeys>;

beforeEach(() => {
sessionManager = new MemoryStorage<ExtraKeys>();
});

it("should set and get an item in storage: StorageKeys", async () => {
await sessionManager.setSessionItem(StorageKeys.accessToken, "testValue");
expect(await sessionManager.getSessionItem(StorageKeys.accessToken)).toBe(
"testValue",
);
});

it("should remove an item from storage: StorageKeys", async () => {
await sessionManager.setSessionItem(StorageKeys.accessToken, "testValue");
expect(await sessionManager.getSessionItem(StorageKeys.accessToken)).toBe(
"testValue",
);

await sessionManager.removeSessionItem(StorageKeys.accessToken);
expect(
await sessionManager.getSessionItem(StorageKeys.accessToken),
).toBeNull();
});

it("should clear all items from storage: StorageKeys", async () => {
await sessionManager.setSessionItem(StorageKeys.accessToken, "testValue");
expect(await sessionManager.getSessionItem(StorageKeys.accessToken)).toBe(
"testValue",
);

sessionManager.destroySession();
expect(
await sessionManager.getSessionItem(StorageKeys.accessToken),
).toBeNull();
});

it("should set and get an item in extra storage", async () => {
await sessionManager.setSessionItem(ExtraKeys.testKey, "testValue");
expect(await sessionManager.getSessionItem(ExtraKeys.testKey)).toBe(
"testValue",
);
});

it("should remove an item from extra storage", async () => {
await sessionManager.setSessionItem(ExtraKeys.testKey, "testValue");
expect(await sessionManager.getSessionItem(ExtraKeys.testKey)).toBe(
"testValue",
);

sessionManager.removeSessionItem(ExtraKeys.testKey);
expect(await sessionManager.getSessionItem(ExtraKeys.testKey)).toBeNull();
});

it("should clear all items from extra storage", async () => {
await sessionManager.setSessionItem(ExtraKeys.testKey, "testValue");
expect(await sessionManager.getSessionItem(ExtraKeys.testKey)).toBe(
"testValue",
);

sessionManager.destroySession();
expect(await sessionManager.getSessionItem(ExtraKeys.testKey)).toBeNull();
});
});
80 changes: 80 additions & 0 deletions lib/sessionManager/stores/memory.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
import { storageSettings } from "../index.js";
import { StorageKeys, type SessionManager } from "../types.js";
import { splitString } from "../utils.js";

/**
* Provides a memory based session manager implementation for the browser.
* @class MemoryStorage
*/
export class MemoryStorage<V = StorageKeys> implements SessionManager<V> {
private memCache: Record<string, unknown> = {};

/**
* Clears all items from session store.
* @returns {void}
*/
async destroySession(): Promise<void> {
this.memCache = {};
}

/**
* Sets the provided key-value store to the memory cache.
* @param {string} itemKey
* @param {unknown} itemValue
* @returns {void}
*/
async setSessionItem(
itemKey: V | StorageKeys,
itemValue: unknown,
): Promise<void> {
if (typeof itemValue === "string") {
splitString(itemValue, storageSettings.maxLength).forEach(
(key, index) => {
this.memCache[`${storageSettings.keyPrefix}${key}${index}`] =
itemValue;
},
);
}
this.memCache[`${storageSettings.keyPrefix}${String(itemKey)}0`] =
itemValue;
}

/**
* Gets the item for the provided key from the memory cache.
* @param {string} itemKey
* @returns {unknown | null}
*/
async getSessionItem(itemKey: V | StorageKeys): Promise<unknown | null> {
if (
this.memCache[`${storageSettings.keyPrefix}${String(itemKey)}0`] ===
undefined
) {
return null;
}

let itemValue = "";
let index = 0;
let key = `${storageSettings.keyPrefix}${String(itemKey)}${index}`;
while (this.memCache[key] !== undefined) {
itemValue += this.memCache[key];
index++;
key = `${storageSettings.keyPrefix}${String(itemKey)}${index}`;
}

return itemValue;
}

/**
* Removes the item for the provided key from the memory cache.
* @param {string} itemKey
* @returns {void}
*/
async removeSessionItem(itemKey: V | StorageKeys): Promise<void> {
// Remove all items with the key prefix
for (const key in this.memCache) {
if (key.startsWith(`${storageSettings.keyPrefix}${String(itemKey)}`)) {
delete this.memCache[key];
}
}
}
}
51 changes: 51 additions & 0 deletions lib/sessionManager/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
/**
* This interfaces provides the contract that an session management utility must
* satisfiy in order to work with this SDK, please vist the example provided in the
* README, to understand how this works.
*/
type Awaitable<T> = Promise<T>;

export enum StorageKeys {
accessToken = "accessToken",
idToken = "idToken",
refreshToken = "refreshToken",
}

export type StorageSettingsType = {
storePassword: string;
keyPrefix: string;
maxLength: number;
};

export interface SessionManager<V = StorageKeys> {
/**
*
* Gets the item for the provided key from the storage.
* @param itemKey
* @returns
*/
getSessionItem: <T = unknown>(
itemKey: V | StorageKeys,
) => Awaitable<T | unknown | null>;
/**
*
* Sets the provided key-value store to the storage.
* @param itemKey
* @param itemValue
*/
setSessionItem: <T = unknown>(
itemKey: V | StorageKeys,
itemValue: T,
) => Awaitable<void>;
/**
*
* Removes the item for the provided key from the storage.
* @param itemKey
*/
removeSessionItem: (itemKey: V | StorageKeys) => Awaitable<void>;
/**
*
* Destroys the session
*/
destroySession: () => Awaitable<void>;
}
47 changes: 47 additions & 0 deletions lib/sessionManager/utils.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
// utils.test.ts
import { splitString } from "./utils";
import { describe, expect, it } from "vitest";

describe("splitString", () => {
it("should split the string into equal parts", () => {
const str = "abcdefghij";
const length = 2;
const result = splitString(str, length);
expect(result).toEqual(["ab", "cd", "ef", "gh", "ij"]);
});

it("should handle strings that are not perfectly divisible by the length", () => {
const str = "abcdefghi";
const length = 3;
const result = splitString(str, length);
expect(result).toEqual(["abc", "def", "ghi"]);
});

it("should handle an empty string", () => {
const str = "";
const length = 3;
const result = splitString(str, length);
expect(result).toEqual([]);
});

it("should handle a length greater than the string length", () => {
const str = "abc";
const length = 5;
const result = splitString(str, length);
expect(result).toEqual(["abc"]);
});

it("should handle a length of 1", () => {
const str = "abc";
const length = 1;
const result = splitString(str, length);
expect(result).toEqual(["a", "b", "c"]);
});

it("should handle a length of 0", () => {
const str = "abc";
const length = 0;
const result = splitString(str, length);
expect(result).toEqual([]);
});
});
10 changes: 10 additions & 0 deletions lib/sessionManager/utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
export function splitString(str: string, length: number): string[] {
if (length <= 0) {
return [];
}
const result = [];
for (let i = 0; i < str.length; i += length) {
result.push(str.slice(i, i + length));
}
return result;
}
25 changes: 11 additions & 14 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,20 +22,17 @@
"main": "dist/js-utils.cjs",
"types": "dist/index.d.ts",
"devDependencies": {
"@eslint/js": "^9.2.0",
"@types/node": "^20.12.7",
"@vitest/coverage-v8": "^1.5.2",
"eslint": "^9.2.0",
"globals": "^15.2.0",
"prettier": "^3.2.5",
"typescript": "^5.4.5",
"typescript-eslint": "^7.9.0",
"vite": "^5.2.10",
"vite-plugin-dts": "^3.9.0",
"vitest": "^1.5.2"
},
"dependencies": {
"jose": "^5.2.4"
"@eslint/js": "^9.9.1",
"@types/node": "^22.5.1",
"@vitest/coverage-v8": "^2.0.5",
"eslint": "^9.9.1",
"globals": "^15.9.0",
"prettier": "^3.3.3",
"typescript": "^5.5.4",
"typescript-eslint": "^8.3.0",
"vite": "^5.4.2",
"vite-plugin-dts": "^4.0.3",
"vitest": "^2.0.5"
},
"packageManager": "[email protected]+sha256.2df78e65d433d7693b9d3fbdaf431b2d96bb4f96a2ffecd51a50efe16e50a6a8"
}
Loading

0 comments on commit 7d906a5

Please sign in to comment.