What
ObjectStackProtocolImplementation.duplicatePackage (packages/metadata-protocol/src/protocol.ts, in the request.organizationId branch of its source-row scan) dedups the scanned sys_metadata rows with a NUL-separated key built from the row's type and name only — literally `${row?.type}` + "" + `${row?.name}` — keeping the org-scoped row over the env-wide one.
That key is (type, name) with no discriminator, so for a type whose identity the spec declares as a pair it collapses two rows that are two different things.
email_template is such a type: EmailTemplateDefinitionSchema states that "multiple rows with the same name but different locale form an i18n bundle" and that a template "is resolved by (name, locale)". #7730 taught the SchemaRegistry that key; #7774 / PR #7931 taught the /meta list merges the same key, with the shared table now in @objectstack/metadata-core (ITEM_KEY_DISCRIMINATORS + itemDiscriminator). This scan was not in either card's declared surface and still keys on the name alone.
Reachability, measured — narrow but real
sys_metadata's overlay uniqueness is (type, name, organization_id, package_id) (idx_sys_metadata_overlay_active, packages/metadata-core/src/objects/sys-metadata.object.ts), and the table has no locale column — the locale lives in the metadata JSON body. So:
- within one org, two rows differing only by body locale cannot exist ⇒ no collapse;
- across the env-wide (
organization_id IS NULL) and org tiers they can ⇒ an env-wide auth.welcome customized in en-US plus an org-scoped auth.welcome customized in zh-CN are two distinct bundle members, and this scan keeps only the org one. The duplicated package then ships one locale of a two-locale customization.
Registry-shipped (code-authored) members are unaffected — this scan reads sys_metadata overlays only, so the exposure is limited to customized templates that were customized at both scopes.
Suggested shape
The same fix PR #7931 applies to the sibling dedup inside getMetaItems: append the canonical-normalized discriminator when the type declares one, and nothing otherwise, so every undiscriminated type keeps a byte-identical key. Both the table and a body-reading helper are already available to this file — itemDiscriminator from @objectstack/metadata-core, and storedRowDiscriminator (local to protocol.ts), which skips the JSON parse entirely for an undiscriminated type.
There is no coverage of this path's dedup today.
Provenance
Found while implementing #7774 (PR #7931). Deliberately not fixed there: different feature, different endpoint, outside that PR's declared file surface. Filed unassigned — nobody is on it.
Generated by Claude Code
What
ObjectStackProtocolImplementation.duplicatePackage(packages/metadata-protocol/src/protocol.ts, in therequest.organizationIdbranch of its source-row scan) dedups the scannedsys_metadatarows with aNUL-separated key built from the row'stypeandnameonly — literally`${row?.type}`+""+`${row?.name}`— keeping the org-scoped row over the env-wide one.That key is
(type, name)with no discriminator, so for a type whose identity the spec declares as a pair it collapses two rows that are two different things.email_templateis such a type:EmailTemplateDefinitionSchemastates that "multiple rows with the samenamebut differentlocaleform an i18n bundle" and that a template "is resolved by(name, locale)". #7730 taught theSchemaRegistrythat key; #7774 / PR #7931 taught the/metalist merges the same key, with the shared table now in@objectstack/metadata-core(ITEM_KEY_DISCRIMINATORS+itemDiscriminator). This scan was not in either card's declared surface and still keys on the name alone.Reachability, measured — narrow but real
sys_metadata's overlay uniqueness is(type, name, organization_id, package_id)(idx_sys_metadata_overlay_active,packages/metadata-core/src/objects/sys-metadata.object.ts), and the table has no locale column — the locale lives in themetadataJSON body. So:organization_id IS NULL) and org tiers they can ⇒ an env-wideauth.welcomecustomized inen-USplus an org-scopedauth.welcomecustomized inzh-CNare two distinct bundle members, and this scan keeps only the org one. The duplicated package then ships one locale of a two-locale customization.Registry-shipped (code-authored) members are unaffected — this scan reads
sys_metadataoverlays only, so the exposure is limited to customized templates that were customized at both scopes.Suggested shape
The same fix PR #7931 applies to the sibling dedup inside
getMetaItems: append the canonical-normalized discriminator when the type declares one, and nothing otherwise, so every undiscriminated type keeps a byte-identical key. Both the table and a body-reading helper are already available to this file —itemDiscriminatorfrom@objectstack/metadata-core, andstoredRowDiscriminator(local toprotocol.ts), which skips the JSON parse entirely for an undiscriminated type.There is no coverage of this path's dedup today.
Provenance
Found while implementing #7774 (PR #7931). Deliberately not fixed there: different feature, different endpoint, outside that PR's declared file surface. Filed unassigned — nobody is on it.
Generated by Claude Code