Skip to content

fix: keep credential and excluded fields out of generated routes, forms and reports - #503

Merged
dmealing merged 18 commits into
mainfrom
fm/mo-adopter-fixes
Oct 29, 2026
Merged

dmealing merged 18 commits into
mainfrom
fm/mo-adopter-fixes

Conversation

@dmealing

@dmealing dmealing commented Oct 28, 2026 •

Copy link
Copy Markdown
Member

What Changed

  • Generated routes in all five ports (TypeScript, C#, Java, Kotlin, Python) no longer accept writes to @formExclude or credential fields, and no longer return credential values on read. Report list and export output omits credential columns, and the report export path is filtered, sorted and hidden the same way.

  • Generated React and Angular forms skip credential fields, so secret hashes are no longer rendered as editable inputs. Covered by new per-port codegen and conformance tests (including an API-contract secret-fields corpus).

  • Docs: adds recipes for drizzle-kit-owned migrations, guarding generated routes inside your own auth scope, and proving a verify gate fails. Rewrites the Cloudflare Workers recipe around a real per-request D1 example. Codegen now rejects dialect: "d1" as an error instead of silently using the Postgres branch; dialect: "sqlite" is the correct value for D1. CHANGELOG updated.

  • A served report that groups by a credential column, or takes its min or max, no longer leaks it: the derived column stays a credential in every port, so the list route omits it, a report export names no column for it, and it is neither filterable nor sortable (a hidden column that could be filtered on would still be an oracle). count over a credential is a number and is still returned.

  • Upgrade note: a @required field that is also write-denied makes the generated create fail, since the route now ignores that field's value.

Testing exception and gate history

  • The gate took 3 runs on this branch. Run 3 covered the report-credential redaction, the filter and sort allowlist fix and the merge of main.
  • Live validation covered the TypeScript path only. The other ports are covered by per-port lanes run locally: C# MetaObjects.Codegen.Tests 1051 pass and the integration report and secret-fields lanes 114 pass; Java 43 targeted pass; Python 57 targeted pass; Kotlin report classes pass; TypeScript metadata 3769 and codegen-ts 2897 pass; and the api-contract secret-fields corpus. The full scripts/ci-local.sh lanes were not run, and hosted CI is off. The Java, Kotlin and C# report redaction is pinned by generated-source and compile tests, plus a C# test of the JSON export, not an HTTP check.
  • The three docs recipes (Workers + D1, drizzle-kit, auth scope) were checked by reading only.
  • Known open item, deliberately deferred: the registered @formExclude description ("omitted from generated forms") is stale, because writes also ignore the field. Fixing it changes expected-registry.json and four registries, so it is carried into a separate change rather than this PR.
Validation

Risk Assessment

⚠️ Medium: The only change since the last review round is one new C# test, which is sound. Earlier rounds covered the rest of this large cross-port credential change, and I did not re-read those files in this pass.

Testing

Baseline (ts-fast, ts-unit) plus targeted runs: TS generated routes deny writes to @formExclude and credential fields and never return credentials over HTTP against Postgres (pass); TS report credential omission and TPH M:N credential tests (pass); codegen dialect validation (pass live through the CLI: d1 exits 1 with a clear sqlite hint, sqlite generates, exit 0); verify-gate-not-vacuous test (pass). Not verified live: Java, Kotlin, C# and Python secret-field write denial and read redaction, the docs recipe examples run as written, and the full regression suite. Overall inconclusive.

  • Live validation: ⚠️ inconclusive - 6 of 10 scenarios driven live against the product
Scenario Result Live Evidence
TS generated Account routes refuse writes to @formExclude/credential fields and never return credentials (#484) ✅ pass live bun test packages/integration-tests/test/api-contract-secret-fields.test.ts (Postgres testcontainer, generated server over HTTP)
TS report export omits a credential column from JSON and grouped output ✅ pass live bun test packages/runtime-ts/test/report-hidden-column.test.ts; packages/codegen-ts/test/report-credential.test.ts
TS TPH M:N onto a base hides subtype credentials ✅ pass live bun test packages/codegen-ts/test/tph-m2m-credential.test.ts
Adopter sets dialect "d1" and gets a clear error, not a silent Postgres output ✅ pass live Disposable project under /tmp; run(['gen']) exits 1 with message 'invalid codegen dialect "d1"... set dialect: "sqlite"'
Adopter sets dialect "sqlite" for D1 and gen succeeds ✅ pass live Same disposable project, dialect "sqlite" plus outDir; run(['gen']) exits 0
Prove-your-gate-fails: seeded break makes verify exit 1 ✅ pass live bun test packages/cli/test/integration/verify-gate-not-vacuous.test.ts (2 pass)
Java/Kotlin/C#/Python generated routes refuse writes to excluded fields and never return credentials ⏸️ untested no Only the TS generated server was driven live. Java/Kotlin/C#/Python runners were not run in this phase; their coverage is in unit and generator tests (not live). Running them needs the Maven/Gradle/do…
Cloudflare Workers + D1 per-request example runs as written ⏸️ untested no Recipe content checked by reading only. Running it needs Wrangler and a Workers runtime, which were not set up in this workspace.
drizzle-kit migrations recipe (re-export generated pgTable) runs as written ⏸️ untested no Recipe read only. Running drizzle-kit against a live database was not done in this phase.
Generated routes mounted inside an auth scope (guarding-generated-routes recipe) runs as written ⏸️ untested no Recipe read only; no example app was built to mount the routes under an auth scope.
  • Outcome: ⚠️ 2 issues (1 warning, 1 info) across 1 run (14m46s)

Pipeline

Updates from git push no-mistakes

✅ **Intent** - passed

✅ No issues found.

✅ **Rebase** - passed

✅ No issues found.

🔧 **Review** - 1 issue found → auto-fixed ✅
  • ⚠️ server/csharp/MetaObjects.Codegen/Runtime/ReportExportHttp.cs:71 - This finding is about test quality, not intent conformance; ignore the category tag. The new C# runtime path ReportExportHttp.Json(http, segment, body, omit, envelope) serializes the body to a JsonNode and removes credential keys with Strip (lines 71-91). It is the only thing that keeps a grouped or min/max credential out of a C# report's ?format=json export. No test executes it. ReportCredentialCodegenTests checks that the generated route text contains rows, new string[] { "pinHash" } and that the code compiles, and the api-contract report/ corpus has no credential column. A plausible wrong behavior these tests would accept: with envelope: true, env["rows"] is looked up under a casing the serializer options did not write, so Strip sees null and the export still returns pinHash. The independent oracle is the documented contract in docs/features/api-contract.md: a credential report column is never returned, as the key is absent. TS (runtime-ts/test/report-hidden-column.test.ts) and Python (test_served_report_list_omits_the_credential_key_over_http) drive this over HTTP. Remedy: add an xUnit test that calls ReportExportHttp.Json with a DefaultHttpContext (with and without envelope), runs the IResult into a response body stream, parses the JSON, and asserts that pinHash is absent and accounts is present. The Java and Kotlin report redaction (redactAll(rows, "pinHash"), redact<R>(objectMapper, …)) is also checked only by generated text. It reuses the redactor that the secret-fields corpus already drives, so this finding does not ask for it, following the standing TPH decision.

🔧 Fix applied.
✅ Re-checked - no issues remain.

⚠️ **Test** - 2 issues (1 warning, 1 info)
  • ℹ️ server/typescript/packages/cli/test/integration/verify-gate-not-vacuous.test.ts - Full scripts/ci-local.sh (Java, Kotlin, C#, Python lanes) was not run here. Java/Kotlin/C#/Python secret-field behavior was not driven live.
  • ⚠️ live validation verdict: inconclusive (6 of 10 scenarios were driven live against the product); untested: Java/Kotlin/C#/Python generated routes refuse writes to excluded fields and never return credentials, Cloudflare Workers + D1 per-request example runs as written, drizzle-kit migrations recipe (re-export generated pgTable) runs as written, Generated routes mounted inside an auth scope (guarding-generated-routes recipe) runs as written
  • Live validation: ⚠️ inconclusive - 6 of 10 scenarios driven live against the product
Scenario Result Live Evidence
TS generated Account routes refuse writes to @formExclude/credential fields and never return credentials (#484) ✅ pass live bun test packages/integration-tests/test/api-contract-secret-fields.test.ts (Postgres testcontainer, generated server over HTTP)
TS report export omits a credential column from JSON and grouped output ✅ pass live bun test packages/runtime-ts/test/report-hidden-column.test.ts; packages/codegen-ts/test/report-credential.test.ts
TS TPH M:N onto a base hides subtype credentials ✅ pass live bun test packages/codegen-ts/test/tph-m2m-credential.test.ts
Adopter sets dialect "d1" and gets a clear error, not a silent Postgres output ✅ pass live Disposable project under /tmp; run(['gen']) exits 1 with message 'invalid codegen dialect "d1"... set dialect: "sqlite"'
Adopter sets dialect "sqlite" for D1 and gen succeeds ✅ pass live Same disposable project, dialect "sqlite" plus outDir; run(['gen']) exits 0
Prove-your-gate-fails: seeded break makes verify exit 1 ✅ pass live bun test packages/cli/test/integration/verify-gate-not-vacuous.test.ts (2 pass)
Java/Kotlin/C#/Python generated routes refuse writes to excluded fields and never return credentials ⏸️ untested no Only the TS generated server was driven live. Java/Kotlin/C#/Python runners were not run in this phase; their coverage is in unit and generator tests (not live). Running them needs the Maven/Gradle/do…
Cloudflare Workers + D1 per-request example runs as written ⏸️ untested no Recipe content checked by reading only. Running it needs Wrangler and a Workers runtime, which were not set up in this workspace.
drizzle-kit migrations recipe (re-export generated pgTable) runs as written ⏸️ untested no Recipe read only. Running drizzle-kit against a live database was not done in this phase.
Generated routes mounted inside an auth scope (guarding-generated-routes recipe) runs as written ⏸️ untested no Recipe read only; no example app was built to mount the routes under an auth scope.
  • scripts/ci-local.sh --only ts-fast --only ts-unit --strict-toolchains
  • bun test packages/codegen-ts/test/codegen-dialect-validation.test.ts packages/codegen-ts/test/report-credential.test.ts packages/codegen-ts/test/tph-m2m-credential.test.ts packages/runtime-ts/test/report-hidden-column.test.ts packages/integration-tests/test/api-contract-secret-fields.test.ts (in server/typescript) — 20 pass, 0 fail; secret-fields scenarios use a Postgres testcontainer (docker available)
  • live CLI: disposable project in /tmp, bun runner on packages/cli/src/index.ts run(['gen']) with dialect "d1" → exit 1, message names "sqlite" and migrate transport; dialect "sqlite" with outDir → exit 0
  • bun test ./packages/cli/test/integration/verify-gate-not-vacuous.test.ts — 2 pass, 0 fail
  • Checked recipe presence: docs/recipes/cloudflare-workers.md (D1 sqlite dialect section), drizzle-kit-migrations.md, prove-your-gate-fails.md, guarding-generated-routes.md
  • Not run: full scripts/ci-local.sh; Java/Kotlin/C#/Python secret-field runners; Cloudflare Workers example run as written; drizzle-kit re-export run as written; auth-scope mount example run as written
⚠️ **Document** - 1 warning
  • ⚠️ spec/metamodel/ui.json:11 - The registered @formExclude description says only "omitted from generated forms". Since Generated forms and write routes expose credential fields; @formExclude does not block writes #484 the generated create, PATCH and PUT routes also ignore the field, and reads of it are unchanged. The same string is copied into server/{typescript,python,csharp,java} registry sources, fixtures/registry-conformance/expected-registry.json, fixtures/metamodel-docs/expected/types/field.md and site-reference/types/field.html.

🔧 No changes applied.
1 warning still open:

  • ⚠️ spec/metamodel/ui.json:11 - Registered @formExclude description says only "omitted from generated forms". Generated create, PATCH and PUT routes also ignore the field. Same string is copied into the TS, Python, C# and Java registry sources, fixtures/registry-conformance/expected-registry.json, and the generated fixtures/metamodel-docs/expected/types/field.md and site-reference/types/field.html. Not fixed in this phase: the owner is registry source and conformance fixture (executable behavior, tests), which this phase may not edit. Fix: one identical sentence in every registry source and expected-registry.json, then regenerate the two docs with the repo generator. Suggested text: 'Omit the field from generated forms and ignore it in the body of generated create, PATCH and PUT routes (reads are unchanged).' Do not say it is hidden from responses.
✅ **Lint** - passed

✅ No issues found.

✅ **Push** - passed

✅ No issues found.

…lds on write, never return credentials (#484)

A field marked @formExclude, or a credential (a field declaring a view.password
child), is now ignored by the generated create, PATCH and PUT in every port and
a credential is never returned by list, get, write echoes, M:N targets or TPH
rows. Generated forms and api docs omit credentials too. Pinned by the new
api-contract secret-fields/ sub-corpus (generated lane, all five ports).

Also: meta gen / meta verify --codegen refuse an unknown codegen dialect
("d1" points at "sqlite"; meta init --d1 scaffolds "sqlite"), and five docs
recipes (drizzle-kit migrations, Workers + D1 per-request example, guarding
generated routes, proving a gate fails).

Fixes #484
…hon redaction covers dataclass and mapping rows (#484)
…er boundary, prove denied writes on stored rows (#484)
@dmealing

dmealing commented Oct 28, 2026 •

Copy link
Copy Markdown
Member Author

Fix is up in #503.

Which ports were affected: all five. On main the new fixtures/api-contract-conformance/secret-fields/ corpus (generated lane) failed in TypeScript, Java, Kotlin, Python and C#, both scenarios in each: a GET returned pinHash, and a PATCH wrote the forged value into tokenHash and pinHash. After the fix both scenarios pass in all five ports, and the write path is also asserted by reading the stored row back.

Existing vocabulary only, no new attributes. A credential is a field that declares or inherits a child view.password. A field is write-denied when its effective @formExclude is true or it is a credential. Generated create, PATCH and PUT ignore write-denied fields (stripped, never a 400, like @mutability: writeOnce). A credential key is omitted from list, get, write echoes, M:N targets and TPH rows. A @formExclude-only field is still returned. Filter and sort allowlists are unchanged.

Judgment calls to review:

  • view.password is the credential marker because it is the only credential vocabulary that exists. There is no override to let a generated route write a denied field; hand-write the route for that.
  • Behaviour change for upgraders: a @required field that is also write-denied makes the generated create fail, because the value is dropped. This is in the CHANGELOG entry and docs/features/api-contract.md.
  • Served reports are redacted in every port: a report that groups by a credential, or takes its min or max, omits the column from the list route and its export, and the column is neither filterable nor sortable.
  • Some TPH shared-column cases, where one subtype marks a column and another does not, are pinned by codegen text checks, not an HTTP scenario.

The same PR also carries the five docs recipes and a clear error for dialect: "d1" (use "sqlite"). The Workers/D1, drizzle-kit and auth-scope recipes are typechecked, not run.

…#484)

A report column is derived from its @Of field and lost its view.password, so a report
that groups by a credential, or takes its min or max, served it from the list route and
the export. A derived field over a credential is now a credential in every port: the list
route omits the key and an export names no column for it. Also documents it in the
CHANGELOG entry for #484, the api-contract and reporting guides.
…rtable (#484)

A served report marks every derived field filterable and sortable, so a column hidden from
the response could still be probed with a like-prefix filter or a sort, recovering the secret
one prefix at a time. A derived field over a credential now carries no filter or sort entry
in every port. Also documents it in the CHANGELOG, api-contract and reporting guides.
@dmealing dmealing changed the title fix(codegen-ts): stop generated routes writing or returning credential and excluded fields fix: keep credential and excluded fields out of generated routes, forms and reports Oct 29, 2026
@dmealing
dmealing merged commit 1dfb18a into main Oct 29, 2026
1 check passed
@dmealing
dmealing deleted the fm/mo-adopter-fixes branch October 29, 2026 00:45
dmealing added a commit that referenced this pull request Oct 30, 2026
)

* fix: replace regex and string-rewrite matching with authoritative parsers across ports (audit findings 5, 7-15, 17, 18)

Currency parsing via Intl, SQL splitter, view comparators, Python YAML 1.2,
Mustache tag scanning, forgiving XML reader, JSONC-aware upgrade rewriter,
leak-scan renames, Java authored-name serialization, ERR_COLUMN_NAME_COLLISION,
pnpm workspace parsing and assorted smaller instances.

* fix: import the shared support class in the Spring and Kotlin filter-allowlist generators

An ejected copy is package-renamed, so same-package bare-name resolution
stopped compiling once #503 started calling the support class there.

* fix: Roslyn namespace/handoff detection, ASCII Pydantic patterns, route-segment collision (audit finding 18 follow-ups)

* feat: enum strip fold-collision error, numeric CSV exemption, one filter glob grammar in Java

ERR_ENUM_NORMALIZE_COLLISION in all ports; CSV formula guard skips strict
numeric literals; <filters> use Scope.compilePattern semantics and fail
generation when a pattern would select fewer objects than before.

* fix(render,cli): decode every JSON escape and close fences properly in extract; make eject --undo order-sensitive

The forgiving JSON reader dropped \uXXXX, \b and \f escapes and the fence matcher
closed at the first inner triple backtick, in all five ports. eject --undo treated a
copy whose only edit was a reorder as stock (TS, Python, C#). Python LONG coercion no
longer rounds through float. Adds three shared extract-conformance cases.

* fix: address pre-gate review findings (schema-valid pattern, caches, workspace walk, splitter dialect, constraint fallback)

* refactor: simplify pass over the audit follow-up changes

* no-mistakes(review): apply reviewer inline fixes

* no-mistakes(review): Fix XML child loss, eject whitespace, delimiter verify, JPY parse

* no-mistakes(review): apply reviewer inline fixes

* no-mistakes(review): Drop C# source inference and Roslyn; tighten delimiter checks

* no-mistakes(review): Pass inlinedAs only for partials; root ref renders

* no-mistakes(review): Add csharp.namespace config, match pnpm globs, fix {{=}} crash

* no-mistakes(review): Reject non-identifier derived C# namespace segments

* no-mistakes(review): apply reviewer inline fixes

* no-mistakes(review): Resolve C# namespaces via package bindings; require Pydantic 2.8

* no-mistakes(review): Validate all package-binding namespaces as C# identifier paths

* no-mistakes(review): Accept csharp.namespace in TS config schema; explicit handoff note

* no-mistakes(review): apply reviewer inline fixes

* no-mistakes(review): Skip pnpm test dirs, reject C# keyword namespaces

* no-mistakes(document): Fixed four stale doc facts from the regex-followups change: Java <filters> upgrade example (java.md and CHANGELOG), eject reorder wording (own-your-codegen.md), CSV numeric exemption (reporting.md). Edits verified by grep and diff stat.

* no-mistakes(document): Fixed document-1: removed the stale single-Math.round claim from docs/ports/typescript-client.md, the parseCurrency header in currency.ts, and the currency-input.tsx comment. Doc and comment edits only; no remaining findings.

* no-mistakes(lint): Update AGENTS.md and site payload fixture count to 421
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant