From 476a8544b3b77bdcb6fb62815cfaaad9f8109501 Mon Sep 17 00:00:00 2001 From: Olexii Kyrychenko Date: Sun, 4 Oct 2026 14:47:40 +0300 Subject: [PATCH] fix(core): freeze cached normalized scopes (local ticket 06) --- .changeset/frozen-normalized-scopes.md | 5 +++++ packages/core/src/store/__tests__/scope.test.ts | 11 +++++++++++ packages/core/src/store/scope/scope.utils.ts | 4 ++-- 3 files changed, 18 insertions(+), 2 deletions(-) create mode 100644 .changeset/frozen-normalized-scopes.md diff --git a/.changeset/frozen-normalized-scopes.md b/.changeset/frozen-normalized-scopes.md new file mode 100644 index 0000000..1dd3c1b --- /dev/null +++ b/.changeset/frozen-normalized-scopes.md @@ -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. diff --git a/packages/core/src/store/__tests__/scope.test.ts b/packages/core/src/store/__tests__/scope.test.ts index e984948..4bad105 100644 --- a/packages/core/src/store/__tests__/scope.test.ts +++ b/packages/core/src/store/__tests__/scope.test.ts @@ -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", () => { diff --git a/packages/core/src/store/scope/scope.utils.ts b/packages/core/src/store/scope/scope.utils.ts index 928b3b9..f828273 100644 --- a/packages/core/src/store/scope/scope.utils.ts +++ b/packages/core/src/store/scope/scope.utils.ts @@ -34,7 +34,7 @@ function evictOldestNormalizedScopeCacheEntry(): void { } function getCachedNormalizedScope(scopes: ReadonlyArray): ReadonlyArray { - const normalizedScope = [...new Set(scopes)].sort(); + const normalizedScope = Object.freeze([...new Set(scopes)].sort()); const cacheKey = getNormalizedScopeCacheKey(normalizedScope); const cachedScope = normalizedScopeCache.get(cacheKey); @@ -52,7 +52,7 @@ function getCachedNormalizedScope(scopes: ReadonlyArray): 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 {