payload-doctor 0.5.4

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 ADDED
@@ -0,0 +1,279 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project are documented here. The format is loosely
4
+ based on [Keep a Changelog](https://keepachangelog.com).
5
+
6
+ ## 0.5.4
7
+
8
+ Minor polish from a second-pass review. No behaviour change on findings (fixtures still 0 FP).
9
+
10
+ ### Changed
11
+ - `--fix` header now states the file count: `5× in 3 files — a.ts (3), b.ts (2)`.
12
+
13
+ ### Docs
14
+ - Reframed the `returnsTrue()` non-goal as scope demarcation (a hypothetical
15
+ `always-truthy-expression` check is a separate concern), not risk-avoidance.
16
+
17
+ ## 0.5.3
18
+
19
+ Closeout of the v0.5.2 code-review feedback (review rated 5/5). No behaviour change on
20
+ real projects (fixtures unchanged: 0 false positives).
21
+
22
+ ### Changed
23
+ - `--fix` groups locations by file (`5× — a.ts (3), b.ts (2), +1 more`) instead of showing
24
+ only the first occurrence — easier to see where a rule actually fires.
25
+ - Context-specific fix snippets extended beyond the `mass-assignment` pilot to
26
+ `missing-owner-enforcement` (names the slug and the owner field in a beforeChange stamp),
27
+ `collection-missing-access` (names the slug), and `reserved-field-name` (names the field).
28
+
29
+ ### Docs
30
+ - Clarified that the self-loop check in cycle detection is intentional: Tarjan places a
31
+ self-referencing node in a size-1 SCC, so an SCC-size test alone would miss real self-loops.
32
+ - Documented a non-goal: `returnsTrue()` matches only a literal `true`, not constant-folded
33
+ expressions like `() => (!false)` (vanishingly rare, and folding would add FP risk).
34
+
35
+ ## 0.5.2
36
+
37
+ Hardening from an external code review (rated 4.8/5, production-ready). All P0 and P1
38
+ items addressed; behaviour of the checks is unchanged on real projects (still 0 FPs on
39
+ the test fixtures). P2 ideas backlogged.
40
+
41
+ ### Fixed (P0)
42
+ - CLI: `--min-score` now validates its argument (must be a number 0–100) and exits with a
43
+ clear message otherwise, instead of silently using `NaN`.
44
+ - CLI: the ts-morph project setup is wrapped in error handling, and an empty scan (no
45
+ `.ts/.tsx/.js/.jsx` files under the path) now prints a helpful message and exits 2
46
+ instead of silently scanning nothing.
47
+
48
+ ### Changed (P1)
49
+ - Cycle detection (`circular-relationship`) now uses Tarjan's SCC — O(V+E) instead of a
50
+ per-node DFS — so it scales to large relationship graphs. Output is unchanged.
51
+ - `package.json` parsing in `dependency-version-mismatch` is defensively validated
52
+ (non-object root, non-object `dependencies`, non-string version specs are skipped, never throw).
53
+ - `returnsTrue()` is now AST-based instead of regex: it matches only an *unconditional*
54
+ `() => true` / `() => { return true }`, so it no longer false-matches on `() => true && x`
55
+ or a `return true` guarded by an `if`. More precise open-access detection.
56
+ - `--fix` output is now context-aware: each rule shows where it fires (`file:line`, with a
57
+ count) and checks can supply a context-specific snippet — e.g. `mass-assignment` names the
58
+ actual field and collection in its suggested field-level `access.create`.
59
+
60
+ ## 0.5.1
61
+
62
+ Staying on **0.5.x** until the checks run cleanly across many more real projects
63
+ (target: ~10 more real-world runs before any 0.6/1.0). Data-driven against real
64
+ a real 285-file Payload project.
65
+
66
+ ### Added
67
+ - `mass-assignment` (security, error/warning): an **auth** collection with open
68
+ `create` (public or any authenticated user) and a privileged field
69
+ (`roles`/`isAdmin`/…) lacking field-level `access.create` and a sanitizing
70
+ beforeChange hook — a registrant could send `roles: ['admin']` on create.
71
+ Scoped to auth collections; complements `user-writable-privileged-field` (update path).
72
+
73
+ ### Changed (false-positive & severity tuning from real runs)
74
+ - `local-api-override-access`: server/job context (cron, webhooks, sync/worker/job
75
+ files, migrations) → `info` (the Local API default is expected there). User-facing
76
+ routes/actions unchanged.
77
+ - `missing-owner-enforcement`: admin/system-restricted `create` → `info` (system stamps
78
+ the owner). User-facing create incl. `ownerOrAdmin` stays `warning`.
79
+ - `unsafe-richtext-render`: `warning` → `info` (a static tool can't prove the HTML source
80
+ is sanitized; it's an audit pointer).
81
+ - `reserved-field-name`: (a) only checks objects inside a real `fields: [...]` array, so
82
+ raw-query aliases like `{ name: 'm.name' }` are no longer flagged; (b) ignores reserved
83
+ names on fields nested in `array`/`blocks`/`group` (array rows legitimately carry `id`).
84
+ Mongo-illegal `.`/`$` still flagged for real fields at any depth.
85
+ - `hook-n-plus-one`: fixed (string-literal) collection → `warning` (batchable N+1);
86
+ dynamic collection per item → `info` (likely heterogeneous, not trivially batchable).
87
+ - `hardcoded-secret`: in trusted system paths (seed/migrations/scripts) → `info`, so a
88
+ dev/seed default doesn't tank the score.
89
+
90
+ ### Severity columns
91
+ - The per-rule Summary shows an `Xe/Yw/Zi` breakdown, in the text report and `--json`.
92
+
93
+ Total: 29 rules (26 per-file in `--list` + duplicate-slug, dependency-version-mismatch,
94
+ circular-relationship cross-file).
95
+
96
+ Deferred (need real-world validation, likely FP-prone or out of static scope):
97
+ `missing-rate-limit` (limiting often lives in middleware/infra), `exposed-admin-api`
98
+ (admin exposure is a deploy/infra concern, not visible in TS source).
99
+
100
+ ## 0.5.0
101
+
102
+ ### Added
103
+ - **Security depth:**
104
+ - `auth-weak-config` (warning): auth lockout disabled (`maxLoginAttempts: 0`) or a
105
+ `tokenExpiration` over 30 days. Only flags explicit weakenings — Payload's defaults
106
+ are safe, so a *missing* option is not flagged.
107
+ - `sensitive-data-logged` (warning): a `console.*` call that logs a password/token/
108
+ secret/apiKey/ssn/cvv value.
109
+ - **Schema & performance hygiene:**
110
+ - `reserved-field-name` (warning): a field name colliding with Payload-reserved fields
111
+ (`id`/`_id`/`createdAt`/`updatedAt`/`_status`/…) or illegal in MongoDB (`.`/`$`), and
112
+ collection slugs colliding with `payload-*` internals. Generic SQL keywords are NOT
113
+ flagged — the SQL adapter quotes identifiers, so they work.
114
+ - `excessive-max-depth` (warning): `maxDepth`/`defaultDepth` above 10.
115
+ - `missing-index-on-filter-field` (info): a commonly-filtered field (email/slug/username/
116
+ externalId/sku) without `index: true`.
117
+ - `hook-n-plus-one` (warning): a Local API read (`find`/`findByID`/`findGlobal`) inside a
118
+ loop or `map`/`forEach`/… callback — the classic N+1.
119
+ - `circular-relationship` (info, cross-file): relationship/upload fields forming a cycle
120
+ (A → B → A or self). Cycles are often legitimate, so it's an info reminder to bound `maxDepth`.
121
+ - The per-rule **Summary** now shows a severity breakdown per rule, e.g. `(6e/35w/17i)`,
122
+ in both the text report and the `--json` `summary` field.
123
+
124
+ Total: 28 rules (25 per-file in `--list` + duplicate-slug, dependency-version-mismatch,
125
+ circular-relationship as cross-file passes).
126
+
127
+ ## 0.4.1
128
+
129
+ ### Changed
130
+ - **Summary by rule now prints at the TOP** of the report (was at the bottom) — the
131
+ overview is visible without scrolling past every finding.
132
+ - `side-effect-in-get` is downgraded to `warning` on `unsubscribe`/`opt-out` routes
133
+ (email one-click unsubscribe is a GET convention), with a note about mail-client
134
+ prefetch and RFC 8058 List-Unsubscribe-Post. Cron stays `info`, everything else `error`.
135
+
136
+ ### Added
137
+ - `--json` output now includes a `summary` array (per-rule rollup: ruleId, severity,
138
+ count, files). JSON `schema` bumped to `2`.
139
+ - Documented the score formula in `--help` and the README:
140
+ `max(0, 100 − 10·errors − 3·warnings)`; info findings don't affect it.
141
+
142
+ ## 0.4.0
143
+
144
+ ### Added
145
+ - Four new static checks:
146
+ - **`dependency-version-mismatch`** (config, error): reads `package.json` and
147
+ flags mixed `payload` / `@payloadcms/*` versions — a top cause of mysterious
148
+ build/runtime breaks. Skips ranges/tags/`workspace:*` it can't compare.
149
+ - **`relationship-missing-relationTo`** (correctness, error): a `relationship`
150
+ or `upload` field without `relationTo`.
151
+ - **`select-without-options`** (correctness, error): a `select`/`radio` field
152
+ without `options`.
153
+ - **`duplicate-field-name`** (correctness, error): two fields with the same
154
+ `name` in the same `fields` array (per-array namespace, no cross-namespace FPs).
155
+
156
+ ## 0.3.1
157
+
158
+ ### Added
159
+ - Four new checks:
160
+ - **`collection-missing-slug`** (config, error): a collection/global config
161
+ with `fields` but no `slug`. Only high-confidence configs are flagged
162
+ (inline `collections:`/`globals:` array elements, or objects typed
163
+ `CollectionConfig`/`GlobalConfig`), so nested field groups and tabs are safe.
164
+ - **`hook-missing-return`** (correctness, warning): a transforming hook
165
+ (`beforeChange`/`beforeValidate`/`beforeRead`/`afterRead`/`beforeDuplicate`)
166
+ that never returns a value — Payload uses the return value, so the change is
167
+ silently dropped. Side-effect-only hooks (`afterChange`…) are not flagged.
168
+ - **`duplicate-slug`** (config, error): the same slug on more than one
169
+ collection/global (cross-file check).
170
+ - **`admin-hidden-not-access`** (security, warning): a field with
171
+ `admin.hidden: true` and no field-level `access` — it is hidden in the Admin
172
+ UI but still returned by the REST/GraphQL API.
173
+ - **`--fix`**: prints a suggested fix snippet per rule. It never modifies files.
174
+
175
+ ### Changed
176
+ - Sharpened the tagline/description to make clear this is for **Payload CMS**
177
+ (the framework), not for validating API request/response payloads.
178
+
179
+ ## 0.3.0
180
+
181
+ ### Added
182
+ - **`unsafe-richtext-render` check** (new `rendering` category): flags
183
+ `dangerouslySetInnerHTML` rendering CMS/rich-text content without visible
184
+ sanitization — the block-render seam where stored XSS hides. Only fires on
185
+ content that looks CMS-related (generic React stays out of scope).
186
+ - **`--summary`** flag and an always-on "Summary by rule" rollup (count +
187
+ distinct files per rule), so large reports don't scroll forever.
188
+
189
+ ### Changed
190
+ - Score weighting: `error=10`, `warning=3`, **`info=0`** (info findings no longer
191
+ drag the score down).
192
+ - `side-effect-in-get` is downgraded to `info` inside `/cron/` routes (Vercel
193
+ cron requires GET); a reminder to keep the write idempotent is included.
194
+
195
+ ## 0.2.3
196
+
197
+ ### Changed
198
+ - Repository/author links now point to `github.com/metakraft`.
199
+
200
+ ## 0.2.2
201
+
202
+ ### Fixed
203
+ - A non-existent target path now prints a clear `Path not found:` error and exits
204
+ with code 2, instead of the ambiguous "No source files found" message.
205
+
206
+ ### Added
207
+ - Scanning a single file (not just a directory) is now supported.
208
+
209
+ ## 0.2.1
210
+
211
+ ### Added
212
+ - Author / maintainer attribution (Leander M. von Kraft — Tierramor Agency) in
213
+ README, SKILL.md and `package.json`.
214
+ - `package.json` metadata: `author`, `homepage`, `repository`, `bugs`.
215
+ - `SECURITY.md`, `CONTRIBUTING.md`, and a GitHub Actions CI workflow that runs
216
+ `npm test` on Node 18/20/22.
217
+ - README disclaimer: provided as-is, free and open source, without warranty or
218
+ support obligation.
219
+
220
+ ## 0.2.0
221
+
222
+ Tuned against a real-world Payload project to cut false positives.
223
+
224
+ ### Fixed (false positives)
225
+ - **Blocks and globals are no longer mistaken for collections.** A new config
226
+ classifier uses array position, type annotation (`CollectionConfig` / `Block` /
227
+ `GlobalConfig`) and file path, and treats anything nested inside a `fields`
228
+ array as a block. This stops `collection-missing-access`, `open-access-function`,
229
+ `missing-owner-enforcement`, `user-writable-privileged-field` and
230
+ `token-field-readable` from firing on blocks.
231
+ - **Public registration** (`create: () => true` on an `auth` collection) is now
232
+ `info`, not `error`.
233
+ - **Explicit system writes** (`overrideAccess: true` with no `user`, e.g. cron /
234
+ webhooks) are now `info` instead of `error`; `overrideAccess: true` together
235
+ with a `user` is still reported by `override-access-true-with-user` (no double
236
+ reporting).
237
+
238
+ ### Added
239
+ - **Inline suppression** (ESLint-style): `// payload-doctor-disable-next-line`,
240
+ `// payload-doctor-disable-line`, `// payload-doctor-disable` — optionally
241
+ scoped to specific rule ids, or all rules when none is given. Suppressed count
242
+ is shown in text and JSON output.
243
+ - `-V` / `--version` flag; version printed in the report header, `--help`, and as
244
+ `toolVersion` in JSON.
245
+ - Self-test fixtures for blocks, suppression and system writes.
246
+
247
+ ## 0.1.0
248
+
249
+ Initial release.
250
+
251
+ ### Checks
252
+ - Access control: `local-api-override-access`, `override-access-true-with-user`,
253
+ `collection-missing-access`, `open-access-function`, `missing-owner-enforcement`,
254
+ `user-writable-privileged-field`.
255
+ - Routes: `cron-not-fail-closed`, `side-effect-in-get`, `leaks-error-message`.
256
+ - Config & privacy: `hardcoded-secret`, `wide-open-cors`, `token-field-readable`.
257
+
258
+ ### Tooling
259
+ - 0-100 health score with `great` / `needs-work` / `critical` bands.
260
+ - Text and `--json` output, `--verbose` hints, `--list`, `--min-score`,
261
+ `--no-exit-code`, `--no-color`. CI-friendly exit codes.
262
+ - Auto-detects `.ts/.tsx/.js/.jsx`; skips `node_modules`, `dist`, `.next`,
263
+ `build` and treats `migrations/seeds/scripts/tests` as trusted system code.
264
+
265
+ ### False-positive hardening
266
+ - Word-segment matching (camelCase aware) so `hashtags`/`tokenizer` no longer
267
+ match the `hash`/`token` sensitive-field rule.
268
+ - Tightened the error-leak regex so identifiers like `code.message` /
269
+ `response.message` are no longer mistaken for `error.message`.
270
+ - `local-api-override-access` skips calls whose options object uses a spread
271
+ (the spread may carry `overrideAccess: false`).
272
+ - `missing-owner-enforcement` no longer fires when a `beforeValidate`/
273
+ `beforeChange` hook is present (it may be imported) or the owner field has a
274
+ `defaultValue`.
275
+ - Dropped speculative field names (`plan`, `tier`) from the privileged-field rule.
276
+
277
+ ### Tests
278
+ - Self-test fixtures: `vulnerable` (must be flagged), `clean` and `tricky`
279
+ (must produce zero findings). Run with `npm test`.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Leander M. von Kraft (Tierramor Agency)
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,180 @@
1
+ # payload-doctor
2
+
3
+ Static **security & correctness linter for [Payload CMS](https://payloadcms.com)** —
4
+ the TypeScript headless CMS. It scans your collections, access control, hooks,
5
+ routes and config for known anti-patterns — the kind that AI coding agents and
6
+ humans alike get wrong — and prints a **0–100 health score** with actionable findings.
7
+
8
+ > **Note:** this is for **Payload CMS** (the framework). It does *not* inspect or
9
+ > validate API request/response payloads (JSON/XML). If you came looking for that,
10
+ > this isn't it.
11
+
12
+ Think of it as a `react-doctor` for Payload. One command, no install:
13
+
14
+ ```bash
15
+ npx -y payload-doctor@latest .
16
+ ```
17
+
18
+ ## Why
19
+
20
+ Payload's **Local API bypasses access control by default** (`overrideAccess` is
21
+ `true` unless you set it to `false`). It's the single most expensive footgun in a
22
+ Payload app: a route can authenticate a user and still hand them someone else's
23
+ records, because the collection's `access` functions never run. payload-doctor
24
+ catches that and a dozen related issues before they reach production.
25
+
26
+ ## Usage
27
+
28
+ ```bash
29
+ # scan the current project
30
+ npx -y payload-doctor@latest .
31
+
32
+ # show fix hints
33
+ npx -y payload-doctor@latest . --verbose
34
+
35
+ # machine-readable output for CI / dashboards
36
+ npx -y payload-doctor@latest . --json
37
+
38
+ # big report? show only the per-rule rollup
39
+ npx -y payload-doctor@latest . --summary
40
+
41
+ # print a suggested fix per rule (never modifies files)
42
+ npx -y payload-doctor@latest . --fix
43
+
44
+ # fail a CI job if the score drops below a threshold
45
+ npx -y payload-doctor@latest . --min-score 80
46
+
47
+ # print the version
48
+ npx -y payload-doctor@latest --version
49
+ ```
50
+
51
+ **Recommended workflow:** run it → fix the errors first → re-run and watch the
52
+ score climb. Keep a clean git state before applying fixes.
53
+
54
+ Score bands: **75–100 great · 50–74 needs work · 0–49 critical.**
55
+
56
+ The score is `max(0, 100 − 10·errors − 3·warnings)`; `info` findings don't affect
57
+ it. Ten or more errors floor it at `0` by design — once you're in the red, track
58
+ the dropping error/warning counts (and the per-rule summary) to gauge progress
59
+ rather than the score alone.
60
+
61
+ Exit code is `1` when any `error`-severity finding is present (or the score is
62
+ below `--min-score`), `0` otherwise — drop it straight into CI or a pre-commit
63
+ hook. Use `--no-exit-code` while you're adopting it.
64
+
65
+ ## Suppressing intentional cases
66
+
67
+ Some findings are deliberate — an OAuth callback that writes on `GET`, a cron
68
+ job using `overrideAccess: true`. Silence them inline, ESLint-style:
69
+
70
+ ```ts
71
+ // payload-doctor-disable-next-line local-api-override-access
72
+ await payload.update({ collection: 'jobs', id, data })
73
+
74
+ // payload-doctor-disable-line side-effect-in-get
75
+
76
+ // payload-doctor-disable side-effect-in-get ← whole file; omit the rule to silence all
77
+ ```
78
+
79
+ Rule names may be written with or without the `payload-doctor/` prefix. The
80
+ number of suppressed findings is reported so suppressions stay visible.
81
+
82
+ ## Checks
83
+
84
+ | Rule | Category | Severity | What it catches |
85
+ |------|----------|----------|-----------------|
86
+ | `local-api-override-access` | security | error / warning | Local API call without `overrideAccess: false` — access control bypassed |
87
+ | `override-access-true-with-user` | security | error | `overrideAccess: true` while passing a `user` — control skipped on purpose |
88
+ | `collection-missing-access` | security | warning | Collection with no explicit `access` block |
89
+ | `open-access-function` | security | error / info | `access.{create,update,delete}` returns `true` (anyone can write) |
90
+ | `missing-owner-enforcement` | security | warning | User-owned collection that doesn't force ownership on create |
91
+ | `user-writable-privileged-field` | security | error | `roles` / `isAdmin` / … field without field-level `access.update` |
92
+ | `mass-assignment` | security | error/warning | Privileged field settable on create (auth collection, open create, no field `access.create`) |
93
+ | `cron-not-fail-closed` | security | error | Secret/cron guard that is fail-open when the secret is unset |
94
+ | `side-effect-in-get` | correctness | error | `GET` handler performs a write (prefetch / email scanners trigger it) |
95
+ | `leaks-error-message` | security | warning | Internal `error.message` / stack returned to the client |
96
+ | `hardcoded-secret` | security | error | Secret, key or connection string committed as a string literal |
97
+ | `wide-open-cors` | config | warning | CORS set to `'*'` |
98
+ | `token-field-readable` | privacy | warning | Token/hash/secret field exposed via API (no field-level `read`) |
99
+ | `unsafe-richtext-render` | rendering | info | `dangerouslySetInnerHTML` renders CMS/rich-text HTML — review the sink (source may or may not be sanitized) |
100
+ | `collection-missing-slug` | config | error | Collection/global config without a `slug` |
101
+ | `duplicate-slug` | config | error | Same slug on more than one collection/global |
102
+ | `hook-missing-return` | correctness | warning | Transforming hook (`beforeChange`/`afterRead`…) returns nothing |
103
+ | `admin-hidden-not-access` | security | warning | `admin.hidden` field with no field access — still returned by the API |
104
+ | `dependency-version-mismatch` | config | error | Mixed `payload` / `@payloadcms/*` versions in package.json |
105
+ | `relationship-missing-relationTo` | correctness | error | `relationship`/`upload` field without `relationTo` |
106
+ | `select-without-options` | correctness | error | `select`/`radio` field without `options` |
107
+ | `duplicate-field-name` | correctness | error | Two fields with the same `name` in one `fields` array |
108
+ | `auth-weak-config` | security | warning | Auth lockout disabled (`maxLoginAttempts: 0`) or very long `tokenExpiration` |
109
+ | `sensitive-data-logged` | security | warning | `console.*` logs a password/token/secret value |
110
+ | `reserved-field-name` | config | warning | Field/slug collides with a Payload-reserved or Mongo-illegal identifier |
111
+ | `excessive-max-depth` | config | warning | `maxDepth`/`defaultDepth` above 10 |
112
+ | `missing-index-on-filter-field` | config | info | Commonly-filtered field (email/slug/…) without `index: true` |
113
+ | `hook-n-plus-one` | correctness | warning | Local API read inside a loop / `map` (N+1 query) |
114
+ | `circular-relationship` | correctness | info | `relationship`/`upload` fields form a cycle (watch `maxDepth`) |
115
+
116
+ List them anytime with `npx -y payload-doctor@latest --list`.
117
+
118
+ > The checks are static heuristics, like any linter. They are tuned for low
119
+ > false-positives, but always review findings in context. Files under
120
+ > `migrations/`, `seeds/`, `scripts/` and tests are treated as trusted system
121
+ > code and skipped for the request-context rules.
122
+
123
+ ## Use with AI coding agents
124
+
125
+ This repo ships a `SKILL.md`, so it works as an agent skill too:
126
+
127
+ ```bash
128
+ npx skills add https://github.com/metakraft/payload-doctor --skill payload-doctor
129
+ ```
130
+
131
+ Then your agent can run the doctor after generating Payload code, fix the
132
+ flagged issues, and re-run to verify — closing the gap between "AI writes code"
133
+ and "code ships".
134
+
135
+ ## Contributing
136
+
137
+ A check is a small object implementing the `Check` interface (see `src/types.ts`)
138
+ that receives a `ts-morph` `SourceFile` and returns `Finding[]`. Add yours to the
139
+ relevant file in `src/checks/` and register it in `src/checks/index.ts`. There are
140
+ self-test fixtures in `test/fixtures/` — an intentionally insecure project
141
+ (`vulnerable`), a secure reference (`clean`), and a `tricky` set of legitimate
142
+ patterns that must NOT produce findings (the false-positive guard). Build and run
143
+ the assertion suite with:
144
+
145
+ ```bash
146
+ npm install
147
+ npm test
148
+ ```
149
+
150
+ `npm test` builds the project and asserts that `vulnerable` is flagged while
151
+ `clean` and `tricky` stay silent. Add a fixture whenever you add or change a
152
+ check, and keep the false-positive rate low — a noisy linter gets disabled.
153
+
154
+ PRs that add checks, reduce false-positives, or add fixtures are welcome.
155
+
156
+ ## Bugs & feature requests
157
+
158
+ Please use **[GitHub Issues](https://github.com/metakraft/payload-doctor/issues)** —
159
+ there are templates for bug reports (incl. false positives) and new-check requests.
160
+ Open-ended questions and "show & tell" go in
161
+ **[Discussions](https://github.com/metakraft/payload-doctor/discussions)**; suspected
162
+ security issues via a private **Security Advisory** (see `SECURITY.md`). `npm bugs`
163
+ from a project using the package opens the issue tracker directly.
164
+
165
+ ## Author
166
+
167
+ Leander M. von Kraft — [www.metakraft.de](https://www.metakraft.de)
168
+ Tierramor Agency — AI-native project work.
169
+
170
+ ## Disclaimer
171
+
172
+ Provided as-is, free and open source, **without warranty or any obligation** —
173
+ no support, no guarantees, no liability. The checks are static heuristics: they
174
+ help, but you remain responsible for reviewing findings and securing your own
175
+ code. Use at your own risk.
176
+
177
+ ## License
178
+
179
+ [MIT](./LICENSE). Free to use, modify and distribute. No rights reserved beyond
180
+ the attribution the MIT license asks for.
package/SKILL.md ADDED
@@ -0,0 +1,50 @@
1
+ ---
2
+ name: payload-doctor
3
+ description: Static security and correctness auditor for Payload CMS projects. Use this skill whenever working on Payload CMS access control, collection configuration, hooks, custom endpoints, Next.js route handlers or server actions that read or write Payload data, or before merging any user-data route. Run it after generating or changing Payload code to catch access-control bypasses and related anti-patterns. Trigger on "Payload security", "Payload access control", "overrideAccess", "Local API bypass", "audit Payload", "Payload doctor", "is this Payload collection secure", "check my Payload config", "cron fail closed", "force ownership on create", "side-effect GET", or any request to review the security of a Payload collection, hook, endpoint or route.
4
+ ---
5
+
6
+ # payload-doctor
7
+
8
+ A CLI that statically audits a Payload CMS project for security and correctness
9
+ anti-patterns and prints a 0-100 health score with file:line findings.
10
+
11
+ ## How to run it
12
+
13
+ From the project root:
14
+
15
+ ```bash
16
+ npx -y payload-doctor@latest . --verbose
17
+ ```
18
+
19
+ Add `--json` for machine-readable output, `--min-score N` to enforce a threshold
20
+ in CI, `--list` to see all checks.
21
+
22
+ ## Workflow
23
+
24
+ 1. Run the doctor on the Payload project.
25
+ 2. Fix `error`-severity findings first, then warnings.
26
+ 3. Re-run and confirm the score climbed and the findings are gone.
27
+ 4. Keep a clean git state before applying fixes.
28
+
29
+ Score bands: 75-100 great, 50-74 needs work, 0-49 critical. Exit code is non-zero
30
+ when any error is present.
31
+
32
+ ## What it checks
33
+
34
+ Access control (Local API `overrideAccess` bypass, open access functions,
35
+ missing collection access, ownership not forced on create, user-writable
36
+ privileged fields), routes (fail-open cron/secret guards, side-effect GETs, error
37
+ leaks), config (hardcoded secrets, wide-open CORS) and privacy (token/hash fields
38
+ readable via API). See README for the full table.
39
+
40
+ ## Interpreting findings
41
+
42
+ Findings are static heuristics tuned for low false-positives. Always confirm in
43
+ context. The most important rule is `local-api-override-access`: Payload's Local
44
+ API defaults to `overrideAccess: true`, which bypasses collection access control,
45
+ so request-context calls must pass `overrideAccess: false` and `user` (or verify
46
+ ownership manually).
47
+
48
+ ---
49
+
50
+ By Leander M. von Kraft — www.metakraft.de · Tierramor Agency. MIT licensed, provided as-is without warranty.