diff --git a/src/domain/services/index/IncrementalIndexUpdater.ts b/src/domain/services/index/IncrementalIndexUpdater.ts index 2a87428ab..2161e337c 100644 --- a/src/domain/services/index/IncrementalIndexUpdater.ts +++ b/src/domain/services/index/IncrementalIndexUpdater.ts @@ -244,10 +244,6 @@ export default class IncrementalIndexUpdater { const fresh: Record = createNullProto(); // nosemgrep: ts-no-record-string-unknown-outside-adapters -- 0025B; nosemgrep: ts-no-unknown-outside-adapters -- 0025B nodeProps = fresh; shard.set(prop.nodeId, fresh); - } else if (Object.getPrototypeOf(nodeProps) !== null) { - const safeProps = mergeIntoNullProto(nodeProps); - shard.set(prop.nodeId, safeProps); - nodeProps = safeProps; } nodeProps[prop.key] = prop.value; } diff --git a/test/unit/domain/services/IncrementalIndexUpdater.test.ts b/test/unit/domain/services/IncrementalIndexUpdater.test.ts index 4028b925c..60eb98966 100644 --- a/test/unit/domain/services/IncrementalIndexUpdater.test.ts +++ b/test/unit/domain/services/IncrementalIndexUpdater.test.ts @@ -705,6 +705,56 @@ describe('IncrementalIndexUpdater', () => { expect(Reflect.get(Object.getPrototypeOf(aProps), 'polluted')).toBeUndefined(); expect(({} as Record)['polluted']).toBeUndefined(); }); + + it('writes loaded prop bags only after null-prototype normalization', () => { + const state = buildState({ + nodes: ['A'], + edges: [], + props: [{ nodeId: 'A', key: 'name', value: 'Alice' }], + }); + const tree1 = buildTree(state); + const shardKey = computeShardKey('A'); + tree1[`props_${shardKey}.cbor`] = defaultCodec.encode([['A', { name: 'Alice' }]]).slice(); + const capturedPropBags: object[] = []; + const codec = { + encode(data: TEncoded): Uint8Array { + if (Array.isArray(data)) { + for (const entry of data) { + if (Array.isArray(entry) && typeof entry[0] === 'string' && typeof entry[1] === 'object' && entry[1] !== null) { + capturedPropBags.push(entry[1]); + } + } + } + return defaultCodec.encode(data).slice(); + }, + decode(bytes: Uint8Array): TDecoded { + return defaultCodec.decode(bytes); + }, + }; + const diff = { + nodesAdded: [], + nodesRemoved: [], + edgesAdded: [], + edgesRemoved: [], + propsChanged: [{ nodeId: 'A', key: '__proto__', value: { polluted: true }, prevValue: undefined }], + }; + + const updater = new IncrementalIndexUpdater({ codec }); + updater.computeDirtyShards({ + diff, + state, + loadShard: (path) => tree1[path], + }); + + expect(capturedPropBags).toHaveLength(1); + const [capturedPropBag] = capturedPropBags; + if (capturedPropBag === undefined) { + throw new Error('expected one captured prop bag'); + } + expect(Object.getPrototypeOf(capturedPropBag)).toBe(null); + expect(Reflect.get(capturedPropBag, 'name')).toBe('Alice'); + expect(Reflect.get({}, 'polluted')).toBeUndefined(); + }); }); describe('empty diff', () => {