fix(core): guard filter-token near-miss lookup against Object.prototype (objectui#9129) - #9396
Conversation
resolveContextTokens indexed the spec's near-miss suggestion map with a plain bracket index, so a lower-cased token that happens to be an inherited Object.prototype member name (constructor, __proto__) resolved to that member instead of undefined, and the warning asserted a "suggestion" that was native-code / object text rather than a real token. The lookup is read-only and the resolved value is always passed through untouched, so no filter widens or narrows and no record is mis-matched by this -- the only observable effect was a confusing console.warn string. Fix: build the lookup over a null-prototype copy of the suggestion map instead of special-casing the two known spellings, closing the whole class of collisions (any inherited member, present or future). Pin asserts silence for `constructor` / `__proto__` plus a lit near-miss control, beside the existing near-miss cases. Part of #9129 Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011QreXiyMEqKLN4U5daMPVa
|
| Step | Outcome |
|---|---|
| Build packages | success |
| Check console performance budget | failure |
Which half objected:
| Eager-closure half | Verdict |
|---|---|
| Aggregate closure ceiling | ✅ pass |
| Per-chunk ceilings | ✅ pass |
| Ceiling sensitivity (headroom) | |
| Ceiling freshness (checkout vs. base branch) | ✅ pass |
⚠️ A broken gauge half is a verdict about the ceiling, not about the bundle: that line has drifted out of range of the regression it exists to catch, or the report behind it cannot be trusted. It does not say anything grew. TheCheck console performance budgetstep log carries the ceiling and the number it was compared against.
Reason: The entry chunk measured 144.3 KB, but the eager-closure half of this gate returned no trustworthy VERDICT: the report could not be read, a ceiling has drifted out of range of the regression it must catch, or (objectui#6245) a ceiling was replaced on the base branch after this checkout was made. The step log says which. This is not a passing budget — and it is not a size regression either.
See the workflow run for details.
📦 Bundle Size Report
| Package | Size | Gzipped |
|---|---|---|
| app-shell (consoleActionDispatch.js) | 0.20KB | 0.19KB |
| app-shell (index.js) | 16.69KB | 6.21KB |
| app-shell (runtime-config.js) | 20.68KB | 7.36KB |
| app-shell (types.js) | 0.01KB | 0.04KB |
| app-shell (urlParams.js) | 10.06KB | 3.86KB |
| auth (ActiveOrganizationStorage.js) | 25.05KB | 9.16KB |
| auth (AuthContext.js) | 0.31KB | 0.24KB |
| auth (AuthGuard.js) | 2.07KB | 1.00KB |
| auth (AuthProvider.js) | 40.18KB | 10.59KB |
| auth (AuthShell.js) | 3.49KB | 1.40KB |
| auth (ForgotPasswordForm.js) | 12.21KB | 3.45KB |
| auth (LoginForm.js) | 18.15KB | 5.39KB |
| auth (PreviewBanner.js) | 0.90KB | 0.50KB |
| auth (RegisterForm.js) | 6.65KB | 2.22KB |
| auth (SocialSignInButtons.js) | 9.61KB | 3.89KB |
| auth (UserMenu.js) | 3.41KB | 1.23KB |
| auth (auth-gate-events.js) | 1.29KB | 0.66KB |
| auth (authStyles.js) | 5.04KB | 1.72KB |
| auth (createAuthClient.js) | 40.21KB | 10.80KB |
| auth (createAuthenticatedFetch.js) | 8.46KB | 3.43KB |
| auth (index.js) | 3.19KB | 1.44KB |
| auth (invitation-status.js) | 1.22KB | 0.70KB |
| auth (org-roles.js) | 6.66KB | 2.78KB |
| auth (phone-identifier.js) | 1.11KB | 0.66KB |
| auth (types.js) | 0.59KB | 0.35KB |
| auth (useAuth.js) | 5.30KB | 1.02KB |
| auth (useWorkspaceAdminStatus.js) | 11.08KB | 4.58KB |
| collaboration (CommentThread.js) | 26.08KB | 7.56KB |
| collaboration (LiveCursors.js) | 3.17KB | 1.27KB |
| collaboration (PresenceAvatars.js) | 6.49KB | 2.64KB |
| collaboration (PresenceProvider.js) | 2.79KB | 1.13KB |
| collaboration (index.js) | 1.68KB | 0.73KB |
| collaboration (useCollaborationTranslation.js) | 6.05KB | 2.52KB |
| collaboration (useCommentSearch.js) | 1.98KB | 0.88KB |
| collaboration (useConflictResolution.js) | 7.75KB | 1.86KB |
| collaboration (useMentionNotifications.js) | 1.81KB | 0.68KB |
| collaboration (usePresence.js) | 6.33KB | 1.84KB |
| collaboration (useRealtimeSubscription.js) | 7.91KB | 2.01KB |
| components (index.js) | 502.02KB | 115.16KB |
| core (index.js) | 8.52KB | 3.41KB |
| create-plugin (index.js) | 27.94KB | 9.51KB |
| data-objectstack (index.js) | 211.58KB | 58.68KB |
| fields (index.js) | 247.89KB | 62.50KB |
| i18n (LocalizationContext.js) | 1.76KB | 0.96KB |
| i18n (builtinAggregateLabels.js) | 0.86KB | 0.49KB |
| i18n (currency.js) | 1.22KB | 0.64KB |
| i18n (fallbackInterpolation.js) | 6.25KB | 2.77KB |
| i18n (i18n.js) | 8.87KB | 3.64KB |
| i18n (index.js) | 5.22KB | 2.26KB |
| i18n (pickLocalized.js) | 9.86KB | 3.95KB |
| i18n (provider.js) | 32.15KB | 10.49KB |
| i18n (useDisplayLocale.js) | 2.85KB | 1.45KB |
| i18n (useObjectLabel.js) | 34.34KB | 9.17KB |
| i18n (useSafeTranslation.js) | 5.60KB | 2.33KB |
| layout (index.js) | 38.83KB | 10.95KB |
| mobile (MobileProvider.js) | 0.92KB | 0.49KB |
| mobile (ResponsiveContainer.js) | 0.94KB | 0.38KB |
| mobile (breakpoints.js) | 1.51KB | 0.70KB |
| mobile (createOfflineDataSource.js) | 5.61KB | 1.75KB |
| mobile (index.js) | 1.99KB | 0.87KB |
| mobile (offlineQueue.js) | 3.91KB | 1.35KB |
| mobile (pwa.js) | 0.97KB | 0.49KB |
| mobile (serviceWorker.js) | 1.48KB | 0.62KB |
| mobile (serviceWorkerSource.js) | 3.41KB | 1.48KB |
| mobile (useBreakpoint.js) | 1.54KB | 0.65KB |
| mobile (useGesture.js) | 6.96KB | 1.98KB |
| mobile (useOfflineSync.js) | 1.99KB | 0.72KB |
| mobile (usePullToRefresh.js) | 2.53KB | 0.85KB |
| mobile (useResponsive.js) | 0.72KB | 0.42KB |
| mobile (useSpecGesture.js) | 4.39KB | 1.66KB |
| mobile (useTouchTarget.js) | 1.01KB | 0.54KB |
| permissions (MePermissionsProvider.js) | 13.52KB | 4.88KB |
| permissions (PermissionContext.js) | 0.31KB | 0.25KB |
| permissions (PermissionGuard.js) | 0.89KB | 0.45KB |
| permissions (PermissionProvider.js) | 6.24KB | 2.16KB |
| permissions (discardProofCache.js) | 1.04KB | 0.55KB |
| permissions (evaluator.js) | 8.39KB | 3.10KB |
| permissions (index.js) | 0.93KB | 0.41KB |
| permissions (store.js) | 0.91KB | 0.42KB |
| permissions (useFieldPermissions.js) | 1.28KB | 0.53KB |
| permissions (usePermissions.js) | 4.83KB | 2.27KB |
| plugin-ai (index.js) | 14.81KB | 3.63KB |
| plugin-calendar (index.js) | 49.25KB | 13.99KB |
| plugin-charts (index.js) | 71.34KB | 19.90KB |
| plugin-chatbot (index.js) | 195.34KB | 46.51KB |
| plugin-dashboard (index.js) | 131.22KB | 34.59KB |
| plugin-designer (index.js) | 215.94KB | 44.33KB |
| plugin-detail (index.js) | 253.46KB | 65.85KB |
| plugin-editor (index.js) | 2.23KB | 1.05KB |
| plugin-form (index.js) | 136.77KB | 34.17KB |
| plugin-gantt (index.js) | 166.95KB | 41.04KB |
| plugin-grid (index.js) | 211.66KB | 57.50KB |
| plugin-kanban (index.js) | 46.00KB | 14.30KB |
| plugin-list (index.js) | 112.58KB | 27.65KB |
| plugin-map (index.js) | 20.64KB | 6.86KB |
| plugin-markdown (index.js) | 13.88KB | 4.80KB |
| plugin-report (index.js) | 43.41KB | 11.93KB |
| plugin-timeline (index.js) | 30.07KB | 8.74KB |
| plugin-tree (index.js) | 9.55KB | 3.32KB |
| plugin-view (index.js) | 84.42KB | 20.79KB |
| providers (DataSourceProvider.js) | 0.75KB | 0.39KB |
| providers (MetadataProvider.js) | 1.37KB | 0.59KB |
| providers (ThemeProvider.js) | 1.90KB | 0.85KB |
| providers (UploadProvider.js) | 11.66KB | 3.50KB |
| providers (index.js) | 0.45KB | 0.23KB |
| providers (types.js) | 0.01KB | 0.04KB |
| react-runtime (index.js) | 5.62KB | 2.34KB |
| react (LazyPluginLoader.js) | 4.47KB | 1.63KB |
| react (SchemaRenderer.js) | 96.00KB | 31.71KB |
| react (data-invalidation.js) | 5.05KB | 2.08KB |
| react (index.js) | 4.63KB | 2.18KB |
| react (schema-input.js) | 4.25KB | 2.04KB |
| react (spec-input.js) | 0.20KB | 0.18KB |
| sdui-parser (codegen.js) | 6.58KB | 2.74KB |
| sdui-parser (dashboard-widget-options.js) | 3.08KB | 1.30KB |
| sdui-parser (index.js) | 5.66KB | 2.50KB |
| sdui-parser (input-type.js) | 2.84KB | 1.40KB |
| sdui-parser (kanban-quick-add.js) | 3.89KB | 1.87KB |
| sdui-parser (parse.js) | 25.28KB | 7.80KB |
| sdui-parser (provenance.js) | 3.66KB | 1.82KB |
| sdui-parser (types.js) | 0.28KB | 0.23KB |
| sdui-parser (validate.js) | 14.82KB | 4.99KB |
| types (ai.js) | 0.20KB | 0.17KB |
| types (api-types.js) | 0.20KB | 0.18KB |
| types (app.js) | 2.87KB | 1.00KB |
| types (base.js) | 0.20KB | 0.18KB |
| types (blocks.js) | 0.20KB | 0.18KB |
| types (complex.js) | 2.93KB | 1.49KB |
| types (crud.js) | 0.20KB | 0.18KB |
| types (dashboard-filter-alias.js) | 6.23KB | 2.74KB |
| types (data-display.js) | 3.75KB | 1.85KB |
| types (data-protocol.js) | 0.20KB | 0.19KB |
| types (data.js) | 0.20KB | 0.18KB |
| types (designer.js) | 1.85KB | 0.85KB |
| types (disclosure.js) | 0.20KB | 0.18KB |
| types (error-code.js) | 1.54KB | 0.88KB |
| types (expression.js) | 0.20KB | 0.18KB |
| types (feedback.js) | 0.20KB | 0.18KB |
| types (field-types.js) | 0.20KB | 0.18KB |
| types (form.js) | 0.20KB | 0.18KB |
| types (http-inflight.js) | 8.87KB | 3.73KB |
| types (http-retry.js) | 4.32KB | 2.02KB |
| types (icon-key-migration.js) | 4.26KB | 1.63KB |
| types (index.js) | 4.74KB | 2.25KB |
| types (layout.js) | 0.20KB | 0.18KB |
| types (managed-by.js) | 0.19KB | 0.18KB |
| types (mobile.js) | 4.73KB | 2.28KB |
| types (navigation.js) | 0.20KB | 0.18KB |
| types (objectql.js) | 0.20KB | 0.18KB |
| types (overlay.js) | 0.20KB | 0.18KB |
| types (permissions.js) | 0.20KB | 0.18KB |
| types (plugin-scope.js) | 0.20KB | 0.18KB |
| types (record-components.js) | 0.20KB | 0.19KB |
| types (record-semantics.js) | 1.28KB | 0.67KB |
| types (registry.js) | 0.20KB | 0.18KB |
| types (reports.js) | 0.20KB | 0.18KB |
| types (select-option.js) | 0.20KB | 0.19KB |
| types (spec-report.js) | 5.05KB | 1.93KB |
| types (spec-ui-namespace.js) | 0.20KB | 0.19KB |
| types (strict-authoring-face.js) | 14.04KB | 5.36KB |
| types (system-fields.js) | 3.33KB | 1.54KB |
| types (theme.js) | 6.28KB | 2.87KB |
| types (ui-action.js) | 8.11KB | 3.32KB |
| types (views.js) | 0.20KB | 0.18KB |
| types (widget.js) | 0.20KB | 0.18KB |
Size Limits
- ✅ Core packages should be < 50KB gzipped
- ✅ Component packages should be < 100KB gzipped
⚠️ Plugin packages should be < 150KB gzipped
|
| measurement | reading |
|---|---|
| entry chunk | ✅ 144.3 KB gzip / 350 KB |
| aggregate eager closure | ✅ 3106.1 KB / 3134.8 KB (52 of 528 chunks) |
| per-chunk ceilings (4 weighed) | ✅ all four — vendor-objectstack, ui-components, framework, i18n-locale-en |
| ceiling freshness | ✅ all 3 constants unchanged on main since this checkout's base |
The single ❌ is a verdict about the gauge, not about the bundle — the checker exited 2 (BUDGET_CLOSURE_HEADROOM_STATUS: error, rendered kind: not-measured), because the ui-components row's headroom has fallen to 1.9 KB = 0.02x of the 89.0 KB regression the gate exists to catch, under its 0.10x floor.
The gate's own output, verbatim:
⚠️ READ THIS BEFORE AUDITING YOUR OWN DIFF. This row's headroom is a standing debt that predates this change, and it moves under traffic that has nothing to do with the chunk — measured at three gzipped bytes across five unrelated merges. So this verdict is NOT an accusation that your diff spent the bytes, and the amount it names is very likely not yours. What it asserts is only that the row is tighter than the day it was pinned.
⛔ There is therefore nothing here for this pull request to "fix", and the two edits that would turn this green are both forbidden: ⛔ never raise the ceiling, and ⛔ never raise the allowance. Paying the row down is the open decision on the chunk, ⛔ not a task for whichever change the queue happened to weigh — take it there, and say on this pull request that you did.
Taken there, as instructed. The open decision on this chunk is objectui#9251 (decision(components): Door 2 — the eager icons record carries 1,767 lucide modules, pm:dispatched) with objectui#9204 (perf(components), priority:p1) as the implementation row. ⛔ This seat filed nothing new — both already exist and are held by the seat working that chunk, so a third card would be a duplicate. ⛔ This PR neither raises the ceiling nor raises the allowance, and its diff (3 files, packages/core/src/utils/filter-tokens.ts + a pin + a changeset) touches no bundled component surface.
Why this does not block the queue, verified rather than assumed: performance-budget.yml's on: carries only push and pull_request — ⛔ no merge_group — and the workflow's own comment records that "this job cannot hold the merge queue the way Lint can". It is outside the 9-check required set, which is Lint · Type Check · Test (shard 1/4…4/4) · Build & E2E · Build Docs · Changeset Declaration.
⇒ All three conditions for queueing with a red non-required check are met: red by design on a pushed branch per the gate's own source text, no merge_group participation, and this comment naming the gate and the cause. ⛔ Nothing here was weakened to get there.
Generated by Claude Code
Fixes #9129
resolveContextTokens(packages/core/src/utils/filter-tokens.ts) looked a near-missplaceholder spelling up in
@objectstack/spec'sCONTEXT_TOKEN_SUGGESTIONSmap with aplain bracket index keyed on
token.toLowerCase(). Two spellings that surviveWHOLE_TOKEN_RE([a-zA-Z0-9_]+) lower-case to inheritedObject.prototypemembernames —
constructorand__proto__— so the index resolved toObject.prototype.constructor/Object.prototype.__proto__instead ofundefined,and the resulting warning asserted a "suggestion" that was native-code / object text:
not a token, not spellable, not anything an author can act on.
This is not prototype pollution, and this PR does not describe it as one. The index
is a read, never an assignment; the resolved filter value is passed through untouched
either way, so no filter is ever widened or narrowed and no record is ever mis-matched.
The only observable effect of the bug was a confusing string inside a single
console.warncall.Fix
Per the card's own framing, this closes the class rather than special-casing the two
names measured today (which would just reproduce the defect the next time a future
lower-case
Object.prototypemember is added): the lookup now runs over anull-prototype copy of the suggestion map —
— built once, local to this one call site.
@objectstack/spec's own map is untouched;the identical shape in its
classifyFilterToken(same map, wider token regex, and atyped return field rather than a log string) is objectstack#17762, filed separately by
triage and out of scope here — fixing this side does not cover that one.
Tests
Pin added beside the existing near-miss cases in
packages/core/src/utils/__tests__/filter-token-proto-guard-9129.test.ts, assertingsilence for
{constructor}and{__proto__}(never today's[native code]/[object Object]strings — pinning those would only prove the two names arespecial-cased) plus a lit near-miss control (
{user_id}still warns with a realsuggestion), proving the suggestion feature itself is not broken by the fix.
Ablation (mutation + restore, on a committed tree)
Fix committed first, then mutated
NEAR_MISS_SUGGESTIONS[...]back toCONTEXT_TOKEN_SUGGESTIONS[...]under atrap … EXIT INT TERMrestoring fromHEAD:git diff HEADempty,3 passed (3).Verification
Gate family derived by hand from
package.json(nodispatch-gates.mjsin this repo).Dependency closure (
pnpm --filter '@object-ui/core^...' build) built first(
@object-ui/types,@object-ui/test-support), then:pnpm --filter @object-ui/core build(tsc+ dist-completeness)pnpm exec vitest run packages/core/packages/coretype-check(tsc --noEmit+tsc -p tsconfig.test.json)pnpm exec eslinton the 2 touched source/test filesanywarnings, none on touched lines)node scripts/check-changeset-presence.mjsnode scripts/check-changeset-no-major.mjsmajorbumpgrep -naPon the 3 changed files)No public export of
@object-ui/corechanged (NEAR_MISS_SUGGESTIONSis a privatemodule-level
const), so per the repo's own scope rule for byte-identical publicsurfaces, no downstream-consumer sweep or added consumer tests are needed.
Lint (full), Type Check (full), Test (4 shards), Build & E2E, Build Docs are left to CI
per this repo's "local runs targeted gates, CI runs the farm" convention — none of them
is implicated by a change confined to one private lookup inside
@object-ui/core.Acceptance notes
Nothing found outside this card's fence during the fix.
🤖 Generated with Claude Code
https://claude.ai/code/session_011QreXiyMEqKLN4U5daMPVa
Generated by Claude Code