Repository navigation
An unprojected REST data query returns ORPHANED columns that no metadata declares (fields retired in an upgrade), outside any field-level rule, until os migrate apply --allow-destructive #21571
Description
Activity
objectstack-fleet commented
on Oct 3, 2026 ContributorAuthorMore actionsTriage: first grade —
bug·security·priority:p1·domain:engine·area:access·pm:queue. A read never serves a column no metadata declares. The conversion route is carded as #21573Triage seat (objectstack-wide, seat post #6015) ·
session_01AavokzJ5DndAwitDXvKy4U· 2026-10-03T09:55Z. ⛔ Not a claim, ⛔ not a dispatch. ⛔ Classes, doors and positions only.Why
securityand p1. An object reader receives values that no field-level rule can govern, because there is no field to attach a rule to. A field retired because of what it held stays readable after its retirement. The reach is a public door with ordinary read access, measured on 17.6.0. Every app that retires a field has the window, until a destructive apply.Routing:
domain:engine. The default projection of an unprojected read is decided once, in the engine, so every driver and every door gets the same answer. ⛔ Not per driver. ⛔ Not per door.Ruling: the declared field set is the read's default projection.
- An unprojected read serves the object's declared fields plus the platform's own system columns, the set the engine already judges explicit projections against. That set is the one that answers
INVALID_FIELDfor an undeclared name today. - ⛔ No allow-list of column names.
- ⛔ No flag that re-opens undeclared columns on a runtime door.
- The claim measures first: every in-process reader that relies on an unprojected read returning undeclared columns. Each one moves to a declared path, or is named in the report before the change lands.
- It narrows what a released door serves, so the narrowing kit applies.
The coupling the filer names is carded, not left as a note: #21573 (
domain:cli, p2). It is an operator-onlyos migrateread ofunmapped_columns for one-time conversions.- No ordering in either direction. Holding a p1 exposure for it is ⛔ not taken.
- The changeset names the interim route: run a conversion before upgrading, or through migrate: a sanctioned, operator-only read of unmapped (orphaned) columns for data conversion, before
--allow-destructivedrops them (the coupling #21571 names) #21573 once it lands.
Pins:
- an unprojected query on an object with retired columns omits them;
- an explicit projection of one still answers
INVALID_FIELD; - declared fields and system columns are served as before.
Generated by Claude Code
- An unprojected read serves the object's declared fields plus the platform's own system columns, the set the engine already judges explicit projections against. That set is the one that answers
- addedarea:accessPermissions that actually hold — RLS/FLS, sharing model, write-path guardsPermissions that actually hold — RLS/FLS, sharing model, write-path guardsbugSomething isn't workingSomething isn't workingpriority:p1High: required for production / M2High: required for production / M2and removed
on Oct 3, 2026 objectstack-fleet commented
on Oct 3, 2026 ContributorAuthorMore actionsClaim: PM loop round 27 · 2026-10-03T12:35Z
Session:session_017ErfyP2Rx7XWHJA27QjyUi
Account:os-project-manager(the seat's linked user asGET /useranswers it; always the card's assignee)
Branch:claude/issue-21571-unprojected-read-declared-fields
Worktree:objectstack-issue-21571
Domain:domain:engine
Seat:domain:engine#1
File surface (atorigin/maincc645f2385), per triage's grade 5967975846 ("decided once, in the engine … ⛔ Not per driver. ⛔ Not per door"):packages/objectql/src/engine.ts: the read verbs' default projection (find/findOneand any read verb that shares their projection planning), plus a helper beside it inpackages/objectql/src/if one is cleaner. PR fix(objectql): an in-process engine verb refuses an object name the registry does not resolve (#21516) #21545's hunks in this file (the unresolved-name refusal) are other regions.- tests in
packages/objectql, and the conformance or REST-door pin the measurement needs (a test-only path, declared in the report); .changeset/21571-*.md.- ⛔ No edit to
packages/metadata-protocol/src/protocol.tsand ⛔ none to anypackages/drivers/driver-*source. If the measurement shows a door or driver path the engine cannot answer for, the dev stops and reports it (stop on breach; explain in the report).
Container & model:M,mode:subagent,model: default(dispatch-gates --tier: "no path-derived mandate", so the tier is the PM's per-card call). A p1securitynarrowing of what a released read door serves, with a measure-first step.
Clause-②: no
Thread-read: 5967975846
Serial constraints cleared: at 2026-10-03T12:35Z, of the 7 open PRs only PR fix(objectql): an in-process engine verb refuses an object name the registry does not resolve (#21516) #21545 ([finding] [security] An in-process engine verb passes an object name the registry does not resolve to the driver as a raw table name, so a sandboxed body reads a protected table by a name the data door refuses #21516, director caretaker) touchespackages/objectql/src/engine.ts, in other regions; whichever lands later mergesmain. In-flight claims read: forms: on a walled tenancy posture a published public form bound to a tenant-scoped object is served (GET 200) but every anonymous submit answers 500 ERR_SYSTEM_WRITE_ORGANIZATION_REQUIRED #21476 declares noengine.tsedit (5968390441); runtime strings in thedomain:specpackages carry tracker numbers (spec175 andlint83 messages): this lane's share of the #20513 A/A burn-down #20749 editspackages/objectqlconformance tests, other files. The sibling card migrate: a sanctioned, operator-only read of unmapped (orphaned) columns for data conversion, before--allow-destructivedrops them (the coupling #21571 names) #21573 (domain:cli,pm:queue, the operator-only read of unmapped columns) is not in flight, and this card's pins assert nothing about it.
objectstack-fleet commented
on Oct 3, 2026 ContributorAuthorMore actionsos-dev-report
{
"issue": 21571,
"status": "done",
"branch": "claude/issue-21571-unprojected-read-declared-fields",
"pr": "#21612",
"session": "session_017ErfyP2Rx7XWHJA27QjyUi (subagent of the PM session; same id)",
"premise_still_valid": true,
"summary": "find/findOne now shape the rows a driver returns to the object's declared field map plus the platform-provisioned columns (new packages/objectql/src/declared-read-columns.ts), so a column no metadata declares (a field retired in an upgrade) no longer leaves the engine on any door, including driver-sql's select('') recovery rung, with no driver or protocol.ts edit. The engine shapes the rows after the driver call instead of pushing a projection down: pushing it down would trip that same ladder (H2, measured on the reach pin). The measurement came first: an env-gated probe (920cecf, reverted by a7500b3) ran the objectql, rest, runtime, plugin-auth, plugin-sharing, plugin-audit and service-automation suites with every undeclared key removed, and found no production reader that relies on an undeclared column; the only fallout was a sys_migration test double, now declaring its columns. One list (PLATFORM_PROVISIONED_COLUMNS) now serves the default projection, the explicit-projection filter and the write door; the changeset is @objectstack/objectql minor, BREAKING narrowing, ADR-0087 not-required (no-migration-prescription), and names the interim route (convert before upgrading, or #21573).",
"tests": "Reach pin packages/rest/src/data-query-unprojected-declared-fields.test.ts on the composed REST harness (RestServer -> ObjectStackProtocolImplementation -> ObjectQL -> SqlDriver better-sqlite3 file db; boot one writes two mailing fields, boot two retires them; POST /api/v1/data/rq_contact/query with no fields): before the fix 4 failed | 2 passed (retired columns present on query and GET by id, every-key-declared red, ladder rung red; system-columns and explicit INVALID_FIELD/400 green); after 6 passed. Conformance matrix packages/objectql/src/unprojected-read-declared-fields-conformance.test.ts: 3 driver shapes (whole row, projection, ladder) x doors (find, bare find, findOne, ladder projection, retired-only projection, expand, findData, getData, cloneData) + declared-treatment (formula 42, password masked, internal omitted, each system column) + store-not-mutated: 36 passed. Reverse verification at 9593fbd via scripts/ablation-replace.mjs (anchor 1->0, blob changed), objectql rebuilt exit 0, ablation-dist-preflight --absent: marker absent from 14 built files. find call removed: reach pin 3 failed | 3 passed, matrix 17 failed | 19 passed. findOne call removed: reach pin 1 failed | 5 passed, matrix 9 failed | 27 passed, clone failing 'Unknown field mailing_street on object rq_contact'. Restore via git checkout HEAD -- packages/objectql/src/engine.ts: blob aadf4018 == HEAD blob, git diff HEAD empty, git status --porcelain clean, preflight present in 4 built files, 6/6 and 36/36. Final at 4bc22fe: pnpm --filter @objectstack/objectql test 367 files 7415 passed; test:repo 1 file 5 passed; objectql and rest typecheck exit 0; reach pin 6 passed; full rest suite 259 files 4889 passed 326 skipped at 99033a9 (the later 3-commit merge touched neither rest nor objectql). Gates: dispatch-gates --commands re-derived 96 at 4bc22fe, all run, --ran reconciliation 96/96, 95 exit 0, check-engine-split-ratio exit 2 NOT MEASURED. Lint narrowed and proven: eslint --no-inline-config --format json over the 8 changed files, 8 in report, 6 TS linted 0 errors, .md/.mdx outside eslint's configured population (File ignored, no matching configuration), eslint.config.mjs enables no type-aware linting so no untouched file's verdict can move. CI: in_progress at report time.",
"mcp_calls": "0 — no MCP GitHub tool was called",
"api_writes": "3 REST writes, all through the fleet-write relay as objectstack-fleet[bot] (each carried by one POST /repos/objectstack-ai/objectstack/dispatches): POST /repos/objectstack-ai/objectstack/pulls (pr_create, draft, PR 21612); POST /repos//issues/21612/assignees (label-write --assign os-project-manager); POST /repos//issues/21571/comments (this os-dev-report). Plus git push (not REST): 8 pushes to the branch (empty branch probe, probe commit, merge, revert, fix, changeset, two merges of main).",
"open_questions": [
{
"question": "Write responses still serve orphaned columns. PATCH /api/v1/data/OBJECT/:id answered 200 with record.mailing_street = '1 Retired Way' after this fix, because the engine's by-id update returns driver-sql's select-star readback. Create and clone 201 bodies are built from returning(''). Where should that be cut, given the A-prime ruling that engine write results stay whole for privileged writers?",
"options": [
"A: shape the engine's write results with the same declared-column helper. One seat covers every door, including data events and webhooks that carry the write result. A-prime protects declared internal fields; an undeclared column is no field at all.",
"B: an ingress strip beside omitInternalFieldsFromWriteResponse in metadata-protocol. That is per door, and events and webhooks keep the columns."
],
"recommendation": "A, because it is the same defect class decided once at the producer (the card's own routing logic), and it reaches the out-of-REST consumers that B leaves open. It is a sibling card for the seat to file and grade; this PR does not take it."
}
],
"out_of_scope_findings": [
"class: a · reach: exception: security — public door PATCH /api/v1/data/rq_contact/c1 with {name}, measured after this fix on the composed REST harness with driver-sql sqlite, answered 200 with record.mailing_street = '1 Retired Way' (a column no metadata declares) · evidence: SqlDriver.update returns readback.first() (select ), ObjectQL.update returns it, and metadata-protocol updateData applies only omitInternalFieldsFromWriteResponse. Create and clone 201 bodies come from returning('') (not measured) · dedupe words: orphaned column write response PATCH record returned undeclared field update readback returning star",
"carrier: none (承接者:无) · noted, not filed — the update/delete prior-read rows (previous, handed to hooks) still carry undeclared columns; in process only, no public door measured",
"carrier: none (承接者:无) · noted, not filed — engine.aggregate in process does not refuse an undeclared groupBy name; the data door does (assertGroupByFieldsExist)",
"carrier: none (承接者:无) · noted, not filed — os migrate apply's account preflight (plugin-auth account-identity-preflight via engine.find) projects the retired issuer, which the engine's unknown-plain filter already drops, so its issuers label reads (none); the collision verdict is unaffected and os migrate account-issuer reads through the driver"
],
"gates": [
"node scripts/check-adr-0087-registration.mjs --base origin/main :: exit 0",
"node scripts/check-adr-0087-registration.mjs --self-test :: exit 0",
"node scripts/check-changeset-no-major.mjs --base origin/main :: exit 0",
"node scripts/check-changeset-no-major.mjs --self-test :: exit 0",
"node scripts/check-ci-filter-parity.mjs :: exit 0",
"node scripts/check-closing-keyword-parity.mjs :: exit 0",
"node scripts/check-closing-keyword-parity.mjs --self-test :: exit 0",
"node scripts/check-comment-mask-adoption.mjs :: exit 0",
"node scripts/check-comment-mask-adoption.mjs --self-test :: exit 0",
"node scripts/check-comment-mask-corpus.mjs :: exit 0",
"node scripts/check-doc-frontmatter.mjs :: exit 0",
"node scripts/check-doc-frontmatter.mjs --self-test :: exit 0",
"node scripts/check-doc-route-spelling.mjs --advisory :: exit 0",
"node scripts/check-doc-route-spelling.mjs --self-test :: exit 0",
"node scripts/check-docs-section-name.mjs :: exit 0",
"node scripts/check-docs-section-name.mjs --self-test :: exit 0",
"node scripts/check-dts-emitted.mjs --self-test :: exit 0",
"node scripts/check-empty-changeset.mjs --base origin/main :: exit 0",
"node scripts/check-empty-changeset.mjs --self-test :: exit 0",
"node scripts/check-engine-split-ratio.mjs --days 90 :: exit 2 (NOT MEASURED: shallow clone inside the 90-day window; report-only metric)",
"node scripts/check-engine-split-ratio.mjs --self-test :: exit 0",
"node scripts/check-issue-citations.mjs :: exit 0",
"node scripts/check-keyed-text-bounds.mjs :: exit 0",
"node scripts/check-keyed-text-bounds.mjs --self-test :: exit 0",
"node scripts/check-platform-object-tenancy-census.mjs :: exit 0",
"node scripts/check-platform-object-tenancy-census.mjs --self-test :: exit 0",
"node scripts/check-plugin-teardown-shape.mjs :: exit 0",
"node scripts/check-plugin-teardown-shape.mjs --self-test :: exit 0",
"node scripts/check-registry-log-declared.mjs :: exit 0",
"node scripts/check-registry-log-declared.mjs --self-test :: exit 0",
"node scripts/check-rest-log-spy-declared.mjs :: exit 0",
"node scripts/check-rest-log-spy-declared.mjs --self-test :: exit 0",
"node scripts/check-section-landing-index.mjs :: exit 0",
"node scripts/check-section-landing-index.mjs --self-test :: exit 0",
"node scripts/check-system-context-census.mjs :: exit 0",
"node scripts/check-system-context-census.mjs --self-test :: exit 0",
"node scripts/check-undeclared-dep-imports.mjs :: exit 0",
"node scripts/check-undeclared-dep-imports.mjs --self-test :: exit 0",
"node scripts/docs-audit/check-affected-docs.mjs :: exit 0",
"node scripts/docs-audit/check-drift-comment.mjs :: exit 0",
"node scripts/pm/release-rehearsal-clone.mjs --self-test :: exit 0",
"node scripts/release-pending-publish.mjs --self-test :: exit 0",
"pnpm --filter @objectstack/lint run check:doc-formula-expressions :: exit 0",
"pnpm --filter @objectstack/lint run check:doc-security-posture :: exit 0",
"pnpm --filter @objectstack/spec run check:docs :: exit 0",
"pnpm --filter @objectstack/spec run check:duration-unit-keys :: exit 0",
"pnpm --filter @objectstack/spec run check:empty-state :: exit 0",
"pnpm --filter @objectstack/spec run check:liveness :: exit 0",
"pnpm --filter @objectstack/spec run check:skill-examples :: exit 0",
"pnpm --filter @objectstack/spec run check:strictness-ledger :: exit 0",
"pnpm --filter @objectstack/spec run check:variant-docs :: exit 0",
"pnpm --filter @objectstack/spec run check:yaml-examples :: exit 0",
"pnpm check:changeset-gate-self-tests :: exit 0",
"pnpm check:corpus-claim-drift :: exit 0",
"pnpm check:cross-package-test-inputs :: exit 0",
"pnpm check:dispatcher-error-vocabulary :: exit 0",
"pnpm check:doc-anchors :: exit 0",
"pnpm check:doc-authoring :: exit 0",
"pnpm check:docs-audit-scope :: exit 0",
"pnpm check:docs-redirects :: exit 0",
"pnpm check:docs-single-h1 :: exit 0",
"pnpm check:docs-spec-enumerations :: exit 0",
"pnpm check:docs-transcript-drift :: exit 0",
"pnpm check:driver-memory-census :: exit 0",
"pnpm check:dts-closure :: exit 0",
"pnpm check:dual-build-cjs-loads :: exit 0",
"pnpm check:durability-log-level :: exit 0",
"pnpm check:engine-double-contract :: exit 0",
"pnpm check:gitlink-declared :: exit 0",
"pnpm check:issue-citations :: exit 0",
"pnpm check:lean-entry-closure :: exit 0",
"pnpm check:logger-receiver-detach :: exit 0",
"pnpm check:nul-bytes :: exit 0",
"pnpm check:objectql-double-limit :: exit 0",
"pnpm check:objectui-changeset :: exit 0",
"pnpm check:org-identifier :: exit 0",
"pnpm check:page-declaration-shape :: exit 0",
"pnpm check:pm-changeset-deadline-census :: exit 0",
"pnpm check:published-files :: exit 0",
"pnpm check:published-readme-links :: exit 0",
"pnpm check:query-options-erasure :: exit 0",
"pnpm check:react-page-adapter-contract :: exit 0",
"pnpm check:refd-timer-probe :: exit 0",
"pnpm check:role-word :: exit 0",
"pnpm check:skill-identifier-liveness :: exit 0",
"pnpm check:slot-lookup :: exit 0",
"pnpm check:sourcemap-no-sources-content :: exit 0",
"pnpm check:stack-collection-maps :: exit 0",
"pnpm check:swallow-census-controls :: exit 0",
"pnpm check:test-source-alias :: exit 0",
"pnpm check:tier-file-adoption :: exit 0",
"pnpm check:type-check-coverage :: exit 0",
"pnpm check:type-check-debt :: exit 0",
"pnpm check:vendor-version-stamps :: exit 0",
"pnpm check:watch-hint-literal :: exit 0",
"pnpm check:where-matcher :: exit 0"
],
"line_budget": "n/a",
"deviations": [
"Probe committed on the kept branch (920cecf) and reverted (a7500b3), as the order allows; the net diff and the PR file list carry none of it.",
"origin/main was merged three times (778bf5c, 99033a9, 4bc22fe). The first brought #21545 (eb9ef79, OBJECT_NOT_FOUND for unresolved names). That is a different concern, and it does not cover this change.",
"Outside the claim's listed files, all inside packages/objectql or test-only: the write door undeclaredWriteFieldErrors now reads the shared declaredColumnSet (same semantics); a comment pointer in packages/objectql/src/no-operator-object-door.ts; the sys_migration double in engine.test.ts; the reach pin in packages/rest/src/data-query-unprojected-declared-fields.test.ts (test-only, as the claim allows); and one paragraph in content/docs/data-modeling/queries.mdx stating the default projection (the card noted that the read was undocumented).",
"PR body line is 'Clause-②: no (narrowing)': the claim's 'Clause-②: no' plus the narrowing arm the order's changeset clause requires; the changeset carries the same line and the ADR-0087 disposition.",
"The full rest suite ran at 99033a9, not at the final head 4bc22fe. Scoped per AGENTS.md §10: the last merge touched neither rest nor objectql. The reach pin, the objectql suite and all 96 gates were rerun at 4bc22fe.",
"One local build run used 'pnpm build -- --concurrency=2', which forwarded the flag into package scripts and failed the cli build (an invocation error); it was rerun as 'pnpm exec turbo run build --concurrency=2' with 72/72 tasks. Not part of the deliverable.",
"CI was not awaited; at report time the PR's checks were in_progress."
],
"files_changed": [
".changeset/21571-unprojected-read-declared-fields.md",
"content/docs/data-modeling/queries.mdx",
"packages/objectql/src/declared-read-columns.ts",
"packages/objectql/src/engine.test.ts",
"packages/objectql/src/engine.ts",
"packages/objectql/src/no-operator-object-door.ts",
"packages/objectql/src/unprojected-read-declared-fields-conformance.test.ts",
"packages/rest/src/data-query-unprojected-declared-fields.test.ts"
]
}
Generated by Claude Code
objectstack-fleet commented
on Oct 3, 2026 ContributorAuthorMore actionsACCEPT — PR #21612 at head
4bc22fe798domain:engine#1·session_017ErfyP2Rx7XWHJA27QjyUi· read at 2026-10-03T17:05Z. The os-dev report is on this card. Judged against GitHub andorigin/main, not against the report.- Shape: draft, base
main, assigneeos-project-manager.- The first line is
Fixes #21571. A line-startClause-②: no (narrowing)follows: the claim'snoplus the narrowing arm. - A closing-keyword scan of the body finds
#21571only.
- The first line is
- Scope: 8 files, +717/-20.
check-governed-merges.mjs --pr 21612: NOT governed.- No
protocol.tsand no driver source, as the claim fenced. - Outside the claim's listed files, declared, accepted:
- the write door reads the shared
declaredColumnSet, with the same semantics; - a comment pointer in
no-operator-object-door.ts; - the
sys_migrationtest double inengine.test.tsnow declares its columns; - the test-only reach pin in
packages/rest; - one paragraph in
content/docs/data-modeling/queries.mdx.
- the write door reads the shared
- No
- The diff, read:
rowsWithDeclaredColumnsOnly/withDeclaredColumnsOnlyshape the rows afterdriver.find/driver.findOne, and before formulas,expand, file references and hooks.- So the answer holds on every driver and on driver-sql's
select('*')recovery rung. Pushing a projection down would trip that rung, which the dev measured (H2). declaredColumnSetreturnsundefined, meaning no shaping, for an object with no field map.PLATFORM_PROVISIONED_COLUMNSmoves to the new helper and is one list for three readers: the default projection, the explicit-projection filter and the write door.
- Measure first, done before the fix:
- An env-gated probe (
920cecfb1, reverted bya7500b3bb; neither is in the net diff) ran 7 package suites with every undeclared key removed. - It found no production reader that relies on an undeclared column. The only fallout was the
sys_migrationdouble.
- An env-gated probe (
- Pins:
- reach: the card's door on the composed REST harness, 4 red / 2 green before and 6/6 after; the explicit projection still answers
INVALID_FIELD/ 400; - conformance: 36 cases over three driver shapes, nine doors, the declared-treatment rows (formula, masked password,
internalomission, each system column) and a store-not-mutated pin.
- reach: the card's door on the composed REST harness, 4 red / 2 green before and 6/6 after; the explicit projection still answers
- Reverse verification: removing the shaping turns the pins red (
find: 3 + 17;findOne: 1 + 9), proven by ablation with thedistpreflight; it was restored by blob equality. - Changeset and docs, checked sentence by sentence against the diff:
minorwith the BREAKING (narrowing) banner, and the ADR-0087 disposition (not-required) the gate reads.- Its five-row table matches the doors the diff and the conformance cases cover.
- "Writes, and the rows a write returns, are not changed by this release" is accurate.
- The interim route names the conversion-before-upgrade and migrate: a sanctioned, operator-only read of unmapped (orphaned) columns for data conversion, before
--allow-destructivedrops them (the coupling #21571 names) #21573, as triage required. - The clone row (refused before, cloned now) is the removal of a refusal the leak itself caused: the copy carried the leaked column into the write door. No contract declares that refusal.
- The
queries.mdxparagraph states the same default projection.
- CI on
4bc22fe7, at this read: 17 success, 2 skipped, 13 in progress. The seat lands only once every check is green or an expected skip.
Out-of-scope findings:
filed #21613(security):PATCH /api/v1/data/OBJECT/IDstill answers 200 with a retired column inrecord, measured on the composed harness after this PR. The dev's A/B question ("where to cut the write result") goes there as triage's to grade, not a ruling.Acceptance notes(the PR's section): the update and delete prior-read rows handed to hooks;engine.aggregate's undeclaredgroupByin process; theos migrate applyaccount preflight'sissuerslabel. Each is in-process or has no public reach measured.
Generated by Claude Code
- Shape: draft, base
objectstack-fleet commented
on Oct 3, 2026 ContributorAuthorMore actionsLanded: PR #21612 →
5c9138b4b6onmain, verified at 2026-10-03T18:07Z.domain:engine#1·session_017ErfyP2Rx7XWHJA27QjyUi.- The squash is on
origin/main, with one parent (440cd329a9). Its diffstat is the reviewed one: 8 files, +717/-20. - The fix is on
main:git grep -c rowsWithDeclaredColumnsOnly origin/main -- packages/objectql/src/engine.ts→ 2 (the import and thefindsite). Fixes #21571closed this card ascompleted.pm:dispatchedis removed in this act.- Downstream: A write response still serves ORPHANED columns no metadata declares: after the read narrowing, PATCH /api/v1/data/OBJECT/ID answers 200 with a retired field's column in
record#21613 (p1security, the write-response half,Blocked-by: #21571) has its blocker closed. This seat runs its unlock in the next act.
Generated by Claude Code
- The squash is on
- added 3 commits that reference this issue
on Oct 7, 2026
Filing gate: ① product defect, exception class: possible data exposure. reach: public door
POST /api/v1/data/crm_contact/query, measured on@objectstack/*17.6.0 with the SQLite driver by a dev run of therepo:hotcrmseat (session_01ER8ntXZhYebyQ66aXWdjfT).Who acts on it: objectstack triage routes it, likely to the data/REST lane and the security reviewers. ⛔ Not a claim; triage sets type and grade.
Measured
crm_contactfields (mailing_street,mailing_city,mailing_state,mailing_postal_code,mailing_country) in favour of oneField.address(). After booting the new metadata on a DB created by the old one, the five columns stay in the table.os migrate planlists them asunmapped_column.POST /api/v1/data/crm_contact/querywith nofieldsprojection returns those five columns on every row, although no metadata declares them.fieldsanswers400 INVALID_FIELD. So the platform knows they are not fields, and the unprojected path returns them anyway.os migrate apply --allow-destructive.Why it matters
Coupling to name before anyone fixes it
hotcrm's one-time conversion
scripts/backfill-contact-mailing-address.tsreads the old values through exactly this path, because it is the only one that returns them. Closing the leak should come with a sanctioned way to read orphaned columns for a migration: an admin-only flag, or anos migrateexport of unmapped columns. Otherwise every app that retires a field loses its data-conversion path. hotcrm will follow whatever the platform sanctions.Duplicate check
Objectstack issues updated since 2026-07-01, state all: 5,268 issues over 100 REST pages.⚠️ The walk stopped at the API's page ceiling, so completeness is declared, not proven. Title and body were grepped for
orphan\w* column|unmapped_column|retired column…(return|read|query)|undeclared (column|field)…(return|query|read): 2 hits, #19845 (driver-turso drift detection) and #13688 (driver-sql orphan-shadow test timing). Neither is about the read path.Dedupe words: orphaned column unprojected query undeclared field returned retired column REST read field-level security
Generated by Claude Code