You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
meta-plural-url-bypass: PUT /meta/fields/<name> walks around the whole two-tier registry gate — 4 registry types have no entry in PLURAL_TO_SINGULAR #7894
canonicalMetaType — the ONE canonical spelling of a metadata type at the /meta read/write/delete boundary (#4432) — folds plural→singular through PLURAL_TO_SINGULAR. That map is a manifest collection map (objects: [...], apps: [...]), and it is missing entries for four registry types. For those, the plural URL spelling is not folded: it is treated as an unregistered plugin type, which every authorization gate treats as permissive by construction.
Result: the plural URL is a door around the singular URL's lock.
Measured, live (booted showcase, admin bearer, on top of #7743's fix)
PUT /api/v1/meta/field/showcase_task.title → 403 NOT_OVERRIDABLE ← #7743's fix, correct
PUT /api/v1/meta/fields/showcase_task.title → 200 {"success":true,"state":"active",
"message":"Saved fields 'showcase_task.title' …"}
Note the receipt: Saved fields. The row is persisted with type='fields' — a second namespace for the same item, which is precisely the defect class #4432 was filed about ("one plural-spelled read minted a plural registry entry … one overlay row shadowed an entire code-authored listing, and survived the DELETE that was supposed to lift it").
Why the gates let it through
For an unmapped spelling 'fields':
isOverlayAllowed('fields') → false
isRuntimeCreateAllowed('fields') → true — no static registry entry, so getMetaTypes() synthesises allowRuntimeCreate: true
orgScopedWriteRefusal → returns null, explicitly: "Statically-declared types only… a type with no entry in DEFAULT_METADATA_TYPE_REGISTRY is plugin-registered at runtime"
Every one of those is correct behaviour for a genuinely plugin-registered type. The bug is that 'fields' is not one — it is a registry type wearing a spelling the boundary map does not know.
Blast radius (measured against DEFAULT_METADATA_TYPE_REGISTRY at bcea363)
Registry types with no singular target anywhere in PLURAL_TO_SINGULAR:
Plus email_template, reachable only as camelCase emailTemplates; the REST-shaped /meta/email_templates is unmapped. (allowOrgOverride: true, so no lock to bypass.)
So the one type where this is a live authorization hole today is field, which is why it surfaced under #7743.
It is a different defect with a wider remedy, and the narrow patch is the wrong shape. Two candidate fixes, both needing a decision:
Add the missing keys to PLURAL_TO_SINGULAR.⚠️ Careful: that map is also iterated by metadata-authoring-lint.ts to emit "did you mean" hints for stack-level manifest collections. Adding fields: there would advertise a top-level fields: [...] collection that does not exist and collides conceptually with ObjectSchema.fields.
Split the two roles — a manifest-collection map and a URL-spelling map are two different contracts that happen to overlap. canonicalMetaType would read the URL one, derived from DEFAULT_METADATA_TYPE_REGISTRY so a new type cannot arrive unmapped (Prime Directive Convert to monorepo with scoped packages #8: registry-derived, never a hand-written list).
A third option worth pricing: refuse an unknown plural at the boundary rather than silently treating it as a plugin type — an unmapped spelling of a declared type is a caller error, and answering 200 to it is how the second namespace gets minted.
⛔ Do not fix this by teaching isNestedArtifactField (or any other predicate) to accept 'fields' — that is a spelling-tolerant lookup one layer below the boundary, which is the exact pattern #4432's doc comment rejects, and it would still persist the row under type='fields'.
Repro
Boot the showcase with writable runtime packages; obtain an admin bearer.
PUT /api/v1/meta/fields/showcase_task.title with the same body → 200.
SELECT type, name FROM sys_metadata → a row with type='fields'.
Source
Found while writing #7743's route-level pin: the plural case was written expecting a 403 and measured 200, then confirmed against the live showcase. Not present in #7743's original report.
Summary
canonicalMetaType— the ONE canonical spelling of a metadata type at the/metaread/write/delete boundary (#4432) — folds plural→singular throughPLURAL_TO_SINGULAR. That map is a manifest collection map (objects: [...],apps: [...]), and it is missing entries for four registry types. For those, the plural URL spelling is not folded: it is treated as an unregistered plugin type, which every authorization gate treats as permissive by construction.Result: the plural URL is a door around the singular URL's lock.
Measured, live (booted showcase, admin bearer, on top of #7743's fix)
Note the receipt:
Saved fields. The row is persisted withtype='fields'— a second namespace for the same item, which is precisely the defect class #4432 was filed about ("one plural-spelled read minted a plural registry entry … one overlay row shadowed an entire code-authored listing, and survived the DELETE that was supposed to lift it").Why the gates let it through
For an unmapped spelling
'fields':isOverlayAllowed('fields')→falseisRuntimeCreateAllowed('fields')→true— no static registry entry, sogetMetaTypes()synthesisesallowRuntimeCreate: trueorgScopedWriteRefusal→ returnsnull, explicitly: "Statically-declared types only… a type with no entry inDEFAULT_METADATA_TYPE_REGISTRYis plugin-registered at runtime"SysMetadataRepository.assertAllowed('fields', 'runtime-only')→!STATIC_REGISTRY_TYPES.has('fields')→ returns, allowedEvery one of those is correct behaviour for a genuinely plugin-registered type. The bug is that
'fields'is not one — it is a registry type wearing a spelling the boundary map does not know.Blast radius (measured against
DEFAULT_METADATA_TYPE_REGISTRYatbcea363)Registry types with no singular target anywhere in
PLURAL_TO_SINGULAR:allowOrgOverrideallowRuntimeCreatefieldseedexternal_catalogtranslationPlus
email_template, reachable only as camelCaseemailTemplates; the REST-shaped/meta/email_templatesis unmapped. (allowOrgOverride: true, so no lock to bypass.)So the one type where this is a live authorization hole today is
field, which is why it surfaced under #7743.Why #7743 did not fix it
It is a different defect with a wider remedy, and the narrow patch is the wrong shape. Two candidate fixes, both needing a decision:
PLURAL_TO_SINGULAR.metadata-authoring-lint.tsto emit "did you mean" hints for stack-level manifest collections. Addingfields:there would advertise a top-levelfields: [...]collection that does not exist and collides conceptually withObjectSchema.fields.canonicalMetaTypewould read the URL one, derived fromDEFAULT_METADATA_TYPE_REGISTRYso a new type cannot arrive unmapped (Prime Directive Convert to monorepo with scoped packages #8: registry-derived, never a hand-written list).A third option worth pricing: refuse an unknown plural at the boundary rather than silently treating it as a plugin type — an unmapped spelling of a declared type is a caller error, and answering 200 to it is how the second namespace gets minted.
⛔ Do not fix this by teaching
isNestedArtifactField(or any other predicate) to accept'fields'— that is a spelling-tolerant lookup one layer below the boundary, which is the exact pattern #4432's doc comment rejects, and it would still persist the row undertype='fields'.Repro
PUT /api/v1/meta/field/showcase_task.title {name:'title', label:'X', type:'text'}→ 403 (with org-override-registry-gate: thefieldoverlay lock is not enforced — an artifact-backed field PUT is accepted 200 (and is inert) #7743's fix).PUT /api/v1/meta/fields/showcase_task.titlewith the same body → 200.SELECT type, name FROM sys_metadata→ a row withtype='fields'.Source
Found while writing #7743's route-level pin: the plural case was written expecting a 403 and measured 200, then confirmed against the live showcase. Not present in #7743's original report.