Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
9eb7c77
fix(core): _count now respects field-level read access on the relatio…
borisno2 Sep 3, 2026
fc77a88
chore(deps): bump @vercel/blob from 2.6.1 to 2.8.0 (#1183)
dependabot[bot] Sep 3, 2026
67dce2e
fix(core): hook context is the same secured StackContext as a transac…
borisno2 Sep 5, 2026
ddf7ff4
chore(deps): bump @radix-ui/react-label from 2.1.12 to 2.1.15 (#1182)
dependabot[bot] Sep 5, 2026
05df5a4
chore(deps-dev): bump @vitest/browser from 4.1.10 to 4.1.11 (#1185)
dependabot[bot] Sep 5, 2026
fcb1ddc
Version Packages (#1030)
github-actions[bot] Sep 6, 2026
98d5fc6
docs(adr): ADR-0052 keys a hook's context to the app's DB (#1211) (#1…
borisno2 Sep 6, 2026
11ea14a
Thread the app's Prisma client type through TypeInfo into hook contex…
borisno2 Sep 6, 2026
cc083cc
Version Packages (#1215)
github-actions[bot] Sep 6, 2026
2e1ee3d
Fix fragment reads through generated CustomDB losing narrowing (#1233…
borisno2 Sep 7, 2026
70927bc
chore(deps-dev): bump @types/react from 19.2.14 to 19.2.18 (#1270)
dependabot[bot] Sep 7, 2026
69eaedb
chore(deps): bump @aws-sdk/s3-request-presigner in the aws-sdk group …
dependabot[bot] Sep 7, 2026
2976f57
Fix a hook's context.db losing virtual/transformed-field typing (#1268)
borisno2 Sep 7, 2026
3501b65
Fix generated Context to derive transaction() from core's StackContex…
borisno2 Sep 7, 2026
78e03d6
chore(deps): bump sharp from 0.35.3 to 0.35.4 (#1274)
dependabot[bot] Sep 7, 2026
48383ac
chore(deps-dev): bump @typescript-eslint/parser from 8.63.0 to 8.69.0…
dependabot[bot] Sep 7, 2026
b32ffe5
chore(deps): bump tailwind-merge from 3.5.0 to 3.6.0 (#1280)
dependabot[bot] Sep 7, 2026
f1af019
chore(deps-dev): bump globals from 17.8.0 to 17.12.0 (#1279)
dependabot[bot] Sep 7, 2026
bc660e8
Version Packages (#1267)
github-actions[bot] Sep 7, 2026
6a66454
Fix generated Crud read methods not assignable to structural seams (#…
borisno2 Sep 7, 2026
399105c
Version Packages (#1323)
github-actions[bot] Sep 7, 2026
1a616c1
Add trailing overload to core's AugmentedFind* and thread TDb through…
borisno2 Sep 8, 2026
7aa75b0
Version Packages (#1351)
github-actions[bot] Sep 8, 2026
2798809
chore(deps): bump @radix-ui/react-popover from 1.1.20 to 1.1.23 (#1363)
dependabot[bot] Sep 8, 2026
d9eb716
chore(deps): bump the aws-sdk group with 2 updates (#1362)
dependabot[bot] Sep 8, 2026
661467e
chore(deps): bump @radix-ui/react-select from 2.3.4 to 2.3.7 (#1381)
dependabot[bot] Sep 10, 2026
82c7663
chore(deps): bump pg and @types/pg (#1380)
dependabot[bot] Sep 10, 2026
1da6535
fix(core): refuse unchecked nested set/updateMany/deleteMany, gate di…
borisno2 Sep 10, 2026
b837b3f
Merge main into prisma-8
borisno2 Sep 11, 2026
92d3caf
chore: regenerate lockfile for the merged manifests
borisno2 Sep 11, 2026
46ec9bf
fix: three auto-merge defects the textual merge left compiling-broken
borisno2 Sep 11, 2026
1ead103
fix(core): carry main's #1111 field-access gate onto the count entry
borisno2 Sep 11, 2026
5cc83e9
fix(cli): the generator stopped emitting main's prisma-client/CustomD…
borisno2 Sep 11, 2026
e19174a
chore: align the vitest family on 4.1.11 and format the merged docs
borisno2 Sep 11, 2026
02c526d
docs: point the rag-ollama-demo plugin-write note at ADR-0068
Sep 11, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 0 additions & 5 deletions .changeset/gentle-hounds-listen.md

This file was deleted.

2 changes: 1 addition & 1 deletion .changeset/loud-comets-invent.md
Original file line number Diff line number Diff line change
Expand Up @@ -143,7 +143,7 @@ field a `resolveInput` hook derives is embedded like any other.

The embedding and its metadata are write-denied to application code: an ordinary create or
update naming them throws. The plugin writes them itself, past that denial and running no
hook of the list's (ADR-0066), after the write's transaction settles. Applications that maintain their own vectors opt out explicitly:
hook of the list's (ADR-0068), after the write's transaction settles. Applications that maintain their own vectors opt out explicitly:

```typescript
manualVector: embedding({ dimensions: 1536, allowManualWrites: true })
Expand Down
5 changes: 0 additions & 5 deletions .changeset/olive-pandas-share.md

This file was deleted.

2 changes: 1 addition & 1 deletion .changeset/quiet-columns-settle.md
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ It takes the `AccessContext` `Plugin.runtime` receives as its first argument; th
`StackContext` `getContext` returns carries no ORM handle and is refused by name. That
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.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

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.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Fixed in 02c526dexamples/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
    writePluginOwnedField and "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 reach sudo()…") and
    packages/core/src/config/types.ts:3197 (the StackContext vs StackBaseContext split). In both
    the parenthetical attaches to a claim about writePluginOwnedField specifically, 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.


A write refused by name is a wiring defect that fails identically on every row, so the RAG
plugin's failure log now reports all three refusals — and `WriteCollectionMissingError`
Expand Down
5 changes: 0 additions & 5 deletions .changeset/quiet-otters-listen.md

This file was deleted.

22 changes: 22 additions & 0 deletions .changeset/stale-planets-refuse.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
---
'@opensaas/stack-core': minor
---

The nested-write access gap reported in #1384 is closed by refusal, not by gating

Nested `set`, `updateMany` and `deleteMany` under a relationship key used to
reach the database as an unchecked pass-through: the target list's access was
never consulted, no hooks ran, and an unscoped `where` could reach rows well
outside the parent's own subtree. Nested `disconnect` could name a target row
the caller could not read.

Both are closed here by ADR-0050 rather than by per-kind access machinery:
nested relation input leaves the write payload entirely, so `create`, `update`,
`delete`, `connectOrCreate`, `disconnect`, `set`, `updateMany` and `deleteMany`
are all refused with `NestedRelationInputError`. The refusal is unconditional —
`sudo()` does not lift it — which is strictly stronger than the interim
non-sudo-only refusal #1385 shipped on the previous line. Clearing an edge is
now `null` on the relationship field; every other case is a write against the
target list, wrapped in `context.transaction()` when it must land atomically.

See `rugged-terminals-persist.md` for the full write-surface change.
6 changes: 0 additions & 6 deletions .changeset/swift-otters-gather.md

This file was deleted.

5 changes: 0 additions & 5 deletions .changeset/tame-icons-jump.md

This file was deleted.

5 changes: 0 additions & 5 deletions .changeset/tangy-plums-jog.md

This file was deleted.

5 changes: 0 additions & 5 deletions .changeset/thick-camels-jog.md

This file was deleted.

5 changes: 0 additions & 5 deletions .changeset/tiny-eagles-sparkle.md

This file was deleted.

5 changes: 0 additions & 5 deletions .changeset/tiny-otters-hide.md

This file was deleted.

2 changes: 1 addition & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -354,7 +354,7 @@ export function myPlugin(pluginConfig: MyConfig): Plugin {
}
```

A plugin writing a column it owns and the application cannot write — an embedding, say — uses `writePluginOwnedField` from `@opensaas/stack-core/extend` rather than driving the list's own pipeline under `sudo()`. Re-running the pipeline over a payload naming one field recomputes derived fields from input that is not there. See ADR-0066.
A plugin writing a column it owns and the application cannot write — an embedding, say — uses `writePluginOwnedField` from `@opensaas/stack-core/extend` rather than driving the list's own pipeline under `sudo()`. Re-running the pipeline over a payload naming one field recomputes derived fields from input that is not there. See ADR-0068.

**Runtime Access:**

Expand Down
2 changes: 1 addition & 1 deletion CONTEXT.md
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,7 @@ The single module that runs the canonical, secured write sequence (operation-lev
_Avoid_: operation handler, mutation service

**Plugin-owned field write**:
The write a plugin makes to a field it computes and application code is denied — an embedding, say. It is not an application update: it carries that field's columns alone and completes a write the application already made, whose hooks have already run against the caller's real input, so it runs **no** hook. `writePluginOwnedField` on `@opensaas/stack-core/extend` is the whole of it: resolve the field against the config the context carries, split the value through that field's own `splitColumns`, issue one id-scoped `UPDATE` marked with the engine origin. It reaches no field but the one named — enforced, since the caller supplies no column layout — and a list, a field or a value (`undefined`) it cannot make sense of is refused by name. Driving it through `sudo().db` instead re-runs the list's pipeline over a payload naming one field, which recomputes a derived field from input that is not there and destroys it (ADR-0066).
The write a plugin makes to a field it computes and application code is denied — an embedding, say. It is not an application update: it carries that field's columns alone and completes a write the application already made, whose hooks have already run against the caller's real input, so it runs **no** hook. `writePluginOwnedField` on `@opensaas/stack-core/extend` is the whole of it: resolve the field against the config the context carries, split the value through that field's own `splitColumns`, issue one id-scoped `UPDATE` marked with the engine origin. It reaches no field but the one named — enforced, since the caller supplies no column layout — and a list, a field or a value (`undefined`) it cannot make sense of is refused by name. Driving it through `sudo().db` instead re-runs the list's pipeline over a payload naming one field, which recomputes a derived field from input that is not there and destroys it (ADR-0068).
_Avoid_: sudo write, plugin update, escalated update

**Hook Pipeline**:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -135,3 +135,40 @@ Four spellings today say what a field's TypeScript face is: `getTypeScriptType()
- **The dependency set is emitted twice from one pass** — as data for the engine (ADR-0051) and as the `needs` type here. They cannot drift because the generator computes the set once and renders it in both forms; a build effort that computes them separately has reintroduced the two-walker shape ADR-0051 deleted.
- **ADR-0032 is amended**: its rejected mapped-type option is adopted now the registry exists; its interface rule stands and extends to the generics core exports. **ADR-0040 is amended**: its "shrinks to what the contract cannot express" consequence resolves to the four items above. **ADR-0051 is amended**: the emitted set also has a type-level form, and its silent-break consequence is narrowed to untyped hooks.
- **`CONTEXT.md` gains "Contract remainder"** for the per-list facts the generator authors because the contract cannot.

## Amendment — a hook's `context` is keyed too ([#1211](https://github.com/OpenSaasAU/stack/issues/1211))

The Hook arguments section above has three consequences; there is a fourth, and
it was missing rather than decided against. Every hook-args type hardcodes an
unparameterised `context`, so a hook's context statically resolves over
`PrismaClientLike = any`. `AccessControlledDB<any>` is a mapped type over
`keyof any` intersected with an index signature: it declares no named delegate,
is therefore assignable to nothing, and accepts `context.db.typoedListName`
without complaint. A consumer passing a hook's `context` into its own typed
function has no expressible spelling and must write
`context as unknown as Context`.

This record's own keying is what closes it. `TypeInfo` is the config-facing
seam, so it carries the app's `DB` alongside `item`, `output`, `inputs` and
`fields`, and core's generic hook types read `context` off it — a hook's
`context` is keyed to the same `DB` the generated `Context` instantiates, not
widened. The consequence "Core gains the generics and loses `any`" already
deletes `PrismaClientLike` and `AccessControlledDB`'s structural probing; this
says the hook args must be keyed at the same time rather than left reading a
deleted default.

What a hook's context is keyed to is settled here; what it _carries_ is not.
Under this record every hook — `resolveInput`, `validate`,
`beforeOperation`, `afterOperation` and a field `resolveOutput` — receives
`StackBaseContext<DB, S, PluginServices>`, emitted per app as `BaseContext`:
the secured `db`, the session and the ambient plumbing, and nothing that can
start a transaction or change who is asking. `sudo()`, `withSession()` and
`transaction()` live on `StackContext` (`Context`), which a server action or
page component holds. `beforeTransaction` / `afterTransaction` keep the plain
base-client-bound context of ADR-0028, keyed to the same `DB`.

`main` closes this ahead of the build, threading the client type through
`TypeInfo` under the pre-contract keying (#1211). That work is not wasted and
not a second mechanism: the seam it establishes — the client type enters at
`TypeInfo`, and core's hook types read it from there — is the seam this record
keeps. What changes under the contract is only what `TypeInfo` holds.
Loading
Loading