Skip to content

Commit 149d99c

Browse files
committed
feat: add memory storage manager
1 parent ca6c240 commit 149d99c

File tree

9 files changed

+345
-5
lines changed

9 files changed

+345
-5
lines changed

lib/index.ts

+2-1
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,3 @@
1+
export type * from "./types";
12
export * from "./main";
2-
export * from "./types";
3+
export * from "./sessionManager";

lib/main.test.ts

+5-4
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,8 @@ import {
44
sanitizeRedirect,
55
mapLoginMethodParamsForUrl,
66
generateAuthUrl,
7-
Scopes,
8-
IssuerRouteTypes,
97
} from "./index";
8+
import { IssuerRouteTypes, Scopes } from "./types";
109
import type { LoginMethodParams, LoginOptions } from "./index";
1110

1211
describe("base64UrlEncode", () => {
@@ -75,9 +74,10 @@ describe("generateAuthUrl", () => {
7574
redirectURL: "https://example.com",
7675
audience: "audience123",
7776
prompt: "login",
77+
state: "state123",
7878
};
7979
const expectedUrl =
80-
"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";
80+
"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";
8181

8282
const result = generateAuthUrl(domain, IssuerRouteTypes.login, options);
8383
expect(result.toString()).toBe(expectedUrl);
@@ -109,9 +109,10 @@ describe("generateAuthUrl", () => {
109109
scope: [Scopes.openid, Scopes.profile, Scopes.offline_access],
110110
redirectURL: "https://example2.com",
111111
prompt: "create",
112+
state: "state123",
112113
};
113114
const expectedUrl =
114-
"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";
115+
"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";
115116

116117
const result = generateAuthUrl(domain, IssuerRouteTypes.login, options);
117118
expect(result.toString()).toBe(expectedUrl);

lib/sessionManager/index.ts

+25
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
type StorageSettingsType = {
2+
storePassword: string;
3+
keyPrefix: string;
4+
maxLength: number;
5+
};
6+
7+
export const storageSettings: StorageSettingsType = {
8+
/**
9+
* The password to encrypt the store. (cookies only)
10+
*/
11+
storePassword: "",
12+
/**
13+
* The prefix to use for the storage keys.
14+
*/
15+
keyPrefix: "kinde-",
16+
/**
17+
* The maximum length of the storage.
18+
*
19+
* If the length is exceeded the items will be split into multiple storage items.
20+
*/
21+
maxLength: 2000,
22+
};
23+
24+
export { MemoryStorage } from "./stores/memory.js";
25+
export * from "./types.ts";
+112
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
import { describe, it, expect, beforeEach } from "vitest";
2+
import { MemoryStorage } from "./memory";
3+
import { StorageKeys } from "../types";
4+
5+
enum ExtraKeys {
6+
testKey = "testKey2",
7+
}
8+
9+
describe("MemoryStorage standard keys", () => {
10+
let sessionManager: MemoryStorage;
11+
12+
beforeEach(() => {
13+
sessionManager = new MemoryStorage();
14+
});
15+
16+
it("should set and get an item in session storage", async () => {
17+
await sessionManager.setSessionItem(StorageKeys.accessToken, "testValue");
18+
expect(await sessionManager.getSessionItem(StorageKeys.accessToken)).toBe(
19+
"testValue",
20+
);
21+
});
22+
23+
it("should remove an item from session storage", async () => {
24+
await sessionManager.setSessionItem(StorageKeys.accessToken, "testValue");
25+
expect(await sessionManager.getSessionItem(StorageKeys.accessToken)).toBe(
26+
"testValue",
27+
);
28+
29+
await sessionManager.removeSessionItem(StorageKeys.accessToken);
30+
expect(
31+
await sessionManager.getSessionItem(StorageKeys.accessToken),
32+
).toBeNull();
33+
});
34+
35+
it("should clear all items from session storage", async () => {
36+
await sessionManager.setSessionItem(StorageKeys.accessToken, "testValue");
37+
expect(await sessionManager.getSessionItem(StorageKeys.accessToken)).toBe(
38+
"testValue",
39+
);
40+
41+
sessionManager.destroySession();
42+
expect(
43+
await sessionManager.getSessionItem(StorageKeys.accessToken),
44+
).toBeNull();
45+
});
46+
});
47+
48+
describe("MemoryStorage keys: storageKeys", () => {
49+
let sessionManager: MemoryStorage<ExtraKeys>;
50+
51+
beforeEach(() => {
52+
sessionManager = new MemoryStorage<ExtraKeys>();
53+
});
54+
55+
it("should set and get an item in storage: StorageKeys", async () => {
56+
await sessionManager.setSessionItem(StorageKeys.accessToken, "testValue");
57+
expect(await sessionManager.getSessionItem(StorageKeys.accessToken)).toBe(
58+
"testValue",
59+
);
60+
});
61+
62+
it("should remove an item from storage: StorageKeys", async () => {
63+
await sessionManager.setSessionItem(StorageKeys.accessToken, "testValue");
64+
expect(await sessionManager.getSessionItem(StorageKeys.accessToken)).toBe(
65+
"testValue",
66+
);
67+
68+
await sessionManager.removeSessionItem(StorageKeys.accessToken);
69+
expect(
70+
await sessionManager.getSessionItem(StorageKeys.accessToken),
71+
).toBeNull();
72+
});
73+
74+
it("should clear all items from storage: StorageKeys", async () => {
75+
await sessionManager.setSessionItem(StorageKeys.accessToken, "testValue");
76+
expect(await sessionManager.getSessionItem(StorageKeys.accessToken)).toBe(
77+
"testValue",
78+
);
79+
80+
sessionManager.destroySession();
81+
expect(
82+
await sessionManager.getSessionItem(StorageKeys.accessToken),
83+
).toBeNull();
84+
});
85+
86+
it("should set and get an item in extra storage", async () => {
87+
await sessionManager.setSessionItem(ExtraKeys.testKey, "testValue");
88+
expect(await sessionManager.getSessionItem(ExtraKeys.testKey)).toBe(
89+
"testValue",
90+
);
91+
});
92+
93+
it("should remove an item from extra storage", async () => {
94+
await sessionManager.setSessionItem(ExtraKeys.testKey, "testValue");
95+
expect(await sessionManager.getSessionItem(ExtraKeys.testKey)).toBe(
96+
"testValue",
97+
);
98+
99+
sessionManager.removeSessionItem(ExtraKeys.testKey);
100+
expect(await sessionManager.getSessionItem(ExtraKeys.testKey)).toBeNull();
101+
});
102+
103+
it("should clear all items from extra storage", async () => {
104+
await sessionManager.setSessionItem(ExtraKeys.testKey, "testValue");
105+
expect(await sessionManager.getSessionItem(ExtraKeys.testKey)).toBe(
106+
"testValue",
107+
);
108+
109+
sessionManager.destroySession();
110+
expect(await sessionManager.getSessionItem(ExtraKeys.testKey)).toBeNull();
111+
});
112+
});

lib/sessionManager/stores/memory.ts

+80
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
import { storageSettings } from "../index.js";
2+
import { StorageKeys, type SessionManager } from "../types.js";
3+
import { splitString } from "../utils.js";
4+
5+
/**
6+
* Provides a memory based session manager implementation for the browser.
7+
* @class MemoryStorage
8+
*/
9+
export class MemoryStorage<V = StorageKeys> implements SessionManager<V> {
10+
private memCache: Record<string, unknown> = {};
11+
12+
/**
13+
* Clears all items from session store.
14+
* @returns {void}
15+
*/
16+
async destroySession(): Promise<void> {
17+
this.memCache = {};
18+
}
19+
20+
/**
21+
* Sets the provided key-value store to the memory cache.
22+
* @param {string} itemKey
23+
* @param {unknown} itemValue
24+
* @returns {void}
25+
*/
26+
async setSessionItem(
27+
itemKey: V | StorageKeys,
28+
itemValue: unknown,
29+
): Promise<void> {
30+
if (typeof itemValue === "string") {
31+
splitString(itemValue, storageSettings.maxLength).forEach(
32+
(key, index) => {
33+
this.memCache[`${storageSettings.keyPrefix}${key}${index}`] =
34+
itemValue;
35+
},
36+
);
37+
}
38+
this.memCache[`${storageSettings.keyPrefix}${String(itemKey)}0`] =
39+
itemValue;
40+
}
41+
42+
/**
43+
* Gets the item for the provided key from the memory cache.
44+
* @param {string} itemKey
45+
* @returns {unknown | null}
46+
*/
47+
async getSessionItem(itemKey: V | StorageKeys): Promise<unknown | null> {
48+
if (
49+
this.memCache[`${storageSettings.keyPrefix}${String(itemKey)}0`] ===
50+
undefined
51+
) {
52+
return null;
53+
}
54+
55+
let itemValue = "";
56+
let index = 0;
57+
let key = `${storageSettings.keyPrefix}${String(itemKey)}${index}`;
58+
while (this.memCache[key] !== undefined) {
59+
itemValue += this.memCache[key];
60+
index++;
61+
key = `${storageSettings.keyPrefix}${String(itemKey)}${index}`;
62+
}
63+
64+
return itemValue;
65+
}
66+
67+
/**
68+
* Removes the item for the provided key from the memory cache.
69+
* @param {string} itemKey
70+
* @returns {void}
71+
*/
72+
async removeSessionItem(itemKey: V | StorageKeys): Promise<void> {
73+
// Remove all items with the key prefix
74+
for (const key in this.memCache) {
75+
if (key.startsWith(`${storageSettings.keyPrefix}${String(itemKey)}`)) {
76+
delete this.memCache[key];
77+
}
78+
}
79+
}
80+
}

lib/sessionManager/types.ts

+51
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
/**
2+
* This interfaces provides the contract that an session management utility must
3+
* satisfiy in order to work with this SDK, please vist the example provided in the
4+
* README, to understand how this works.
5+
*/
6+
type Awaitable<T> = Promise<T>;
7+
8+
export enum StorageKeys {
9+
accessToken = "accessToken",
10+
idToken = "idToken",
11+
refreshToken = "refreshToken",
12+
}
13+
14+
export type StorageSettingsType = {
15+
storePassword: string;
16+
keyPrefix: string;
17+
maxLength: number;
18+
};
19+
20+
export interface SessionManager<V = StorageKeys> {
21+
/**
22+
*
23+
* Gets the item for the provided key from the storage.
24+
* @param itemKey
25+
* @returns
26+
*/
27+
getSessionItem: <T = unknown>(
28+
itemKey: V | StorageKeys,
29+
) => Awaitable<T | unknown | null>;
30+
/**
31+
*
32+
* Sets the provided key-value store to the storage.
33+
* @param itemKey
34+
* @param itemValue
35+
*/
36+
setSessionItem: <T = unknown>(
37+
itemKey: V | StorageKeys,
38+
itemValue: T,
39+
) => Awaitable<void>;
40+
/**
41+
*
42+
* Removes the item for the provided key from the storage.
43+
* @param itemKey
44+
*/
45+
removeSessionItem: (itemKey: V | StorageKeys) => Awaitable<void>;
46+
/**
47+
*
48+
* Destroys the session
49+
*/
50+
destroySession: () => Awaitable<void>;
51+
}

lib/sessionManager/utils.test.ts

+47
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
// utils.test.ts
2+
import { splitString } from "./utils";
3+
import { describe, expect, it } from "vitest";
4+
5+
describe("splitString", () => {
6+
it("should split the string into equal parts", () => {
7+
const str = "abcdefghij";
8+
const length = 2;
9+
const result = splitString(str, length);
10+
expect(result).toEqual(["ab", "cd", "ef", "gh", "ij"]);
11+
});
12+
13+
it("should handle strings that are not perfectly divisible by the length", () => {
14+
const str = "abcdefghi";
15+
const length = 3;
16+
const result = splitString(str, length);
17+
expect(result).toEqual(["abc", "def", "ghi"]);
18+
});
19+
20+
it("should handle an empty string", () => {
21+
const str = "";
22+
const length = 3;
23+
const result = splitString(str, length);
24+
expect(result).toEqual([]);
25+
});
26+
27+
it("should handle a length greater than the string length", () => {
28+
const str = "abc";
29+
const length = 5;
30+
const result = splitString(str, length);
31+
expect(result).toEqual(["abc"]);
32+
});
33+
34+
it("should handle a length of 1", () => {
35+
const str = "abc";
36+
const length = 1;
37+
const result = splitString(str, length);
38+
expect(result).toEqual(["a", "b", "c"]);
39+
});
40+
41+
it("should handle a length of 0", () => {
42+
const str = "abc";
43+
const length = 0;
44+
const result = splitString(str, length);
45+
expect(result).toEqual([]);
46+
});
47+
});

lib/sessionManager/utils.ts

+10
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
export function splitString(str: string, length: number): string[] {
2+
if (length <= 0) {
3+
return [];
4+
}
5+
const result = [];
6+
for (let i = 0; i < str.length; i += length) {
7+
result.push(str.slice(i, i + length));
8+
}
9+
return result;
10+
}

readme.md

+13
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,19 @@ pnpm install @kinde/js-utils
2727

2828
`generateAuthUrl` - builds a authentication URL to redirect users to start auth flow
2929

30+
### Session Managers
31+
32+
exports `storageSettings` which can be used to configure the storage methods.
33+
34+
```json
35+
{
36+
keyPrefix: "kinde-", // Prefix to be used on all storage items
37+
maxLength: 2000, // Max length of storage block, will auto split into separate chunks if needed
38+
}
39+
```
40+
41+
`MemoryStorage` - This holds the data in a simple memory store
42+
3043
## Kinde documentation
3144

3245
[Kinde Documentation](https://kinde.com/docs/) - Explore the Kinde docs

0 commit comments

Comments
 (0)