Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
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
5 changes: 5 additions & 0 deletions .changeset/frozen-normalized-scopes.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@okyrychenko-dev/react-action-guard": patch
---

Freeze normalized scope arrays so JavaScript mutation cannot corrupt cached results shared by other consumers. Stable identity, sorting, deduplication, default and empty scope semantics, and bounded cache eviction are preserved.
11 changes: 11 additions & 0 deletions packages/core/src/store/__tests__/scope.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,17 @@ describe("scope", () => {
it("should return the canonical scope list", () => {
expect(normalizeScope(scope)).toEqual(expected);
});

it("should protect cached scopes from JavaScript mutation", () => {
const normalized = normalizeScope(scope);

expect(Reflect.set(normalized, "0", "poisoned")).toBe(false);
expect(Reflect.set(normalized, "length", 0)).toBe(false);
expect(() => Array.prototype.push.call(normalized, "poisoned")).toThrow(TypeError);
expect(Object.isFrozen(normalized)).toBe(true);
expect(normalizeScope(scope)).toBe(normalized);
expect(normalizeScope(scope)).toEqual(expected);
});
});

it("should reuse the canonical list for equivalent scopes", () => {
Expand Down
4 changes: 2 additions & 2 deletions packages/core/src/store/scope/scope.utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ function evictOldestNormalizedScopeCacheEntry(): void {
}

function getCachedNormalizedScope(scopes: ReadonlyArray<string>): ReadonlyArray<string> {
const normalizedScope = [...new Set(scopes)].sort();
const normalizedScope = Object.freeze([...new Set(scopes)].sort());
const cacheKey = getNormalizedScopeCacheKey(normalizedScope);
const cachedScope = normalizedScopeCache.get(cacheKey);

Expand All @@ -52,7 +52,7 @@ function getCachedNormalizedScope(scopes: ReadonlyArray<string>): ReadonlyArray<
}

/**
* Returns a stable, deduplicated, sorted scope list.
* Returns a frozen, stable, deduplicated, sorted scope list.
* An omitted scope means global; an empty list remains empty.
*/
export function normalizeScope(scope?: Scope): ReadonlyArray<string> {
Expand Down
Loading