@appxdigital/appx-core 0.1.121-beta.11 → 0.1.121-beta.5

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.
package/CHANGELOG.md DELETED
@@ -1,123 +0,0 @@
1
- # Changelog
2
-
3
- All notable changes to `@appxdigital/appx-core` are documented here.
4
-
5
- The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
6
- This project follows the `MAJOR.MINOR.PATCH` scheme in `package.json`.
7
-
8
- ## How to read this file
9
-
10
- - **This file is the collapsed, production changelog** — one section per version published to the `latest` dist-tag. Granular per-build notes for `alpha`/`beta` prereleases live in [`PRERELEASES.md`](./PRERELEASES.md); the release process is documented in [`RELEASING.md`](./RELEASING.md).
11
- - **Every released version has a section.** Newest first.
12
- - **Changes that require action in an existing project are listed under `### Migration`** — copy-paste ready, small enough to hand to an AI coding agent. Items outside that block are drop-in.
13
- - **Scaffold-only changes** (files under `cli/scaffold/`) affect *new* projects created with `appx-core create`. Existing projects own their copies of those files and apply the migration by hand.
14
-
15
- ---
16
-
17
- ## [0.1.121] — 2026-07-14
18
-
19
- Hardening release: request validation, boot configuration, generated-CRUD DTOs, and data-access create enforcement.
20
-
21
- > ## ⚠️ REQUIRED migration — upgrading the dependency is NOT enough
22
- >
23
- > **This is a security release, and its protections are INERT until you adapt your codebase.** Bumping `@appxdigital/appx-core` to `0.1.121` (including any `0.1.121-beta.x`) and doing nothing else leaves your app **vulnerable** — generated `POST`/`PUT` endpoints keep accepting arbitrary fields (mass-assignment), because the request-validation DTOs only take effect once you regenerate them **and** wire them into your controllers.
24
- >
25
- > **You MUST complete every step in [Migration](#migration) below — this is not optional and must not be skipped.** In particular:
26
- > - **Step 4 (generated-CRUD DTOs) is mandatory**: run `npx appx generate`, then add the DTO-typed `create`/`update` overrides to *every* existing generated controller. Generation alone does nothing; the controllers must import and use the DTOs.
27
- > - Steps 1–3 (`main.ts` / `app.module.ts`) are required for the boot-time hardening (validation pipe, session secret, CORS, `NODE_ENV`) to apply.
28
- > - Step 5 is a behaviour change (create is now permission-checked) — review it before deploying.
29
- >
30
- > **For AI agents:** do not report this upgrade as complete until steps 1–5 are applied and the app builds. If you only changed the version number, the task is unfinished.
31
-
32
- ### Added
33
-
34
- - **`setupCoreSecurity(app, options?)`** — one call that applies baseline HTTP hardening: a global `ValidationPipe` (`{ transform: true, whitelist: true, forbidNonWhitelisted: true }`), CORS (`cors` option, default `{ origin: 'http://localhost:3000', credentials: true }`, throws if `origin: '*'` is combined with `credentials: true`), and **Helmet** security headers (on by default; `helmet` option to tune/disable). `helmet` is a new peer dependency.
35
- - **`buildCoreSessionOptions({ secret, store, cookieName?, ttlSeconds?, secure?, sameSite? })`** — builds hardened `express-session` options. Throws if `secret` is missing or `< 32` chars (no fallback); sets `cookie: { httpOnly, secure (prod), sameSite: 'lax', maxAge }`.
36
- - **`coreEnvFilePath()`** — returns `['.env.${NODE_ENV}', '.env']` (env-specific file wins, plain `.env` is a fallback) and throws if `NODE_ENV` is unset.
37
- - **Per-model DTO generation** (`appx generate`). Each model+action gets a `class-validator` DTO so generated controllers type their request body concretely and the whitelist pipe can strip unknown fields. Two files:
38
- - `src/generated/dto/<model>/<action>-<model>.generated.dto.ts` — base class, **always overwritten**, under the gitignored `src/generated/`. Never hand-edit.
39
- - `src/modules/<model>/dto/<action>-<model>.dto.ts` — a hand-owned subclass, generated **once**, for custom validation. The controller imports this.
40
- - **`/// @NoWrite` field annotation** — excludes a field from the generated create/update DTOs (all roles). Fields annotated `/// @Role(none)` are also treated as non-writable.
41
- - **Create-permission enforcement in the data-access proxy.** `create` / `createMany` validate that the incoming data satisfies the permission's `conditions` before insert (own-scalar fields against the data; relation conditions by looking up the referenced parent), and default-deny when the model/role has no `create` permission. A development-only warning fires when a created row would not satisfy the model's `find` conditions.
42
- - **`restrictedFields` on `create`** in `CoreController` (previously only `update`, which also read the wrong permission key so it never applied under `updateMany`).
43
-
44
- ### Writable-field policy (generated DTOs)
45
-
46
- A scalar field is included unless it is: the primary key (`@id`), a server-managed timestamp (`@default(now())` / `@updatedAt`), `/// @NoWrite`, or `/// @Role(none)`. Relation navigation fields are excluded; their scalar foreign keys are kept. Enums map to `@IsEnum`, scalars to the matching `@IsString`/`@IsInt`/`@IsBoolean`/`@IsNumber`/`@IsDateString`; `Json`/`Bytes` use `@Allow()`.
47
-
48
- ### Changed (scaffold — new projects only)
49
-
50
- - `main.ts` calls `setupCoreSecurity(app, { cors: { origin: corsOrigin, credentials: true } })`, builds session options via `buildCoreSessionOptions`, and reads `CORS_ORIGIN` from config.
51
- - `app.module.ts` uses `coreEnvFilePath()` for `envFilePath`.
52
- - Generated `package.json` adds `helmet`; the `start` script sets `NODE_ENV=development`.
53
- - `AuthController.getAllSessions` / `closeSpecificSession` use `@UseGuards(AuthenticatedGuard)`.
54
-
55
- ### Migration
56
-
57
- > **Required for every existing project. All five steps must be applied** — the edits are to files you own (`main.ts` / `app.module.ts` / `package.json` / your generated controllers), so they are safe to hand to an AI coding agent, but they are **not optional**. The app is not secured until they are done and it builds.
58
-
59
- **1. Request validation & HTTP hardening — `src/main.ts`.**
60
- ```ts
61
- import { setupCoreSecurity } from '@appxdigital/appx-core';
62
- // remove: app.useGlobalPipes(new ValidationPipe({ transform: true }));
63
- // remove: app.enableCors({ origin: 'http://localhost:3000', credentials: true });
64
- const corsOrigin = configService.get<string>('CORS_ORIGIN') ?? 'http://localhost:3000';
65
- setupCoreSecurity(app, { cors: { origin: corsOrigin, credentials: true } });
66
- ```
67
- - Install Helmet: `npm install helmet@^8.0.0` (applied automatically by `setupCoreSecurity`; tune with `{ helmet: {...} }`, disable with `{ helmet: false }`).
68
- - If you register a `ValidationPipe` with custom options, pass them through: `setupCoreSecurity(app, { validationPipe: { /* overrides */ } })`.
69
- - With `whitelist` + `forbidNonWhitelisted`, body properties not declared on a `class-validator` DTO are rejected (`400`). Confirm every DTO declares every field it legitimately accepts before deploying.
70
- - Set `CORS_ORIGIN` to your real front-end origin in production. Do not combine `'*'` with credentials — the helper throws at boot. For a public credential-less API pass `{ cors: { origin: '*', credentials: false } }`.
71
-
72
- **2. Session secret & cookie flags — `src/main.ts`.**
73
- ```ts
74
- import { buildCoreSessionOptions } from '@appxdigital/appx-core';
75
- // replace your inline session({ secret: … }) (drop any hardcoded fallback secret) with:
76
- app.use(session(buildCoreSessionOptions({
77
- secret: configService.get<string>('SESSION_SECRET'),
78
- cookieName: configService.get<string>('SESSION_COOKIE_NAME'),
79
- ttlSeconds: configService.get<number>('SESSION_TTL') || 86400,
80
- store: new CorePrismaSessionStore(prismaService, { ttl: sessionTTL }),
81
- })));
82
- ```
83
- Ensure `SESSION_SECRET` is set (≥ 32 chars) in every environment — the app now refuses to boot without it.
84
-
85
- **3. Environment file & `NODE_ENV` — `src/app.module.ts`.**
86
- ```ts
87
- import { coreEnvFilePath } from '@appxdigital/appx-core';
88
- // ConfigModule.forRoot({ … envFilePath: coreEnvFilePath() })
89
- ```
90
- Set `NODE_ENV` explicitly (`production` / `development`) in every deploy and start script — boot fails if it is unset. If you use a bare `nest start`, change it to `cross-env NODE_ENV=development nest start`. (`coreEnvFilePath()` loads `.env.<NODE_ENV>` with `.env` as a fallback, so a plain `.env` still works.)
91
-
92
- **4. Generated-CRUD DTOs — MANDATORY (this is the mass-assignment fix; do not skip).**
93
-
94
- Without this step, your generated `POST`/`PUT` endpoints still accept arbitrary fields — the vulnerability this release closes stays open. Two parts, **both required**:
95
-
96
- (a) Regenerate:
97
- ```bash
98
- npm install @appxdigital/appx-core@^0.1.121
99
- npx appx generate
100
- ```
101
- (b) **Add the DTO-typed overrides to EVERY existing generated controller** (they are generated once and never overwritten, so `appx generate` will NOT add them for you — you must edit each controller by hand or delete and regenerate it):
102
- ```ts
103
- import { Body, Param, Post, Put } from '@nestjs/common';
104
- import { Permission } from '@appxdigital/appx-core';
105
- import { Create<Model>Dto } from './dto/create-<model-kebab>.dto';
106
- import { Update<Model>Dto } from './dto/update-<model-kebab>.dto';
107
-
108
- @Post() @Permission('create')
109
- async create(@Body() data: Create<Model>Dto) { return super.create(data as any); }
110
-
111
- @Put(':id') @Permission('updateMany')
112
- async update(@Param('id') id: string, @Body() data: Update<Model>Dto) { return super.update(id, data as any); }
113
- ```
114
- (Or delete an un-customized controller and re-run `appx generate` for the new template.) Add custom validation in the hand-owned `dto/*.dto.ts` subclasses — never in the `.generated.dto.ts` base. Lock sensitive columns with `/// @NoWrite` (all roles) or `restrictedFields` on the permission (per role), then regenerate. To drop a field from one endpoint's DTO, extend `OmitType(<Model>GeneratedDto, ['field'] as const)`. Full details in [`docs/dtos.md`](../docs/dtos.md).
115
-
116
- > **Nested relation writes are now an explicit, permission-gated allowlist — this is a behaviour restriction; a migrating agent MUST flag it to the developer.** Generated **create** DTOs accept only two nested operators, each authorized by ABAC: **`create`** (checked against the related model's `create` rule) and **`connect`** (checked against the related model's new dedicated **`connect`** permission — a `connect` rule is required; no rule ⇒ refused). Every other operator (`set`, `disconnect`, `update`, `upsert`, `delete`, …) is rejected (`400`). Nested writes are available on **`create` only** — `update` resolves to `updateMany`, whose payload is scalar-only. **Keep `forbidNonWhitelisted: true` on (fail loud); never `whitelist`-strip on writes.**
117
- >
118
- > **For AI agents doing this migration on an existing project:** this can break existing behaviour. Before deploying, tell the developer explicitly that (a) any nested operator other than `create`/`connect` used through CRUD will now `400`, (b) every model that is nested-`connect`ed now needs a `connect` permission or the request fails, and (c) nested writes no longer work on update endpoints. Do not proceed silently. See [`docs/dtos.md`](../docs/dtos.md#relations--nested-writes) and [`docs/permissions.md`](../docs/permissions.md).
119
-
120
- **5. Create-permission enforcement (behaviour change — review before upgrading).**
121
- - Ensure every model/role that legitimately creates records has an explicit `create` permission (or `'ALL'`). Without one, create now returns `403` (previously unchecked). Internal framework flows (registration, sessions, tokens) are unaffected.
122
- - If you supplied arbitrary values for a field a `create` condition constrains (e.g. an owner id), those requests now return `403` unless the value matches — use `setUserIdField` to have the framework set it.
123
- - **`restrictedFields` under `updateMany` now applies** (it was a no-op before). Fields listed there are stripped on `PUT`. Remove any field that should remain writable.
package/PRERELEASES.md DELETED
@@ -1,151 +0,0 @@
1
- # Prerelease log
2
-
3
- > ## ⚠️ Every `0.1.121-beta.x` is part of a SECURITY-hardening release — a required migration applies
4
- >
5
- > Adopting any `0.1.121` beta from `0.1.120` (or earlier) requires the **mandatory migration in [`CHANGELOG.md`](./CHANGELOG.md) under `[0.1.121]`** — most importantly **regenerating the per-model DTOs and wiring them into your controllers** (`npx appx generate` + adding the DTO-typed `create`/`update` overrides to every generated controller). **Bumping the version without doing this leaves generated `POST`/`PUT` endpoints accepting arbitrary fields.**
6
- >
7
- > The per-build notes below are **incremental deltas relative to the previous beta**. When an entry says "no code change" or "regenerate not required", it means *nothing new to do versus the prior beta* — it does **not** waive the `[0.1.121]` migration you owe when coming from `0.1.120`.
8
- >
9
- > **AI agents:** do not report a `0.1.121` upgrade as done until the `CHANGELOG.md [0.1.121]` migration steps 1–5 are applied and the project builds.
10
-
11
- Granular notes for **alpha / beta** builds of both packages —
12
- `@appxdigital/appx-core` (library) and `@appxdigital/appx-core-cli` (CLI) —
13
- newest first. Entries are labelled by package. These are the builds you install
14
- to test a change in a real project before it ships:
15
-
16
- ```bash
17
- npm install @appxdigital/appx-core@beta # library, latest beta
18
- npm install @appxdigital/appx-core@0.1.121-beta.0 # a specific library build
19
- npx @appxdigital/appx-core-cli@beta create # CLI, latest beta
20
- ```
21
-
22
- Each prerelease gets its own entry here with fine-grained notes. When a version
23
- is promoted to production it is **collapsed** into a single curated section in
24
- [`CHANGELOG.md`](./CHANGELOG.md) with aggregated changes and consolidated
25
- migration steps — that public changelog is the one an upgrading project reads.
26
- Nothing here is a substitute for it.
27
-
28
- Prereleases are excluded from normal semver range resolution, so a project on
29
- `^0.1.x` never pulls one by accident — you always opt in via the `@beta` /
30
- `@alpha` tag or an exact version.
31
-
32
- ---
33
-
34
- ## [0.1.121-beta.11] — unreleased
35
-
36
- **Docs.** No code change. Documented that nested writes over HTTP CRUD are
37
- **one level deep** (a nested `create` DTO exposes the related model's scalars +
38
- `connect`, not its own relations, to avoid circular DTOs). The proxy's
39
- authorization is recursive; the DTO surface is the cap — deeper nesting `400`s,
40
- and multi-level nested writes belong in an explicit endpoint. See `docs/dtos.md`
41
- and `docs/limitations.md`.
42
-
43
- ## [0.1.121-beta.10] — unreleased
44
-
45
- **Library.** **Nested relation writes on generated CRUD are now an explicit,
46
- permission-gated allowlist.** A `create` body may nest **`create`** and
47
- **`connect`** on its relations; both are authorized by ABAC (`create` → the
48
- related model's `create` rule, recursively; `connect` → the related model's new
49
- dedicated **`connect`** permission — a `connect` rule is required, because being
50
- able to *read* a record does not authorize associating it). Every other nested
51
- operator (`set` / `disconnect` / `update` / `upsert` / `delete` / …) is rejected
52
- at both the DTO layer (not emitted → `400`) and the proxy (throws). Nested
53
- writes are available on **`create` only** (`update` resolves to `updateMany`,
54
- which is scalar-only).
55
-
56
- - **Behaviour restriction — review on upgrade.** If a project used other nested
57
- operators through CRUD, or connected models without a `connect` rule, those
58
- requests now fail. Add `connect` rules where needed; move unsupported nested
59
- writes to explicit endpoints. Keep `forbidNonWhitelisted: true` (fail loud).
60
-
61
- ## [0.1.121-beta.9] — unreleased
62
-
63
- **Docs / packaging.** No code change.
64
-
65
- - Added a **`docs/` folder** (now shipped inside the npm package) — the
66
- framework's source-of-truth guide: `permissions.md` (RBAC config +
67
- `@Permission`/`@ExposeModels`), `data-access.md` (the Prisma proxy, ABAC
68
- enforcement, single-record method contract, relations, transactions) and
69
- `limitations.md` (not-ready features incl. `aggregate`/GraphQL, and caveats).
70
- - **Clarified the `[0.1.121]` migration in `CHANGELOG.md`.** It is now explicit
71
- that regenerating the per-model DTOs and wiring them into every controller is
72
- **mandatory** (it is the mass-assignment fix), and that a version bump without
73
- it leaves `POST`/`PUT` endpoints accepting arbitrary fields.
74
-
75
- ## [0.1.121-beta.8] — unreleased
76
-
77
- **Library.**
78
-
79
- - **Single-record data-access methods now have an honest typed contract.**
80
- `prisma.model.*` re-aliases the methods that can't carry ABAC conditions, in
81
- both the types and the runtime dispatch: `findUnique`→`findFirst`,
82
- `findUniqueOrThrow`→`findFirstOrThrow`, `update`→`updateMany`,
83
- `delete`→`deleteMany`. A mismatched call now fails at compile time instead of
84
- the old runtime throw. **Breaking (types only):** `update()`/`delete()` return
85
- `{ count }` and `findUnique()` takes a non-unique `where` with a nullable
86
- return — adjust the call sites the compiler flags. A `*Many` permission action
87
- now inherits its singular rule (`updateMany`→`update`, `deleteMany`→`delete`,
88
- `createMany`→`create`), so a config can declare just `update`/`delete`/`create`.
89
- - **`@ExposeModels(...models)`** — expose models on a route WITHOUT a permission
90
- action, for public / GUEST endpoints that must read an otherwise-restricted
91
- model without adding a `GUEST` rule for it. `@Permission(action, models)` keeps
92
- working unchanged. Exposed-model matching is case-insensitive.
93
-
94
- ## [0.1.121-beta.7] — unreleased
95
-
96
- **Library.** `createMany` now inherits the `create` permission when not declared
97
- explicitly — in both the data-access proxy and the `RbacGuard`. A
98
- `permissions.config.ts` can declare a single `create` rule and have it cover
99
- `createMany` too (mirrors the existing `findMany → count/findFirst` fallback).
100
- One-directional: declaring only `createMany` does **not** enable `create`.
101
-
102
- - If you previously duplicated `create` and `createMany`, the `createMany` entry
103
- is now redundant (harmless to keep). No action required.
104
-
105
- ## [0.1.121-beta.6] — unreleased
106
-
107
- **Packaging.** The npm tarball now ships `CHANGELOG.md`, `PRERELEASES.md`, and
108
- `RELEASING.md` (previously only `dist/` + `README.md`). Migration steps are now
109
- readable from inside an installed `node_modules/@appxdigital/appx-core`, so an
110
- upgrading project (or an AI agent) can apply them without cloning the repo. No
111
- code change.
112
-
113
- ## [0.1.121-beta.5] — unreleased
114
-
115
- **Library.** Field-level access control (`/// @Role(...)`) now applies to
116
- **nested** relation selections, not just top-level fields. Previously a query
117
- that selected a restricted field *through* a relation
118
- (`select: { relation: { select: { restrictedField: true } } }`) received that
119
- field; the data-access proxy now omits it at every depth, for both `select` and
120
- `include`, while preserving any `where` / `orderBy` you set on the relation.
121
-
122
- - Exercise reads that pull fields through relations (nested `select` / `include`,
123
- including multi-level) and confirm the values you expect still come back — a
124
- field annotated `@Role(<roles>)` should be absent for roles not listed.
125
- - No API change; regenerate is not required.
126
-
127
- ## [0.1.121-beta.0] — unreleased
128
-
129
- First prerelease of the `0.1.121` hardening set. Testing target before this is
130
- promoted to `latest`. Full aggregated notes and migration live under
131
- `[0.1.121]` in `CHANGELOG.md`; the highlights to exercise in a project:
132
-
133
- - **`setupCoreSecurity(app, options?)`** — one call for the global
134
- `ValidationPipe` (whitelist + `forbidNonWhitelisted`), CORS, and Helmet.
135
- Verify existing DTOs still accept every field they legitimately take (the
136
- whitelist rejects undeclared body properties with `400`).
137
- - **`buildCoreSessionOptions(...)`** — requires a session secret ≥ 32 chars and
138
- sets hardened cookie flags. Confirm your env provides the secret.
139
- - **`coreEnvFilePath()`** — `['.env.${NODE_ENV}', '.env']`; throws if
140
- `NODE_ENV` is unset. Confirm boot scripts set `NODE_ENV`.
141
- - **Per-model generated DTOs** — regenerate (`appx generate`) and check the
142
- generated controllers compile and accept your writable fields.
143
- - **Create-permission enforcement in the data-access proxy** — `create` /
144
- `createMany` now validate incoming data against the permission conditions and
145
- default-deny when a model/role has no `create` permission. Exercise every
146
- create path in the consuming app and confirm intended creates still succeed;
147
- set explicit `create` permissions (or `ALL`) where needed.
148
-
149
- > How to try it: bump this package to `0.1.121-beta.0` in the project under
150
- > test (`npm install @appxdigital/appx-core@beta`), run the app's own suite,
151
- > and smoke-test create/update flows and boot. Report back before promotion.
package/RELEASING.md DELETED
@@ -1,168 +0,0 @@
1
- # Releasing
2
-
3
- This repo publishes **two independent packages**, each versioned on its own:
4
-
5
- - **`@appxdigital/appx-core`** — the runtime library (root `package.json`),
6
- released via **`v*`** tags (`release.yml`).
7
- - **`@appxdigital/appx-core-cli`** — the `appx-core` CLI (`cli/package.json`),
8
- released via **`cli-v*`** tags (`release-cli.yml`).
9
-
10
- Both follow the same posture: publishing is deliberate — **it only happens when
11
- you push a git tag** — and production (`latest`) pauses for a human approval
12
- before it goes live.
13
-
14
- ## Channels
15
-
16
- Library (`v*` tags):
17
-
18
- | You push tag | npm dist-tag | Consumers install with | Gated? |
19
- | ------------------ | ------------ | --------------------------------- | --------------- |
20
- | `v1.2.3-alpha.N` | `alpha` | `…/appx-core@alpha` | no |
21
- | `v1.2.3-beta.N` | `beta` | `…/appx-core@beta` | no |
22
- | `v1.2.3-rc.N` | `next` | `…/appx-core@next` | no |
23
- | `v1.2.3` | `latest` | `…/appx-core` (default) | **yes** — approval |
24
-
25
- CLI (`cli-v*` tags) — identical channels for `@appxdigital/appx-core-cli`:
26
-
27
- | You push tag | npm dist-tag | Consumers install with | Gated? |
28
- | -------------------- | ------------ | ------------------------------- | --------------- |
29
- | `cli-v1.2.3-beta.N` | `beta` | `…/appx-core-cli@beta` | no |
30
- | `cli-v1.2.3` | `latest` | `…/appx-core-cli` (default) | **yes** — approval |
31
-
32
- Prereleases are excluded from `^1.2.x` range resolution, so a project on a caret
33
- range never pulls one by accident — testers opt in via the tag or an exact
34
- version. Merging to `main` **never** publishes; it only runs CI (library build +
35
- tests on mysql and postgres, plus a CLI smoke check).
36
-
37
- ## Two files, two audiences
38
-
39
- - **`PRERELEASES.md`** — granular, one entry per alpha/beta build. What a tester
40
- reads.
41
- - **`CHANGELOG.md`** — collapsed, one section per production release with
42
- aggregated changes and consolidated migration steps. What an upgrading project
43
- reads. When you promote a version, distil its prerelease entries into a single
44
- `CHANGELOG.md` section.
45
-
46
- ## One-time setup (before the first publish)
47
-
48
- 1. **npm Trusted Publisher (OIDC) — one per package.** On npmjs.com → the
49
- package → *Settings* → *Trusted Publisher* → GitHub Actions. Add **two**,
50
- both owner `appxdigital`, repo `appx-core`:
51
- - `@appxdigital/appx-core` → workflow `release.yml`
52
- - `@appxdigital/appx-core-cli` → workflow `release-cli.yml`
53
-
54
- This lets CI mint a short-lived publish token per run — **no `NPM_TOKEN`
55
- secret is stored anywhere**, and every publish carries supply-chain
56
- provenance.
57
-
58
- 2. **GitHub `production` environment (the approval gate).** Repo → *Settings* →
59
- *Environments* → *New environment* → name it exactly `production` → enable
60
- *Required reviewers* and add yourself (and/or the team). The
61
- `publish-production` job targets this environment, so a clean tag will wait
62
- for a click before publishing to `latest`.
63
-
64
- ## Cutting a release
65
-
66
- The tag must match `package.json`'s version exactly, or the release job fails
67
- fast (a guard against mistagging). Convenience scripts keep them in lockstep and
68
- create the tag for you — they **do not push**, so pushing stays a deliberate
69
- step:
70
-
71
- There are two per package — a **test build** and a **production release**:
72
-
73
- | Script | What it does | Tag | Goes to |
74
- | ------------------------------- | ------------------------------------- | --------------- | ----------------- |
75
- | `npm run release:beta` | library test build (`…-beta.N`) | `v…-beta.N` | `beta` channel |
76
- | `npm run release:production` | promote library to everyone | `vX.Y.Z` | `latest` (gated) |
77
- | `npm run release:cli:beta` | CLI test build (`…-beta.N`) | `cli-v…-beta.N` | `beta` channel |
78
- | `npm run release:cli:production`| promote CLI to everyone | `cli-vX.Y.Z` | `latest` (gated) |
79
-
80
- `release:beta` publishes only to people who opt in with `@beta`; `release:production`
81
- is what everyone gets by default, and it pauses for your approval before going
82
- live. Each command bumps the version, commits, and creates the tag, then prints
83
- the exact `git push origin HEAD --follow-tags` for you to run when ready — they
84
- never push on their own.
85
-
86
- Rarer cases go through the underlying script directly:
87
- `node scripts/release.mjs lib alpha` (alpha channel), `… lib minor` (a feature
88
- bump), or `… lib X.Y.Z-beta.0` (an explicit version — e.g. the first beta of a
89
- version already sitting in `package.json`).
90
-
91
- The manual steps below show what those scripts do under the hood.
92
-
93
- ### Beta (test in projects first)
94
-
95
- ```bash
96
- # set the version + create the matching tag + commit, without auto-pushing
97
- npm version 0.1.121-beta.0 --message "release: %s"
98
- git push origin main --follow-tags
99
- ```
100
-
101
- The `Release` workflow builds, tests, and publishes to the `beta` dist-tag.
102
- Then, in a project you want to test against:
103
-
104
- ```bash
105
- npm install @appxdigital/appx-core@beta
106
- ```
107
-
108
- Iterate with `npm version prerelease --preid beta` (→ `-beta.1`, `-beta.2`, …),
109
- each push publishing a new granular `PRERELEASES.md` entry.
110
-
111
- ### Promote to production (`latest`)
112
-
113
- When the beta is validated in real projects:
114
-
115
- ```bash
116
- # drop the prerelease suffix -> clean version
117
- npm version 0.1.121 --message "release: %s"
118
- git push origin main --follow-tags
119
- ```
120
-
121
- The workflow builds and tests, then **pauses** on the `production` environment.
122
- Approve it in the repo's *Actions* tab → the running release → *Review
123
- deployments* → *Approve and deploy*. It then publishes to `latest`.
124
-
125
- > Alternatively, promotion can be a pure retag without republishing:
126
- > `npm dist-tag add @appxdigital/appx-core@0.1.121-beta.N latest`. The
127
- > tag-driven workflow above is preferred because it re-runs tests and records
128
- > provenance for the exact `latest` artifact.
129
-
130
- ### Alpha (rough, one or two projects)
131
-
132
- ```bash
133
- npm version 0.1.122-alpha.0 --message "release: %s"
134
- git push origin main --follow-tags
135
- # in the project: npm install @appxdigital/appx-core@alpha
136
- ```
137
-
138
- ### CLI (`@appxdigital/appx-core-cli`)
139
-
140
- Same flow, but bump `cli/package.json` and use the `cli-v` tag prefix. The tag
141
- must match `cli/package.json`'s version.
142
-
143
- ```bash
144
- # beta:
145
- npm --prefix cli version 1.0.21-beta.0 --message "release cli: %s"
146
- git commit -am "release cli: 1.0.21-beta.0" # npm version --prefix does not commit
147
- git tag cli-v1.0.21-beta.0
148
- git push origin main --follow-tags
149
- # test: npx @appxdigital/appx-core-cli@beta create …
150
-
151
- # production (gated — approve in the Actions tab):
152
- npm --prefix cli version 1.0.21 --message "release cli: %s"
153
- git commit -am "release cli: 1.0.21"
154
- git tag cli-v1.0.21
155
- git push origin main --follow-tags
156
- ```
157
-
158
- > `npm version --prefix cli` bumps `cli/package.json` but does not create the
159
- > git commit/tag (that only happens when run in the repo root). Commit and tag
160
- > explicitly with the `cli-v` prefix as shown.
161
-
162
- ## Manual fallback
163
-
164
- If CI is unavailable, you can still publish by hand: `rimraf dist && npm run
165
- build && npm publish --tag <dist-tag> --access public` for the library, and
166
- `cd cli && npm publish --tag <dist-tag> --access public` for the CLI. This
167
- bypasses the tests, the approval gate, and provenance, so prefer the tag-driven
168
- flow.
package/docs/README.md DELETED
@@ -1,21 +0,0 @@
1
- # AppX Core — Documentation
2
-
3
- Authoritative usage documentation for `@appxdigital/appx-core`. **This folder is the source of truth** for how the framework behaves — humans and AI agents should read it here rather than inferring patterns from source code. If code and these docs disagree, that is a bug in one of them; fix it, don't guess.
4
-
5
- ## Contents
6
-
7
- - **[permissions.md](./permissions.md)** — the authorization model: the `@Permission` and `@ExposeModels` decorators, the `permissions.config.ts` shape, roles/actions, row-level `conditions`, field-level control, action fallbacks, and the `GUEST` (unauthenticated) flow.
8
- - **[dtos.md](./dtos.md)** — the generated CRUD validation DTOs: their two-file structure, **how to remove a field** (`OmitType` / `@NoWrite` / `restrictedFields`), how to add validation, and why nested relation writes don't belong on generic CRUD.
9
- - **[data-access.md](./data-access.md)** — how you talk to the database: the Prisma proxy (`prisma.model.*`), how ABAC filtering and field omission are enforced on every call, the redirected single-record methods, relation behaviour, bypass hatches, and transactions.
10
- - **[graphql.md](./graphql.md)** — the **read-only** GraphQL API generated from your schema: queries, nested selections, and how ABAC filters them.
11
- - **[limitations.md](./limitations.md)** — **known limitations and features that are NOT ready for use** (e.g. `aggregate`, GraphQL writes). Read this before relying on any method not covered in the guides above.
12
-
13
- ## The one-paragraph mental model
14
-
15
- AppX Core wraps the Prisma client in a proxy that enforces **attribute-based access control (ABAC)** on every database call. You declare row-level and field-level rules once in `permissions.config.ts`; the proxy injects them into each query automatically based on the current request's user role. Controllers/resolvers are generated from your Prisma schema. The rules of the road: **go through the proxy** (`prismaService.model.*`) so ABAC applies, **declare a permission for every model/role/action** you want to allow (everything else is default-denied), and **check [limitations.md](./limitations.md)** before using a method.
16
-
17
- ## Conventions used in these docs
18
-
19
- - `$USER_ID` refers to `PermissionPlaceholder.USER_ID` — a placeholder replaced with the current user's id at query time.
20
- - "the proxy" means the `PrismaService` client returned by `prismaService.model` (and the `prisma.user` / `prisma.session` / `prisma.userRefreshToken` accessors).
21
- - "default-deny" means: if no rule matches, the request is refused (a `403` at the guard, or a thrown `HttpException` at the proxy) — never silently allowed.
@@ -1,108 +0,0 @@
1
- # Data Access & ABAC Enforcement
2
-
3
- How you read and write the database, and exactly what the proxy does to every call. Pair this with [permissions.md](./permissions.md) (which defines the rules) and [limitations.md](./limitations.md) (what not to use yet).
4
-
5
- ---
6
-
7
- ## Always go through the proxy
8
-
9
- Inject `PrismaService` and use `prismaService.model.*`:
10
-
11
- ```ts
12
- const projects = await this.prisma.model.project.findMany({ where: { status: 'active' } });
13
- ```
14
-
15
- `prismaService.model` returns a proxied Prisma client. **Only calls through this proxy get ABAC.** If you reach the raw Prisma client directly (a second `PrismaClient`, raw SQL, `$queryRaw`), you bypass all filtering and field omission. There is **no defence-in-depth at the controller layer** for row rules — the proxy is the single enforcement point.
16
-
17
- There are also three narrow typed accessors — `prismaService.user`, `prismaService.session`, `prismaService.userRefreshToken` — which are the proxied delegates for those framework models (used by auth flows, often with the bypass options below).
18
-
19
- Model names are **case-insensitive** at the proxy (`prisma.model.project` / `prisma.model.Project` resolve the same delegate and the same config).
20
-
21
- ---
22
-
23
- ## What the proxy does on every call
24
-
25
- For each `prisma.model.<model>.<method>(args, options?)`:
26
-
27
- 1. **Field omission** (reads only) — removes columns the role may not read (`/// @Role(...)`), recursing into nested `select`/`include` at every depth.
28
- 2. **Condition injection** — looks up `PermissionsConfig[model][role][action]`; if `'ALL'`, no filter; if `{ conditions }`, `AND`-merges them into your `where` (so your filter and the permission filter both apply); if nothing (after fallbacks), throws `403`.
29
- 3. **Executes** the (possibly rewritten) query.
30
-
31
- The role comes from the current request's user (`GUEST` if unauthenticated). Requests run inside an async context established by the framework's middleware/interceptor.
32
-
33
- ---
34
-
35
- ## Method contract (important)
36
-
37
- The proxy can only enforce ABAC on methods that accept a `where` it can inject conditions into. Single-record methods keyed on a unique id cannot, so they are **re-aliased** — in both the TypeScript types and at runtime:
38
-
39
- | You call | Actually runs | Params / return you get |
40
- |---|---|---|
41
- | `findUnique()` | `findFirst()` | non-unique `where`, nullable result |
42
- | `findUniqueOrThrow()` | `findFirstOrThrow()` | non-unique `where` |
43
- | `update()` | `updateMany()` | `{ where, data }`, returns `{ count }` |
44
- | `delete()` | `deleteMany()` | `{ where }`, returns `{ count }` |
45
-
46
- **Consequences:**
47
-
48
- - `update()` / `delete()` return `{ count }`, **not** the affected record. Re-read if you need the row.
49
- - These operate on **all matching rows**, not one — always pass a `where`. With ABAC conditions injected, a cross-owner `update({ where: { id } })` simply affects `{ count: 0 }` rather than erroring.
50
- - The TypeScript types reflect this: calling `update()` with a lone unique `where` and expecting a record back is a **compile error**, not a runtime surprise.
51
-
52
- Use `findMany` / `findFirst` / `findFirstOrThrow` / `updateMany` / `deleteMany` directly if you prefer the explicit names.
53
-
54
- ---
55
-
56
- ## Field omission (`@Role`)
57
-
58
- A column annotated `/// @Role(ADMIN)` is returned only to roles in the list; for others the proxy drops it from the `select` so it comes back `undefined`. `/// @Role(none)` hides it from everyone (e.g. `password`).
59
-
60
- - Omission applies to nested relations too: `include: { owner: true }` won't leak `owner`'s `@Role`-restricted fields to an unauthorized role.
61
-
62
- ---
63
-
64
- ## Relations in conditions and includes
65
-
66
- - **Row conditions** may traverse relations to any depth: `{ conditions: { task: { project: { ownerId: $UID } } } }`. Both `belongsTo` (to-one) and `hasMany` / M:N (`some`/`every`/`none`) are supported. Note Prisma semantics: `every` is **true on an empty list**.
67
- - **Including a to-one relation is inner-join filtered.** When you `include`/`select` a `belongsTo` relation, the related model's read rules are merged into the **parent** row's `where`:
68
- - If the related model has **no** read rule for the role, including it throws `403`. Grant a read rule on every model you expose through an `include`.
69
- - If the related row exists but doesn't satisfy the rule, the **whole parent row is dropped** (`findFirst` → `null`, `findMany` → the row is omitted). The parent is **not** returned with the relation set to `null`. In short: you can't see a row whose related record you're not allowed to see. This is intentional and fails closed.
70
- - **To-many relations are filtered in place**: the parent is kept, and the child list is narrowed to the rows the caller may read (possibly empty).
71
-
72
- ---
73
-
74
- ## Bypass hatches
75
-
76
- Use sparingly and deliberately; each disables part of the protection.
77
-
78
- - **Per-call options** — `prisma.<accessor>.<method>(args, { BYPASS_FILTERING, BYPASS_OMISSION })`. `BYPASS_FILTERING` skips condition injection (and create-enforcement); `BYPASS_OMISSION` skips field omission. Available on the typed accessors (`prisma.user`, `prisma.session`, `prisma.userRefreshToken`).
79
- - **`@ExposeModels(...)` / `@Permission(action, [models])`** — expose models for a request so they skip row/field filtering, scoped to the handler. See [permissions.md](./permissions.md).
80
- - **`prismaService.withExposedModels(models, async () => { ... })`** — service-level scoped exposure for a block of code (e.g. registration pre-checks). Model names are lowercased/case-insensitive.
81
-
82
- Framework flows that must write without a permission (registration, sessions, tokens) use `BYPASS_FILTERING`.
83
-
84
- ---
85
-
86
- ## Creates
87
-
88
- `create` / `createMany` can't push conditions into a `WHERE`, so the proxy **validates the incoming data against the create rule's `conditions` before insert**:
89
-
90
- - Own-scalar conditions are checked against `data`; `belongsTo` relation conditions are checked by looking up the referenced parent.
91
- - A model/role with **no** `create` permission is **default-denied** (`403`).
92
- - It **fails closed**: unsupported condition shapes for create (list-relation `some`/`every`/`none`, exotic operators) throw rather than silently allow. For those, use `setUserIdField` to set an owner id server-side and validate inputs in DTOs.
93
- - In development only, a warning fires if a just-created row wouldn't satisfy the model's *read* rule (i.e. the creator couldn't read it back).
94
-
95
- ---
96
-
97
- ## Transactions
98
-
99
- Set `USE_TRANSACTION=true` (env) to wrap each request handler in a Prisma `$transaction`; per-endpoint control via the transaction metadata. The framework commits the transaction **before** sending the HTTP response, so a read-after-write in a dependent request can't miss the write. Inside a transaction the proxy uses the request's transaction client automatically.
100
-
101
- ---
102
-
103
- ## Blocked / redirected methods — quick reference
104
-
105
- - `findUnique` / `findUniqueOrThrow` → run as `findFirst` / `findFirstOrThrow` (see contract above).
106
- - `update` / `delete` → run as `updateMany` / `deleteMany`.
107
- - Raw client access, `$queryRaw`, a second `PrismaClient` → **not proxied, no ABAC**. Don't use them for user-scoped data.
108
- - `aggregate` → see [limitations.md](./limitations.md) (not ready).