Found while implementing objectui#6489 (PR #6520), which closed the same construction defect in the sibling page MetadataFieldsPage. Filed rather than folded in: different function, different write path, and #6489's dispatch fenced its file face to the fields map construction.
The code
packages/plugin-designer/src/MetadataObjectsPage.tsx, handleObjectsChange:
constnextByName: Record<string,ObjectDefinition>={};for(constoofnext)nextByName[o.name]=o;consterrors: string[]=[];// Deletionsfor(constnameofObject.keys(prev)){if(!nextByName[name]){try{awaitclient.reset('object',name);}catch(err){…}}}Two prototype-reachable reads in one loop, and the consequential one is the read, not the write.
The hazard: !nextByName[name] answers out of Object.prototype
nextByName is a plain object literal, so a lookup for any name that exists on Object.prototype returns a truthy inherited value even when the designer's new list does not contain that object. Measured:
object named `constructor` reads as present: true => delete detected: false
object named `__proto__` reads as present: true => delete detected: false
So deleting such an object from the Object Manager issues noclient.reset('object', …) at all. The row disappears from the UI, the save reports success, and the object is still there after a reload — a silent no-op rather than a refusal.
Both names are legal object names. Measured against the installed @objectstack/spec 17.2.0:
ObjectSchema.safeParse({ name: 'constructor', label: 'C', fields: {} }) => success = true
ObjectSchema.safeParse({ name: '__proto__', label: 'P', fields: {} }) => success = true
constructor is the realistic one — it is an ordinary lowercase identifier a business object could plausibly be called, with none of __proto__'s "obviously exotic" look.
The same two secondary hazards #6489 measured
nextByName[o.name] = o for a nameless object keys as the literal string "undefined" (this map is a local lookup rather than a serialised body, so the impact here is misrouted delete detection, not a corrupt document).nextByName['__proto__'] = o invokes the prototype setter rather than creating a key. That one is self-cancelling for this loop — the later read finds the value on the prototype — which is exactly why it hides.
Suggested shape
The fix objectui#6240 and objectui#6489 both landed: build through Object.fromEntries and read the lookup as an own property (Object.prototype.hasOwnProperty.call(…)), or use a Map, which has neither hazard and is the natural fit for a lookup that is never serialised. Whether the nameless/duplicate cases should also be refused here is a triage question — this page's writes are per-object rather than one map, so the #6489 refusals do not port across unchanged.
Refs: objectui#6489 (the same construction in the sibling page, fixed in PR #6520) · objectui#6240 (the app-shell writer) · objectui#5761 (the parity family)
Generated by Claude Code
Found while implementing objectui#6489 (PR #6520), which closed the same construction defect in the sibling page
MetadataFieldsPage. Filed rather than folded in: different function, different write path, and #6489's dispatch fenced its file face to thefieldsmap construction.The code
packages/plugin-designer/src/MetadataObjectsPage.tsx,handleObjectsChange:Two prototype-reachable reads in one loop, and the consequential one is the read, not the write.
The hazard:
!nextByName[name]answers out ofObject.prototypenextByNameis a plain object literal, so a lookup for any name that exists onObject.prototypereturns a truthy inherited value even when the designer's new list does not contain that object. Measured:So deleting such an object from the Object Manager issues no
client.reset('object', …)at all. The row disappears from the UI, the save reports success, and the object is still there after a reload — a silent no-op rather than a refusal.Both names are legal object names. Measured against the installed
@objectstack/spec17.2.0:constructoris the realistic one — it is an ordinary lowercase identifier a business object could plausibly be called, with none of__proto__'s "obviously exotic" look.The same two secondary hazards #6489 measured
nextByName[o.name] = ofor a nameless object keys as the literal string"undefined"(this map is a local lookup rather than a serialised body, so the impact here is misrouted delete detection, not a corrupt document).nextByName['__proto__'] = oinvokes the prototype setter rather than creating a key. That one is self-cancelling for this loop — the later read finds the value on the prototype — which is exactly why it hides.Suggested shape
The fix objectui#6240 and objectui#6489 both landed: build through
Object.fromEntriesand read the lookup as an own property (Object.prototype.hasOwnProperty.call(…)), or use aMap, which has neither hazard and is the natural fit for a lookup that is never serialised. Whether the nameless/duplicate cases should also be refused here is a triage question — this page's writes are per-object rather than one map, so the #6489 refusals do not port across unchanged.Refs: objectui#6489 (the same construction in the sibling page, fixed in PR #6520) · objectui#6240 (the app-shell writer) · objectui#5761 (the parity family)
Generated by Claude Code