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.
@@ -0,0 +1,444 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.configChecks = void 0;
4
+ const ts_morph_1 = require("ts-morph");
5
+ const util_1 = require("../util");
6
+ const CONNECTION_STRING = /(mongodb(\+srv)?|postgres(ql)?|mysql|redis):\/\/[^/\s:]+:[^@/\s]+@/;
7
+ const KEY_PREFIX = /^(sk-[a-z]|sk_live_|sk_test_|rk_live_|AKIA[0-9A-Z]{16}|ghp_|xox[bp]-|AIza)/;
8
+ /** Hardcoded secrets / credentials in source instead of env vars. */
9
+ const hardcodedSecret = {
10
+ id: 'hardcoded-secret',
11
+ category: 'security',
12
+ describe: 'Secret, key or credential hardcoded as a string literal',
13
+ run(file, ctx) {
14
+ const findings = [];
15
+ // In trusted system code (seed/migrations/scripts) a literal is usually a
16
+ // dev/seed default, not a leaked production secret -> info, don't tank the score.
17
+ const sev = (0, util_1.isSystemPath)(file.getFilePath()) ? 'info' : 'error';
18
+ // secret: '...' (e.g. PAYLOAD_SECRET) assigned a literal, not process.env
19
+ for (const pa of file.getDescendantsOfKind(ts_morph_1.SyntaxKind.PropertyAssignment)) {
20
+ const name = pa.getName().replace(/['"`]/g, '').toLowerCase();
21
+ if (!/^(secret|payload_secret|apikey|api_key|password|privatekey)$/.test(name))
22
+ continue;
23
+ const init = pa.getInitializer();
24
+ if (!init || !ts_morph_1.Node.isStringLiteral(init))
25
+ continue;
26
+ const val = init.getLiteralValue();
27
+ if (val.length < 6)
28
+ continue;
29
+ findings.push((0, util_1.makeFinding)(pa, ctx, 'hardcoded-secret', 'security', sev, `"${name}" is assigned a hardcoded literal instead of an environment variable`, 'Read it from process.env and fail closed when it is missing.'));
30
+ }
31
+ // connection strings with embedded credentials, and known key prefixes
32
+ for (const lit of file.getDescendantsOfKind(ts_morph_1.SyntaxKind.StringLiteral)) {
33
+ const val = lit.getLiteralValue();
34
+ if (CONNECTION_STRING.test(val)) {
35
+ findings.push((0, util_1.makeFinding)(lit, ctx, 'hardcoded-secret', 'security', sev, 'Connection string with embedded credentials found in source', 'Move it to an environment variable.'));
36
+ }
37
+ else if (KEY_PREFIX.test(val)) {
38
+ findings.push((0, util_1.makeFinding)(lit, ctx, 'hardcoded-secret', 'security', sev, 'Value looks like a live API key committed to source', 'Rotate the key and load it from the environment.'));
39
+ }
40
+ }
41
+ return findings;
42
+ },
43
+ };
44
+ /** Wide-open CORS (`'*'`) accepts requests from any origin. */
45
+ const wideOpenCors = {
46
+ id: 'wide-open-cors',
47
+ category: 'config',
48
+ describe: "CORS configured as '*' (any origin)",
49
+ run(file, ctx) {
50
+ const findings = [];
51
+ for (const pa of file.getDescendantsOfKind(ts_morph_1.SyntaxKind.PropertyAssignment)) {
52
+ if (pa.getName().replace(/['"`]/g, '') !== 'cors')
53
+ continue;
54
+ const init = pa.getInitializer();
55
+ if (!init)
56
+ continue;
57
+ const text = init.getText().replace(/\s+/g, '');
58
+ if (text === "'*'" || text === '"*"' || text === "['*']" || text === '["*"]') {
59
+ findings.push((0, util_1.makeFinding)(pa, ctx, 'wide-open-cors', 'config', 'warning', 'CORS allows any origin (*)', 'List the exact origins you trust instead of "*".'));
60
+ }
61
+ }
62
+ return findings;
63
+ },
64
+ };
65
+ const SENSITIVE_WORDS = new Set([
66
+ 'token',
67
+ 'hash',
68
+ 'secret',
69
+ 'apikey',
70
+ 'apisecret',
71
+ 'resetkey',
72
+ 'accesskey',
73
+ ]);
74
+ /**
75
+ * Token/hash/secret fields are exposed through the REST/GraphQL API unless
76
+ * field-level read access denies it. admin.hidden only hides the admin UI.
77
+ */
78
+ const tokenFieldReadable = {
79
+ id: 'token-field-readable',
80
+ category: 'privacy',
81
+ describe: 'Sensitive field readable via API (no field-level read:false)',
82
+ run(file, ctx) {
83
+ const findings = [];
84
+ for (const obj of file.getDescendantsOfKind(ts_morph_1.SyntaxKind.ObjectLiteralExpression)) {
85
+ if (!(0, util_1.isCollectionConfig)(obj))
86
+ continue;
87
+ const slug = (0, util_1.unquote)((0, util_1.propText)(obj, 'slug')) ?? 'unknown';
88
+ for (const f of (0, util_1.getFieldObjects)(obj)) {
89
+ const name = (0, util_1.unquote)((0, util_1.propText)(f, 'name'));
90
+ if (!name || !(0, util_1.hasSegment)(name, SENSITIVE_WORDS))
91
+ continue;
92
+ const accessInit = (0, util_1.propInit)(f, 'access');
93
+ const guarded = accessInit &&
94
+ ts_morph_1.Node.isObjectLiteralExpression(accessInit) &&
95
+ (0, util_1.hasProp)(accessInit, 'read');
96
+ if (guarded)
97
+ continue;
98
+ findings.push((0, util_1.makeFinding)(f, ctx, 'token-field-readable', 'privacy', 'warning', `Sensitive field "${name}" on "${slug}" can be read through the API`, 'Add field access: { read: () => false }. admin.hidden only hides the UI, not the API.'));
99
+ }
100
+ }
101
+ return findings;
102
+ },
103
+ };
104
+ /**
105
+ * A collection/global config without a slug. Payload requires a unique slug per
106
+ * collection (it drives the DB collection and admin/API routes). We only flag
107
+ * high-confidence configs — inline elements of a `collections:`/`globals:` array,
108
+ * or objects explicitly typed `CollectionConfig`/`GlobalConfig` — so nested field
109
+ * groups and tabs (which also carry `fields` but no slug) are never mistaken.
110
+ */
111
+ const collectionMissingSlug = {
112
+ id: 'collection-missing-slug',
113
+ category: 'config',
114
+ describe: 'Collection/global config without a slug',
115
+ run(file, ctx) {
116
+ const findings = [];
117
+ for (const obj of file.getDescendantsOfKind(ts_morph_1.SyntaxKind.ObjectLiteralExpression)) {
118
+ if (!(0, util_1.hasProp)(obj, 'fields') || (0, util_1.hasProp)(obj, 'slug'))
119
+ continue;
120
+ if ((0, util_1.hasProp)(obj, 'type') || (0, util_1.hasProp)(obj, 'name'))
121
+ continue; // it's a field, not a config
122
+ if ((0, util_1.hasSpread)(obj))
123
+ continue; // a spread may supply the slug
124
+ const arrProp = (0, util_1.enclosingArrayProp)(obj);
125
+ const inConfigArray = arrProp === 'collections' || arrProp === 'globals';
126
+ const typed = /CollectionConfig|GlobalConfig/.test((0, util_1.directTypeHint)(obj));
127
+ if (!inConfigArray && !typed)
128
+ continue;
129
+ findings.push((0, util_1.makeFinding)(obj, ctx, 'collection-missing-slug', 'config', 'error', 'Collection/global config has no slug — Payload requires a unique slug', 'Add a slug, e.g. slug: "posts".'));
130
+ }
131
+ return findings;
132
+ },
133
+ };
134
+ // Hooks whose return value is consumed by Payload (the transformed data/doc).
135
+ const TRANSFORMING_HOOKS = new Set([
136
+ 'beforeChange',
137
+ 'beforeValidate',
138
+ 'beforeRead',
139
+ 'afterRead',
140
+ 'beforeDuplicate',
141
+ ]);
142
+ /** Does a function node return a value at its own scope (not a nested callback)? */
143
+ function functionReturnsValue(fn) {
144
+ if (ts_morph_1.Node.isArrowFunction(fn)) {
145
+ const body = fn.getBody();
146
+ if (body && !ts_morph_1.Node.isBlock(body))
147
+ return true; // implicit expression-body return
148
+ }
149
+ const body = fn.getBody?.();
150
+ if (!body || !ts_morph_1.Node.isBlock(body))
151
+ return false;
152
+ let found = false;
153
+ body.forEachDescendant((node, traversal) => {
154
+ if (ts_morph_1.Node.isFunctionDeclaration(node) ||
155
+ ts_morph_1.Node.isFunctionExpression(node) ||
156
+ ts_morph_1.Node.isArrowFunction(node) ||
157
+ ts_morph_1.Node.isMethodDeclaration(node)) {
158
+ traversal.skip();
159
+ return;
160
+ }
161
+ if (ts_morph_1.Node.isReturnStatement(node) && node.getExpression()) {
162
+ found = true;
163
+ traversal.stop();
164
+ }
165
+ });
166
+ return found;
167
+ }
168
+ /**
169
+ * A transforming hook (beforeChange/beforeValidate/afterRead…) that never returns
170
+ * a value silently drops the change — Payload uses the return value as the new
171
+ * data/doc. Side-effect-only hooks (afterChange, afterDelete…) are not flagged.
172
+ */
173
+ const hookMissingReturn = {
174
+ id: 'hook-missing-return',
175
+ category: 'correctness',
176
+ describe: 'Transforming hook (beforeChange/afterRead…) returns nothing',
177
+ run(file, ctx) {
178
+ const findings = [];
179
+ for (const pa of file.getDescendantsOfKind(ts_morph_1.SyntaxKind.PropertyAssignment)) {
180
+ const name = pa.getName().replace(/['"`]/g, '');
181
+ if (!TRANSFORMING_HOOKS.has(name))
182
+ continue;
183
+ const init = pa.getInitializer();
184
+ if (!init || !ts_morph_1.Node.isArrayLiteralExpression(init))
185
+ continue;
186
+ for (const el of init.getElements()) {
187
+ if (!ts_morph_1.Node.isArrowFunction(el) && !ts_morph_1.Node.isFunctionExpression(el))
188
+ continue;
189
+ if (functionReturnsValue(el))
190
+ continue;
191
+ findings.push((0, util_1.makeFinding)(el, ctx, 'hook-missing-return', 'correctness', 'warning', `${name} hook returns nothing — Payload uses the return value, so the change is discarded`, 'Return data (before* hooks) or doc (afterRead) at the end of the hook.'));
192
+ }
193
+ }
194
+ return findings;
195
+ },
196
+ };
197
+ /**
198
+ * admin.hidden only hides a field in the Admin UI — it is still returned by the
199
+ * REST/GraphQL API. Without field-level read access that is a false sense of
200
+ * security. We flag fields with admin.hidden:true and no field `access`.
201
+ */
202
+ const adminHiddenNotAccess = {
203
+ id: 'admin-hidden-not-access',
204
+ category: 'security',
205
+ describe: 'Field hidden in admin UI but still readable via API (no field access)',
206
+ run(file, ctx) {
207
+ const findings = [];
208
+ for (const obj of file.getDescendantsOfKind(ts_morph_1.SyntaxKind.ObjectLiteralExpression)) {
209
+ if (!(0, util_1.hasProp)(obj, 'name') || !(0, util_1.hasProp)(obj, 'type'))
210
+ continue; // must be a field
211
+ if ((0, util_1.hasProp)(obj, 'access'))
212
+ continue; // dev set field access -> assume intentional
213
+ const adminInit = (0, util_1.propInit)(obj, 'admin');
214
+ if (!adminInit || !ts_morph_1.Node.isObjectLiteralExpression(adminInit))
215
+ continue;
216
+ if ((0, util_1.propText)(adminInit, 'hidden') !== 'true')
217
+ continue;
218
+ const name = (0, util_1.unquote)((0, util_1.propText)(obj, 'name')) ?? 'field';
219
+ findings.push((0, util_1.makeFinding)(obj, ctx, 'admin-hidden-not-access', 'security', 'warning', `Field "${name}" uses admin.hidden but has no field access — it is still returned by the API`, 'admin.hidden hides only the Admin UI. Add access: { read: () => false } (or top-level hidden: true) to keep it out of API responses.'));
220
+ }
221
+ return findings;
222
+ },
223
+ };
224
+ const RELATION_TYPES = new Set(['relationship', 'upload']);
225
+ /** relationship/upload field without relationTo — Payload cannot resolve the relation. */
226
+ const relationshipMissingRelationTo = {
227
+ id: 'relationship-missing-relationTo',
228
+ category: 'correctness',
229
+ describe: 'relationship/upload field without relationTo',
230
+ run(file, ctx) {
231
+ const findings = [];
232
+ for (const obj of file.getDescendantsOfKind(ts_morph_1.SyntaxKind.ObjectLiteralExpression)) {
233
+ const type = (0, util_1.unquote)((0, util_1.propText)(obj, 'type'));
234
+ if (!type || !RELATION_TYPES.has(type))
235
+ continue;
236
+ if ((0, util_1.hasProp)(obj, 'relationTo') || (0, util_1.hasSpread)(obj))
237
+ continue;
238
+ const name = (0, util_1.unquote)((0, util_1.propText)(obj, 'name')) ?? 'field';
239
+ findings.push((0, util_1.makeFinding)(obj, ctx, 'relationship-missing-relationTo', 'correctness', 'error', `${type} field "${name}" has no relationTo — Payload cannot resolve the relation`, 'Add relationTo: "collection-slug" (or an array of slugs for polymorphic relations).'));
240
+ }
241
+ return findings;
242
+ },
243
+ };
244
+ const OPTION_TYPES = new Set(['select', 'radio']);
245
+ /** select/radio field without options — required by Payload. */
246
+ const selectWithoutOptions = {
247
+ id: 'select-without-options',
248
+ category: 'correctness',
249
+ describe: 'select/radio field without options',
250
+ run(file, ctx) {
251
+ const findings = [];
252
+ for (const obj of file.getDescendantsOfKind(ts_morph_1.SyntaxKind.ObjectLiteralExpression)) {
253
+ const type = (0, util_1.unquote)((0, util_1.propText)(obj, 'type'));
254
+ if (!type || !OPTION_TYPES.has(type))
255
+ continue;
256
+ if ((0, util_1.hasProp)(obj, 'options') || (0, util_1.hasSpread)(obj))
257
+ continue;
258
+ const name = (0, util_1.unquote)((0, util_1.propText)(obj, 'name')) ?? 'field';
259
+ findings.push((0, util_1.makeFinding)(obj, ctx, 'select-without-options', 'correctness', 'error', `${type} field "${name}" has no options`, 'Add options: [{ label, value }, …].'));
260
+ }
261
+ return findings;
262
+ },
263
+ };
264
+ /**
265
+ * Two fields with the same name in the same fields array collide. We check each
266
+ * literal `fields:` array independently (its own namespace), so nested groups
267
+ * are handled correctly and there are no cross-namespace false positives.
268
+ */
269
+ const duplicateFieldName = {
270
+ id: 'duplicate-field-name',
271
+ category: 'correctness',
272
+ describe: 'Duplicate field name within the same fields array',
273
+ run(file, ctx) {
274
+ const findings = [];
275
+ for (const pa of file.getDescendantsOfKind(ts_morph_1.SyntaxKind.PropertyAssignment)) {
276
+ if (pa.getName().replace(/['"`]/g, '') !== 'fields')
277
+ continue;
278
+ const init = pa.getInitializer();
279
+ if (!init || !ts_morph_1.Node.isArrayLiteralExpression(init))
280
+ continue;
281
+ const seen = new Set();
282
+ for (const el of init.getElements()) {
283
+ if (!ts_morph_1.Node.isObjectLiteralExpression(el))
284
+ continue;
285
+ const name = (0, util_1.unquote)((0, util_1.propText)(el, 'name'));
286
+ if (!name)
287
+ continue;
288
+ if (seen.has(name)) {
289
+ findings.push((0, util_1.makeFinding)(el, ctx, 'duplicate-field-name', 'correctness', 'error', `Duplicate field name "${name}" in the same fields array — the fields collide`, 'Field names must be unique within their level; rename one.'));
290
+ }
291
+ seen.add(name);
292
+ }
293
+ }
294
+ return findings;
295
+ },
296
+ };
297
+ // Field names Payload reserves / auto-manages; redefining them collides.
298
+ const RESERVED_FIELD_NAMES = new Set(['id', '_id', 'createdAt', 'updatedAt', '_status', 'blockType', 'blockName']);
299
+ // Payload's internal collection slugs.
300
+ const RESERVED_SLUGS = new Set([
301
+ 'payload-preferences',
302
+ 'payload-migrations',
303
+ 'payload-locked-documents',
304
+ 'payload-jobs',
305
+ ]);
306
+ // Field names that are very commonly used as query filters.
307
+ const FILTER_FIELD_NAMES = new Set(['email', 'slug', 'username', 'externalid', 'sku']);
308
+ /**
309
+ * Weak auth config: lockout disabled (`maxLoginAttempts: 0`) or a very long
310
+ * `tokenExpiration`. Payload's defaults are safe, so we only flag explicit
311
+ * weakenings — not a missing option (the default still applies).
312
+ */
313
+ const authWeakConfig = {
314
+ id: 'auth-weak-config',
315
+ category: 'security',
316
+ describe: 'Auth config weakens brute-force lockout or uses a very long token lifetime',
317
+ run(file, ctx) {
318
+ const findings = [];
319
+ for (const pa of file.getDescendantsOfKind(ts_morph_1.SyntaxKind.PropertyAssignment)) {
320
+ if (pa.getName().replace(/['"`]/g, '') !== 'auth')
321
+ continue;
322
+ const auth = pa.getInitializer();
323
+ if (!auth || !ts_morph_1.Node.isObjectLiteralExpression(auth))
324
+ continue;
325
+ const maxAttempts = (0, util_1.propText)(auth, 'maxLoginAttempts');
326
+ if (maxAttempts === '0') {
327
+ findings.push((0, util_1.makeFinding)(auth, ctx, 'auth-weak-config', 'security', 'warning', 'maxLoginAttempts: 0 disables brute-force lockout entirely', 'Use a small positive number (Payload defaults to 5) to lock accounts after failed logins.'));
328
+ }
329
+ const exp = (0, util_1.propText)(auth, 'tokenExpiration');
330
+ if (exp && /^\d+$/.test(exp) && Number(exp) > 2592000) {
331
+ findings.push((0, util_1.makeFinding)(auth, ctx, 'auth-weak-config', 'security', 'warning', `tokenExpiration is ${exp}s (> 30 days) — a long-lived token widens the window if it leaks`, 'Prefer a short tokenExpiration and refresh, unless this collection truly needs long sessions.'));
332
+ }
333
+ }
334
+ return findings;
335
+ },
336
+ };
337
+ /**
338
+ * Field name or collection slug that collides with Payload-reserved identifiers
339
+ * or is illegal in MongoDB (contains "." or starts with "$"). Generic SQL keywords
340
+ * are intentionally NOT flagged — the SQL adapter quotes identifiers, so they work.
341
+ */
342
+ const reservedFieldName = {
343
+ id: 'reserved-field-name',
344
+ category: 'config',
345
+ describe: 'Field name / slug collides with a Payload-reserved or Mongo-illegal identifier',
346
+ run(file, ctx) {
347
+ const findings = [];
348
+ // Reserved names (id/createdAt/…) only collide at the collection document top
349
+ // level. Inside array/blocks rows `id` is normal, and group/tab fields are
350
+ // namespaced — so skip the reserved-name check there (Mongo-illegal still applies).
351
+ const inSubFieldScope = (obj) => {
352
+ let cur = obj.getParent();
353
+ while (cur) {
354
+ if (ts_morph_1.Node.isObjectLiteralExpression(cur)) {
355
+ const t = (0, util_1.propText)(cur, 'type')?.replace(/['"`]/g, '');
356
+ if (t === 'array' || t === 'blocks' || t === 'group')
357
+ return true;
358
+ }
359
+ cur = cur.getParent();
360
+ }
361
+ return false;
362
+ };
363
+ for (const obj of file.getDescendantsOfKind(ts_morph_1.SyntaxKind.ObjectLiteralExpression)) {
364
+ // field name — only objects that are actually fields in a `fields: [...]`
365
+ // array. This excludes query/select/alias objects elsewhere (e.g. a
366
+ // `{ name: 'm.name' }` projection in a raw query) that merely look field-ish.
367
+ if ((0, util_1.hasProp)(obj, 'name') && (0, util_1.hasProp)(obj, 'type') && (0, util_1.enclosingArrayProp)(obj) === 'fields') {
368
+ const name = (0, util_1.unquote)((0, util_1.propText)(obj, 'name'));
369
+ if (name) {
370
+ const illegal = name.includes('.') || name.startsWith('$');
371
+ const reserved = RESERVED_FIELD_NAMES.has(name) && !inSubFieldScope(obj);
372
+ if (reserved || illegal) {
373
+ findings.push((0, util_1.makeFinding)(obj, ctx, 'reserved-field-name', 'config', 'warning', `Field name "${name}" is reserved by Payload or illegal in MongoDB`, 'Rename the field (Payload manages id/_id/createdAt/updatedAt/_status itself; "." and "$" are invalid in Mongo keys).', `// rename "${name}" — Payload manages id/_id/createdAt/updatedAt/_status itself,\n// and "."/"$" are invalid in MongoDB keys`));
374
+ }
375
+ }
376
+ }
377
+ // collection slug
378
+ if ((0, util_1.isCollectionConfig)(obj)) {
379
+ const slug = (0, util_1.unquote)((0, util_1.propText)(obj, 'slug'));
380
+ if (slug && RESERVED_SLUGS.has(slug)) {
381
+ findings.push((0, util_1.makeFinding)(obj, ctx, 'reserved-field-name', 'config', 'warning', `Collection slug "${slug}" collides with a Payload-internal collection`, 'Choose a different slug; payload-* slugs are reserved for Payload internals.'));
382
+ }
383
+ }
384
+ }
385
+ return findings;
386
+ },
387
+ };
388
+ /** maxDepth/defaultDepth set very high can make populated queries blow up. */
389
+ const excessiveMaxDepth = {
390
+ id: 'excessive-max-depth',
391
+ category: 'config',
392
+ describe: 'maxDepth / defaultDepth set above 10',
393
+ run(file, ctx) {
394
+ const findings = [];
395
+ for (const pa of file.getDescendantsOfKind(ts_morph_1.SyntaxKind.PropertyAssignment)) {
396
+ const name = pa.getName().replace(/['"`]/g, '');
397
+ if (name !== 'maxDepth' && name !== 'defaultDepth')
398
+ continue;
399
+ const init = pa.getInitializer();
400
+ if (!init || !ts_morph_1.Node.isNumericLiteral(init))
401
+ continue;
402
+ const val = Number(init.getText());
403
+ if (val > 10) {
404
+ findings.push((0, util_1.makeFinding)(pa, ctx, 'excessive-max-depth', 'config', 'warning', `${name} is ${val} — deep relationship population can balloon query cost and response size`, 'Keep depth small (often 1–2) and request more only where needed.'));
405
+ }
406
+ }
407
+ return findings;
408
+ },
409
+ };
410
+ /** Commonly-filtered fields without an index pay a scan cost on every lookup. */
411
+ const missingIndexOnFilterField = {
412
+ id: 'missing-index-on-filter-field',
413
+ category: 'config',
414
+ describe: 'Commonly-filtered field (email/slug/…) without index: true',
415
+ run(file, ctx) {
416
+ const findings = [];
417
+ for (const obj of file.getDescendantsOfKind(ts_morph_1.SyntaxKind.ObjectLiteralExpression)) {
418
+ if (!(0, util_1.hasProp)(obj, 'name') || !(0, util_1.hasProp)(obj, 'type'))
419
+ continue;
420
+ const name = (0, util_1.unquote)((0, util_1.propText)(obj, 'name'));
421
+ if (!name || !FILTER_FIELD_NAMES.has(name.toLowerCase()))
422
+ continue;
423
+ if ((0, util_1.propText)(obj, 'index') === 'true' || (0, util_1.propText)(obj, 'unique') === 'true')
424
+ continue;
425
+ findings.push((0, util_1.makeFinding)(obj, ctx, 'missing-index-on-filter-field', 'config', 'info', `Field "${name}" is commonly filtered but has no index — add index: true if you query by it`, 'Set index: true (or unique: true) to avoid full collection scans.'));
426
+ }
427
+ return findings;
428
+ },
429
+ };
430
+ exports.configChecks = [
431
+ hardcodedSecret,
432
+ wideOpenCors,
433
+ tokenFieldReadable,
434
+ collectionMissingSlug,
435
+ hookMissingReturn,
436
+ adminHiddenNotAccess,
437
+ relationshipMissingRelationTo,
438
+ selectWithoutOptions,
439
+ duplicateFieldName,
440
+ authWeakConfig,
441
+ reservedFieldName,
442
+ excessiveMaxDepth,
443
+ missingIndexOnFilterField,
444
+ ];
@@ -0,0 +1,15 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.ALL_CHECKS = void 0;
4
+ const access_1 = require("./access");
5
+ const routes_1 = require("./routes");
6
+ const config_1 = require("./config");
7
+ const rendering_1 = require("./rendering");
8
+ const quality_1 = require("./quality");
9
+ exports.ALL_CHECKS = [
10
+ ...access_1.accessChecks,
11
+ ...routes_1.routeChecks,
12
+ ...config_1.configChecks,
13
+ ...rendering_1.renderingChecks,
14
+ ...quality_1.qualityChecks,
15
+ ];