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
Found while landing the four sourced bounds for #12059 (PR #12143). Filed unassigned.
What
packages/spec/src/shared/identifiers.zod.ts declares the platform's identifier
schemas with a .min() and a regex, and no .max():
exportconstSnakeCaseIdentifierSchema=lazySchema(()=>z.string().min(2,{message: 'Identifier must be at least 2 characters'}).regex(/^[a-z][a-z0-9_]*$/,{ … }));
SystemIdentifierSchema and EventNameSchema in the same file have the same
shape — .min() + regex, no upper bound. 20 non-test modules under packages/spec/src/ reference the first two, including PermissionSetSchema .name, PositionSchema.name, AppSchema.name, object/field name schemas via branded-types.zod.ts, and the metadata-type base in shared/metadata-types.zod.ts.
So the spec — the contract layer — accepts an identifier of any length, while
every column that stores one is bounded:
Column
Bound
sys_permission_set.name
100
sys_position.name
100
sys_capability.name
100
sys_metadata.name
255
Why it matters (declared ≠ enforced, pointed the other way)
The usual shape of this class is a declaration the runtime ignores. This is the
inverse: the runtime enforces a bound the declaration never made. Measured on a
real ObjectQL engine over a real SqlDriver, inserting a permission set whose
name is one character over the column's width:
len 101 → ValidationError: API Name must be ≤ 100 characters (got 101)
at validateRecord (packages/objectql/src/validation/record-validator.ts:1058)
len 120 / 255 / 256 / 300 → the same refusal
That refusal is correct behaviour. What is missing is that nothing in the spec
says it will happen: a package author reading PermissionSetSchema sees a
snake_case identifier with a floor of 2 and no ceiling, and learns the real
ceiling only when a write fails at boot — or, for a metadata declaration that is
never materialized, does not learn it at all.
Two consequences observed while sourcing #12059's bounds:
Every identifier bound in the platform is derived by transitivity from a
storage column rather than from the contract. That is what PR fix(plugin-audit,plugin-security): declare sourced bounds on the four keyed text columns that break MySQL schema-sync #12143 had to
do for sys_audience_binding_suggestion.permission_set_name (100, inherited
from sys_permission_set.name) and what sys_user_position.position already
did (100, inherited from sys_position.name). Each such bound is only as
stable as the column it was read off, and nothing links the two.
The bounds disagree with each other — 100 for the three config-object
names, 255 for sys_metadata.name — so an identifier's real ceiling depends
on which table it lands in. A permission set name of 150 characters is a legal
metadata row and an illegal permission set.
Direction (for triage — not prescribing a fix)
Declare a .max() on the shared identifier schemas, chosen to match the
narrowest column that stores one (100 on current evidence), so the contract
states the bound it already enforces; and/or
⛔ Not claimed: that any real declaration is affected today. The longest built-in
permission set name is well under 40 characters and the longest reverse-domain
package id in this repo is 57, so the measured corpus is entirely inside every
bound above. This is a contract-accuracy defect, not a live breakage — which is
why it is filed as a finding rather than a bug.
Related: #12059 (the four keyed text bounds), #11374 (route A).
Found while landing the four sourced bounds for #12059 (PR #12143). Filed unassigned.
What
packages/spec/src/shared/identifiers.zod.tsdeclares the platform's identifierschemas with a
.min()and a regex, and no.max():SystemIdentifierSchemaandEventNameSchemain the same file have the sameshape —
.min()+ regex, no upper bound. 20 non-test modules underpackages/spec/src/reference the first two, includingPermissionSetSchema .name,PositionSchema.name,AppSchema.name, object/field name schemas viabranded-types.zod.ts, and the metadata-type base inshared/metadata-types.zod.ts.So the spec — the contract layer — accepts an identifier of any length, while
every column that stores one is bounded:
sys_permission_set.namesys_position.namesys_capability.namesys_metadata.nameWhy it matters (declared ≠ enforced, pointed the other way)
The usual shape of this class is a declaration the runtime ignores. This is the
inverse: the runtime enforces a bound the declaration never made. Measured on a
real
ObjectQLengine over a realSqlDriver, inserting a permission set whosename is one character over the column's width:
That refusal is correct behaviour. What is missing is that nothing in the spec
says it will happen: a package author reading
PermissionSetSchemasees asnake_case identifier with a floor of 2 and no ceiling, and learns the real
ceiling only when a write fails at boot — or, for a metadata declaration that is
never materialized, does not learn it at all.
Two consequences observed while sourcing #12059's bounds:
storage column rather than from the contract. That is what PR fix(plugin-audit,plugin-security): declare sourced bounds on the four keyed text columns that break MySQL schema-sync #12143 had to
do for
sys_audience_binding_suggestion.permission_set_name(100, inheritedfrom
sys_permission_set.name) and whatsys_user_position.positionalreadydid (100, inherited from
sys_position.name). Each such bound is only asstable as the column it was read off, and nothing links the two.
names, 255 for
sys_metadata.name— so an identifier's real ceiling dependson which table it lands in. A permission set name of 150 characters is a legal
metadata row and an illegal permission set.
Direction (for triage — not prescribing a fix)
.max()on the shared identifier schemas, chosen to match thenarrowest column that stores one (100 on current evidence), so the contract
states the bound it already enforces; and/or
way PR fix(plugin-audit,plugin-security): declare sourced bounds on the four keyed text columns that break MySQL schema-sync #12143's pin reads the referenced column from the same registration
surface rather than restating its width.
⛔ Not claimed: that any real declaration is affected today. The longest built-in
permission set name is well under 40 characters and the longest reverse-domain
package id in this repo is 57, so the measured corpus is entirely inside every
bound above. This is a contract-accuracy defect, not a live breakage — which is
why it is filed as a
findingrather than a bug.Related: #12059 (the four keyed text bounds), #11374 (route A).