Skip to content

fix(nextly): a Single read hides a denied field from its hooks - #1800

Merged
mobeenabdullah merged 10 commits into
mainfrom
fix/single-read-redacts-before-field-hooks
Sep 11, 2026
Merged

mobeenabdullah merged 10 commits into
mainfrom
fix/single-read-redacts-before-field-hooks

Conversation

@mobeenabdullah

@mobeenabdullah mobeenabdullah commented Sep 11, 2026 •

Copy link
Copy Markdown
Collaborator

What

A Single read now applies field-level access.read rules before its field-level afterRead hooks and again after them, sharing one redactions store and one grants resolver: the two passes a collection read has always run.

The defect

single-query-service.ts ran runFieldHooks on the assembled document and applyFieldReadAccess only afterwards. So a denied field's own afterRead ran and was handed its value, and a hook on an allowed sibling could read the denied value from data and copy it onto its own field, where the later redaction would not look. The response was redacted, but app code had already seen the value. The collection read had redacted before and after its hooks since it existed. Surfaced while documenting the hook order for #1777.

The principle

Access decides on what is stored; hooks shape what is returned.

The first pass decides every field on the stored document. The second pass, after the hooks, exists to catch a denied key a hook put back, which it does by name: the key's own rule runs on the value the hook wrote. It does not re-judge a value the first pass allowed against a condition a hook changed, which would be a hook influencing access; the collection read has always behaved this way. This is how Payload orders read access and afterRead.

Both passes share one ReadAccessRedactions store and one readAccessGrants lookup, so the caller's roles and permissions are read once and both passes judge with one authority.

The document-rule half, and why it is gone

This PR also moved a Single's custom document-level read rule to before the hooks (founder decision, option A, after five review rounds showed that reconstructing redacted values for a post-hook rule could not be done soundly).

While this PR was open, #1659 (decide access with one mechanism, not two) removed the stored access-rule engine outright: AccessControlService, the StoredAccessRule types, and the access_rules column on dynamic_collections and dynamic_singles. A Single's document-level rule was that engine's only authored surface, so there is no longer a rule for this step to judge. The merge of main therefore drops that half: the 7.5 judgement, the two tests that wrote access_rules directly, and its sentence in the docs and the changeset. The fixture and custom-read-constraint.integration.test.ts were deleted by #1659 itself.

The principle above is unchanged and is still why the two field-access passes sit where they do.

Tests

single-read-redacts-before-field-hooks.integration.test.ts, through findSingle: a denied field's hook does not run and its sibling's hook sees undefined; a hook that reintroduces the denied value is stripped; the positive control, a user the rule allows, sees the hook run with the value. A unit case pins one grants lookup across both passes.

Control run against origin/main's single-query-service.ts: the mechanism test fails. All 15 Singles integration suites pass and the package builds.

Patch changeset, every package.

Closes the ledger's single-read-redacts-after-field-hooks.

The Single read ran afterRead before field read access, so a denied
field's own hook ran with its value and a hook on an allowed sibling
could read it. It now runs the two passes a collection read runs, one
before the hooks and one after, sharing a redactions store; the
document-level rule judges a snapshot with the removed values restored
as evidence, so a rule that inspects a denied field still sees it.
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Sep 11, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-09-11T23:40:33.893765Z 88da9dc Manual request
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@mobeenabdullah

Copy link
Copy Markdown
Collaborator Author

@codex review

@coderabbitai

coderabbitai Bot commented Sep 11, 2026 •

Copy link
Copy Markdown

Warning

Review limit reached

Next included review available in 8 minutes.

Check out review usage here.

View limit details

Limit details: You’ve used the included review currently available.

You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository.

Learn how review limits work.

Review configuration:

⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Advanced

Run ID: e4d1b71c-2feb-4dc2-9f08-d71d10abba9c

📥 Commits

Reviewing files that changed from the base of the PR and between a5bf4d1 and 88da9dc.

⛔ Files ignored due to path filters (1)
  • .changeset/a-single-read-hides-a-denied-field-from-its-hooks.md is excluded by !.changeset/**
📒 Files selected for processing (5)
  • docs/configuration/fields.mdx
  • packages/nextly/src/domains/singles/__tests__/single-read-redacts-before-field-hooks.integration.test.ts
  • packages/nextly/src/domains/singles/services/single-query-service.ts
  • packages/nextly/src/shared/lib/__tests__/field-level-registry.test.ts
  • packages/nextly/src/shared/lib/field-level-registry.ts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@mobeenabdullah

Copy link
Copy Markdown
Collaborator Author

Docs note: #1777 documents the Single read's current ordering (hooks first, redaction after) in docs/configuration/fields.mdx. That sentence lives only on #1777's branch, so it cannot be updated here without carrying that PR. Whichever of the two merges second, a one-line follow-up makes the docs say both read paths redact before their field hooks; I will open it then.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: ddaaaf5918

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread packages/nextly/src/domains/singles/services/single-query-service.ts Outdated
…ook may replace

A hook that returns a fresh container leaves the redaction store's row
keys pointing at objects the document no longer holds, so the judge lost
the evidence for a denied nested value. The removed values are captured
by path right after the first pass and put back onto the judge's detached
copy by path; the response object is never touched.
@mobeenabdullah

Copy link
Copy Markdown
Collaborator Author

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 667d1a694e

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread packages/nextly/src/shared/lib/field-level-registry.ts Outdated
Comment thread packages/nextly/src/domains/singles/services/single-query-service.ts Outdated

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 92b832d151

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread packages/nextly/src/shared/lib/field-level-registry.ts Outdated
… ids, and into the second pass

Capture reads the rows of a container the pass removed whole from the
removed value, so a denied child's evidence survives its parent's
removal. Rows sharing an id get an occurrence suffix. The second
field-access pass restores the path-keyed evidence first and removes
again what it put back, so a container a hook rebuilt is judged against
what the first pass removed from it.
@mobeenabdullah

Copy link
Copy Markdown
Collaborator Author

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 249b0a3fea

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread packages/nextly/src/shared/lib/field-level-registry.ts Outdated
Comment thread packages/nextly/src/shared/lib/field-level-registry.ts Outdated
…s a rule's writes

A row the capture saw is restored from the path it had then, so a hook
that reorders id-less rows does not move their evidence; only a repeater
rebuilt row by row and reordered is left to position. Evidence values
are detached on capture and on every restore, so what a rule writes into
its argument never reaches the pass after it.
@github-actions

github-actions Bot commented Sep 11, 2026 •

Copy link
Copy Markdown
Contributor

Whole-Repository Code Hygiene Summary

Full dead-code, duplication, and complexity report for the PR branch as it stands now. Playground is excluded. Quality gate enforcement on introduced issues is performed by the Changed files job.

🌿 Fallow

Warning

Review needed

⚠️ 73 code issues · ⚠️ 685 clone groups · ⚠️ 1031 health findings

See inline review comments for per-finding details.

Code issues (73)
Category Count
Unused files 2
Unused exports 5
Unused dependencies 19
Unused devDependencies 6
Unresolved imports 2
Unlisted dependencies 1
Circular dependencies 38
Duplication (685 groups · 28738 lines · 4.1%)
Locations Lines Tokens
schemas/_dialect-bundles/mysql.relations.ts:40-134
schemas/_dialect-bundles/postgres.relations.ts:40-134
schemas/_dialect-bundles/sqlite.relations.ts:40-134
95 593
cli/commands/db-sync-demote.ts:70-75
cli/commands/db-sync-promote.ts:38-43
cli/commands/dev-build.ts:100-105
cli/commands/dev-build.ts:179-184
cli/commands/dev-build.ts:299-304
cli/commands/dev-build.ts:411-416
cli/commands/dev-build.ts:552-557
cli/commands/dev-server.ts:575-580
cli/commands/dev-server.ts:840-845
cli/commands/dev-server.ts:1143-1148
cli/commands/migrate-field-groups.ts:110-115
6 70
entries/EntryList/EntryTableSkeleton.tsx:74-98
collection/components/CollectionTableSkeleton.tsx:94-118
field-group/components/FieldGroupTableSkeleton.tsx:90-114
plugins/components/PluginsTableSkeleton.tsx:86-110
singles/components/SinglesTableSkeleton.tsx:77-101
src/components/table-skeleton.tsx:100-124
25 89
collections/config/validate-config.ts:380-433
field-groups/config/validate-field-group.ts:185-238
singles/config/validate-single.ts:190-243
54 152
dispatcher/handlers/collection-dispatcher.ts:925-967
field-groups/services/field-group-table-provisioning.ts:186-236
singles/services/reconcile-single-companion.ts:110-160
51 149

… and 680 more groups.

Across 422 files.

Complexity (1031 functions above threshold)
File Function Severity Cyclomatic Cognitive CRAP Lines
singles/services/single-mutation-service.ts:937 <arrow> critical 245 ! 307 ! 13210.4 ! 1588
collections/services/collection-mutation-service.ts:6110 <arrow> critical 169 ! 160 ! 6338.2 ! 1261
src/init/reload-config.ts:1319 applyReload critical 144 ! 228 ! 4623 ! 1433
shared/lib/entry-validation.ts:223 validateFieldValue critical 109 ! 157 ! 2675.3 ! 432
blocks-engine/src/measure-bytes.ts:646 surveyDocument critical 102 ! 250 ! 137.1 ! 658

5017 files, 76718 functions analyzed (thresholds: cyclomatic > 20, cognitive > 15, CRAP >= 30)

Codebase health

Metric Value
Maintainability 91.7 / 100
Avg complexity 1.8

Tip

Run fallow fix --dry-run to preview auto-fixes.
Add /** @public */ above exports to preserve them.

@mobeenabdullah

Copy link
Copy Markdown
Collaborator Author

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: b399573487

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread packages/nextly/src/shared/lib/field-level-registry.ts Outdated
Comment thread packages/nextly/src/shared/lib/field-level-registry.ts Outdated
Comment thread packages/nextly/src/domains/singles/services/single-query-service.ts Outdated
@pkg-pr-new

pkg-pr-new Bot commented Sep 11, 2026 •

Copy link
Copy Markdown

Open in StackBlitz

@nextlyhq/adapter-drizzle

npm i https://pkg.pr.new/@nextlyhq/adapter-drizzle@274db50

@nextlyhq/adapter-mysql

npm i https://pkg.pr.new/@nextlyhq/adapter-mysql@274db50

@nextlyhq/adapter-postgres

npm i https://pkg.pr.new/@nextlyhq/adapter-postgres@274db50

@nextlyhq/adapter-sqlite

npm i https://pkg.pr.new/@nextlyhq/adapter-sqlite@274db50

@nextlyhq/admin

npm i https://pkg.pr.new/@nextlyhq/admin@274db50

@nextlyhq/admin-css

npm i https://pkg.pr.new/@nextlyhq/admin-css@274db50

@nextlyhq/blocks-engine

npm i https://pkg.pr.new/@nextlyhq/blocks-engine@274db50

@nextlyhq/blocks-react

npm i https://pkg.pr.new/@nextlyhq/blocks-react@274db50

@nextlyhq/builder

npm i https://pkg.pr.new/@nextlyhq/builder@274db50

create-nextly-app

npm i https://pkg.pr.new/create-nextly-app@274db50

@nextlyhq/eslint-plugin

npm i https://pkg.pr.new/@nextlyhq/eslint-plugin@274db50

nextly

npm i https://pkg.pr.new/nextly@274db50

@nextlyhq/plugin-form-builder

npm i https://pkg.pr.new/@nextlyhq/plugin-form-builder@274db50

@nextlyhq/plugin-page-builder

npm i https://pkg.pr.new/@nextlyhq/plugin-page-builder@274db50

@nextlyhq/plugin-sdk

npm i https://pkg.pr.new/@nextlyhq/plugin-sdk@274db50

@nextlyhq/plugin-seo

npm i https://pkg.pr.new/@nextlyhq/plugin-seo@274db50

@nextlyhq/storage-s3

npm i https://pkg.pr.new/@nextlyhq/storage-s3@274db50

@nextlyhq/storage-uploadthing

npm i https://pkg.pr.new/@nextlyhq/storage-uploadthing@274db50

@nextlyhq/storage-vercel-blob

npm i https://pkg.pr.new/@nextlyhq/storage-vercel-blob@274db50

@nextlyhq/ui

npm i https://pkg.pr.new/@nextlyhq/ui@274db50

commit: 274db50

@github-actions github-actions Bot added scope: core nextly type: docs Documentation only labels Sep 11, 2026
…th one grants lookup

The pass records what it removes by path while the rows it parsed still
exist, so a container it serialised back to a JSON string keeps its
nested evidence; the walk taken afterwards is gone. Row segments encode
the id, so an id that looks like an occurrence marker cannot collide.
Both passes over a Single share one grants resolver, so the caller's
roles and permissions are read once and judged with one authority.
@mobeenabdullah

Copy link
Copy Markdown
Collaborator Author

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 375c15d7c7

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread packages/nextly/src/shared/lib/field-level-registry.ts Outdated
Comment thread packages/nextly/src/shared/lib/field-level-registry.ts Outdated
Comment thread packages/nextly/src/shared/lib/field-level-registry.ts Outdated
Comment thread packages/nextly/src/domains/singles/services/single-query-service.ts Outdated
…p the evidence layer

Access decides on what is stored; hooks shape what is returned. The
custom document-level read rule is judged on the assembled stored
document before any afterRead hook and before field access, so it sees
denied values without reconstructing them onto whatever the hooks
returned. The path-keyed evidence machinery that reconstruction needed
is removed. The two field-access passes around the field hooks stay,
sharing one redactions store and one grants resolver, as on the
collection read.
@mobeenabdullah

Copy link
Copy Markdown
Collaborator Author

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 274db505d6

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

…ts-before-field-hooks

# Conflicts:
#	packages/nextly/src/domains/collections/__tests__/_fixtures/single-read-rule.ts
#	packages/nextly/src/domains/singles/services/single-query-service.ts
@mobeenabdullah

Copy link
Copy Markdown
Collaborator Author

Merged main (88da9dc). The document-rule half of this PR is gone, because the mechanism it reordered no longer exists.

#1659 (refactor(nextly): decide access with one mechanism, not two) removed the stored access-rule engine: AccessControlService, the StoredAccessRule types and the access_rules column on dynamic_collections and dynamic_singles. A Single's document-level custom read rule was that engine's only authored surface. With it deleted, the step this PR moved to 7.5 has nothing to judge, and the fixture and custom-read-constraint.integration.test.ts were deleted by that same commit.

So the merge keeps the half that still has a mechanism and drops the half that does not:

  • Kept: field-level access.read applied before the field hooks and again after, sharing one ReadAccessRedactions store and one readAccessGrants lookup, as the collection read has always done. This is the defect that started the PR: a denied field's own afterRead ran and saw the value, and a sibling's hook could copy it.
  • Dropped: the 7.5 document-rule judgement, the two tests that set access_rules directly, and the sentence about it in docs/configuration/fields.mdx and in the changeset.

The founder's decision (access decides on what is stored, hooks shape what is returned) is unchanged and still the reason the two field-access passes sit where they do; there is simply no document-level rule left for it to govern here.

Verification: the three remaining tests pass; against origin/main's single-query-service.ts the mechanism test ("does not run a denied field's afterRead, and hides it from a sibling's") fails. All 15 Singles integration suites pass, and the package builds.

@codex review

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. Nice work!

Reviewed commit: 88da9dcfaf

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

@mobeenabdullah
mobeenabdullah merged commit e8e8d78 into main Sep 11, 2026
16 of 17 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

scope: core nextly type: docs Documentation only

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant