From 8edcdbcbf92a1565a83000cd6db498aad57d6d19 Mon Sep 17 00:00:00 2001 From: praveen-kumary Date: Thu, 5 Nov 2026 18:03:57 +0530 Subject: [PATCH] fix(framework): guard type predicates against non-object and primitive values Predicates like isAuthoringTypeChoiceDescriptor and isAuthoringTypeConstructorDescriptor previously checked 'kind' in value directly without verifying that value is a non-null object. When encountering primitive values (e.g. the string 'typeChoice' during namespace traversal or preset checks), JavaScript's in operator threw TypeError: Cannot use 'in' operator to search for 'kind' in typeChoice. This adds typeof value === 'object' && value !== null guards across authoring type predicates and recursive namespace helper walks, aligning with the project's type predicate rules. Fixes #30939, #30935 Signed-off-by: praveen-kumary --- .../src/shared/authoring-type-choice.ts | 7 +++- .../src/shared/framework-authoring.ts | 41 +++++++++++++------ .../framework-components.authoring.test.ts | 32 +++++++++++++++ .../psl-parser/src/contributed-type-scope.ts | 4 +- .../contract-ts/src/removed-field-presets.ts | 5 ++- .../test/removed-field-presets.test.ts | 12 ++++++ 6 files changed, 85 insertions(+), 16 deletions(-) diff --git a/packages/1-framework/1-core/framework-components/src/shared/authoring-type-choice.ts b/packages/1-framework/1-core/framework-components/src/shared/authoring-type-choice.ts index bed3a665919c..b4f6365aefba 100644 --- a/packages/1-framework/1-core/framework-components/src/shared/authoring-type-choice.ts +++ b/packages/1-framework/1-core/framework-components/src/shared/authoring-type-choice.ts @@ -231,7 +231,12 @@ function memberNamed( function subNamespace( member: AuthoringTypeNamespaceMember | undefined, ): AuthoringTypeNamespace | undefined { - return member === undefined || isAuthoringTypeDescriptor(member) ? undefined : member; + return member === undefined || + typeof member !== 'object' || + member === null || + isAuthoringTypeDescriptor(member) + ? undefined + : member; } /** The sub-namespace at `path` of a type namespace, or `undefined` when a segment names no sub-namespace. */ diff --git a/packages/1-framework/1-core/framework-components/src/shared/framework-authoring.ts b/packages/1-framework/1-core/framework-components/src/shared/framework-authoring.ts index 21c5f5ad14fd..0ee3c4a56550 100644 --- a/packages/1-framework/1-core/framework-components/src/shared/framework-authoring.ts +++ b/packages/1-framework/1-core/framework-components/src/shared/framework-authoring.ts @@ -608,26 +608,36 @@ function isAuthoringTemplateRecord(value: unknown): value is Record { diff --git a/packages/1-framework/1-core/framework-components/test/framework-components.authoring.test.ts b/packages/1-framework/1-core/framework-components/test/framework-components.authoring.test.ts index 2075f535daef..cacfc9b9a91c 100644 --- a/packages/1-framework/1-core/framework-components/test/framework-components.authoring.test.ts +++ b/packages/1-framework/1-core/framework-components/test/framework-components.authoring.test.ts @@ -9,7 +9,13 @@ import { classifyEnumMemberType, instantiateAuthoringTypeConstructor, isAuthoringArgRef, + isAuthoringEntityTypeDescriptor, + isAuthoringModelAttributeDescriptor, + isAuthoringPslBlockDescriptor, + isAuthoringTypeAliasDescriptor, + isAuthoringTypeChoiceDescriptor, isAuthoringTypeConstructorDescriptor, + isAuthoringTypeDescriptor, resolveAuthoringTemplateValue, validateAuthoringHelperArguments, } from '../src/shared/framework-authoring'; @@ -29,6 +35,32 @@ describe('authoring template resolution', () => { expect(isAuthoringTypeConstructorDescriptor(typeNamespace)).toBe(false); }); + it('safely rejects non-object and primitive values without throwing TypeError', () => { + const nonObjects = [ + 'typeChoice', + 'typeConstructor', + 'typeAlias', + '', + 123, + 0, + true, + false, + Symbol('kind'), + null, + undefined, + ]; + + for (const val of nonObjects) { + expect(isAuthoringTypeConstructorDescriptor(val)).toBe(false); + expect(isAuthoringTypeAliasDescriptor(val)).toBe(false); + expect(isAuthoringTypeChoiceDescriptor(val)).toBe(false); + expect(isAuthoringTypeDescriptor(val)).toBe(false); + expect(isAuthoringEntityTypeDescriptor(val)).toBe(false); + expect(isAuthoringPslBlockDescriptor(val)).toBe(false); + expect(isAuthoringModelAttributeDescriptor(val)).toBe(false); + } + }); + it('rejects arg refs with an invalid index', () => { expect(isAuthoringArgRef({ kind: 'arg', index: 0 })).toBe(true); diff --git a/packages/1-framework/2-authoring/psl-parser/src/contributed-type-scope.ts b/packages/1-framework/2-authoring/psl-parser/src/contributed-type-scope.ts index 07a80f97f4d4..f61319556af3 100644 --- a/packages/1-framework/2-authoring/psl-parser/src/contributed-type-scope.ts +++ b/packages/1-framework/2-authoring/psl-parser/src/contributed-type-scope.ts @@ -67,9 +67,11 @@ export interface ContributedTypeScope { } export function isContributedTypeDescriptor( - value: ContributedTypeDescriptor | ContributedTypeNamespace, + value: unknown, ): value is ContributedTypeDescriptor { return ( + typeof value === 'object' && + value !== null && 'kind' in value && (value.kind === 'typeConstructor' || value.kind === 'typeAlias' || value.kind === 'typeChoice') ); diff --git a/packages/2-sql-family/2-authoring/contract-ts/src/removed-field-presets.ts b/packages/2-sql-family/2-authoring/contract-ts/src/removed-field-presets.ts index 52a686e72866..7afbf8b9d600 100644 --- a/packages/2-sql-family/2-authoring/contract-ts/src/removed-field-presets.ts +++ b/packages/2-sql-family/2-authoring/contract-ts/src/removed-field-presets.ts @@ -175,11 +175,14 @@ function codecIdAt( path: readonly string[], options: Readonly> | undefined, ): string | undefined { + if (typeof types !== 'object' || types === null) return undefined; const [name, ...rest] = path; if (name === undefined || !Object.hasOwn(types, name)) return undefined; const value = types[name]; if (value === undefined) return undefined; - if (!isAuthoringTypeDescriptor(value)) return codecIdAt(value, rest, options); + if (!isAuthoringTypeDescriptor(value)) { + return typeof value === 'object' && value !== null ? codecIdAt(value, rest, options) : undefined; + } if (rest.length > 0) return undefined; if (isAuthoringTypeChoiceDescriptor(value)) { const args = value.args.map((argument) => diff --git a/packages/2-sql-family/2-authoring/contract-ts/test/removed-field-presets.test.ts b/packages/2-sql-family/2-authoring/contract-ts/test/removed-field-presets.test.ts index 358a5a20020a..e73701694943 100644 --- a/packages/2-sql-family/2-authoring/contract-ts/test/removed-field-presets.test.ts +++ b/packages/2-sql-family/2-authoring/contract-ts/test/removed-field-presets.test.ts @@ -248,4 +248,16 @@ describe('a read of a removed field preset from the SQL field helpers', () => { Object.keys(postgresField), ]).toEqual([{ codecId: 'pg/text@1' }, ['column', 'namedType']]); }); + + it('safely tolerates namespaces containing non-object and primitive members', () => { + const typesWithPrimitives = { + ...postgresLike, + invalidPrimitive: 'typeChoice', + invalidNull: null, + invalidUndefined: undefined, + } as unknown as Parameters[1]; + + const testField = refusingRemovedFieldPresets(field, typesWithPrimitives); + expect(refusalOf(testField, 'id.uuidv4String')).toBeDefined(); + }); });