Skip to content
Open
Changes from 1 commit
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 packages/commons/docs-loader/src/readonly-docs-loader.ts
Original file line number Diff line number Diff line change
Expand Up @@ -563,10 +563,14 @@ const getRoot = async (

const getRootCached = (cacheConfig: Required<CacheConfig>) =>
cache(async (domainKey: string, authState: AuthState, authConfig: AuthEdgeConfig | undefined) => {
const authCacheKey = authState.authed
? `authed:${authState.user?.roles?.sort().join(",") || "noroles"}`
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
? `authed:${authState.user?.roles?.sort().join(",") || "noroles"}`
? `authed:${authState.user?.roles?.slice().sort().join(",") || "noroles"}`

The code mutates the original authState.user.roles array in-place by calling .sort() directly on it. This should create a copy before sorting to avoid mutating shared state.

View Details

Analysis

Array mutation bug in cache key generation mutates authState.user.roles

What fails: getRootCached() in packages/commons/docs-loader/src/readonly-docs-loader.ts:567 mutates authState.user.roles array when generating cache keys by calling .sort() directly on the original array

How to reproduce:

const authState = { authed: true, user: { roles: ["admin", "user", "editor"] } };
// Line 567: authState.user?.roles?.sort().join(",")
console.log(authState.user.roles); // ["admin", "editor", "user"] - mutated!

Result: The same mutated authState object is passed to pruneWithAuthState() for authorization decisions, causing authorization logic to see sorted roles instead of original JWT order

Expected: Authorization should use original role order from JWT; cache key generation should not mutate shared state

Fix: Create array copy before sorting: authState.user?.roles?.slice().sort().join(",")

: "anon";

return await unstable_cache(
(domainKey: string, authState: AuthState, authConfig: AuthEdgeConfig | undefined) =>
getRoot(domainKey, authState, authConfig, cacheConfig),
[domainKey, cacheConfig.cacheKeySuffix],
[domainKey, cacheConfig.cacheKeySuffix, authCacheKey],
{ tags: [domainKey, "getRoot"] }
)(domainKey, authState, authConfig);
});
Expand Down