Merge main into prisma-8 - #1416
Conversation
…nship (#1120) A relationship hidden via `access: { read: () => false }` still leaked its true count through both the admin list view's _count path and a caller- supplied `_count.select` — neither ever checked the counting list's own field-level read access, only the related list's operation-level query access. resolveCountAccessEntryForList now takes the counting field's own FieldAccess and denies the count when field-level read access denies it, mirroring the check filterReadableFields already runs for an ordinary include of the same field. A synthetic back-relation has no field of its own and stays exempt. Closes #1111 Claude-Session: https://claude.ai/code/session_01BycBeXShej1q9zyF5EiuhT Co-authored-by: Claude <noreply@anthropic.com>
Bumps [@vercel/blob](https://github.com/vercel/storage/tree/HEAD/packages/blob) from 2.6.1 to 2.8.0. - [Release notes](https://github.com/vercel/storage/releases) - [Changelog](https://github.com/vercel/storage/blob/main/packages/blob/CHANGELOG.md) - [Commits](https://github.com/vercel/storage/commits/@vercel/blob@2.8.0/packages/blob) --- updated-dependencies: - dependency-name: "@vercel/blob" dependency-version: 2.8.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
) The Hook arguments section keyed `item`, `resolvedData` and `needs` but left `context` unparameterised, so the Prisma 8 keying would have re-landed the gap #1211 reports on main: a hook's context resolves over `PrismaClientLike = any`, `AccessControlledDB<any>` is assignable to nothing, and a typo'd delegate type-checks. Records that TypeInfo carries the app's DB and core's hook types read `context` off it, and that main's pre-contract fix establishes the same seam rather than a second mechanism. Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
#1214) TypeInfo gains a `prisma` member (defaulted to PrismaClientLike, fully additive) and every list/field hook-args type now types `context` as StackContext<TTypeInfo['prisma']> / AccessContext<TTypeInfo['prisma']> instead of the unparameterised default. Previously context.db resolved through AccessControlledDB<any> - a mapped type over keyof any that contributes no named property - so a hook's context was assignable to nothing app-specific and consumers needed `context as unknown as Context` to use it anywhere typed. The CLI generator emits the new `prisma` member on each list's Lists.<List>.TypeInfo, pointing at the project's own generated PrismaClient, so no config changes are required downstream. Adds a dedicated compile-time regression test (against core's real built types, not a stub) proving context.db is now assignable to the generated CustomDB with no cast and resolves real per-list row types, and extends the #952 large-schema fixture to also generate the Lists namespace and exercise list-level/field-level hooks reading context.db without reintroducing TS2589. Closes #1211 Claude-Session: https://claude.ai/code/session_01UUfo5pf9kkNAYQ54X9EEhp Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
#1264) context.db.<list>.findUnique/findFirst/findMany (and singleton get) only had the generic *Args-based overload, so passing a query fragment still compiled but the return type stayed the unnarrowed {List}GetPayload<T> instead of ResultOf<fragment> — an unselected field was silently readable with no compile error. These methods now carry the same fragment overload core's AccessControlledDB exposes (AugmentedFindUnique/First/Many), hand- written to match rather than wrapped through core's generics, which reintroduced #952's TS2589-style depth blowup on schemas with cross- referencing GetPayload chains. Full bidirectional assignability between AccessControlledDB and CustomDB (and dropping the `as unknown as` cast in the generated context factory) remains blocked by two narrower, separately-tracked gaps: virtual-field folding (#1232) and singleton get() having no AccessControlledDB counterpart — documented at the cast site. Claude-Session: https://claude.ai/code/session_01KzSpQGvfQCzFrvBJ2cRZAL Co-authored-by: Claude <noreply@anthropic.com>
#1277) BaseContext/Context previously re-declared StackContext's extra members (serverAction, sudo, withSession) by hand, and the list was missing `transaction` — so `context.transaction(...)` failed to typecheck even though it works at runtime. BaseContext now omits db/session/serverAction/ transaction/sudo/withSession from StackContext instead of AccessContext, so any future StackContext member reaches the generated Context automatically; the four self-referential members are re-declared on Context to resolve to the generated type. Closes #1261 Claude-Session: https://claude.ai/code/session_01V5k8pivgut8oLJGe8jz5sU Co-authored-by: Claude <noreply@anthropic.com>
Bumps [sharp](https://github.com/lovell/sharp) from 0.35.3 to 0.35.4. - [Release notes](https://github.com/lovell/sharp/releases) - [Commits](lovell/sharp@v0.35.3...v0.35.4) --- updated-dependencies: - dependency-name: sharp dependency-version: 0.35.4 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
…#1275) Bumps [@typescript-eslint/parser](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/parser) from 8.63.0 to 8.69.0. - [Release notes](https://github.com/typescript-eslint/typescript-eslint/releases) - [Changelog](https://github.com/typescript-eslint/typescript-eslint/blob/main/packages/parser/CHANGELOG.md) - [Commits](https://github.com/typescript-eslint/typescript-eslint/commits/v8.69.0/packages/parser) --- updated-dependencies: - dependency-name: "@typescript-eslint/parser" dependency-version: 8.69.0 dependency-type: direct:development update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Bumps [tailwind-merge](https://github.com/dcastil/tailwind-merge) from 3.5.0 to 3.6.0. - [Release notes](https://github.com/dcastil/tailwind-merge/releases) - [Commits](dcastil/tailwind-merge@v3.5.0...v3.6.0) --- updated-dependencies: - dependency-name: tailwind-merge dependency-version: 3.6.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Bumps [globals](https://github.com/sindresorhus/globals) from 17.8.0 to 17.12.0. - [Release notes](https://github.com/sindresorhus/globals/releases) - [Commits](sindresorhus/globals@v17.8.0...v17.12.0) --- updated-dependencies: - dependency-name: globals dependency-version: 17.12.0 dependency-type: direct:development update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
…1287) (#1289) * Fix #1287: generated Crud read methods not assignable to structural seams 0.42.1's fragment overload on findUnique/findFirst/findMany (and singleton get) left a two-member overload set (fragment, then the generic SelectSubset-based member) that TypeScript won't structurally match against a plain seam once SelectSubset carries Prisma's real conditional- intersection shape, and broke Parameters<> (resolves against the last overload, collapsing an unresolved generic T to never). Adds a third, trailing, non-generic overload member over the plain {List}FindXArgs to each method. Verified empirically that its position among the three doesn't affect assignability, and that ordinary calls (bare, or with select/include) still resolve through the generic member first, keeping its T-narrowed return. Closes #1287 Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Gxv5F6T184gXxqWd7c8mqf * Restrict the trailing overload member so it can't bypass select+include exclusivity Code review on #1289 caught a regression the first commit introduced: giving the trailing member the full {List}FindXArgs (select/include both present as independently-optional keys) let overload resolution fall through to it for a `{ select, include }` call the generic member's SelectSubset guard correctly rejects — silently accepting a combination Prisma itself forbids. Restrict the trailing member's parameter type to omit select/include/query entirely (Pick<..., 'where'> for findUnique, Omit<..., 'select' | 'include' | 'query'> elsewhere). A literal call naming those keys no longer structurally matches this member at all, so it stays reachable only by the seam-assignability and Parameters<> checks it exists for, never by a real call. Verified empirically that the exclusivity guard, seam assignability, and Parameters<> resolution all hold together. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Gxv5F6T184gXxqWd7c8mqf --------- Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
… getContext (#1337) * Add trailing overload to core's AugmentedFind* and thread TDb through getContext Core's AugmentedFindUnique/AugmentedFindFirst/AugmentedFindMany now carry the same trailing non-generic overload the generated CustomDB emits (#1287), and getContext gains a third, unconstrained, defaulted TDb type parameter so a caller can ask for StackContext<TPrisma, CustomDB> directly instead of erasing the type with `as unknown as`. The generated context.ts factory uses this to drop to a single, honest `as Context<TSession>` cast. Closes #1328 Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0167m3BzYP3jiUABj89QWeoF * Note the Parameters<> resolution change from the trailing overload in the changeset Per review feedback on PR #1337 from the issue author, who verified the fix against a real downstream call site and confirmed TDb is the load-bearing half; documents the trailing overload's effect on Parameters<> as an observable (intentional) side effect. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0167m3BzYP3jiUABj89QWeoF --------- Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Bumps [@radix-ui/react-popover](https://github.com/radix-ui/primitives/tree/HEAD/packages/react/popover) from 1.1.20 to 1.1.23. - [Changelog](https://github.com/radix-ui/primitives/blob/main/packages/react/popover/CHANGELOG.md) - [Commits](https://github.com/radix-ui/primitives/commits/HEAD/packages/react/popover) --- updated-dependencies: - dependency-name: "@radix-ui/react-popover" dependency-version: 1.1.23 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Bumps the aws-sdk group with 2 updates: [@aws-sdk/client-s3](https://github.com/aws/aws-sdk-js-v3/tree/HEAD/clients/client-s3) and [@aws-sdk/s3-request-presigner](https://github.com/aws/aws-sdk-js-v3/tree/HEAD/packages/s3-request-presigner). Updates `@aws-sdk/client-s3` from 3.1121.0 to 3.1127.0 - [Release notes](https://github.com/aws/aws-sdk-js-v3/releases) - [Changelog](https://github.com/aws/aws-sdk-js-v3/blob/main/clients/client-s3/CHANGELOG.md) - [Commits](https://github.com/aws/aws-sdk-js-v3/commits/v3.1127.0/clients/client-s3) Updates `@aws-sdk/s3-request-presigner` from 3.1126.0 to 3.1127.0 - [Release notes](https://github.com/aws/aws-sdk-js-v3/releases) - [Changelog](https://github.com/aws/aws-sdk-js-v3/blob/main/packages/s3-request-presigner/CHANGELOG.md) - [Commits](https://github.com/aws/aws-sdk-js-v3/commits/v3.1127.0/packages/s3-request-presigner) --- updated-dependencies: - dependency-name: "@aws-sdk/client-s3" dependency-version: 3.1127.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: aws-sdk - dependency-name: "@aws-sdk/s3-request-presigner" dependency-version: 3.1127.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: aws-sdk ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Bumps [@radix-ui/react-select](https://github.com/radix-ui/primitives/tree/HEAD/packages/react/select) from 2.3.4 to 2.3.7. - [Changelog](https://github.com/radix-ui/primitives/blob/main/packages/react/select/CHANGELOG.md) - [Commits](https://github.com/radix-ui/primitives/commits/HEAD/packages/react/select) --- updated-dependencies: - dependency-name: "@radix-ui/react-select" dependency-version: 2.3.7 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Bumps [pg](https://github.com/brianc/node-postgres/tree/HEAD/packages/pg) and [@types/pg](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/pg). These dependencies needed to be updated together. Updates `pg` from 8.22.0 to 8.23.0 - [Changelog](https://github.com/brianc/node-postgres/blob/master/CHANGELOG.md) - [Commits](https://github.com/brianc/node-postgres/commits/pg@8.23.0/packages/pg) Updates `@types/pg` from 8.20.0 to 8.23.1 - [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases) - [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/pg) --- updated-dependencies: - dependency-name: pg dependency-version: 8.23.0 dependency-type: direct:production update-type: version-update:semver-minor - dependency-name: "@types/pg" dependency-version: 8.23.1 dependency-type: direct:development update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
…sconnect (#1385) Nested set/updateMany/deleteMany were a pass-through straight to Prisma with no target-list access check and no hooks. Non-sudo contexts now get a NestedRelationInputError instead; sudo is unaffected. Nested disconnect's target-row form ({ disconnect: { id } }) now requires the target list's operation.query access, reusing the connect reachability check; the to-one boolean form ({ disconnect: true }) is unchanged. Closes #1384 Claude-Session: https://claude.ai/code/session_01HHpw6shZoER4AjaaYg6QC9 Co-authored-by: Claude <noreply@anthropic.com>
prisma-8 is an orphan branch: it shares no git merge-base with main. The content fork point is 93a3736 (#1119), 28 main commits back, so this is a three-way merge computed against that base rather than a merge of unrelated histories. Resolutions of substance: - Nested write access (#1385 on main vs ADR-0050 here). main refused nested set/updateMany/deleteMany for non-sudo callers and gated disconnect's target-row form. prisma-8 removed nested relation input from the payload outright, unconditionally, sudo included. The prisma-8 refusal is the strictly stronger property; main's nested-operations.ts is deleted here and its dead re-export of NestedRelationInputError dropped from core's barrel (it auto-merged into a duplicate export of a deleted module). - Hook context (#1176/#1179, main's ADR-0066). main gave hooks the full StackContext; prisma-8's ADR-0052 split BaseContext (no sudo/withSession/ transaction) from Context, in core, in the generator, and in every emitted bundle. prisma-8's split stands; main's ADR-0066 is marked superseded, and its doc text that auto-merged into packages/core/CLAUDE.md and ADR-0052's #1211 amendment is corrected to the merged behaviour. - _count field-level read access (#1120/#1111). Verified the property still holds: secured/include.ts omits a denied relation pre-query and Field Visibility drops it post-query. main's fieldAccess parameter on resolveCountAccessEntryForList is moot — its only caller is unreachable from context.db here. - The old typed-query/generated-bundle surface (Fragment/ResultOf, AugmentedFind*, AccessContext<TPrisma, TDb>, CustomDB, Prisma.*Args) is taken from prisma-8 throughout; prisma-8 replaced it. - ADR-0066 was claimed by both branches. prisma-8's is renumbered to 0068 and every reference to it updated. - Dependency bumps from main carried forward and aligned monorepo-wide: @types/react ^19.2.18, pg ^8.23.0, @types/pg ^8.23.1. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Picks up main's dependabot bumps (@typescript-eslint/parser 8.69.0, globals 17.12.0, @types/react 19.2.18) and the monorepo-wide pg 8.23.0 / @types/pg 8.23.1 alignment the merge settled. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
- access-filter.ts called resolveCountAccessEntryForList with main's third `fieldAccess` argument against prisma-8's two-parameter signature. - hook-pipeline.ts and hooks/index.ts imported StackContext from context/index.js, which does not export it on this line; both carried main's #1179 hook-context typing, reverted to prisma-8's AccessContext. Also reverts the pg/@types/pg bump from main's #1380: @prisma/orm-postgres 8.0.0-rc.8 pins pg 8.22.0 and @types/pg 8.20.4 exactly, and a second copy of @types/pg makes core's testing harness fail to compile. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
prisma-8's resolveCountAccessEntryForList had lost the fieldAccess parameter #1120 added, because the secured read gates the same field one layer up in secured/include.ts and the function's only caller is unreachable from context.db here. Porting the parameter back keeps the gate attached to the entry itself, so a future caller cannot reopen #1111 by omitting it, and restores main's own test of it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…B members main's #1211 added `prisma: import('./prisma-client/client.ts').PrismaClient` and `db: import('./types.ts').CustomDB` to every list's TypeInfo. Both name a module prisma-8 does not emit, so the merge would have written a bundle that does not type-check into every example — a generation break the package type-suites do not see. Reverted to prisma-8's generator, with main's test of the removed member. Also removes five main-only CLI test files covering the generated surface ADR-0052 replaced: the fragment/CustomDB seam (#1233, #1287, #1328) and the hook context.db typing (#1232). Their subjects are carried by prisma-8's own contract-keyed suites. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
main's #1185 bumped @vitest/browser to ^4.1.11 while vitest stayed ^4.1.10, leaving an unmet peer. Every vitest-family specifier moves to ^4.1.11 rather than reverting the bump. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard. |
🦋 Changeset detectedLatest commit: 02c526d The changes in this PR will be included in the next version bump. This PR includes changesets to release 9 packages
Not sure what this means? Click here to learn what changesets are. Click here if you're a maintainer who wants to add another changeset to this PR |
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
Coverage Report for Core Package Coverage (./packages/core)
File Coverage
|
||||||||||||||||||||||||||||||||||||||||||||
Coverage Report for UI Package Coverage (./packages/ui)
File CoverageNo changed files found. |
Coverage Report for CLI Package Coverage (./packages/cli)
File CoverageNo changed files found. |
Coverage Report for Auth Package Coverage (./packages/auth)
File CoverageNo changed files found. |
Coverage Report for Storage Package Coverage (./packages/storage)
File CoverageNo changed files found. |
Coverage Report for RAG Package Coverage (./packages/rag)
File Coverage
|
||||||||||||||||||||||||||||||||||||||
Coverage Report for Storage S3 Package Coverage (./packages/storage-s3)
File CoverageNo changed files found. |
Coverage Report for Storage Vercel Package Coverage (./packages/storage-vercel)
File CoverageNo changed files found. |
borisno2
left a comment
There was a problem hiding this comment.
APPROVE
Reviewed from a fresh gh pr checkout 1416 worktree. I did not take "merged cleanly" or "CI green" as evidence for anything; every claim below was re-derived by running.
Verdict
No blocking findings. No security property is weakened, no side of the merge is lost, every example still works. One follow-up residue (a stale ADR pointer) and one PR-description inaccuracy, both noted below.
1. The access-control judgement (#1385) — verified by running, and it holds
I wrote my own probe rather than trusting secured-write.test.ts, and ran it against the real harness. 24/24 passed:
| Probe | Result |
|---|---|
8 nested kinds (create / update / delete / connectOrCreate / disconnect / set / updateMany / deleteMany) on create, under sudo(), owning relationship field |
all 8 → NestedRelationInputError |
the same 8 on update, under sudo() |
all 8 → NestedRelationInputError |
the same 8 injected by a resolveInput hook, caller payload clean ({ title } only), non-sudo and sudo |
all 8 → NestedRelationInputError |
Control probe (to prove the harness is real and the gate is not simply refusing everything): { connect: { id } }, null-clear, a plain sudo create and a read-back all succeed. Passed.
The second refusal genuinely fires. I mutated it out — commented write-pipeline.ts:339 (refuseNestedRelationInput(…, resolvedData)) — and all 8 post-hook probes went red immediately. Worth recording what they degrade to: without :339 the payload falls through to lowerRelationInput, which throws MalformedRelationInputError, so nothing reaches the driver even then — but it happens after the hook chain and beforeOperation side effects have run, and under the wrong error name. The second call site is load-bearing, not belt-and-braces.
The "unconditional" claim is structural and I confirmed it by reading: refuseNestedRelationInput has no _isSudo branch, and neither call site (:182 on inputData, :339 on resolvedData) is guarded. main's three-kind gate was sudo-exempt and dispatch-time-only. Eight kinds, no exemption, checked twice — strictly stronger. Agreed.
2. The disconnect gate is genuinely moot — confirmed
Confirmed by reading relationship-input.ts end to end, not by taking the summary:
reachable()survives and is the same shapemain's gate reused: target list'soperation.queryANDed with the identity criterion, evaluated in the database, non-existent and unreadable folded into one answer.- It now covers both spellings —
lowerRelationInputcalls it forkind: 'owning'({ connect: { id } }) and forkind: 'foreignKey'(authorIdnamed directly). That is the #1331 oracle closed, whichmaindoes not have. - The
disconnectreplacement — assigningnull— writeslowered[key.column] = null. It names no target row, so there is nothing for a target-list read to gate. - The unlinking is not ungoverned:
filterWritableFieldsruns at:342,lowerRelationInputat:355, so thenullis gated by the owning relationship field's own write access first.field-access.ts:313-318maps the raw<field>Idcolumn back to that same owning field, so the FK spelling cannot dodge it either.
No spelling reaches a row the caller could not otherwise reach. Reasoning confirmed.
3. The four auto-merges, and the hunt for a fifth
All four confirmed fixed:
cli/generator/lists.ts— byte-identical toorigin/prisma-8; noCustomDB/prisma-client/client.tsmembers. Proved it the hard way: regenerated all twelve examples plus the contract fixture, thenpnpm build:examples→ 20/20 tasks, which is a fulltsc/Next type-check of every emitted bundle. The only surviving mentions ofCustomDBare the negative assertions intypes.test.ts:161.core/src/index.ts— single export ofNestedRelationInputError, fromcontext/relationship-input.js(:178-185).context/nested-operations.tsis absent. File is byte-identical toprisma-8.hook-pipeline.ts/hooks/index.ts— byte-identical toprisma-8; noStackContextimport fromcontext/index.js.access-filter.ts— the 3-argument call now meets a 3-parameter signature;packages/core87 files / 1623 tests pass.
I looked for a fifth and did not find one. Method, so you can judge how complete that is: I diffed the merged tree against both parents and took the intersection — the set of files that differ from prisma-8 and from main, i.e. all genuinely merged content. It is 45 files, and only seven are source: access-filter.ts, access-filter.test.ts, relationship-count.ts (the #1111 port), and config/types.ts / extend.ts / multi-column-read-write.test.ts / rag/src/* (pure ADR renumber — I diffed them and every changed line is 0066→0068 and nothing else). Everything else is manifests, CHANGELOGs, changesets, docs and the lockfile, each of which I walked:
- Every
main-changed file that this PR does not touch (30 of them) is accounted for: 5 are deleted byprisma-8outright, the rest are the CLI generator surface and core context surface ADR-0050/0052 replaced, plus three docsprisma-8rewrote. - Every
maindependency bump is carried exceptpg/@types/pg; nothing else silently reverted. - All 9 consumed changesets verified present verbatim in the CHANGELOGs at 0.42.3.
pnpm install --frozen-lockfileresolves clean.
4. The hook-context resolution — right call, and the superseding is justified
Resolving for prisma-8 is correct. ADR-0052's BaseContext / Context split is not a local preference: it is emitted per app by the generator, baked into the contract, and instantiated across twelve converted examples. Satisfying main's ADR-0066 would mean re-opening a spec, which is not a merge resolution.
"A capability reduction, never a security regression" holds. A hook context that cannot reach sudo(), withSession() or transaction() grants strictly less authority than one that can — the direction of the change is monotone. I checked the one case where a reduction could have pushed callers somewhere worse: a plugin needing to write a column the application is denied. That has a first-class replacement (writePluginOwnedField, ADR-0068), which exists and is tested. And the surviving half — a hook's db bound to the write's own transaction client (ADR-0010/0028/0023) — is intact.
Marking main's ADR-0066 superseded is justified. Two things make it legitimate rather than presumptuous: the record is arriving in this tree as part of this merge, so the integrator is the right person to state its status here; and the note does not rewrite the record's argument — it adds a status line and a scoped block naming exactly what is reversed (the four derive-a-new-context operations), exactly what survives (the transaction binding, and why the record was written), and why _resolveOutputChain is moot by construction rather than dropped. That is the correct shape for a supersede. The three corrected documents check out against the code: access-control.md's new section names err.listName / fieldKey / kinds (matching the class — main's err.listKey was wrong), and its { tags: { deleteMany: {} } } example does land on NestedRelationInputError rather than NonOwningRelationInputError, because kindsIn is checked before the inverse branch.
5. The ported #1111 gate — correct, and cannot refuse anything legitimate
The port is faithful to main's: same optional fieldAccess parameter, same isFieldReadableForPredicate evaluation, same "checked before the related list's query access", same undefined-means-exempt for a synthetic back-relation (which has no field of its own on the counting list). main's test came across with it and passes.
It cannot refuse anything legitimate: the only caller is buildAccessScopedInclude, which has no in-repo caller outside its own tests — context.db reaches counts through secured/include.ts. The one behavioural edge (a row-dependent read rule denies at predicate time) is main's own documented semantics, and it is unreachable here anyway. Defensive parity on a dormant surface, which is the right call over relying on unreachability.
6. The pg revert — confirmed
node_modules/.../@prisma/orm-postgres@8.0.0-rc.8/package.json pins "pg": "8.22.0" and "@types/pg": "8.20.4" — exact, not ranges. Confirmed nothing else on main wanted the newer one: #1380 touched exactly one manifest, examples/rag-openai-chatbot/package.json, and on this line that example no longer declares pg at all. Revert is correct and costs nothing.
7. The renumber — one missed reference (follow-up, not blocking)
pnpm check:adr-duplicates → 68 numbers checked, no duplicates. 0066 (main's, hook context), 0067 and 0068 all present and distinct. I grepped every ADR-0066 / ADR-0068 reference in the tree and checked each against its intended record. 17 are correct. One is not — see the inline comment: examples/rag-ollama-demo/README.md:234 still says (ADR-0066) where it means ADR-0068, so it now points at a real but wrong record. The duplicate checker cannot catch this — it checks numbers, not references.
Related, minor: the PR body says "The two references that meant main's record (ADR-0052, packages/core/CLAUDE.md) were left at 0066." There are no ADR-0066 references anywhere in the tree except the stale one above — those two documents reference the decision by prose, not by number. The description overstates; the tree is fine.
Everything else, re-run locally
| Gate | Result |
|---|---|
pnpm install --frozen-lockfile |
clean |
pnpm build |
11/11 |
| Twelve examples + contract fixture regenerate | no drift (git diff --exit-code → 0) |
| Drift gate still bites | added a field to examples/starter/opensaas.config.ts, regenerated → exit 1, naming contract.ts / contract.d.ts / contract.json; restored → exit 0 |
pnpm build:examples |
20/20 (type-checks every emitted bundle) |
examples/starter boots |
dev database provisioned (6 operations), GET /admin → 200 |
Example suites (starter, starter-auth, json-demo) |
11/11 tasks |
packages/core |
87 files / 1623 tests |
packages/cli |
43 / 414 |
packages/auth |
23 / 505 |
check:doc-ts-blocks |
exit 0 — 126 blocks, 89 compile |
check:doc-ts-blocks --self-test |
55 blocks, 0 mismatches |
check:client-side-effect-imports |
exit 0 |
check:prisma-error-codes |
exit 0 — 1477 files, 26 literals / 7 allowlisted |
check:adr-duplicates |
exit 0 — 68 numbers |
pnpm lint |
0 errors (1 pre-existing warning, migration-generator.ts:851, untouched here) |
pnpm format:check |
clean |
pnpm manypkg check |
workspaces valid |
| Changesets | 115 minor, 20 patch, 0 major — no premature version cut |
CI, per job: test pass (15m7s), e2e (postgres) pass, e2e (dev-database) pass, coverage pass, GitGuardian pass, Vercel pass. I checked the test job's 81 steps individually for #1407: the later gates executed, they were not skipped behind a failure. Steps 5–28 (version consistency, ADR duplicates, error codes, client imports, lint, format, all 13 regenerations, the dirty-regeneration gate, doc-block self-test, doc-block check) are all success. The four skipped steps are the base_ref == 'main' alternates whose integration-branch counterparts ran instead (30/31, 35/36, and 29), plus step 40, which uploads browser screenshots only on failure. Neither known flake (#1404, #1411) appeared in CI or in my local runs.
Findings by category
- Blocking: none.
- Follow-up residue: the stale
ADR-0066inexamples/rag-ollama-demo/README.md:234; the PR-description overstatement about "two references left at 0066".
One process note, not a change request: the description's own numbers are slightly off in places (109/17 changesets vs the 115/20 I count; 1482 vs 1477 files scanned). Nothing turns on it, but since this PR's entire safety case rests on the author's inspection being trusted, the small drift is worth tightening before the release PR quotes it.
| second argument, `sudo`, is now declared as the `StackContext` it always was — a plugin | ||
| reaching `sudo().db` is unaffected, one reaching `ormHandle` off it was already getting | ||
| `undefined` and now fails to compile. See ADR-0066. | ||
| `undefined` and now fails to compile. See ADR-0068. |
There was a problem hiding this comment.
The renumber is correct here, and in 16 other places — but it missed one, and the miss is the bad kind: it now points at a real but wrong record.
examples/rag-ollama-demo/README.md:234 still reads:
Writes the vector and its metadata through core's
writePluginOwnedField— the columns are write-denied to application code, and that write runs no hook of the list's (ADR-0066)
That means ADR-0068. Since this merge adds main's ADR-0066 (the hook-bound context record), the reference no longer dangles — it resolves, to a document about something else entirely. pnpm check:adr-duplicates cannot catch this: it checks that numbers are unique, not that references point where they mean to.
I grepped the whole tree: this is the only remaining ADR-0066 reference outside docs/adr/ itself. One-line fix, follow-up rather than blocking.
There was a problem hiding this comment.
Fixed in 02c526d — examples/rag-ollama-demo/README.md:234 now reads ADR-0068.
You were right that this is the bad kind of miss, and I swept the whole tree on the meaning of each
occurrence rather than counting, since both numbers are legitimately in use:
ADR-0066— exactly one occurrence existed, the one you found. Its sentence is about
writePluginOwnedFieldand "that write runs no hook of the list's", which is squarely the
renumbered record.grep -rn 'ADR-0066'over the tree now returns nothing.ADR-0068— 17 occurrences, all genuinely the renumbered record. Two sit in paragraphs whose
surrounding topic is the hook context (0066's subject) and so deserved a second look:
packages/core/CLAUDE.md:340("because a hook cannot reachsudo()…") and
packages/core/src/config/types.ts:3197(theStackContextvsStackBaseContextsplit). In both
the parenthetical attaches to a claim aboutwritePluginOwnedFieldspecifically, not about what a
hook's context carries, so 0068 is correct in each.
I also corrected the PR description. It claimed two references were deliberately left at 0066 and
covered by the supersede note — that was wrong twice over: there are none, and the two documents it
named (ADR-0052's #1211 amendment and packages/core/CLAUDE.md's "#1176" section) were rewritten
in prose to the merged behaviour and cite no ADR number at all, so there was never anything to
leave. The count there now reads 18.
Gates on the new tip: pnpm check:adr-duplicates passes (68 numbers, no duplicates) and
pnpm --filter opensaas-stack-docs link-check passes. The change is one line of documentation text
— no source, no manifest, no changeset.
| args: CountArgs, | ||
| fieldAccess?: FieldAccess, | ||
| ): Promise<CountAccessEntry> { | ||
| if (fieldAccess) { |
There was a problem hiding this comment.
Confirmed correct, and confirmed it cannot refuse anything legitimate.
The port is faithful to main's: same optional parameter, same isFieldReadableForPredicate evaluation, same ordering (before the related list's query access, so a field-level denial short-circuits), same undefined-means-exempt for a synthetic back-relation. main's test came across with it and passes in the 1623.
On "cannot refuse something legitimate": the one behavioural edge is that a row-dependent read rule denies at predicate time, because there is no fetched row yet. That is main's own documented semantics (the #915 evaluator), not something the port introduced — and it is unreachable here regardless: the only caller is buildAccessScopedInclude, which has no in-repo caller outside its own tests, since context.db reaches counts through secured/include.ts.
Porting rather than leaning on unreachability is the right call. Unreachability is a property of today's call graph; the gate is a property of the function.
| const accessEntry = await resolveCountAccessEntryForList( | ||
| relatedConfig.listConfig, | ||
| args, | ||
| fieldConfig?.access, |
There was a problem hiding this comment.
This is auto-merge #4, and it is genuinely fixed — the 3-argument call now meets a 3-parameter signature.
Worth stating what made it dangerous, since it is the shape to keep watching for: it was not a merge conflict and it was not a test failure. It was a call that simply did not type-check, in a file whose own suite never exercised the path. That class of defect is invisible to "it merged cleanly" and to "CI is green" — it only surfaces when something actually compiles the call.
| @@ -0,0 +1,148 @@ | |||
| # The hook-bound context is the same secured context as a transaction's | |||
|
|
|||
| Status: superseded by [ADR-0050](0050-nested-relation-input-leaves-the-secured-write-surface.md) and [ADR-0052](0052-the-generated-types-declare-the-contract-remainder-and-instantiate-core-generics.md) | |||
There was a problem hiding this comment.
Marking another branch's decision record superseded is a real editorial act, so: I think this one is justified, and for two specific reasons rather than deference.
First, standing. The record is arriving in this tree as part of this merge. It has never been in force on this line. The integrator is the right person to state its status here — this is not reaching across to overwrite a live decision elsewhere.
Second, form. The note does not rewrite the record's argument or edit its body. It adds a status line and a scoped block that names exactly what is reversed (the four derive-a-new-context operations), exactly what survives and why the record was written at all (the transaction binding, ADR-0010/0028/0023), and why _resolveOutputChain is moot by construction rather than quietly dropped. A future reader gets the original reasoning intact plus an accurate account of what happened to it. That is what a supersede is supposed to look like.
I also agree with resolving the underlying conflict for prisma-8. ADR-0052's split is emitted by the generator, baked into the contract and instantiated across twelve examples; reverting it is a respec, not a merge resolution. And the direction is monotone — a hook context without sudo() / withSession() / transaction() grants strictly less authority, with a first-class replacement (writePluginOwnedField) for the one case that needed the escalation.
| a `resolveInput` hook assembled after the types had their say. | ||
|
|
||
| The refusal is **unconditional**: unlike an access denial, `sudo()` does not | ||
| lift it. Each of these was a second write against another list hidden inside |
There was a problem hiding this comment.
"The refusal is unconditional: unlike an access denial, sudo() does not lift it" — verified by running, and the second half of the claim verified by mutation.
I probed all eight kinds three ways: on create under sudo(), on update under sudo(), and injected by a resolveInput hook with a completely clean caller payload ({ title } only) both non-sudo and sudo. 24/24 refused with NestedRelationInputError. A control probe confirms the harness is real rather than refusing everything: { connect: { id } }, null-clear, a plain sudo create and a read-back all succeed.
For the post-hook half I commented out write-pipeline.ts:339 (refuseNestedRelationInput(…, resolvedData)) and all eight post-hook probes went red, then green again on restore. That call site is load-bearing. Worth noting what it degrades to: without it the payload falls through to lowerRelationInput and throws MalformedRelationInputError, so nothing reaches the driver even then — but only after the hook chain and beforeOperation side effects have run, and under an error name describing the wrong problem.
The prose here is accurate against the code, including the detail that { tags: { deleteMany: {} } } lands on NestedRelationInputError and not NonOwningRelationInputError — kindsIn is checked before the inverse branch in refuseNestedRelationInput.
The renumber from 0066 to 0068 missed this reference. ADR-0066 still exists on this line — main's hook-bound-context record, now superseded — so the stale number resolved to a real but unrelated document, which neither the duplicate-number check nor the link check can catch. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
This has landed on Why not the button. This repository allows squash only, and this pull request is a merge commit carrying both parents — deliberately so, because Result: Please leave the branch 🤖 Generated with Claude Code |
Merges
mainintoprisma-8so the release pull request can follow. Targetsprisma-8, notmain.prisma-8is an orphan branchgit merge-base origin/main origin/prisma-8returns nothing —prisma-8's root commit4e7bd4b3has no parent. A plaingit mergewould have refused, and--allow-unrelated-historieswould have treated every fileprisma-8deleted as an add.The content fork point is
93a3736b(spec(prisma-8): the architecture spec handed off from the Wayfinder map (#1119)) — exactly 28maincommits back, matching the brief, and the candidate with the smallest diff toprisma-8's root tree (69 files vs 73 for the next candidate). The merge was therefore computed explicitly against that base withgit merge-recursive 93a3736b -- HEAD origin/main, then committed with both real parents.A side effect worth noting:
prisma-8now containsmain's history, so the follow-upprisma-8→mainrelease PR is an ordinary merge rather than a second unrelated-histories problem.The real overlap
84 files changed on
mainsince the fork, 855 onprisma-8; 55 overlap. 27 conflicted. The rest auto-merged — and four of those auto-merges were wrong, which is where the damage was.The access-control judgement (#1385)
Property: a non-sudo caller must not reach rows outside the parent's subtree through a nested relationship write, and must not name a target row it cannot read.
mainclosed this by gating: refuse nestedset/updateMany/deleteManyfor non-sudo callers (NestedRelationInputError), and gatedisconnect's target-row form behind the target list'soperation.queryaccess.sudo()was exempt from all of it.prisma-8closed it by deletion: ADR-0050 removes nested relation input from the write payload entirely.packages/core/src/context/relationship-input.tsrefusescreate,update,delete,connectOrCreate,disconnect,set,updateManyanddeleteMany— eight kinds againstmain's three — andrefuseNestedRelationInputhas no sudo branch at all.Verdict: the property survives, strictly stronger.
prisma-8refuses a superset ofmain's kinds and refuses them unconditionally.main's own commit anticipated this in its docblock, calling its fix "an interim fail-closed fix mirroring ADR-0050's own refusal shape" for work "that major deletes wholesale".main's disconnect gate is moot, not dropped.disconnectdoes not exist here; the replacement is assigningnullto the relationship field, whichlowerRelationInputlowers to a foreign-key column on the row being written. No target row is named, so there is no target-list read to gate — the unlinking carries the enclosing write's own operation and field-level access. The reachability querymain's gate reused survives asreachable()and is extended on this line to the direct foreign-key spelling (authorId), closing the probing oracle #1331 thatmaindoes not have.Verified by running, not by reading. In the merged tree
secured-write.test.tsdrives all eight kinds end-to-end throughcontext.db.Post.create— "nested<kind>is refused, and nothing is written" — and "a sudo payload naming a synthetic back-relation is refused too" pins the sudo half.relationship-input.test.ts,related-actions.test.tsandrelation-connect.test.tspass alongside them (core: 87 files, 1623 tests).Two structural facts back the "unconditional" claim:
refuseNestedRelationInputhas no_isSudobranch, andwrite-pipeline.tscalls it at:182(the caller'sinputData) and:339(the post-hookresolvedData), neither guarded — so aresolveInputhook that assembles nested relation input is refused too, whichmain's dispatch-time check did not cover.packages/core/src/index.test.tsandpackages/cli/src/generator/types.test.tscarry negative assertions that the deleted fragment/CustomDBnames cannot quietly return; both pass, which is independent evidence the merge did not resurrect the old surface.main'snested-operations.tsis deleted. Its re-export ofNestedRelationInputErrorfrompackages/core/src/index.tshad auto-merged into a duplicate export of a deleted module, alongsideprisma-8's own export of the same name fromrelationship-input.ts; the dead one is removed.The second contested change: the hook context (#1176 / #1179)
The two branches made directly contradictory decisions here, independently.
main's ADR-0066: a hook'scontextis the fullStackContext—sudo(),withSession(),transaction(),serverAction— bound to the write's own transaction.prisma-8's ADR-0052:StackBaseContext("nothing that can start a transaction or change who is asking") is what a hook sees;StackContextis for server actions and page components. This is emitted per app by the generator asBaseContextvsContext, baked into the contract and into all twelve converted examples.Resolved in favour of
prisma-8. Reverting the split to satisfymainwould be a rewrite of theprisma-8spec, not a merge resolution. This is a capability reduction, never a security regression — a narrower hook context grants strictly less.Because this was a deliberate drop rather than a silent one, three documents that asserted
main's behaviour and auto-merged into this tree were corrected rather than left to rot:docs/adr/0066-…is marked superseded by ADR-0050 and ADR-0052, stating what survives (the transaction binding, ADR-0010/0028/0023) and what is withdrawn.packages/core/CLAUDE.md's "hooks: no transaction-bound elevated context from beforeOperation/afterOperation (hook context has no sudo(); getContext().sudo() opens a separate connection) #1176" section is rewritten to the merged behaviour.#1211amendment (which auto-merged frommain) claimed a hook's context "is the sameStackContext<DB, S, PluginServices>the generatedContextinstantiates". Its keying point is true and satisfied here; the claim about what it carries was false and is corrected.main's_resolveOutputChainthreading throughsudo()/withSession()is moot by construction, not dropped: the only contexts exposing those operations are request contexts, whose chain is empty; the contexts carrying a non-empty chain (hook contexts) do not expose them.The third:
_countfield-level read access (#1120 / #1111)mainadded afieldAccessparameter toresolveCountAccessEntryForList, gating a_counton the counting list's own field-levelreadaccess.prisma-8's copy had no such parameter.Investigated every path by which a relationship count reaches a non-sudo caller here. The property holds, by a different mechanism:
secured/include.ts:498-510omits a denied relation pre-query (return nullat:559, so the count is never asked of the database), and for a row-dependent rulefield-visibility.ts:353-362drops the key post-query, withrestoreReductions(secured/read.ts:965-968) writing the count back only under keys Field Visibility kept.main's gated function is dead code here — its only caller,buildAccessScopedCountSelect, is unreachable fromcontext.db, which has nofindManyand no caller-supplied_count.Rather than rely on unreachability,
main's gate is ported ontoprisma-8's function (1ead103) so a future caller cannot reopen #1111 by omitting it, andmain's test of it passes.Four auto-merges that were silently wrong
None of these conflicted. All four compiled or tested green somewhere else.
packages/cli/src/generator/lists.ts—main's Hook args are not generic over the Prisma client, so a hook'scontextis untypeable in a consumer —AccessControlledDB<any>is assignable to nothing #1211 addedprisma: import('./prisma-client/client.ts').PrismaClientanddb: import('./types.ts').CustomDBto every list'sTypeInfo. Neither module exists on this line. This would have written a bundle that does not type-check into every one of the twelve examples — a generation break the package suites do not see. Reverted.packages/core/src/index.ts— duplicateNestedRelationInputErrorexport of a deleted module (above).hook-pipeline.ts/hooks/index.ts— importedStackContextfromcontext/index.js, which does not export it here.access-filter.ts— calledresolveCountAccessEntryForListwithmain's third argument against a two-parameter signature.Five
main-only CLI test files covering the surface ADR-0052 replaced (#1233,#1287,#1328,#1232) are removed; their subjects are carried byprisma-8's contract-keyed suites.Other overlapping files
mainassertedprisma-8assertedaccess/types.tsAugmented*trailing overloads (#1287/#1328),AccessContext<TPrisma, TDb>SecuredQuery,OrmClient, contract-keyed genericsprisma-8— surface deleted;TDbsubsumed by the first-classDBparameterconfig/types.tsTypeInfo.prisma/.dbTypeInfo.output/.needsprisma-8; #1232's property holds structurally (StackBaseContext<DB,…>)internal.tsFragment/FieldSelection/ResultOfquery/index.jsdeletedprisma-8cli/generator/{context,types}.ts, snapshotsCustomDB,prisma-client/client.tsprisma-8docs/{hooks,context-api,migrate-from-keystone}.mdprisma-8docs/concepts/access-control.mddisconnectas gated andsudoas exemptmain's section auto-merged and was false here (err.listKeyvslistName,context.db.postcasing, a sudo exemption that does not exist)main's Version PackagesChanges from
mainthat this line makes meaningless — stated, not droppeddisconnectgate (fix(core): refuse unchecked nested set/updateMany/deleteMany, gate disconnect #1385) — the construct it guarded does not exist._resolveOutputChainthreading throughsudo()/withSession()(hooks: no transaction-bound elevated context from beforeOperation/afterOperation (hook context has no sudo(); getContext().sudo() opens a separate connection) #1176) — unreachable from a hook.pg/@types/pgbump (chore(deps): bump pg and @types/pg #1380) — reverted.@prisma/orm-postgres@8.0.0-rc.8pinspg: "8.22.0"and@types/pg: "8.20.4"exactly; carrying the bump put two incompatible@types/pgcopies in the type graph and brokepackages/core/src/testing/context.ts.Dependency bumps that do apply are carried:
@typescript-eslint/parser8.69.0,globals17.12.0,@types/react^19.2.18, and the vitest family aligned on 4.1.11 (main's #1185 bumped@vitest/browseralone, leaving an unmet peer).ADR-0066 collision
Both branches claimed 0066.
main's is the published number and keeps it.prisma-8's0066-a-plugins-write-of-its-own-column-runs-no-hook.md→0068, with all 18 references updated acrossCLAUDE.md,CONTEXT.md,packages/core/{CLAUDE.md,src/extend.ts,src/config/types.ts,src/access/multi-column-read-write.test.ts},packages/rag/{CLAUDE.md,src/config/plugin.ts,plugin.test.ts,embedding-write.test.ts},docs/content/how-to/rag-advanced.mdand two changesets. One of those 18 was missed on the first pass and caught in review:examples/rag-ollama-demo/README.mdstill read(ADR-0066)on a sentence aboutwritePluginOwnedFieldrunning no hook — a reference that resolved to a real but unrelated record, which is why neithercheck:adr-duplicatesnor the link check flagged it. It now readsADR-0068. No reference is left at 0066. An earlier revision of this description claimed two were deliberately left there; that was wrong. The two documents that assertmain's decision — ADR-0052's#1211amendment andpackages/core/CLAUDE.md's "#1176" section — were rewritten in prose to the merged behaviour and cite no ADR number at all, so there was nothing to leave.grep -rn 'ADR-0066'over the tree now returns nothing.pnpm check:adr-duplicates→ 68 numbers checked, no duplicates.Verified by running
pnpm buildgit diff --exit-codeover the 13 CI paths → 0)examples/starter/prisma/contract.ts→ exit 1; restored → exit 0examples/blogaccess-control suiteexamples/mcp-demosuitecheck:doc-ts-blockscheck:doc-ts-blocks --self-testcheck:client-side-effect-importscheck:prisma-error-codescheck:adr-duplicatespnpm --filter opensaas-stack-docs link-checkpnpm lintmigration-generator.ts, untouched by this merge)pnpm format:checkpnpm manypkg checkminor, 17patch, 0majorNo documentation-checker excuse was added, widened or removed —
git diff origin/prisma-8 -- scripts/ .github/is empty, and neither branch touched those paths since the fork.Flakes observed, neither mine
packages/core/src/db/dev-database.test.ts > two instances in one process take distinct portsfails under parallelturbo, passes standalone.packages/core/src/db/is byte-identical toorigin/prisma-8— port contention, not this merge.packages/uibrowser-mode suite failed once underturbowithFailed to search relationship options: network error, passed on retry and standalone (62/671).Not verified
mcp-demoneededBETTER_AUTH_URLsupplied by hand (CI sets it).pnpm test:e2e(Playwright) was not run.🤖 Generated with Claude Code