@appxdigital/appx-core 0.1.128 → 0.1.130

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (35) hide show
  1. package/CHANGELOG.md +77 -0
  2. package/README.md +22 -19
  3. package/dist/backoffice/appx-core-admin.module.d.ts.map +1 -1
  4. package/dist/backoffice/appx-core-admin.module.js +2 -3
  5. package/dist/common/config/permissionsConfigTypes.d.ts +3 -2
  6. package/dist/common/config/permissionsConfigTypes.d.ts.map +1 -1
  7. package/dist/common/guards/rbac.guard.d.ts.map +1 -1
  8. package/dist/common/guards/rbac.guard.js +6 -0
  9. package/dist/common/utils/schema-annotations.util.d.ts +7 -0
  10. package/dist/common/utils/schema-annotations.util.d.ts.map +1 -0
  11. package/dist/common/utils/schema-annotations.util.js +12 -0
  12. package/dist/config/generate-dtos.d.ts +1 -0
  13. package/dist/config/generate-dtos.d.ts.map +1 -1
  14. package/dist/config/generate-dtos.js +5 -2
  15. package/dist/modules/auth/refresh-token.strategy.d.ts +1 -3
  16. package/dist/modules/auth/refresh-token.strategy.d.ts.map +1 -1
  17. package/dist/modules/auth/refresh-token.strategy.js +9 -6
  18. package/dist/modules/core/core.controller.d.ts.map +1 -1
  19. package/dist/modules/core/core.controller.js +2 -13
  20. package/dist/prisma/permissions-validator.d.ts +1 -1
  21. package/dist/prisma/permissions-validator.d.ts.map +1 -1
  22. package/dist/prisma/permissions-validator.js +37 -11
  23. package/dist/prisma/prisma.service.d.ts +69 -0
  24. package/dist/prisma/prisma.service.d.ts.map +1 -1
  25. package/dist/prisma/prisma.service.js +322 -57
  26. package/docs/README.md +3 -1
  27. package/docs/data-access.md +21 -5
  28. package/docs/dtos.md +2 -2
  29. package/docs/generate.md +2 -2
  30. package/docs/graphql.md +2 -3
  31. package/docs/limitations.md +8 -9
  32. package/docs/permissions.md +14 -5
  33. package/docs/storage.md +89 -0
  34. package/docs/testing.md +74 -0
  35. package/package.json +2 -2
package/CHANGELOG.md CHANGED
@@ -14,6 +14,83 @@ This project follows the `MAJOR.MINOR.PATCH` scheme in `package.json`.
14
14
 
15
15
  ---
16
16
 
17
+ ## [0.1.130] — 2026-10-18
18
+
19
+ Relations reached by a query now take part only through the related rows the caller can read — in filters, in nested selections at any depth, in `orderBy` and in `_count`.
20
+
21
+ ### Added
22
+
23
+ - **`set` on a write rule — values the server writes.** `create: { set: { ownerId: '$USER_ID' } }`, `updateMany: { conditions: {…}, set: { updatedById: '$USER_ID' } }`, or a forced value (`status: 'draft'`). Applied in the data-access proxy on every write the rule allows — generated CRUD, service code and nested creates — overriding the client's value. Set values are authorized like any other: they must satisfy the rule's `conditions`, and a set foreign key needs an explicit `connect` rule (the app refuses to boot without one). Replaces `setUserIdField` (removed — see Migration).
24
+ - **`appx-core create` requires npm 11.** It stops at startup with the upgrade command (`npm install -g npm@11`) when npm is older: npm 10 crashes resolving current dependency trees (`Cannot read properties of null (reading 'edgesOut')`). Created projects declare `engines.npm: ">=11.0.0"`.
25
+
26
+ ### Removed
27
+
28
+ - **`setUserIdField`.** Use `set: { <column>: '$USER_ID' }` on the rule. A config that still contains `setUserIdField` refuses to boot (`removed-set-user-id-field`) with the exact replacement, so it can't be silently ignored.
29
+
30
+ ### Fixed
31
+
32
+ - **Client relation filters respect the related model's read rules.** A `where` that crosses a relation (`some`/`every`/`none`/`is`/`isNot`, or the to-one shorthand) — at the top level, inside a nested selection or inside a `_count` entry — is evaluated over the related rows the role can read. A relation into a model with **no** read rule for the role is rejected (`403`, `Cannot filter by 'model.relation' …`). Applies to every read and to `updateMany`/`deleteMany`, on every transport (REST, GraphQL, service code).
33
+ - **Relations selected below a to-one relation are filtered.** `include: { project: { include: { owner: true, tasks: true } } }` now applies `owner`'s and `tasks`'s read rules, as the first level already did: a deeper unreadable to-one drops the row (same rule as a first-level to-one), a deeper to-many list is narrowed, and a deeper model with no read rule throws `403`.
34
+ - **`_count` counts readable rows only** (`select: { _count: { select: { tasks: true } } }`, `_count: true`). Counting a relation whose model has no read rule for the role throws `403`.
35
+ - **`orderBy` on a related to-one field** requires that record to be readable, like including it: rows whose related record isn't readable are left out. **`orderBy` a to-many `_count`** requires an unconditional (`'ALL'`) read rule on the related model, or the model to be exposed; otherwise `403`.
36
+ - **A nested selection's `where` / `orderBy` / `distinct` can't reference a `@Role`-hidden field** (`403`), as at the top level.
37
+ - **`BYPASS_OMISSION` no longer disables row filtering on `include`d relations**, and a bare to-many `select: { rel: true }` under it no longer throws a `TypeError`.
38
+ - **`RbacGuard` applies the documented read fallback.** `findFirst` / `findFirstOrThrow` / `findUnique` / `findUniqueOrThrow` / `count` are allowed by a `findMany` rule (then `findFirst`), as in the proxy — a config with only `findMany` no longer `403`s `GET /:id`. Custom `@Permission` actions never inherit a read rule.
39
+ - **Schema annotations quoted in backticks are ignored.** A `///` comment that mentions a tag in prose — `` /// unlike `role` (`@NoWrite`) … `` — no longer applies `@NoWrite` / `@Role(...)` to the field (runtime and generated DTOs). Unquoted tags apply as before.
40
+ - **Installing the framework works again since NestJS 12.** `@adminjs/nestjs` accepts any `@nestjs/common` / `@nestjs/core` from 9 up; as a peer, npm resolved it to Nest 12 before the framework's `~11` ranges and the install failed (`ERESOLVE`) — `appx-core create`, and any fresh install without a lockfile. `@adminjs/nestjs` is now a framework **dependency**, so it resolves against the framework's Nest 11; projects declare nothing extra. `create` also installs `@appxdigital/appx-core` from the public registry even when the `@appxdigital` scope is mapped elsewhere (e.g. GitHub Packages).
41
+
42
+ ### Changed
43
+
44
+ - `src/prisma/prisma.service.ts` is held at 100% line and branch coverage in CI.
45
+ - Docs: `withExposedModels` / `@ExposeModels` skip row filtering only — `@Role` field omission still applies; the per-call `BYPASS_*` options work on every proxied model method (untyped); exposing a model doesn't skip the target's `connect` rule.
46
+
47
+ ### Migration
48
+
49
+ > **Behaviour change — a migrating agent MUST flag it to the developer before deploying.** Reads that previously returned data through a relation the role can't read now return less, or `403`. Nothing else changes: queries whose relations are all readable return exactly what they did.
50
+
51
+ 1. **Upgrade and run your test suite / exercise your main read endpoints as each role.** Look for new `403`s and for rows or nested lists that disappeared.
52
+ 2. **New `403 Missing permissions on model X` / `Cannot filter by …` / `Cannot order by …`** — the role reaches model `X` (selected below another relation, filtered through, counted, or sorted by) but has no read rule for it. Add a `findMany`/`findFirst` rule for that role on `X`, or expose `X` for that one request (`@ExposeModels('X')` / `withExposedModels(['X'], …)`) when the endpoint authorizes it itself.
53
+ 3. **A row disappeared (e.g. `findFirst` now `null` → `404`)** — the query selects a to-one relation, possibly several levels deep (`team → owner → user`), whose record the role's read rule doesn't cover. Widen that model's read rule to cover the records the endpoint legitimately shows (e.g. "users who share a team with me"), or stop selecting the relation.
54
+ 4. **A nested list, a `_count` or a filtered result got smaller** — it now covers only related rows the role can read. Same fix: widen the related model's read rule if the endpoint should see those rows.
55
+ 5. **Replace `setUserIdField` with `set`** (the app refuses to boot until you do — `removed-set-user-id-field`). For each rule:
56
+
57
+ ```ts
58
+ create: { conditions: {...}, setUserIdField: 'ownerId' } // before
59
+ create: { conditions: {...}, set: { ownerId: '$USER_ID' } } // after
60
+ ```
61
+
62
+ `set` applies on **every** create — generated `POST`, your services, nested creates — not only the generated `POST`. If a service creates these rows **on behalf of another user** under the same role, it now gets the caller's id instead: give that flow its own endpoint/rule or expose the model for it. The relationship still needs a `connect` rule (e.g. `relations: { owner: { connect: { conditions: { id: '$USER_ID' } } } }`); the boot error names it if missing.
63
+ 6. **`orderBy: { rel: { _count: … } }` now `403`s** when the related model's read rule has conditions. Select the scoped `_count` and sort in code, or expose the model for that request.
64
+
65
+ > **For AI agents:** do not report this upgrade as complete after bumping the version. Run the project's tests and its main read flows per role, and list for the developer every new `403`, missing row and shrunken list you find, with the read rule that would restore it. Widening a read rule is a security decision — propose it, don't apply it silently.
66
+
67
+ ---
68
+
69
+ ## [0.1.129] — 2026-08-17
70
+
71
+ ### Added
72
+
73
+ - **Every new project ships a ready-to-run test suite.** `appx-core create` now scaffolds `vitest.config.ts` and `test/` (Vitest + SWC), with `npm test` / `npm run test:watch` scripts and the needed devDependencies. A run provisions a **throwaway database container** via Testcontainers (matching `DB_PROVIDER` — the database in `.env` is never touched; Docker is the only requirement), pushes the Prisma schema, and boots the real `AppModule` with the production middleware stack (session store, passport, `setupCoreSecurity`). Shipped specs: app boot, register (unknown-field rejection included), session login → `/auth/me`, JWT login → `/auth/refresh` — all asserted at the database level — plus a **route sweep** that probes every registered route as a guest and fails on anything non-public that answers `2xx`. See [docs/testing.md](./docs/testing.md).
74
+
75
+ - **Every new project ships an agent guide.** `appx-core create` drops an `AGENTS.md` at the project root (plus a `CLAUDE.md` pointer) — the rules of the road for coding agents working in the project: proxy-only data access, default-deny permissions, generated-code ownership, read/write surfaces, test conventions, and the pre-production checklist (including removing the Hello-World placeholder). It points at `node_modules/@appxdigital/appx-core/docs/` as the source of truth instead of duplicating it.
76
+
77
+ ### Fixed
78
+
79
+ - **`POST /auth/refresh` now works under a default-deny permissions config.** The refresh-token user lookup runs as a framework auth flow (like login and JWT population). Previously, a permissions config with no `GUEST` read rule on `User` — including the scaffold default — returned `403` for every refresh.
80
+ - **AdminJS is loaded lazily.** The backoffice no longer imports `adminjs` at module load time; it resolves through the same dynamic-import path as the other AdminJS packages. Drop-in — enables importing the package under CommonJS test runners and on Node < 20.19.
81
+
82
+ ### Migration
83
+
84
+ > Drop-in for existing projects. To adopt the test suite in a project created before this version:
85
+
86
+ 1. Copy `vitest.config.ts` and the `test/` directory from a freshly created project (or from this package's `cli/scaffold/`).
87
+ 2. Add the scripts: `"test": "vitest run"`, `"test:watch": "vitest"`.
88
+ 3. `npm install -D vitest unplugin-swc @swc/core supertest @types/supertest @nestjs/testing @testcontainers/mysql @testcontainers/postgresql dotenv`.
89
+ 4. Run `npm test` (requires Docker). Extend `test/helpers/harness.ts`'s `truncateAll` and `test/isolation.spec.ts`'s `PUBLIC_ROUTES` as your schema and routes grow.
90
+ 5. Optionally copy `AGENTS.md` (and the `CLAUDE.md` pointer) from `cli/scaffold/`, replacing `{{PROJECT_NAME}}` with your project's name.
91
+
92
+ ---
93
+
17
94
  ## [0.1.128] — 2026-08-04
18
95
 
19
96
  Read-only GraphQL, reworked into a **namespaced, opt-in-per-module** API.
package/README.md CHANGED
@@ -86,6 +86,8 @@ AppX Core puts **security and data-access control first**, while keeping develop
86
86
  - Namespaced, **opt-in per model**: `model { find get count }`
87
87
  - **Native pagination** (`take` / `skip` / `cursor`), nested relations, and several models per request — prefer it over the CRUD list endpoint for reads/filtering, and enable it on your main queryable models
88
88
  - Same ABAC as REST (row / field / nested-record filtering; filter & sort limited to readable fields)
89
+ - **Tests out of the box**
90
+ - `npm test` provisions a throwaway database container (Docker) and drives the real app over HTTP — register/login/refresh flows plus a route-sweep that denies guests everywhere non-public
89
91
  - **Developer-friendly extension points**
90
92
  - Add custom endpoints with `@Permission(...)`
91
93
  - Use services for business logic, controllers as transport layer
@@ -110,7 +112,7 @@ AppX Core is designed as a **base layer** on top of NestJS:
110
112
 
111
113
  ### Prerequisites
112
114
 
113
- - **Node.js 20+**
115
+ - **Node.js 20+** with **npm 11+** (`npm install -g npm@11`; `appx-core create` checks it)
114
116
  - A running database (**MySQL** or **PostgreSQL** tested/validated)
115
117
 
116
118
  ### 1) Install the CLI
@@ -160,7 +162,7 @@ appx-core create --yes \
160
162
  --db-user root --db-password secret --db-name my_app_db
161
163
  ```
162
164
 
163
- Flags: `--name`, `--db-provider mysql|postgresql`, `--db-host`, `--db-port`, `--db-user`, `--db-password`, `--db-name`, `--show-output`. File upload isn't configured on this path — run `appx-core setup:fileupload` inside the project afterwards.
165
+ Flags: `--name`, `--db-provider mysql|postgresql`, `--db-host`, `--db-port`, `--db-user`, `--db-password`, `--db-name`, `--show-output`. File upload isn't configured on this path — run `appx-core setup:fileupload` inside the project afterwards (see [docs/storage.md](./docs/storage.md)).
164
166
 
165
167
  ### 3) Configure Environment
166
168
 
@@ -487,7 +489,7 @@ model User {
487
489
  }
488
490
  ```
489
491
 
490
- Use `/// @NoWrite` to exclude a field for **all** roles. For **per-role** control (e.g. only ADMIN may set `role`), use `restrictedFields` on the permission instead of `@NoWrite`.
492
+ A field is either CRUD-writable or it isn't — there is no per-role write control on a generated endpoint. For a privileged field (e.g. `role`), annotate it `/// @NoWrite` and set it through a dedicated endpoint. See [docs/dtos.md](./docs/dtos.md).
491
493
 
492
494
  ---
493
495
 
@@ -558,7 +560,7 @@ export const PermissionsConfig = {
558
560
  };
559
561
  ```
560
562
 
561
- Service (Prisma query remains straightforward; ABAC filtering is applied by the core):
563
+ Service (ABAC filtering is applied by the core):
562
564
 
563
565
  ```ts
564
566
  async getMyProfile() {
@@ -603,22 +605,27 @@ This pattern should be used sparingly and intentionally.
603
605
 
604
606
  ## Limitations
605
607
 
606
- ### Prisma method restrictions (permission safety)
608
+ ### Single-record methods are aliases
607
609
 
608
- Avoid these Prisma methods because they can bypass or complicate granular permission filtering:
610
+ The single-record Prisma methods are aliases to their filter-compatible equivalents — in the TypeScript types and at runtime — and take that method's arguments and return its result. ABAC applies to every call.
609
611
 
610
- - `findUnique` → use `findFirst`
611
- - `delete` → use `deleteMany` with a single-item filter
612
- - `update` → use `updateMany` with a single-item filter
612
+ - `findUnique()` / `findUniqueOrThrow()` run `findFirst()` / `findFirstOrThrow()`
613
+ - `update()` runs `updateMany()` — returns `{ count }`, not the record
614
+ - `delete()` runs `deleteMany()` — returns `{ count }`
613
615
 
614
- Operational guidance:
616
+ They act on **all** matching rows, so pass a restrictive `where` when you intend a single record; a call reaching outside your permissions affects `{ count: 0 }`. Full contract in [docs/data-access.md](./docs/data-access.md#method-contract-important).
615
617
 
616
- - Prefer `findMany`, `findFirst`, `updateMany`, and `deleteMany`.
617
- - When you intend a single-record operation, use a restrictive `where` clause.
618
+ ### GraphQL
618
619
 
619
- ### GraphQL status
620
+ GraphQL exposes a **read-only** API — `find` / `get` / `count`, namespaced per model and **opt-in per module** (one line) — that runs through the same ABAC proxy as REST: row filtering, field omission, nested-relation filtering, and filter/sort limited to fields the role can read all apply. It supports nested selections and native pagination (`take`/`skip`/`cursor`), so it's the recommended surface for querying/filtering/paginating reads. See **[docs/graphql.md](./docs/graphql.md)**.
620
621
 
621
- GraphQL exposes a **read-only** API — `find` / `get` / `count`, namespaced per model and **opt-in per module** (one line) — that runs through the same ABAC proxy as REST: row filtering, field omission, nested-relation filtering, and filter/sort limited to fields the role can read all apply. It supports nested selections and native pagination (`take`/`skip`/`cursor`), so it's the recommended surface for querying/filtering/paginating reads. See **[docs/graphql.md](./docs/graphql.md)**. Not yet: mutations and full aggregates — REST CRUD + AdminJS remain the write surfaces.
622
+ Aggregations are not yet available (on the roadmap); `count` and nested `_count` cover the common cases today.
623
+
624
+ ---
625
+
626
+ ## Testing
627
+
628
+ Every new project ships a ready-to-run suite: `npm test` starts a disposable database container matching your `DB_PROVIDER`, pushes the schema, boots the real `AppModule` with the production middleware stack, and runs HTTP-level specs — auth flows asserted at the database level, and a route sweep that probes every registered route as a guest. Docker is the only requirement; the database in `.env` is never touched. See **[docs/testing.md](./docs/testing.md)** for the harness API and how to write specs for your modules.
622
629
 
623
630
  ---
624
631
 
@@ -636,15 +643,11 @@ No. Generated NestJS modules are **editable** and intended to be extended.
636
643
 
637
644
  Planned and under consideration:
638
645
 
639
- - Production-ready GraphQL
640
- - Object storage patterns (S3-friendly out-of-the-box support)
646
+ - GraphQL aggregations
641
647
  - Forgot-password flow
642
648
  - Email templates + sending primitives
643
649
  - Push notifications primitives
644
650
  - WebSockets support
645
- - Stronger testing baseline (unit tests scaffolding and patterns)
646
- - “Agent-ready” development guidance (`agents.txt`, conventions for coding agents)
647
- - Optional AI-assisted project scaffolding
648
651
 
649
652
  ---
650
653
 
@@ -1 +1 @@
1
- {"version":3,"file":"appx-core-admin.module.d.ts","sourceRoot":"","sources":["../../src/backoffice/appx-core-admin.module.ts"],"names":[],"mappings":"AAGA,OAAO,EAAC,aAAa,EAAiB,MAAM,gBAAgB,CAAC;AAO7D,OAAO,EAAC,eAAe,EAAC,MAAM,kCAAkC,CAAC;AAIjE,OAAO,EAAC,qBAAqB,EAAC,MAAM,yCAAyC,CAAC;AAE9E,qBAEa,mBAAmB;IAC5B,MAAM,CAAC,OAAO,CAAC,MAAM,EAAE,eAAe,EAAE,gBAAgB,EAAE,qBAAqB,GAAG,OAAO,CAAC,aAAa,CAAC;CAG3G"}
1
+ {"version":3,"file":"appx-core-admin.module.d.ts","sourceRoot":"","sources":["../../src/backoffice/appx-core-admin.module.ts"],"names":[],"mappings":"AAGA,OAAO,EAAC,aAAa,EAAiB,MAAM,gBAAgB,CAAC;AAM7D,OAAO,EAAC,eAAe,EAAC,MAAM,kCAAkC,CAAC;AAIjE,OAAO,EAAC,qBAAqB,EAAC,MAAM,yCAAyC,CAAC;AAE9E,qBAEa,mBAAmB;IAC5B,MAAM,CAAC,OAAO,CAAC,MAAM,EAAE,eAAe,EAAE,gBAAgB,EAAE,qBAAqB,GAAG,OAAO,CAAC,aAAa,CAAC;CAG3G"}
@@ -37,7 +37,6 @@ const common_1 = require("@nestjs/common");
37
37
  const utils_1 = require("./utils");
38
38
  const utils_2 = require("../config/utils");
39
39
  const nestjs_request_context_1 = require("nestjs-request-context");
40
- const adminjs_1 = require("adminjs");
41
40
  const argon2 = __importStar(require("argon2"));
42
41
  const prisma_service_1 = require("../prisma/prisma.service");
43
42
  let AppxCoreAdminModule = exports.AppxCoreAdminModule = class AppxCoreAdminModule {
@@ -54,7 +53,7 @@ function lowerCase(name) {
54
53
  }
55
54
  async function createAdminJsModule(adminConfig, permissionConfig) {
56
55
  // Due to AdminJS only allowing ESM now, we need to use dynamic imports to load the modules, this function can be found within src/backoffice/utils.ts
57
- const { default: AdminJS } = await (0, utils_1.dynamicImport)('adminjs');
56
+ const { default: AdminJS, BaseRecord } = await (0, utils_1.dynamicImport)('adminjs');
58
57
  const { Database, Resource, convertParam } = await (0, utils_1.dynamicImport)('@adminjs/prisma');
59
58
  const { AdminModule } = await (0, utils_1.dynamicImport)('@adminjs/nestjs');
60
59
  const { default: importExportFeature } = await (0, utils_1.dynamicImport)('@adminjs/import-export');
@@ -106,7 +105,7 @@ async function createAdminJsModule(adminConfig, permissionConfig) {
106
105
  });
107
106
  if (!result)
108
107
  return null;
109
- return new adminjs_1.BaseRecord(this.prepareReturnValues(result), this);
108
+ return new BaseRecord(this.prepareReturnValues(result), this);
110
109
  }
111
110
  async update(pk, params = {}) {
112
111
  const idProperty = this.properties().find((property) => property.isId());
@@ -47,7 +47,8 @@ type MethodsWithWhere<C extends RuntimeClient, M extends ModelKey<C>> = {
47
47
  export type FieldConditions<M extends ModelKey<RuntimeClient>, A extends MethodsWithWhere<RuntimeClient, M>> = WhereOf<RuntimeClient, M, A>;
48
48
  export interface ActionPermission<M extends ModelKey<RuntimeClient> = ModelKey<RuntimeClient>, A extends MethodsWithWhere<RuntimeClient, M> = MethodsWithWhere<RuntimeClient, M>> {
49
49
  conditions: FieldConditions<M, A> | FieldConditions<M, A>[];
50
- setUserIdField?: string;
50
+ /** Column values the server sets on every write this rule allows (overriding the client's). */
51
+ set?: Record<string, unknown>;
51
52
  }
52
53
  /**
53
54
  * A relation-scoped `connect` rule, declared on the SOURCE model and keyed by
@@ -72,7 +73,7 @@ export type RolePermissions<M extends ModelKey<RuntimeClient> = ModelKey<Runtime
72
73
  } & {
73
74
  [custom: string]: 'ALL' | RelationConnectMap | {
74
75
  conditions?: unknown | unknown[];
75
- setUserIdField?: string;
76
+ set?: Record<string, unknown>;
76
77
  };
77
78
  };
78
79
  export type ModelPermissions<M extends ModelKey<RuntimeClient> = ModelKey<RuntimeClient>> = {
@@ -1 +1 @@
1
- {"version":3,"file":"permissionsConfigTypes.d.ts","sourceRoot":"","sources":["../../../src/common/config/permissionsConfigTypes.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"permissionsConfigTypes.d.ts","sourceRoot":"","sources":["../../../src/common/config/permissionsConfigTypes.ts"],"names":[],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"rbac.guard.d.ts","sourceRoot":"","sources":["../../../src/common/guards/rbac.guard.ts"],"names":[],"mappings":"AAAA,OAAO,EACH,WAAW,EACX,gBAAgB,EAInB,MAAM,gBAAgB,CAAC;AACxB,OAAO,EAAC,SAAS,EAAC,MAAM,cAAc,CAAC;AAGvC,OAAO,EAAC,qBAAqB,EAAmC,MAAM,kCAAkC,CAAC;AAEzG,qBACa,SAAU,YAAW,WAAW;IAErC,OAAO,CAAC,SAAS;IACiB,OAAO,CAAC,iBAAiB;gBADnD,SAAS,EAAE,SAAS,EACc,iBAAiB,EAAE,qBAAqB;IAGhF,WAAW,CAAC,OAAO,EAAE,gBAAgB,GAAG,OAAO,CAAC,OAAO,CAAC;CA0CjE"}
1
+ {"version":3,"file":"rbac.guard.d.ts","sourceRoot":"","sources":["../../../src/common/guards/rbac.guard.ts"],"names":[],"mappings":"AAAA,OAAO,EACH,WAAW,EACX,gBAAgB,EAInB,MAAM,gBAAgB,CAAC;AACxB,OAAO,EAAC,SAAS,EAAC,MAAM,cAAc,CAAC;AAGvC,OAAO,EAAC,qBAAqB,EAAmC,MAAM,kCAAkC,CAAC;AAIzG,qBACa,SAAU,YAAW,WAAW;IAErC,OAAO,CAAC,SAAS;IACiB,OAAO,CAAC,iBAAiB;gBADnD,SAAS,EAAE,SAAS,EACc,iBAAiB,EAAE,qBAAqB;IAGhF,WAAW,CAAC,OAAO,EAAE,gBAAgB,GAAG,OAAO,CAAC,OAAO,CAAC;CA+CjE"}
@@ -18,6 +18,7 @@ const core_1 = require("@nestjs/core");
18
18
  const permission_decorator_1 = require("../decorators/permission.decorator");
19
19
  const permissions_config_provider_1 = require("../config/permissions.config.provider");
20
20
  const permissionsConfigTypes_1 = require("../config/permissionsConfigTypes");
21
+ const READ_ACTIONS = ['findMany', 'findFirst', 'findFirstOrThrow', 'findUnique', 'findUniqueOrThrow', 'count'];
21
22
  let RbacGuard = exports.RbacGuard = class RbacGuard {
22
23
  constructor(reflector, permissionsConfig) {
23
24
  this.reflector = reflector;
@@ -46,6 +47,11 @@ let RbacGuard = exports.RbacGuard = class RbacGuard {
46
47
  if (!permission && permissionsConfigTypes_1.SINGULAR_ACTION[action]) {
47
48
  permission = rolePermissions[permissionsConfigTypes_1.SINGULAR_ACTION[action]];
48
49
  }
50
+ // Standard reads share one rule (count / findFirst / findUnique → findMany
51
+ // → findFirst), as in the proxy. Custom actions never inherit a read rule.
52
+ if (!permission && READ_ACTIONS.includes(action)) {
53
+ permission = rolePermissions.findMany || rolePermissions.findFirst;
54
+ }
49
55
  if (!permission) {
50
56
  throw new common_1.ForbiddenException(`Action ${action} not allowed for role ${user.role} on model ${model}`);
51
57
  }
@@ -0,0 +1,7 @@
1
+ /**
2
+ * The part of a schema `///` comment that carries annotations (`@Role(...)`,
3
+ * `@NoWrite`). Backtick-quoted spans are dropped, so a comment can mention a
4
+ * tag in prose — `` /// unlike `role` (`@NoWrite`) … `` — without applying it.
5
+ */
6
+ export declare function annotationText(documentation?: string): string;
7
+ //# sourceMappingURL=schema-annotations.util.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"schema-annotations.util.d.ts","sourceRoot":"","sources":["../../../src/common/utils/schema-annotations.util.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AACH,wBAAgB,cAAc,CAAC,aAAa,CAAC,EAAE,MAAM,GAAG,MAAM,CAE7D"}
@@ -0,0 +1,12 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.annotationText = void 0;
4
+ /**
5
+ * The part of a schema `///` comment that carries annotations (`@Role(...)`,
6
+ * `@NoWrite`). Backtick-quoted spans are dropped, so a comment can mention a
7
+ * tag in prose — `` /// unlike `role` (`@NoWrite`) … `` — without applying it.
8
+ */
9
+ function annotationText(documentation) {
10
+ return (documentation || '').replace(/`[^`]*`/g, '');
11
+ }
12
+ exports.annotationText = annotationText;
@@ -1,3 +1,4 @@
1
+ export declare function isNoWrite(documentation?: string): boolean;
1
2
  /**
2
3
  * Deploy-safe pass: (re)generate the DTO **base** classes under the gitignored
3
4
  * `src/generated/dto/`, one per non-framework model. Overwrite-only — never
@@ -1 +1 @@
1
- {"version":3,"file":"generate-dtos.d.ts","sourceRoot":"","sources":["../../src/config/generate-dtos.ts"],"names":[],"mappings":"AA0UA;;;;;;GAMG;AACH,wBAAgB,gBAAgB,CAAC,SAAS,EAAE,GAAG,EAAE,GAAG,IAAI,CAiBvD;AAED;;;;GAIG;AACH,wBAAgB,mBAAmB,CAAC,SAAS,EAAE,MAAM,GAAG,IAAI,CAU3D"}
1
+ {"version":3,"file":"generate-dtos.d.ts","sourceRoot":"","sources":["../../src/config/generate-dtos.ts"],"names":[],"mappings":"AAkEA,wBAAgB,SAAS,CAAC,aAAa,CAAC,EAAE,MAAM,GAAG,OAAO,CAWzD;AA+PD;;;;;;GAMG;AACH,wBAAgB,gBAAgB,CAAC,SAAS,EAAE,GAAG,EAAE,GAAG,IAAI,CAiBvD;AAED;;;;GAIG;AACH,wBAAgB,mBAAmB,CAAC,SAAS,EAAE,MAAM,GAAG,IAAI,CAU3D"}
@@ -23,9 +23,10 @@ var __importStar = (this && this.__importStar) || function (mod) {
23
23
  return result;
24
24
  };
25
25
  Object.defineProperty(exports, "__esModule", { value: true });
26
- exports.scaffoldDtoSubclass = exports.generateDtoBases = void 0;
26
+ exports.scaffoldDtoSubclass = exports.generateDtoBases = exports.isNoWrite = void 0;
27
27
  const path = __importStar(require("path"));
28
28
  const utils_1 = require("./utils");
29
+ const schema_annotations_util_1 = require("../common/utils/schema-annotations.util");
29
30
  /**
30
31
  * DTO generator.
31
32
  *
@@ -63,7 +64,8 @@ const modulesRoot = path.join(cwd, 'src/modules');
63
64
  // A module-level lookup so the relation-nested-write renderer can resolve a
64
65
  // related model by name without threading it through every call.
65
66
  let modelsByName = {};
66
- function isNoWrite(doc) {
67
+ function isNoWrite(documentation) {
68
+ const doc = (0, schema_annotations_util_1.annotationText)(documentation);
67
69
  if (!doc)
68
70
  return false;
69
71
  if (/@NoWrite\b/i.test(doc))
@@ -77,6 +79,7 @@ function isNoWrite(doc) {
77
79
  }
78
80
  return false;
79
81
  }
82
+ exports.isNoWrite = isNoWrite;
80
83
  function isServerManagedTimestamp(field) {
81
84
  if (field.isUpdatedAt)
82
85
  return true;
@@ -2,15 +2,13 @@ import { Strategy } from 'passport-jwt';
2
2
  import { ConfigService } from '@nestjs/config';
3
3
  import { Request } from 'express';
4
4
  import { PrismaService } from '../../prisma/prisma.service';
5
- import { UserService } from '../user/user.service';
6
5
  declare const RefreshTokenStrategy_base: new (...args: [opt: import("passport-jwt").StrategyOptionsWithRequest] | [opt: import("passport-jwt").StrategyOptionsWithoutRequest]) => Strategy & {
7
6
  validate(...args: any[]): unknown;
8
7
  };
9
8
  export declare class RefreshTokenStrategy extends RefreshTokenStrategy_base {
10
9
  private readonly configService;
11
10
  private readonly prisma;
12
- private readonly userService;
13
- constructor(configService: ConfigService, prisma: PrismaService, userService: UserService);
11
+ constructor(configService: ConfigService, prisma: PrismaService);
14
12
  validate(req: Request, payload: any): Promise<any>;
15
13
  }
16
14
  export {};
@@ -1 +1 @@
1
- {"version":3,"file":"refresh-token.strategy.d.ts","sourceRoot":"","sources":["../../../src/modules/auth/refresh-token.strategy.ts"],"names":[],"mappings":"AAEA,OAAO,EAAa,QAAQ,EAAC,MAAM,cAAc,CAAC;AAClD,OAAO,EAAC,aAAa,EAAC,MAAM,gBAAgB,CAAC;AAC7C,OAAO,EAAC,OAAO,EAAC,MAAM,SAAS,CAAC;AAChC,OAAO,EAAC,aAAa,EAAC,MAAM,6BAA6B,CAAC;AAC1D,OAAO,EAAC,WAAW,EAAC,MAAM,sBAAsB,CAAC;;;;AAEjD,qBACa,oBAAqB,SAAQ,yBAAyC;IAE3E,OAAO,CAAC,QAAQ,CAAC,aAAa;IAC9B,OAAO,CAAC,QAAQ,CAAC,MAAM;IACvB,OAAO,CAAC,QAAQ,CAAC,WAAW;gBAFX,aAAa,EAAE,aAAa,EAC5B,MAAM,EAAE,aAAa,EACrB,WAAW,EAAE,WAAW;IAcvC,QAAQ,CAAC,GAAG,EAAE,OAAO,EAAE,OAAO,EAAE,GAAG;CAmC5C"}
1
+ {"version":3,"file":"refresh-token.strategy.d.ts","sourceRoot":"","sources":["../../../src/modules/auth/refresh-token.strategy.ts"],"names":[],"mappings":"AAEA,OAAO,EAAa,QAAQ,EAAC,MAAM,cAAc,CAAC;AAClD,OAAO,EAAC,aAAa,EAAC,MAAM,gBAAgB,CAAC;AAC7C,OAAO,EAAC,OAAO,EAAC,MAAM,SAAS,CAAC;AAChC,OAAO,EAAC,aAAa,EAAC,MAAM,6BAA6B,CAAC;;;;AAE1D,qBACa,oBAAqB,SAAQ,yBAAyC;IAE3E,OAAO,CAAC,QAAQ,CAAC,aAAa;IAC9B,OAAO,CAAC,QAAQ,CAAC,MAAM;gBADN,aAAa,EAAE,aAAa,EAC5B,MAAM,EAAE,aAAa;IAcpC,QAAQ,CAAC,GAAG,EAAE,OAAO,EAAE,OAAO,EAAE,GAAG;CA0C5C"}
@@ -15,9 +15,8 @@ const passport_1 = require("@nestjs/passport");
15
15
  const passport_jwt_1 = require("passport-jwt");
16
16
  const config_1 = require("@nestjs/config");
17
17
  const prisma_service_1 = require("../../prisma/prisma.service");
18
- const user_service_1 = require("../user/user.service");
19
18
  let RefreshTokenStrategy = exports.RefreshTokenStrategy = class RefreshTokenStrategy extends (0, passport_1.PassportStrategy)(passport_jwt_1.Strategy, 'jwt-refresh') {
20
- constructor(configService, prisma, userService) {
19
+ constructor(configService, prisma) {
21
20
  const refreshSecret = configService.get('JWT_REFRESH_SECRET');
22
21
  if (!refreshSecret) {
23
22
  throw new Error('JWT_REFRESH_SECRET is not defined in environment variables!');
@@ -29,13 +28,18 @@ let RefreshTokenStrategy = exports.RefreshTokenStrategy = class RefreshTokenStra
29
28
  });
30
29
  this.configService = configService;
31
30
  this.prisma = prisma;
32
- this.userService = userService;
33
31
  }
34
32
  async validate(req, payload) {
35
33
  const refreshToken = req.body.refreshToken;
36
34
  if (!refreshToken)
37
35
  throw new common_1.UnauthorizedException('Refresh token not found');
38
- const user = await this.userService.findByField("id", payload.sub);
36
+ // Auth-flow lookup: the refresh request is unauthenticated (GUEST), so
37
+ // this must bypass ABAC like every other auth lookup in this module.
38
+ const user = await this.prisma.user.findFirst({
39
+ where: { id: payload.sub },
40
+ }, {
41
+ BYPASS_FILTERING: true,
42
+ });
39
43
  if (!user) {
40
44
  throw new common_1.UnauthorizedException('User not found');
41
45
  }
@@ -65,6 +69,5 @@ let RefreshTokenStrategy = exports.RefreshTokenStrategy = class RefreshTokenStra
65
69
  exports.RefreshTokenStrategy = RefreshTokenStrategy = __decorate([
66
70
  (0, common_1.Injectable)(),
67
71
  __metadata("design:paramtypes", [config_1.ConfigService,
68
- prisma_service_1.PrismaService,
69
- user_service_1.UserService])
72
+ prisma_service_1.PrismaService])
70
73
  ], RefreshTokenStrategy);
@@ -1 +1 @@
1
- {"version":3,"file":"core.controller.d.ts","sourceRoot":"","sources":["../../../src/modules/core/core.controller.ts"],"names":[],"mappings":"AACA,OAAO,EAAC,WAAW,EAAC,MAAM,gBAAgB,CAAC;AAI3C,OAAO,EAAC,kBAAkB,EAAC,MAAM,yCAAyC,CAAC;AAQ3E,8BACsB,cAAc,CAAC,CAAC;IAM9B,SAAS,CAAC,QAAQ,CAAC,OAAO,EAAE,WAAW,CAAC,CAAC,CAAC;IAL9C,MAAM,KAAK,UAAU,IAAI,MAAM,CAE9B;IAED,SAAS,aACc,OAAO,EAAE,WAAW,CAAC,CAAC,CAAC;IAK9C,SAAS,CAAC,QAAQ,CAAC,kBAAkB,EAAG,kBAAkB,CAAC;IAIrD,OAAO;IAMP,OAAO,CAAc,EAAE,EAAE,MAAM;IAM/B,MAAM,CAAS,IAAI,EAAE,OAAO,CAAC,CAAC,CAAC,GAAG;QAAC,CAAC,GAAG,EAAE,MAAM,GAAG,GAAG,CAAA;KAAC;IAiBtD,MAAM,CAAc,EAAE,EAAE,MAAM,EAAU,IAAI,EAAE,OAAO,CAAC,CAAC,CAAC;IAMxD,MAAM,CAAc,EAAE,EAAE,MAAM;CAGvC"}
1
+ {"version":3,"file":"core.controller.d.ts","sourceRoot":"","sources":["../../../src/modules/core/core.controller.ts"],"names":[],"mappings":"AACA,OAAO,EAAC,WAAW,EAAC,MAAM,gBAAgB,CAAC;AAG3C,OAAO,EAAC,kBAAkB,EAAC,MAAM,yCAAyC,CAAC;AAE3E,8BACsB,cAAc,CAAC,CAAC;IAM9B,SAAS,CAAC,QAAQ,CAAC,OAAO,EAAE,WAAW,CAAC,CAAC,CAAC;IAL9C,MAAM,KAAK,UAAU,IAAI,MAAM,CAE9B;IAED,SAAS,aACc,OAAO,EAAE,WAAW,CAAC,CAAC,CAAC;IAK9C,SAAS,CAAC,QAAQ,CAAC,kBAAkB,EAAG,kBAAkB,CAAC;IAIrD,OAAO;IAMP,OAAO,CAAc,EAAE,EAAE,MAAM;IAM/B,MAAM,CAAS,IAAI,EAAE,OAAO,CAAC,CAAC,CAAC,GAAG;QAAC,CAAC,GAAG,EAAE,MAAM,GAAG,GAAG,CAAA;KAAC;IAOtD,MAAM,CAAc,EAAE,EAAE,MAAM,EAAU,IAAI,EAAE,OAAO,CAAC,CAAC,CAAC;IAMxD,MAAM,CAAc,EAAE,EAAE,MAAM;CAGvC"}
@@ -11,16 +11,14 @@ var __metadata = (this && this.__metadata) || function (k, v) {
11
11
  var __param = (this && this.__param) || function (paramIndex, decorator) {
12
12
  return function (target, key) { decorator(target, key, paramIndex); }
13
13
  };
14
- var CoreController_1;
15
14
  Object.defineProperty(exports, "__esModule", { value: true });
16
15
  exports.CoreController = void 0;
17
16
  const common_1 = require("@nestjs/common");
18
17
  const core_service_1 = require("./core.service");
19
18
  const permission_decorator_1 = require("../../common/decorators/permission.decorator");
20
19
  const rbac_guard_1 = require("../../common/guards/rbac.guard");
21
- const nestjs_request_context_1 = require("nestjs-request-context");
22
20
  const permissions_service_1 = require("../../common/config/permissions.service");
23
- let CoreController = exports.CoreController = CoreController_1 = class CoreController {
21
+ let CoreController = exports.CoreController = class CoreController {
24
22
  static get entityName() {
25
23
  return '';
26
24
  }
@@ -34,15 +32,6 @@ let CoreController = exports.CoreController = CoreController_1 = class CoreContr
34
32
  return this.service.findById(id);
35
33
  }
36
34
  async create(data) {
37
- const user = nestjs_request_context_1.RequestContext.currentContext.req.user;
38
- const role = user?.role || 'GUEST';
39
- const model = this.constructor.entityName;
40
- const rolePermissions = this.permissionsService.getPermissionsConfig()[model]?.[role];
41
- const actionPermission = rolePermissions?.['create'];
42
- if (typeof actionPermission !== 'string' && actionPermission?.setUserIdField) {
43
- // Force the server-owned user id (so a client can't forge ownership).
44
- data[actionPermission.setUserIdField] = user.id;
45
- }
46
35
  return this.service.create(data);
47
36
  }
48
37
  async update(id, data) {
@@ -96,7 +85,7 @@ __decorate([
96
85
  __metadata("design:paramtypes", [String]),
97
86
  __metadata("design:returntype", Promise)
98
87
  ], CoreController.prototype, "delete", null);
99
- exports.CoreController = CoreController = CoreController_1 = __decorate([
88
+ exports.CoreController = CoreController = __decorate([
100
89
  (0, common_1.UseGuards)(rbac_guard_1.RbacGuard),
101
90
  __metadata("design:paramtypes", [core_service_1.CoreService])
102
91
  ], CoreController);
@@ -26,7 +26,7 @@ export interface RelationMeta {
26
26
  /** Relations per model, keyed by the lower-cased model name. */
27
27
  export type SchemaRelations = Record<string, RelationMeta[]>;
28
28
  export type IssueLevel = 'error' | 'warning';
29
- export type IssueCode = 'relation-in-create-condition' | 'missing-connect-required' | 'missing-connect' | 'connect-on-unwritable' | 'unknown-relation';
29
+ export type IssueCode = 'relation-in-create-condition' | 'missing-connect-required' | 'missing-connect' | 'connect-on-unwritable' | 'relation-in-set' | 'removed-set-user-id-field' | 'unknown-relation';
30
30
  export interface ValidationIssue {
31
31
  level: IssueLevel;
32
32
  code: IssueCode;
@@ -1 +1 @@
1
- {"version":3,"file":"permissions-validator.d.ts","sourceRoot":"","sources":["../../src/prisma/permissions-validator.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AACH,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,yCAAyC,CAAC;AAErF,MAAM,MAAM,YAAY,GAAG,WAAW,GAAG,SAAS,CAAC;AAEnD,MAAM,WAAW,YAAY;IACzB,8DAA8D;IAC9D,KAAK,EAAE,MAAM,CAAC;IACd,iEAAiE;IACjE,KAAK,EAAE,MAAM,CAAC;IACd,4DAA4D;IAC5D,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAC3B,UAAU,EAAE,OAAO,CAAC;IACpB,IAAI,EAAE,YAAY,CAAC;IACnB,wGAAwG;IACxG,UAAU,CAAC,EAAE,OAAO,CAAC;CACxB;AAED,gEAAgE;AAChE,MAAM,MAAM,eAAe,GAAG,MAAM,CAAC,MAAM,EAAE,YAAY,EAAE,CAAC,CAAC;AAE7D,MAAM,MAAM,UAAU,GAAG,OAAO,GAAG,SAAS,CAAC;AAE7C,MAAM,MAAM,SAAS,GACf,8BAA8B,GAC9B,0BAA0B,GAC1B,iBAAiB,GACjB,uBAAuB,GACvB,kBAAkB,CAAC;AAEzB,MAAM,WAAW,eAAe;IAC5B,KAAK,EAAE,UAAU,CAAC;IAClB,IAAI,EAAE,SAAS,CAAC;IAChB,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC;CACnB;AAmBD;;;;;;;;;;;;GAYG;AACH,wBAAgB,yBAAyB,CACrC,MAAM,EAAE,qBAAqB,EAC7B,SAAS,EAAE,eAAe,GAC3B,eAAe,EAAE,CA+HnB"}
1
+ {"version":3,"file":"permissions-validator.d.ts","sourceRoot":"","sources":["../../src/prisma/permissions-validator.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AACH,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,yCAAyC,CAAC;AAErF,MAAM,MAAM,YAAY,GAAG,WAAW,GAAG,SAAS,CAAC;AAEnD,MAAM,WAAW,YAAY;IACzB,8DAA8D;IAC9D,KAAK,EAAE,MAAM,CAAC;IACd,iEAAiE;IACjE,KAAK,EAAE,MAAM,CAAC;IACd,4DAA4D;IAC5D,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAC3B,UAAU,EAAE,OAAO,CAAC;IACpB,IAAI,EAAE,YAAY,CAAC;IACnB,wGAAwG;IACxG,UAAU,CAAC,EAAE,OAAO,CAAC;CACxB;AAED,gEAAgE;AAChE,MAAM,MAAM,eAAe,GAAG,MAAM,CAAC,MAAM,EAAE,YAAY,EAAE,CAAC,CAAC;AAE7D,MAAM,MAAM,UAAU,GAAG,OAAO,GAAG,SAAS,CAAC;AAE7C,MAAM,MAAM,SAAS,GACf,8BAA8B,GAC9B,0BAA0B,GAC1B,iBAAiB,GACjB,uBAAuB,GACvB,iBAAiB,GACjB,2BAA2B,GAC3B,kBAAkB,CAAC;AAEzB,MAAM,WAAW,eAAe;IAC5B,KAAK,EAAE,UAAU,CAAC;IAClB,IAAI,EAAE,SAAS,CAAC;IAChB,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC;CACnB;AAmBD;;;;;;;;;;;;GAYG;AACH,wBAAgB,yBAAyB,CACrC,MAAM,EAAE,qBAAqB,EAC7B,SAAS,EAAE,eAAe,GAC3B,eAAe,EAAE,CA0JnB"}
@@ -50,6 +50,8 @@ function validatePermissionsConfig(config, relations) {
50
50
  const hasRelationConnect = (sourceRules, field) => !!sourceRules?.relations?.[field]?.connect;
51
51
  const canCreate = (rules) => !!rules && (rules.create !== undefined || rules.createMany !== undefined);
52
52
  const canUpdate = (rules) => !!rules && (rules.update !== undefined || rules.updateMany !== undefined);
53
+ // Columns a rule's `set` writes server-side.
54
+ const setColumns = (...actionRules) => new Set(actionRules.flatMap((rule) => (rule && typeof rule === 'object' && rule.set ? Object.keys(rule.set) : [])));
53
55
  for (const modelKey of Object.keys(cfg)) {
54
56
  const roles = cfg[modelKey] || {};
55
57
  const rels = relations[modelKey.toLowerCase()] || [];
@@ -57,6 +59,20 @@ function validatePermissionsConfig(config, relations) {
57
59
  const relationFieldNames = new Set(rels.map((r) => r.field));
58
60
  for (const role of Object.keys(roles)) {
59
61
  const rules = roles[role];
62
+ // `setUserIdField` was removed; left in a config it would silently stop
63
+ // forcing the column, so it is rejected with its replacement.
64
+ for (const action of Object.keys(rules || {})) {
65
+ const field = rules[action]?.setUserIdField;
66
+ if (field !== undefined) {
67
+ issues.push({
68
+ level: 'error',
69
+ code: 'removed-set-user-id-field',
70
+ model: modelKey,
71
+ role,
72
+ message: `${modelKey}.${role}.${action}.setUserIdField was removed. Replace it with '${action}: { set: { ${field}: '$USER_ID' } }' (applied on every write path; its foreign key needs a 'connect' rule).`,
73
+ });
74
+ }
75
+ }
60
76
  const creatable = canCreate(rules);
61
77
  const updatable = canUpdate(rules);
62
78
  if (!creatable && !updatable)
@@ -76,12 +92,26 @@ function validatePermissionsConfig(config, relations) {
76
92
  }
77
93
  // Every foreign key a write can SET must have a connect rule (target
78
94
  // or relation-scoped) — otherwise setting it is denied at runtime.
79
- // A @NoWrite / @Role(none) column is never settable.
95
+ // A @NoWrite / @Role(none) column is never client-settable, but a
96
+ // column in the rule's `set` is written server-side.
97
+ const createSet = setColumns(rules.create, rules.createMany);
98
+ const updateSet = setColumns(rules.update, rules.updateMany);
99
+ for (const column of new Set([...createSet, ...updateSet])) {
100
+ if (relationFieldNames.has(column)) {
101
+ issues.push({
102
+ level: 'error',
103
+ code: 'relation-in-set',
104
+ model: modelKey,
105
+ role,
106
+ message: `'set' on ${modelKey}.${role} names relation '${column}'. Set its foreign key column instead.`,
107
+ });
108
+ }
109
+ }
80
110
  for (const r of belongsTo) {
81
111
  const col = r.referencingColumn;
82
112
  const writable = r.fkWritable !== false; // not @NoWrite / @Role(none)
83
- const settableOnCreate = writable && creatable;
84
- const settableOnUpdate = writable && updatable;
113
+ const settableOnCreate = (writable || createSet.has(col)) && creatable;
114
+ const settableOnUpdate = (writable || updateSet.has(col)) && updatable;
85
115
  const relRule = hasRelationConnect(rules, r.field);
86
116
  if (!settableOnCreate && !settableOnUpdate) {
87
117
  // A relation-scoped connect rule for a field that can't be
@@ -99,18 +129,14 @@ function validatePermissionsConfig(config, relations) {
99
129
  }
100
130
  if (relRule || hasConnectRule(r.model, role))
101
131
  continue;
102
- // A server-set owner FK (`setUserIdField`) still needs a connect
103
- // rule — the value is trusted, but the framework authorizes every
132
+ // A server-set owner FK (`set`) still needs a connect
133
+ // rule — the server chose the value, but the framework authorizes every
104
134
  // relationship a write establishes, uniformly. Point at the
105
135
  // relation-scoped self-connect rather than a blanket rule on the
106
136
  // target: `connect: 'ALL'` on User would let this role attach ANY
107
137
  // user, which is far wider than what a self-owned create needs.
108
- const createRule = rules?.create;
109
- const ownerField = typeof createRule === 'object' && createRule
110
- ? createRule.setUserIdField
111
- : undefined;
112
- const fix = ownerField === col
113
- ? `'${col}' is server-set via setUserIdField, but the relationship it creates still needs a connect rule. Scope it to the caller with '${modelKey}.${role}.relations.${r.field}.connect: {conditions: {id: "$USER_ID"}}' — prefer that over a blanket '${r.model}.${role}.connect', which would allow connecting to any ${r.model}.`
138
+ const fix = createSet.has(col) || updateSet.has(col)
139
+ ? `'${col}' is server-set via 'set', but the relationship it creates still needs a connect rule. Scope it to the caller with '${modelKey}.${role}.relations.${r.field}.connect: {conditions: {id: "$USER_ID"}}' — prefer that over a blanket '${r.model}.${role}.connect', which would allow connecting to any ${r.model}.`
114
140
  : `Declare '${r.model}.${role}.connect' (or 'ALL'), or scope it to this relation with '${modelKey}.${role}.relations.${r.field}.connect'.`;
115
141
  if (settableOnCreate && r.isRequired) {
116
142
  issues.push({