Skip to content

Commit 30ee34b

Browse files
feat: add getCookieOptions and removeTrailingSlash utility functions with tests
1 parent 246d2a3 commit 30ee34b

File tree

2 files changed

+205
-0
lines changed

2 files changed

+205
-0
lines changed

lib/utils/getCookieOptions.test.ts

Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
import { describe, it, expect, vi, afterEach } from "vitest";
2+
3+
import {
4+
getCookieOptions,
5+
removeTrailingSlash,
6+
TWENTY_NINE_DAYS,
7+
MAX_COOKIE_LENGTH,
8+
} from "./getCookieOptions";
9+
10+
describe("getCookieOptions", () => {
11+
afterEach(() => {
12+
vi.restoreAllMocks();
13+
});
14+
15+
it("returns the default configuration when env is provided", () => {
16+
const result = getCookieOptions(undefined, {
17+
NODE_ENV: "production",
18+
KINDE_COOKIE_DOMAIN: "example.com/",
19+
});
20+
21+
expect(result).toMatchObject({
22+
maxAge: TWENTY_NINE_DAYS,
23+
domain: "example.com",
24+
maxCookieLength: MAX_COOKIE_LENGTH,
25+
sameSite: "lax",
26+
httpOnly: true,
27+
path: "/",
28+
secure: true,
29+
});
30+
});
31+
32+
it("allows consumers to override default options", () => {
33+
const result = getCookieOptions(
34+
{
35+
secure: false,
36+
sameSite: "none",
37+
path: "/custom",
38+
maxAge: 60,
39+
customOption: "value",
40+
},
41+
{
42+
NODE_ENV: "production",
43+
KINDE_COOKIE_DOMAIN: "example.com",
44+
}
45+
);
46+
47+
expect(result.secure).toBe(false);
48+
expect(result.sameSite).toBe("none");
49+
expect(result.path).toBe("/custom");
50+
expect(result.maxAge).toBe(60);
51+
expect(result.customOption).toBe("value");
52+
});
53+
54+
it("falls back to runtime environment variables when env param is omitted", () => {
55+
const previousNodeEnv = process.env.NODE_ENV;
56+
const previousCookieDomain = process.env.KINDE_COOKIE_DOMAIN;
57+
58+
process.env.NODE_ENV = "production";
59+
process.env.KINDE_COOKIE_DOMAIN = "runtime-domain.io/";
60+
61+
const result = getCookieOptions();
62+
63+
expect(result.domain).toBe("runtime-domain.io");
64+
expect(result.secure).toBe(true);
65+
66+
if (previousNodeEnv === undefined) {
67+
delete process.env.NODE_ENV;
68+
} else {
69+
process.env.NODE_ENV = previousNodeEnv;
70+
}
71+
72+
if (previousCookieDomain === undefined) {
73+
delete process.env.KINDE_COOKIE_DOMAIN;
74+
} else {
75+
process.env.KINDE_COOKIE_DOMAIN = previousCookieDomain;
76+
}
77+
});
78+
79+
it("warns when NODE_ENV is missing and secure option is not provided", () => {
80+
const warnSpy = vi.spyOn(console, "warn").mockImplementation(() => {});
81+
82+
const result = getCookieOptions({}, {});
83+
84+
expect(result.secure).toBe(false);
85+
expect(warnSpy).toHaveBeenCalledWith(
86+
"getCookieOptions: NODE_ENV not set; defaulting secure cookie flag to false. Provide env or override secure to suppress this warning."
87+
);
88+
});
89+
90+
it("warns when KINDE_COOKIE_DOMAIN resolves to an empty string", () => {
91+
const warnSpy = vi.spyOn(console, "warn").mockImplementation(() => {});
92+
93+
const result = getCookieOptions({}, { NODE_ENV: "development", KINDE_COOKIE_DOMAIN: " " });
94+
95+
expect(result.domain).toBeUndefined();
96+
expect(warnSpy).toHaveBeenCalledWith(
97+
"getCookieOptions: KINDE_COOKIE_DOMAIN is empty after trimming and will be ignored."
98+
);
99+
});
100+
});
101+
102+
describe("removeTrailingSlash", () => {
103+
it("removes trailing slashes and trims whitespace", () => {
104+
expect(removeTrailingSlash("example.com/")).toBe("example.com");
105+
expect(removeTrailingSlash(" example.com/ ")).toBe("example.com");
106+
});
107+
108+
it("returns the original string when there is no trailing slash", () => {
109+
expect(removeTrailingSlash("example.com")).toBe("example.com");
110+
});
111+
112+
it("returns undefined for nullish values", () => {
113+
expect(removeTrailingSlash(undefined)).toBeUndefined();
114+
expect(removeTrailingSlash(null)).toBeUndefined();
115+
});
116+
117+
it("returns undefined for whitespace-only strings", () => {
118+
expect(removeTrailingSlash(" ")).toBeUndefined();
119+
});
120+
});

lib/utils/getCookieOptions.ts

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
export interface CookieEnv {
2+
NODE_ENV?: string;
3+
KINDE_COOKIE_DOMAIN?: string;
4+
[key: string]: string | undefined;
5+
}
6+
7+
export type CookieOptionValue = string | number | boolean | undefined | null;
8+
9+
export interface CookieOptions {
10+
maxAge?: number;
11+
domain?: string;
12+
maxCookieLength?: number;
13+
sameSite?: string;
14+
httpOnly?: boolean;
15+
secure?: boolean;
16+
path?: string;
17+
[key: string]: CookieOptionValue;
18+
}
19+
20+
export const TWENTY_NINE_DAYS = 2505600;
21+
export const MAX_COOKIE_LENGTH = 3000;
22+
23+
export const GLOBAL_COOKIE_OPTIONS: CookieOptions = {
24+
sameSite: "lax",
25+
httpOnly: true,
26+
path: "/",
27+
};
28+
29+
const getRuntimeEnv = (): CookieEnv => {
30+
// In browser/react-native bundles process is undefined
31+
if (typeof globalThis === "undefined") {
32+
return {};
33+
}
34+
35+
const maybeProcess = (globalThis as { process?: { env?: CookieEnv } })
36+
.process;
37+
return maybeProcess?.env ?? {};
38+
};
39+
40+
export function removeTrailingSlash(url: string | undefined | null): string | undefined {
41+
if (url === undefined || url === null) return undefined;
42+
43+
url = url.trim();
44+
if (url.length === 0) {
45+
return undefined;
46+
}
47+
48+
if (url.endsWith("/")) {
49+
url = url.slice(0, -1);
50+
}
51+
52+
return url;
53+
}
54+
55+
export const getCookieOptions = (options: CookieOptions = {}, env?: CookieEnv): CookieOptions => {
56+
const resolvedEnv = env ?? getRuntimeEnv();
57+
const rawDomain = resolvedEnv.KINDE_COOKIE_DOMAIN;
58+
const domainFromEnv = removeTrailingSlash(rawDomain);
59+
const secureDefault = resolvedEnv.NODE_ENV === "production";
60+
61+
if (rawDomain && domainFromEnv === undefined && options.domain === undefined) {
62+
console.warn(
63+
"getCookieOptions: KINDE_COOKIE_DOMAIN is empty after trimming and will be ignored."
64+
);
65+
}
66+
67+
const merged: CookieOptions = {
68+
maxAge: TWENTY_NINE_DAYS,
69+
domain: domainFromEnv,
70+
maxCookieLength: MAX_COOKIE_LENGTH,
71+
...GLOBAL_COOKIE_OPTIONS,
72+
...options,
73+
};
74+
75+
if (options.secure === undefined) {
76+
merged.secure = secureDefault;
77+
if (resolvedEnv.NODE_ENV === undefined) {
78+
console.warn(
79+
"getCookieOptions: NODE_ENV not set; defaulting secure cookie flag to false. Provide env or override secure to suppress this warning."
80+
);
81+
}
82+
}
83+
84+
return merged;
85+
};

0 commit comments

Comments
 (0)