refactor(nextly): decide access with one mechanism, not two - #1659
Conversation
|
Too many files changed for review (159 files, 100 file limit). Bypass the limit by tagging |
|
Important Review skippedToo many files! This PR contains 157 files, which is 57 over the limit of 100. To get a review, reduce the PR to 100 files or fewer by splitting it into smaller PRs or changing its base branch. Upgrade to a paid plan to raise the limit. This review couldn't start because sufficient usage credits or metered capacity aren't available. Add credits or update usage-based reviews in the billing tab, then retry. ⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: CHILL Plan: Advanced Run ID: ⛔ Files ignored due to path filters (2)
📒 Files selected for processing (157)
You can disable this status message by setting the 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. Comment |
@nextlyhq/adapter-drizzle
@nextlyhq/adapter-mysql
@nextlyhq/adapter-postgres
@nextlyhq/adapter-sqlite
@nextlyhq/admin
@nextlyhq/admin-css
@nextlyhq/blocks-engine
@nextlyhq/blocks-react
@nextlyhq/builder
create-nextly-app
@nextlyhq/eslint-plugin
nextly
@nextlyhq/plugin-form-builder
@nextlyhq/plugin-page-builder
@nextlyhq/plugin-sdk
@nextlyhq/plugin-seo
@nextlyhq/storage-s3
@nextlyhq/storage-uploadthing
@nextlyhq/storage-vercel-blob
@nextlyhq/ui
commit: |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 68692b3216
ℹ️ About Codex in GitHub
Codex has been enabled to automatically 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 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
|
Hi Faisal — I read this one closely. The idea is right and most of the execution is good, so I want to be specific about what to keep and what to change, and why. First, what you got rightI checked your central claim instead of taking your word for it, and it holds: collections really do have no way to author stored rules — no config option, no admin UI, no REST field. The only mention anywhere in the admin package is an old changelog line. So "two engines, and nobody can configure one of them" is a fair description, and deleting the unreachable one is the right call. Three things I'd single out:
The one thing that has to change before this mergesYour release note opens with:
and the second bullet under it is:
Those two statements contradict each other, and the second one is the important one. Let me spell out what it means for a real site. Picture a customer who set an That is not narrowing. That is widening, and widening access is the one kind of change that has to be announced loudly, because an operator reading "all narrowing" will upgrade without checking anything. Nothing about your code is wrong here — removing the filter follows necessarily from removing the engine. The problem is only that the release note tells operators the opposite of what will happen. What to do: rewrite that section to say plainly that one change widens access, name who it affects (installations whose retained The gap right next to itFollow that scenario one step further. You keep the From their side: they set up access rules, the rules are still visibly there in the database, and they have silently stopped working. What to do: when Nextly starts, if that column contains any data, log a warning saying these rules are no longer enforced and pointing at the release note. There's a pattern for exactly this already — see This is the single change that turns a risky upgrade into a safe one, so I'd treat it as part of the same piece of work as the release note. Three smaller things1. The docs still describe the engine you deleted. The scoped access-control guide still tells readers that 2. A row can vanish from a relationship that the user can read directly. When expanding a relationship you evaluate the target's 3. Access is evaluated once per row. A relationship with 500 rows makes 500 authorization calls for what is one collection-level verdict, all fired at once. If someone's (Items 2 and 3 are the two P2s Codex left — I'm repeating them because they're still open, not to pile on.) AlsoThe branch now conflicts with main and is about a day behind. Rebase before the next round, or the conflicts will grow. Your test deletions look right, by the way — I checked the scariest-looking one ( The checklist
The one thing worth taking awayEverything above comes back to a single habit. A release note is a claim about behaviour, and it needs checking against the diff the same way a test does. "All narrowing" was written once at the top and then contradicted three lines later — not because you didn't understand the change, but because nothing forced the summary to be re-read against the list underneath it. So before you push a change with a release note: read the note and the diff side by side, and for each sentence ask "which line of code makes this true?" If you can't point at one, it isn't verified yet. The engineering here is solid. It's the description that needs to catch up with it — and that's a much easier fix than the other way round. |
68692b3 to
391bf29
Compare
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 391bf29ef0
ℹ️ About Codex in GitHub
Codex has been enabled to automatically 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 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
|
Hi Faisal — I re-read the new head (
So the two blocking items from my review are done. Nice work. Codex found three more on this head, and they are one problem wearing three coatsI verified each against the code — they're real. Here's the shape in plain words. Your refactor made the collection gate ask a new question for anonymous callers: "does this collection's own But three other places answer the same question, and none of them were told about the new rule: 1. The Single gate (P1) — 2. Relationship population (P1) — 3. API-key vocabulary (P2) — Why these matter more than they look: every one of them is a case where a visitor sees more through one door than another — the widening class, which is the whole reason this PR needed the release-note change. And they're all the same shape as the finding you already fixed: the same question answered by more than one piece of code, and the copies drifted. What to doMake the anonymous decision live in one place and have all three doors call it:
Then, the test that proves it: for each door, one test where a target/Single declares That's the whole remaining list from my side. Once these three are in, I'd be comfortable with this merging. |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 1c30dc6d5e
ℹ️ About Codex in GitHub
Codex has been enabled to automatically 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 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 691b2ea78e
ℹ️ About Codex in GitHub
Codex has been enabled to automatically 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 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 4a530f5724
ℹ️ About Codex in GitHub
Codex has been enabled to automatically 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 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
|
Checked
All three are in. As I said at 07:22Z, I am comfortable with this merging on that basis. What I did not gate on: CI for this head — as of 11:20Z several jobs still read queued/in-progress, so read the jobs before merging rather than the rollup. |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 1a51478e36
ℹ️ About Codex in GitHub
Codex has been enabled to automatically 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 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
1a51478 to
3f16e87
Compare
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 3f16e87f4b
ℹ️ About Codex in GitHub
Codex has been enabled to automatically 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 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
| targetCompanions: options.targetCompanions ?? new Map(), | ||
| // And the access verdicts, likewise: the caller is the same on every hop, | ||
| // so a target already judged for it is not asked again. | ||
| targetVerdicts: options.targetVerdicts ?? new Map(), |
There was a problem hiding this comment.
Forward target verdicts through component expansions
When a list or detail read populates repeatable components, the caller supplies one shared targetVerdicts map, but field-group-query-service.ts forwards targetPolicies and targetCompanions while omitting this new cache. Every component instance therefore reaches this fallback with options.targetVerdicts undefined and allocates its own map; hundreds of components referencing the same target can launch hundreds of identical asynchronous access.read callbacks concurrently, and a stateful or transient callback can produce inconsistent admission within one response. Forward access.targetVerdicts alongside the other two caches.
AGENTS.md reference: packages/nextly/AGENTS.md:L31-L37
Useful? React with 👍 / 👎.
3f16e87 to
bed0dac
Compare
Two independent systems answered "may this caller do this", and only one of them was configurable. The code-defined `access` on a collection's or Single's own config is reached from every transport. The stored rules were a second engine -- five rule types evaluated against an `access_rules` JSON column -- with no way to author them: collections had no code, UI or REST surface for the column at all, and a Single had only an undocumented REST field. Two evaluators for one question is a divergence waiting to be found in the gap between them, and the half nobody could configure is the half nobody was checking. So the second engine is removed rather than reconciled. `AccessControlService` and its five evaluators, the `StoredAccessRule` / `CollectionAccessRules` / `SingleAccessRules` types, the operation constants that only served them, and the `access_rules` column on `dynamic_collections` and `dynamic_singles` all go. `AccessOperation` and `ACCESS_OPERATIONS` stay: the RBAC gate is keyed on them. `ExpressionValidator` goes with them, having had no importer at all. Three behaviours change. Two narrow; one widens, and it is the one to read before upgrading. Stored rules are no longer enforced. Any value still in `access_rules` is ignored from this release on. That changes nothing for an installation that never wrote the column, which is most of them; it does change behaviour for one that set a Single's rules through the undocumented REST field or wrote the column directly, and the widest case is an `owner-only` read. That filter was produced by the stored evaluator alone, so a list, count or by-id read that returned only the caller's own rows now returns every row the coarse gate admits for the collection. Any rule still needed is expressed as code-defined `access` before upgrading. An installation whose database still holds rules is told so at startup, with the tables and row counts. An anonymous publish or unpublish is refused outright. It previously fell through to a rule-less public default unless an explicit stored `publish` rule denied it, so a collection with no rules -- which was every collection -- let an unauthenticated caller move a document into the published state. Populating a relationship judges the TARGET COLLECTION for the caller, once per expansion rather than once per row, by evaluating that collection's own code-defined `access.read` with the context a direct read builds: a session caller's rule sees their real roles and effective permissions, a scoped API key's rule sees the key's own grants. The RBAC permission gate is deliberately not consulted there, for the reason it never was: requiring a `read-<target>` grant naming a collection the caller never asked for by name would empty the relationship for every caller whose grants do not list it. A new database never gets the `access_rules` column. One that already has it keeps it, and every schema entry point reports it rather than dropping it: `nextly migrate` refuses and names it, the dev-server reconcile blocks the drop and says so, and `NEXTLY_ALLOW_CORE_DESTRUCTIVE=1` is how an operator removes it. Dropping a column that holds configured rules is their decision, not a side effect of upgrading. One upgrade is not supported and is called out rather than papered over. A database old enough to be missing core columns added since -- a 0.45-era install -- reconciles a drop and several adds on one table, which drizzle-kit pairs to ask whether the drop is a rename; `pushSchema` builds that resolver internally with no way to supply a hints handler, so it throws and the boot degrades to its additive-tables-only baseline. Such a database should run `nextly migrate` before taking this release. `upgrade-sim-045` is skipped for that reason, with the fix that restores it named in its header. Review rounds folded in: The upgrade and boot-warning integration tests run on every dialect with a database in the run, through one fresh-database helper. The Single gate and relationship expansion ask the collection's own code-defined rule for a caller with no session, as the collection gate does. A key's rule sees its grants in the spelling a rule reads and the key's own roles, on every path. The target verdict is decided once per expansion, before any target query. The boot warning states the whole RBAC decision order and is derived from the drift check's snapshot in one bounded pass. A Single's working-draft overlay is read under the live document's related-row access. The scaffold's agent guide and the changeset describe the model that remains. A health baseline generated from `main` keeps the changed-files gate from refusing a change that shrinks a function `main` already carries above threshold.
bed0dac to
80db282
Compare
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 80db282580
ℹ️ About Codex in GitHub
Codex has been enabled to automatically 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 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
| // 1.5. Load the current document first (no auto-create yet), then run the | ||
| // access check before any hooks/DB writes. |
There was a problem hiding this comment.
Check Single update access before reading its row
When a Direct API or plugin call uses overrideAccess: false without prior route authorization and its update rule or RBAC denies, this still selects the live Single row before calling checkSingleAccess. Since this change removes every document-dependent stored rule, the access verdict no longer needs existingDoc; move the gate ahead of selectOne so denied callers do not query the protected table or tie up a slow database before rejection.
AGENTS.md reference: AGENTS.md:L308-L311
Useful? React with 👍 / 👎.
| // caller above does not lift this — the operation has no identity to stamp | ||
| // — so an unauthenticated caller is refused outright rather than falling | ||
| // through to the permission-less default below. | ||
| if (!user && (operation === "publish" || operation === "unpublish")) { |
There was a problem hiding this comment.
Reject anonymous transitions before invoking access callbacks
When a caller with no user attempts publish or unpublish under overrideAccess: false, the earlier anonymous branch invokes the collection's arbitrary code-defined callback before this unconditional 403. An async rule may perform external side effects or expensive work even though its result can never admit the operation; move this guard ahead of the RBAC/code-access branch.
AGENTS.md reference: AGENTS.md:L308-L311
Useful? React with 👍 / 👎.
| // Quiet because the check ran and found nothing, not because it | ||
| // failed on the way. | ||
| expect(logger.debug).not.toHaveBeenCalledWith( | ||
| expect.stringContaining("Could not check for stored access rules") |
There was a problem hiding this comment.
Assert against the diagnostic's actual failure message
When either boot diagnostic fails, runBoundedDiagnostic logs [nextly] Could not check core schema state: ..., not the substring asserted here. A failure before the retired-rule count therefore produces no warning and still satisfies both not.toHaveBeenCalledWith assertions, so these tests labeled as distinguishing a healthy quiet result from a failed check go green on broken wiring; assert the actual failure prefix or add a positive probe that proves the diagnostic ran.
AGENTS.md reference: AGENTS.md:L228-L233
Useful? React with 👍 / 👎.
Access is decided by one mechanism, not two
Two independent systems answered "may this caller do this", and only one of them was configurable. The code-defined
accesson a collection's or Single's own config is reached from every transport. The stored rules were a second engine — five rule types evaluated against anaccess_rulesJSON column — with no way to author them: collections had no code, UI or REST surface for the column at all, and a Single had only an undocumented REST field.Two evaluators for one question is a divergence waiting to be found in the gap between them, and the half nobody could configure is the half nobody was checking. So the second engine is removed rather than reconciled.
AccessControlServiceand its five evaluators, theStoredAccessRule/CollectionAccessRules/SingleAccessRulestypes, the operation constants that only served them, and theaccess_rulescolumn all go.AccessOperationandACCESS_OPERATIONSstay — the RBAC gate is keyed on them.ExpressionValidatorgoes too, having had no importer at all.Behaviour changes — two narrow, one WIDENS
Stored rules are no longer enforced, and that widens access for some installations. Any value still in
access_rulesis ignored from this release on. For most installations that changes nothing, because nothing could write the column. It does change behaviour for one that set a Single's rules through the undocumented REST field, or wrote the column directly — and the widest case is anowner-onlyread. That filter was produced by the stored evaluator alone, so a list, count or by-id read that used to return only the caller's own rows now returns every row the coarse gate admits for the collection. Before upgrading, such an installation should express any rule it still needs as code-definedaccesson the collection's or Single's config; that is the only place a rule is read now.An installation whose database still holds rules is told so at boot. The warning states the whole decision order — super-admin bypass, then a code-defined rule for the operations it names, then database role grants for the rest — so nobody reads their role grants as ignored. On the initialized-boot path, beside the existing core-schema drift warning, Nextly introspects the two registry tables, and when the retired column is present and holds a value on any row,
logger.warns the tables and row counts, says the rules are no longer enforced, and namesnextly migrate/NEXTLY_ALLOW_CORE_DESTRUCTIVE=1as the way to remove the column. A new database, or one that has dropped the column, never sees it. It is derived from the same live snapshot the core-schema drift check reads, in the same bounded pass (runCoreSchemaChecks,init/bounded-diagnostic.ts) — one introspection, one two-second bound — so a stalled or failing query is a debug line, never a reason the boot waits or fails.An anonymous publish or unpublish is refused outright. It previously fell through to a rule-less public default unless an explicit stored
publishrule denied it — so a collection with no rules, which was every collection, let an unauthenticated caller move a document into the published state.Populating a relationship judges the TARGET COLLECTION for the caller, once per expansion rather than once per row, by evaluating that collection's own code-defined
access.readwith the same context a direct read builds. A session caller's rule sees their real roles and effective permissions (viacheckAccess, which builds the canonical context); a scoped API key's rule sees the key's own grants. So every row of one target is admitted or withheld together, a rule that reaches out to a service is asked once per batch rather than once per row, and a row the caller can read directly does not vanish from a relationship pointing at it. The RBAC permission gate is deliberately not consulted there, for the reason it never was: requiring aread-<target>grant naming a collection the caller never asked for by name would empty the relationship for every caller whose grants do not list it. An absent rule admits; a throwing rule denies.The anonymous decision lives in one place and every door asks it.
main's #1704 made the collection gate consult a collection's own code-defined rule for a caller with no session (checkAnonymousCodeAccess, handeduser: null). Two other doors were still answering that question the old way: the Single gate returned "allowed" at!userbefore any rule was read, andjudgeTargetadmitted every anonymous caller on a premise the rebase had made false. Both now ask the same question against the Single or the target and fall through to the public default only when no rule names the operation; an anonymous publish stays refused whatever a rule says.A key's rule sees one permission spelling and the key's own roles on every path.
main's #1772/#1783 landed the same design independently (ReadAccessCallercarriespermissionsandrulePermissions,code-access.tsas the shared leaf, scope roles first); this branch now sits on that and adds only the translation-worklist read going throughreadAccessCaller, and the relationship path using it.A Single's working-draft overlay is read under the live document's access. The overlay replaces the assembled document and re-expands the snapshot's relationships; it used to build its own expansion context with neither enforcement flag, so a relationship in a pending draft exposed target rows the live relationship withheld. One builder (
relatedRowAccess) now produces the context for both. The projection also reads the KEY's roles from its scope when it carries them (asapiKeyWriteAlloweddoes), so a role-deciding rule sees the same set on both doors. And the verdict is cached per target on the expansion (targetVerdicts), so ahasManyfield fetching references concurrently asks its target once rather than once per reference — and it is decided BEFORE any target query: a refused caller costs no schema load, lifecycle resolution or select, and the referenced ids are recorded as withheld. What expansion mirrors is the target's code-defined rule only; it deliberately does not require the target's databaseread-<target>grant (the changeset says so).The column
A new database never gets
access_rules. One that already has it keeps it, and every schema entry point reports it rather than dropping it:nextly migraterefuses and names it —drops column 'dynamic_collections.access_rules'NEXTLY_ALLOW_CORE_DESTRUCTIVE=1is how an operator removes itDropping a column that holds configured rules is their decision, not a side effect of upgrading. That machinery already existed; this change only makes
access_rulesbecome such a drop.One upgrade is not supported and is called out rather than papered over. A database old enough to be missing core columns added since — a 0.45-era install — reconciles a drop and several adds on one table, which drizzle-kit pairs to ask whether the drop is a rename;
pushSchemabuilds that resolver internally with no way to supply a hints handler, so it throws and the boot degrades to its additive-tables-only baseline. Such a database should runnextly migratebefore taking this release.upgrade-sim-045is skipped for that reason, with the fix that restores it named in its header.Not a breaking change
accessRuleswas never accepted bydefineCollectionordefineSingleaccessRulesbody field is now ignored rather than rejected, so no new 400sDocs
packages/nextly/AGENTS.md,docs/guides/routing-and-seo.mdx,docs/guides/isr-caching.mdxand the scaffold'stemplates/base/AGENTS.md.templatedescribed the two-layer model and stored rules for anonymous readers; all three now describe the single mechanism, including the anonymous code-rule branchmaingained in #1704.Tests
All break-verified against code mutations rather than test edits. The two integration tests below that guard the retained column run on every dialect with a database in the run — SQLite in-memory always, Postgres and MySQL through a per-run database (
src/__tests__/helpers/fresh-database.ts), skipped without a URL. Verified locally on all three: 15/15. Break-verified per dialect: a Postgres count branch that answers 0 fails exactly the two Postgres loud cases while SQLite and MySQL stay green; a neutered destructive guard fails the upgrade case on all three.access-rules-orphan-column.test.ts— 5 cases pinning the refusal, the operator override, the dev-path prompt and the additive skip. 4 of 5 fail when the column is restored to the desired schema.access-rules-orphan-column-upgrade.integration.test.ts(×3 dialects) — a current database still carrying the column upgrades cleanly, with the drop blocked and no degraded pass. Fails for the intended reason when the destructive guard is neutered.init/__tests__/retired-access-rules.test.ts(7),retired-access-rules-boot.integration.test.ts(4 × 3 dialects, a real database throughensureFirstRunSetup) — the boot warning names each table and its count, stays quiet when the column is absent or every value is NULL, and never blocks boot. Breaks: dropping the wiring fails both loud cases; counting all rows instead of non-NULL rows fails three; counting without first checking the column exists fails the never-had-it case.related-row-collection-access.integration.test.tsis rewritten onto code-defined access; both leak cases fail when the target verdict admits everything, while the mirrors keep passing. Round 3 adds an anonymous pair (aread: ({ user }) => !!usertarget is withheld from a visitor exactly as the direct read refuses them; a rule-less target stays populated) and an api-key spelling case (apermissions.includes("pages:read")target admits a key scopedread-pagesthrough the relationship as by name) — each fails when its early return or the stored copy is put back.single-query-service.read-access.test.tsgains the Single gate's anonymous cases (rule refuses → 403; no rule → public default; anonymous publish refused even when the rule admits), break-verified the same way. Two cases added in round 2: a rule that readspermissionsadmits a role-holding user on expansion exactly as on a direct read (fails when the session branch is reverted to the narrow context), and three parents expanding one target in a list read invoke the rule once (fails with3 ≠ 1when the per-row judgment is restored).Obsolete suites were deleted rather than left green: they drove
getAccessQueryConstraint, the stored-rule evaluator, or the strict authorization view, none of which exist now.Verification
Run against this branch rebased onto
main:tscproduct + testseslint packages/nextly/srcmainwith none of this branch present;slug-param-is-a-leaf(#1715) andblock-document(#1784) are Windows backslash-vs-slash path assertions in code this branch does not touchfallow audit --base origin/mainwarn(exit 0) with the health baseline; dead code introduced: 0 (theacorn/acorn-walkdevDependencies thatexpression-validator.tsalone imported are removed). The verdict still readsfailon CRAP scores forupdateEntry,createEntry,checkCollectionAccessand neighbours — pre-existing functions this diff shrinks, flagged because changed lines sit inside them — and on near-duplicate blocks between the bulk/mutation/dispatcher paths that predate this change. Neither is refactored here.The pre-existing unit failures are
config-loader-registry-restore,config-loader-watch-serialization,block-manifest,ndjson,local-read-cap,slug-param-is-a-leafand the integration onesdb-sync-localized-companion,ensure-core-tables-registry,draft-published-split. The first five were confirmed on a detachedorigin/maincheckout.Rebase
Rebased onto current
maintwice; the branch is one commit. The second rebase (over #1642's read-path refactor and #1772/#1783's caller work) tookmain's versions of the auth modules and the query service wholesale and re-applied the stored-rule removal on top —resolveRowScopeno longer resolves an access constraint,accessConstraintConditionis gone, andcollection-read-access-parity.test.ts(amainsuite that tests the stored predicate on all three read paths) is deleted as obsolete rather than left green: it tested a mechanism that no longer exists. The first rebase (85 commits) Carried #1695's ambient scope pinning and #1704's anonymous code-rule branch (checkAnonymousCodeAccess) intocheckCollectionAccess; deletedowner-safety-net.tsand its test, which only served the storedowner-onlyrule; renamed the batch row-gate mode from"owner-predicate"to"batch-hoisted", since no predicate remains; bare-Errorallowlist ratchet 95 → 93.Integration was run against freshly recreated databases:
nextly_widget_layoutis a fixed-name system table that cannot be per-file prefixed, so state survives between whole-suite runs and produced a false primary-key failure until the databases were reset.