@agjs/tsforge 0.1.18 → 0.2.0
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/package.json +4 -1
- package/scripts/build-rules-md.ts +78 -21
- package/scripts/sweep.ts +25 -20
- package/scripts/web-sweep.ts +292 -0
- package/src/browser/oracle.ts +29 -1
- package/src/cli.ts +9 -3
- package/src/config/index.ts +8 -0
- package/src/config/profiles.ts +150 -0
- package/src/config/tsforge-config.ts +64 -5
- package/src/detect-gate.ts +34 -1
- package/src/inference/inference.types.ts +8 -0
- package/src/inference/request.ts +5 -1
- package/src/inference/stream.ts +21 -2
- package/src/inference/wire.ts +0 -0
- package/src/loop/feedback/meta-rule-docs.ts +48 -0
- package/src/loop/feedback/rule-docs.ts +150 -0
- package/src/loop/rule-docs.generated.json +131 -1
- package/src/loop/run.ts +3 -0
- package/src/loop/session.ts +12 -5
- package/src/loop/ttsr-defaults.ts +175 -4
- package/src/meta-rules/registry.ts +32 -0
- package/src/meta-rules/rules/ci/no-github-context-in-shell.ts +40 -0
- package/src/meta-rules/rules/ci/no-pull-request-target-untrusted-checkout.ts +42 -0
- package/src/meta-rules/rules/ci/workflow-permissions-explicit.ts +49 -0
- package/src/meta-rules/rules/ci/workflow-permissions-least-privilege.ts +44 -0
- package/src/meta-rules/rules/config/next-image-remote-patterns-no-wildcards.ts +77 -0
- package/src/meta-rules/rules/config/next-instrumentation-present.ts +66 -0
- package/src/meta-rules/rules/config/next-proxy-over-middleware.ts +64 -0
- package/src/meta-rules/rules/config/tsconfig-recommended-flags.ts +75 -0
- package/src/meta-rules/rules/supply-chain/dependency-overrides-require-comment.ts +61 -0
- package/src/meta-rules/rules/supply-chain/fastify-security-plugins.ts +54 -0
- package/src/meta-rules/rules/supply-chain/lockfile-required.ts +51 -0
- package/src/meta-rules/rules/supply-chain/migrations-must-be-checked-in.ts +49 -0
- package/src/meta-rules/rules/supply-chain/no-git-or-tarball-dependencies.ts +70 -0
- package/src/meta-rules/rules/supply-chain/package-manager-field-required.ts +31 -0
- package/src/meta-rules/rules/supply-chain/production-must-not-use-drizzle-push.ts +75 -0
- package/src/meta-rules/rules/supply-chain/single-package-manager.ts +30 -0
- package/src/meta-rules/utils/lockfiles.ts +105 -0
- package/src/meta-rules/utils/workflow-yaml.ts +86 -0
- package/src/rule-packs/authorization/index.ts +26 -0
- package/src/rule-packs/authorization/rules/id-param-requires-object-authz.ts +87 -0
- package/src/rule-packs/authorization/rules/mutating-route-requires-authz.ts +116 -0
- package/src/rule-packs/authorization/rules/server-action-requires-authz.ts +101 -0
- package/src/rule-packs/authorization/utils.ts +285 -0
- package/src/rule-packs/boundary-utils.ts +13 -0
- package/src/rule-packs/code-flow/index.ts +4 -1
- package/src/rule-packs/code-flow/rules/no-throw-literal.ts +67 -0
- package/src/rule-packs/drizzle/index.ts +7 -0
- package/src/rule-packs/drizzle/rules/update-delete-account-scoped-must-filter-scope.ts +106 -0
- package/src/rule-packs/drizzle/rules/update-delete-must-have-where.ts +73 -0
- package/src/rule-packs/drizzle/utils.ts +133 -1
- package/src/rule-packs/fastify/index.ts +38 -0
- package/src/rule-packs/fastify/rules/error-handler-must-set-status.ts +78 -0
- package/src/rule-packs/fastify/rules/prefer-return-over-reply-send.ts +104 -0
- package/src/rule-packs/fastify/rules/require-fp-for-shared-plugins.ts +106 -0
- package/src/rule-packs/fastify/rules/require-plugin-name.ts +54 -0
- package/src/rule-packs/fastify/rules/require-response-schema.ts +62 -0
- package/src/rule-packs/fastify/rules/require-route-schema.ts +104 -0
- package/src/rule-packs/fastify/rules/test-inject-must-close-app.ts +44 -0
- package/src/rule-packs/fastify/utils/fastifyChain.ts +231 -0
- package/src/rule-packs/index.ts +10 -0
- package/src/rule-packs/jwt-cookies/index.ts +10 -0
- package/src/rule-packs/jwt-cookies/rules/auth-cookie-must-set-maxage-or-expires.ts +132 -0
- package/src/rule-packs/jwt-cookies/rules/auth-cookie-must-set-samesite.ts +151 -0
- package/src/rule-packs/jwt-cookies/rules/jwt-must-verify-not-decode.ts +124 -0
- package/src/rule-packs/module-boundaries/index.ts +3 -0
- package/src/rule-packs/module-boundaries/rules/no-react-in-services.ts +111 -0
- package/src/rule-packs/nextjs/index.ts +32 -0
- package/src/rule-packs/nextjs/rules/await-dynamic-request-apis.ts +65 -0
- package/src/rule-packs/nextjs/rules/error-boundary-require-use-client.ts +38 -0
- package/src/rule-packs/nextjs/rules/mutation-should-revalidate-cache.ts +152 -0
- package/src/rule-packs/nextjs/rules/no-html-img-element.ts +45 -0
- package/src/rule-packs/nextjs/rules/no-internal-api-fetch.ts +126 -0
- package/src/rule-packs/nextjs/rules/no-secret-props-to-client.ts +118 -0
- package/src/rule-packs/nextjs/rules/no-sensitive-next-public-env.ts +72 -0
- package/src/rule-packs/nextjs/rules/prefer-lazy-use-state-init.ts +85 -0
- package/src/rule-packs/nextjs/rules/server-action-requires-authz-and-validation.ts +178 -0
- package/src/rule-packs/nextjs/rules/server-only-modules-import-server-only.ts +87 -0
- package/src/rule-packs/nextjs/utils.ts +18 -0
- package/src/rule-packs/react-component-architecture/index.ts +18 -0
- package/src/rule-packs/react-component-architecture/rules/dangerous-html-requires-sanitize.ts +83 -0
- package/src/rule-packs/react-component-architecture/rules/no-anonymous-useEffect.ts +61 -0
- package/src/rule-packs/react-component-architecture/rules/no-component-invocation.ts +55 -0
- package/src/rule-packs/react-component-architecture/rules/no-derived-state-in-effect.ts +204 -0
- package/src/rule-packs/react-component-architecture/rules/no-nested-component.ts +152 -0
- package/src/rule-packs/react-component-architecture/rules/no-react-fc.ts +57 -0
- package/src/rule-packs/rule-catalog.types.ts +21 -0
- package/src/rule-packs/rule-metadata.ts +163 -0
- package/src/rule-packs/runtime-boundaries/index.ts +33 -0
- package/src/rule-packs/runtime-boundaries/rules/no-prototype-polluting-merge.ts +113 -0
- package/src/rule-packs/runtime-boundaries/rules/no-user-controlled-fetch-url.ts +69 -0
- package/src/rule-packs/runtime-boundaries/rules/no-user-controlled-redirect.ts +79 -0
- package/src/rule-packs/runtime-boundaries/rules/upload-must-set-limits.ts +126 -0
- package/src/rule-packs/runtime-boundaries/rules/webhook-must-verify-signature-before-parse.ts +87 -0
- package/src/rule-packs/security/index.ts +35 -0
- package/src/rule-packs/security/rules/catch-must-handle.ts +126 -0
- package/src/rule-packs/security/rules/no-auth-token-in-storage.ts +107 -0
- package/src/rule-packs/security/rules/no-child-process-exec.ts +72 -0
- package/src/rule-packs/security/rules/no-dynamic-regexp.ts +56 -0
- package/src/rule-packs/security/rules/no-inner-html-assignment.ts +42 -0
- package/src/rule-packs/security/rules/no-spawn-with-shell.ts +106 -0
- package/src/rule-packs/structured-logging/index.ts +6 -0
- package/src/rule-packs/structured-logging/rules/caught-error-log-requires-cause.ts +234 -0
- package/src/rule-packs/structured-logging/rules/logger-not-console.ts +146 -0
- package/src/rule-packs/test-conventions/index.ts +9 -0
- package/src/rule-packs/test-conventions/rules/fake-timers-must-be-restored.ts +143 -0
- package/src/rule-packs/test-conventions/rules/no-conditional-expect.ts +77 -0
- package/src/rule-packs/test-conventions/rules/no-real-network-in-unit-tests.ts +174 -0
- package/src/rule-packs/typescript-core/index.ts +30 -0
- package/src/rule-packs/typescript-core/rules/exported-functions-require-return-type.ts +74 -0
- package/src/rule-packs/typescript-core/rules/fetch-must-check-ok.ts +106 -0
- package/src/rule-packs/typescript-core/rules/json-parse-must-validate.ts +97 -0
- package/src/rule-packs/typescript-core/rules/no-unsafe-boundary-cast.ts +70 -0
- package/src/stack-detection/packs.ts +57 -0
- package/strict.web.eslint.config.mjs +32 -1
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
import type { ProfileId } from "../rule-packs/rule-catalog.types";
|
|
2
|
+
|
|
3
|
+
export type { ProfileId };
|
|
4
|
+
|
|
5
|
+
export interface IProfileDefinition {
|
|
6
|
+
readonly id: ProfileId;
|
|
7
|
+
readonly label: string;
|
|
8
|
+
readonly description: string;
|
|
9
|
+
/** Extra packs to include beyond stack detection (deduped at resolve time). */
|
|
10
|
+
readonly extraPacks?: readonly string[];
|
|
11
|
+
/** Rule severity overrides keyed by bare rule name. */
|
|
12
|
+
readonly ruleOverrides?: Readonly<Record<string, "error" | "warn" | "off">>;
|
|
13
|
+
/** Meta-rule ids to elevate to error in this profile. */
|
|
14
|
+
readonly metaRulesAtError?: readonly string[];
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/** Architecture-tier rules enabled only in the opinionated profile. */
|
|
18
|
+
export const ARCHITECTURE_RULES = [
|
|
19
|
+
"component-folder-structure",
|
|
20
|
+
"no-state-in-component-body",
|
|
21
|
+
"no-inline-jsx-functions",
|
|
22
|
+
"index-must-reexport-default",
|
|
23
|
+
"forwardref-display-name",
|
|
24
|
+
"max-hooks-per-file",
|
|
25
|
+
] as const;
|
|
26
|
+
|
|
27
|
+
const architectureOffOverrides = Object.fromEntries(
|
|
28
|
+
ARCHITECTURE_RULES.map((rule) => [rule, "off" as const])
|
|
29
|
+
);
|
|
30
|
+
|
|
31
|
+
export const PROFILE_DEFINITIONS: Readonly<
|
|
32
|
+
Record<ProfileId, IProfileDefinition>
|
|
33
|
+
> = {
|
|
34
|
+
recommended: {
|
|
35
|
+
id: "recommended",
|
|
36
|
+
label: "Recommended",
|
|
37
|
+
description:
|
|
38
|
+
"Safety + framework packs from stack detection; architecture opinions off by default.",
|
|
39
|
+
ruleOverrides: {
|
|
40
|
+
...architectureOffOverrides,
|
|
41
|
+
"prefer-early-return": "warn",
|
|
42
|
+
},
|
|
43
|
+
},
|
|
44
|
+
strict: {
|
|
45
|
+
id: "strict",
|
|
46
|
+
label: "Strict",
|
|
47
|
+
description:
|
|
48
|
+
"Recommended plus CI/supply-chain meta-rules at error and type-aware async rules.",
|
|
49
|
+
extraPacks: ["typescript-core"],
|
|
50
|
+
ruleOverrides: {
|
|
51
|
+
...architectureOffOverrides,
|
|
52
|
+
"prefer-early-return": "warn",
|
|
53
|
+
},
|
|
54
|
+
metaRulesAtError: [
|
|
55
|
+
"workflow-permissions-explicit",
|
|
56
|
+
"lockfile-required",
|
|
57
|
+
"single-package-manager",
|
|
58
|
+
],
|
|
59
|
+
},
|
|
60
|
+
security: {
|
|
61
|
+
id: "security",
|
|
62
|
+
label: "Security",
|
|
63
|
+
description:
|
|
64
|
+
"Recommended plus runtime-boundaries and experimental authorization heuristics.",
|
|
65
|
+
extraPacks: ["runtime-boundaries", "authorization"],
|
|
66
|
+
ruleOverrides: architectureOffOverrides,
|
|
67
|
+
},
|
|
68
|
+
frontend: {
|
|
69
|
+
id: "frontend",
|
|
70
|
+
label: "Frontend",
|
|
71
|
+
description: "Recommended with React/Next architecture rules at warn.",
|
|
72
|
+
ruleOverrides: {
|
|
73
|
+
"no-html-img-element": "warn",
|
|
74
|
+
"no-anonymous-useEffect": "warn",
|
|
75
|
+
"no-derived-state-in-effect": "warn",
|
|
76
|
+
...architectureOffOverrides,
|
|
77
|
+
},
|
|
78
|
+
},
|
|
79
|
+
backend: {
|
|
80
|
+
id: "backend",
|
|
81
|
+
label: "Backend",
|
|
82
|
+
description:
|
|
83
|
+
"Recommended; stack detection adds Fastify/Elysia/Drizzle/BullMQ packs.",
|
|
84
|
+
ruleOverrides: architectureOffOverrides,
|
|
85
|
+
},
|
|
86
|
+
opinionated: {
|
|
87
|
+
id: "opinionated",
|
|
88
|
+
label: "Opinionated",
|
|
89
|
+
description:
|
|
90
|
+
"Full house-style architecture rules including component folder structure.",
|
|
91
|
+
ruleOverrides: {
|
|
92
|
+
"component-folder-structure": "error",
|
|
93
|
+
"no-state-in-component-body": "error",
|
|
94
|
+
"no-inline-jsx-functions": "warn",
|
|
95
|
+
"index-must-reexport-default": "error",
|
|
96
|
+
"forwardref-display-name": "error",
|
|
97
|
+
"max-hooks-per-file": "warn",
|
|
98
|
+
"prefer-early-return": "error",
|
|
99
|
+
},
|
|
100
|
+
},
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
export const DEFAULT_PROFILE: ProfileId = "recommended";
|
|
104
|
+
|
|
105
|
+
export function isProfileId(value: string): value is ProfileId {
|
|
106
|
+
return value in PROFILE_DEFINITIONS;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/** Merge profile overrides with user config overrides (user wins). */
|
|
110
|
+
export function resolveProfileRuleOverrides(
|
|
111
|
+
profileId: ProfileId | undefined
|
|
112
|
+
): Record<string, "error" | "warn" | "off"> {
|
|
113
|
+
const id = profileId ?? DEFAULT_PROFILE;
|
|
114
|
+
const profile = PROFILE_DEFINITIONS[id];
|
|
115
|
+
|
|
116
|
+
return { ...(profile.ruleOverrides ?? {}) };
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export function resolveProfileExtraPacks(
|
|
120
|
+
profileId: ProfileId | undefined
|
|
121
|
+
): readonly string[] {
|
|
122
|
+
const id = profileId ?? DEFAULT_PROFILE;
|
|
123
|
+
const profile = PROFILE_DEFINITIONS[id];
|
|
124
|
+
|
|
125
|
+
return profile.extraPacks ?? [];
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
export function resolveProfileMetaRuleOverrides(
|
|
129
|
+
profileId: ProfileId | undefined
|
|
130
|
+
): Record<string, "error"> {
|
|
131
|
+
const id = profileId ?? DEFAULT_PROFILE;
|
|
132
|
+
const profile = PROFILE_DEFINITIONS[id];
|
|
133
|
+
const result: Record<string, "error"> = {};
|
|
134
|
+
|
|
135
|
+
for (const ruleId of profile.metaRulesAtError ?? []) {
|
|
136
|
+
result[ruleId] = "error";
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
return result;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
export function mergeRuleOverrides(
|
|
143
|
+
profileId: ProfileId | undefined,
|
|
144
|
+
userOverrides: Readonly<Record<string, "error" | "warn" | "off">>
|
|
145
|
+
): Record<string, "error" | "warn" | "off"> {
|
|
146
|
+
return {
|
|
147
|
+
...resolveProfileRuleOverrides(profileId),
|
|
148
|
+
...userOverrides,
|
|
149
|
+
};
|
|
150
|
+
}
|
|
@@ -3,6 +3,14 @@ import { isRecord } from "../lib/guards";
|
|
|
3
3
|
import { PACK_REGISTRY } from "../stack-detection";
|
|
4
4
|
import { parseMcpServers, type IMcpServerConfig } from "../mcp";
|
|
5
5
|
import { parsePlugins, type IExternalPlugin } from "./external-plugins";
|
|
6
|
+
import {
|
|
7
|
+
DEFAULT_PROFILE,
|
|
8
|
+
isProfileId,
|
|
9
|
+
mergeRuleOverrides,
|
|
10
|
+
resolveProfileExtraPacks,
|
|
11
|
+
resolveProfileMetaRuleOverrides,
|
|
12
|
+
type ProfileId,
|
|
13
|
+
} from "./profiles";
|
|
6
14
|
|
|
7
15
|
/**
|
|
8
16
|
* User-defined configuration from tsforge.config.json
|
|
@@ -10,6 +18,9 @@ import { parsePlugins, type IExternalPlugin } from "./external-plugins";
|
|
|
10
18
|
* include/exclude packs, and tune rule severities (eslint packs + meta-rules).
|
|
11
19
|
*/
|
|
12
20
|
export interface ITsforgeProjectConfig {
|
|
21
|
+
/** Rule profile: recommended (default), strict, security, frontend, backend, opinionated. */
|
|
22
|
+
readonly profile?: ProfileId;
|
|
23
|
+
|
|
13
24
|
/** Force-enable a stack by name (skip detection heuristics, force-add its packs). */
|
|
14
25
|
readonly stack?: string;
|
|
15
26
|
|
|
@@ -100,6 +111,31 @@ function warnUnknownPackInInclude(packId: string): void {
|
|
|
100
111
|
warnConfig(msg);
|
|
101
112
|
}
|
|
102
113
|
|
|
114
|
+
function warnInvalidProfile(profileValue: unknown): void {
|
|
115
|
+
warnConfig(
|
|
116
|
+
`tsforge.config.json: "profile" must be one of recommended, strict, security, frontend, backend, opinionated — got "${String(profileValue)}"`
|
|
117
|
+
);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/** Validate and extract profile field. */
|
|
121
|
+
function validateProfile(parsed: unknown): ProfileId | undefined {
|
|
122
|
+
if (typeof parsed !== "string") {
|
|
123
|
+
if (parsed !== undefined) {
|
|
124
|
+
warnInvalidProfile(parsed);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
return undefined;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
if (isProfileId(parsed)) {
|
|
131
|
+
return parsed;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
warnInvalidProfile(parsed);
|
|
135
|
+
|
|
136
|
+
return undefined;
|
|
137
|
+
}
|
|
138
|
+
|
|
103
139
|
/** Validate and extract stack field. */
|
|
104
140
|
function validateStack(parsed: unknown): string | undefined {
|
|
105
141
|
if (typeof parsed === "string") {
|
|
@@ -181,6 +217,7 @@ function buildConfigFields(
|
|
|
181
217
|
parsed: Record<string, unknown>
|
|
182
218
|
): ITsforgeProjectConfig {
|
|
183
219
|
const configFields: {
|
|
220
|
+
profile?: ProfileId;
|
|
184
221
|
stack?: string;
|
|
185
222
|
packs?: { include?: readonly string[]; exclude?: readonly string[] };
|
|
186
223
|
rules?: Record<string, "error" | "warn" | "off">;
|
|
@@ -188,6 +225,14 @@ function buildConfigFields(
|
|
|
188
225
|
plugins?: readonly IExternalPlugin[];
|
|
189
226
|
} = {};
|
|
190
227
|
|
|
228
|
+
if (parsed.profile !== undefined) {
|
|
229
|
+
const profile = validateProfile(parsed.profile);
|
|
230
|
+
|
|
231
|
+
if (profile !== undefined) {
|
|
232
|
+
configFields.profile = profile;
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
191
236
|
if (parsed.stack !== undefined) {
|
|
192
237
|
const stack = validateStack(parsed.stack);
|
|
193
238
|
|
|
@@ -290,6 +335,13 @@ export function resolveActivePacks(
|
|
|
290
335
|
packs.add(config.stack);
|
|
291
336
|
}
|
|
292
337
|
|
|
338
|
+
// Profile extra packs (runtime-boundaries, authorization, typescript-core, …)
|
|
339
|
+
for (const packId of resolveProfileExtraPacks(config.profile)) {
|
|
340
|
+
if (packId in PACK_REGISTRY) {
|
|
341
|
+
packs.add(packId);
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
|
|
293
345
|
// Include: add packs (unknown ids are kept out of the registry lookup warning only)
|
|
294
346
|
for (const packId of config.packs?.include ?? []) {
|
|
295
347
|
if (packId.length === 0) {
|
|
@@ -324,11 +376,9 @@ function isSeverityOverride(value: unknown): value is "error" | "warn" | "off" {
|
|
|
324
376
|
export function normalizeRuleOverrides(
|
|
325
377
|
config: ITsforgeProjectConfig
|
|
326
378
|
): Record<string, "error" | "warn" | "off"> {
|
|
327
|
-
const
|
|
379
|
+
const userOverrides: Record<string, "error" | "warn" | "off"> = {};
|
|
328
380
|
|
|
329
381
|
for (const [key, severity] of Object.entries(config.rules ?? {})) {
|
|
330
|
-
// Runtime data can violate the declared union (hand-built configs in tests,
|
|
331
|
-
// partially validated JSON) — re-check before trusting it.
|
|
332
382
|
if (!isSeverityOverride(severity)) {
|
|
333
383
|
continue;
|
|
334
384
|
}
|
|
@@ -336,9 +386,18 @@ export function normalizeRuleOverrides(
|
|
|
336
386
|
const bareKey = key.startsWith("tsforge/") ? key.slice(8) : key;
|
|
337
387
|
|
|
338
388
|
if (bareKey.length > 0) {
|
|
339
|
-
|
|
389
|
+
userOverrides[bareKey] = severity;
|
|
340
390
|
}
|
|
341
391
|
}
|
|
342
392
|
|
|
343
|
-
return
|
|
393
|
+
return {
|
|
394
|
+
...resolveProfileMetaRuleOverrides(config.profile),
|
|
395
|
+
...mergeRuleOverrides(config.profile, userOverrides),
|
|
396
|
+
};
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
export function resolveProjectProfile(
|
|
400
|
+
config: ITsforgeProjectConfig
|
|
401
|
+
): ProfileId {
|
|
402
|
+
return config.profile ?? DEFAULT_PROFILE;
|
|
344
403
|
}
|
package/src/detect-gate.ts
CHANGED
|
@@ -63,6 +63,11 @@ const ESLINT_BIN = resolveToolBin("eslint");
|
|
|
63
63
|
const TSC_BIN = resolveToolBin("tsc");
|
|
64
64
|
const PRETTIER_BIN = resolveToolBin("prettier");
|
|
65
65
|
const STRICT_CONFIG = join(import.meta.dir, "..", "strict.eslint.config.mjs");
|
|
66
|
+
const TYPE_AWARE_CONFIG = join(
|
|
67
|
+
import.meta.dir,
|
|
68
|
+
"..",
|
|
69
|
+
"strict.type-aware.eslint.config.mjs"
|
|
70
|
+
);
|
|
66
71
|
const BROWSER_CHECK = join(
|
|
67
72
|
import.meta.dir,
|
|
68
73
|
"..",
|
|
@@ -86,6 +91,8 @@ const STRICT_TSCONFIG = `{
|
|
|
86
91
|
"noUncheckedIndexedAccess": true,
|
|
87
92
|
"noImplicitOverride": true,
|
|
88
93
|
"noFallthroughCasesInSwitch": true,
|
|
94
|
+
"useUnknownInCatchVariables": true,
|
|
95
|
+
"erasableSyntaxOnly": true,
|
|
89
96
|
"esModuleInterop": true,
|
|
90
97
|
"forceConsistentCasingInFileNames": true,
|
|
91
98
|
"skipLibCheck": true,
|
|
@@ -108,6 +115,8 @@ const STRICT_TSCONFIG_OVERRIDE = `{
|
|
|
108
115
|
"noUncheckedIndexedAccess": true,
|
|
109
116
|
"noImplicitOverride": true,
|
|
110
117
|
"noFallthroughCasesInSwitch": true,
|
|
118
|
+
"useUnknownInCatchVariables": true,
|
|
119
|
+
"erasableSyntaxOnly": true,
|
|
111
120
|
"skipLibCheck": true,
|
|
112
121
|
"noEmit": true
|
|
113
122
|
}
|
|
@@ -458,7 +467,8 @@ export function prettierWriteCommand(): string {
|
|
|
458
467
|
export async function buildGate(
|
|
459
468
|
cwd: string,
|
|
460
469
|
packs?: readonly string[],
|
|
461
|
-
ruleOverrides?: Readonly<Record<string, "error" | "warn" | "off"
|
|
470
|
+
ruleOverrides?: Readonly<Record<string, "error" | "warn" | "off">>,
|
|
471
|
+
options?: { enableTypeAware?: boolean }
|
|
462
472
|
): Promise<IGate> {
|
|
463
473
|
const parts: string[] = [];
|
|
464
474
|
const labels: string[] = [];
|
|
@@ -475,6 +485,15 @@ export async function buildGate(
|
|
|
475
485
|
parts.push(lint.command);
|
|
476
486
|
labels.push(lint.label);
|
|
477
487
|
|
|
488
|
+
if (options?.enableTypeAware === true) {
|
|
489
|
+
const typeAware = await typeAwareLintPart(cwd);
|
|
490
|
+
|
|
491
|
+
if (typeAware !== null) {
|
|
492
|
+
parts.push(typeAware.command);
|
|
493
|
+
labels.push(typeAware.label);
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
|
|
478
497
|
return { command: parts.join(" && "), label: labels.join(" + ") };
|
|
479
498
|
}
|
|
480
499
|
|
|
@@ -538,3 +557,17 @@ function lintPart(
|
|
|
538
557
|
label: "strict TypeScript (tsforge)",
|
|
539
558
|
};
|
|
540
559
|
}
|
|
560
|
+
|
|
561
|
+
/** Optional type-aware async rules — only when target has tsconfig.json. */
|
|
562
|
+
async function typeAwareLintPart(cwd: string): Promise<IGate | null> {
|
|
563
|
+
const hasTsconfig = await Bun.file(join(cwd, "tsconfig.json")).exists();
|
|
564
|
+
|
|
565
|
+
if (!hasTsconfig) {
|
|
566
|
+
return null;
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
return {
|
|
570
|
+
command: `bun "${ESLINT_BIN}" --no-config-lookup -c "${TYPE_AWARE_CONFIG}" --format json .`,
|
|
571
|
+
label: "type-aware async (tsforge)",
|
|
572
|
+
};
|
|
573
|
+
}
|
|
@@ -8,6 +8,10 @@ export interface IChatMessage {
|
|
|
8
8
|
toolCalls?: IToolCall[];
|
|
9
9
|
/** Tool messages only: the id of the call this message is the result of. */
|
|
10
10
|
toolCallId?: string;
|
|
11
|
+
/** Assistant only: the model's chain-of-thought. DeepSeek's thinking mode
|
|
12
|
+
* REQUIRES the prior turn's `reasoning_content` to be replayed, so it's kept
|
|
13
|
+
* on the message and re-sent (for the deepseek reasoning style). */
|
|
14
|
+
reasoningContent?: string;
|
|
11
15
|
}
|
|
12
16
|
|
|
13
17
|
/** A parsed tool call from the model (name + decoded JSON arguments). */
|
|
@@ -29,6 +33,10 @@ export interface ITokenUsage {
|
|
|
29
33
|
export interface IModelResponse {
|
|
30
34
|
content: string;
|
|
31
35
|
toolCalls: IToolCall[];
|
|
36
|
+
/** The model's chain-of-thought (`reasoning`/`reasoning_content`), when it
|
|
37
|
+
* produced any. Stored on the assistant message for providers (DeepSeek) that
|
|
38
|
+
* require it replayed on the next turn. */
|
|
39
|
+
reasoning?: string;
|
|
32
40
|
/** Server-reported token usage for this call, when available. `promptTokens`
|
|
33
41
|
* is the full context the model just saw — what auto-compaction will watch. */
|
|
34
42
|
usage?: ITokenUsage;
|
package/src/inference/request.ts
CHANGED
|
@@ -99,9 +99,13 @@ export function buildRequestBody(
|
|
|
99
99
|
const omitTemperature =
|
|
100
100
|
style(cfg) === "openai" || opts.temperature === undefined;
|
|
101
101
|
|
|
102
|
+
// DeepSeek's thinking mode requires each prior assistant turn's
|
|
103
|
+
// `reasoning_content` replayed; other providers don't want it.
|
|
104
|
+
const includeReasoning = style(cfg) === "deepseek";
|
|
105
|
+
|
|
102
106
|
return {
|
|
103
107
|
model: cfg.model,
|
|
104
|
-
messages: messages.map(toWire),
|
|
108
|
+
messages: messages.map((m) => toWire(m, includeReasoning)),
|
|
105
109
|
...tokenCapField(cfg),
|
|
106
110
|
...(omitTemperature ? {} : { temperature: opts.temperature }),
|
|
107
111
|
...(cfg.repetitionPenalty === undefined
|
package/src/inference/stream.ts
CHANGED
|
@@ -35,6 +35,7 @@ export async function streamResponse(
|
|
|
35
35
|
calls: new Map(),
|
|
36
36
|
guard: new StreamGuard(),
|
|
37
37
|
content: "",
|
|
38
|
+
reasoning: "",
|
|
38
39
|
ttsr: ttsrManager,
|
|
39
40
|
ttsrFired: null,
|
|
40
41
|
};
|
|
@@ -88,6 +89,7 @@ interface IStreamAcc {
|
|
|
88
89
|
calls: Map<number, IStreamingCall>;
|
|
89
90
|
guard: StreamGuard;
|
|
90
91
|
content: string;
|
|
92
|
+
reasoning: string;
|
|
91
93
|
usage?: ITokenUsage;
|
|
92
94
|
ttsr?: ITtsrWatcher;
|
|
93
95
|
ttsrFired: { readonly name: string; readonly guidance: string } | null;
|
|
@@ -137,6 +139,7 @@ function consumeLines(
|
|
|
137
139
|
// less, not by hiding it from the log.)
|
|
138
140
|
if (delta.reasoning !== undefined && delta.reasoning.length > 0) {
|
|
139
141
|
onToken(delta.reasoning, "reasoning");
|
|
142
|
+
acc.reasoning += delta.reasoning;
|
|
140
143
|
|
|
141
144
|
if (acc.guard.observe(delta.reasoning, "reasoning")) {
|
|
142
145
|
return true;
|
|
@@ -163,6 +166,8 @@ function consumeLines(
|
|
|
163
166
|
|
|
164
167
|
function assemble(acc: IStreamAcc, degenerated: boolean): IModelResponse {
|
|
165
168
|
const usage = acc.usage === undefined ? {} : { usage: acc.usage };
|
|
169
|
+
const reasoning =
|
|
170
|
+
acc.reasoning.length > 0 ? { reasoning: acc.reasoning } : {};
|
|
166
171
|
const toolCalls: IToolCall[] = [...acc.calls.values()].map((c) => ({
|
|
167
172
|
id: c.id,
|
|
168
173
|
name: c.name,
|
|
@@ -181,8 +186,21 @@ function assemble(acc: IStreamAcc, degenerated: boolean): IModelResponse {
|
|
|
181
186
|
|
|
182
187
|
if (toolCalls.length > 0) {
|
|
183
188
|
return degenerated
|
|
184
|
-
? {
|
|
185
|
-
|
|
189
|
+
? {
|
|
190
|
+
content: acc.content,
|
|
191
|
+
toolCalls,
|
|
192
|
+
degenerated,
|
|
193
|
+
...reasoning,
|
|
194
|
+
...ttsrFired,
|
|
195
|
+
...usage,
|
|
196
|
+
}
|
|
197
|
+
: {
|
|
198
|
+
content: acc.content,
|
|
199
|
+
toolCalls,
|
|
200
|
+
...reasoning,
|
|
201
|
+
...ttsrFired,
|
|
202
|
+
...usage,
|
|
203
|
+
};
|
|
186
204
|
}
|
|
187
205
|
|
|
188
206
|
const salvaged = salvageToolCalls(acc.content);
|
|
@@ -192,6 +210,7 @@ function assemble(acc: IStreamAcc, degenerated: boolean): IModelResponse {
|
|
|
192
210
|
toolCalls: salvaged,
|
|
193
211
|
salvaged: salvaged.length,
|
|
194
212
|
...(degenerated ? { degenerated } : {}),
|
|
213
|
+
...reasoning,
|
|
195
214
|
...ttsrFired,
|
|
196
215
|
...usage,
|
|
197
216
|
};
|
package/src/inference/wire.ts
CHANGED
|
Binary file
|
|
@@ -11,6 +11,30 @@ export const META_RULE_DOCS: Record<string, string> = {
|
|
|
11
11
|
"no-overlapping-libs":
|
|
12
12
|
"Remove duplicate or conflicting library versions from the dependency tree; only one canonical version per library is allowed.",
|
|
13
13
|
|
|
14
|
+
"fastify-security-plugins":
|
|
15
|
+
"Add @fastify/helmet, @fastify/cors, and @fastify/rate-limit when using fastify in production.",
|
|
16
|
+
|
|
17
|
+
"lockfile-required":
|
|
18
|
+
"Commit the lockfile for your package manager (package-lock.json, yarn.lock, pnpm-lock.yaml, or bun.lockb) and keep it in sync with package.json.",
|
|
19
|
+
|
|
20
|
+
"single-package-manager":
|
|
21
|
+
"Remove extra lockfiles — use one package manager and delete lockfiles from other tools.",
|
|
22
|
+
|
|
23
|
+
"package-manager-field-required":
|
|
24
|
+
'Add a "packageManager" field to package.json (e.g. "bun@1.3.14") so installs are reproducible across environments.',
|
|
25
|
+
|
|
26
|
+
"no-git-or-tarball-dependencies":
|
|
27
|
+
"Replace git+, git:, or HTTP tarball dependency URLs with registry versions from npm.",
|
|
28
|
+
|
|
29
|
+
"dependency-overrides-require-comment":
|
|
30
|
+
"Add a comment next to overrides/resolutions in package.json explaining why each override is needed.",
|
|
31
|
+
|
|
32
|
+
"production-must-not-use-drizzle-push":
|
|
33
|
+
"Replace `drizzle-kit push` in scripts and CI with checked-in SQL migrations and `drizzle-kit migrate`.",
|
|
34
|
+
|
|
35
|
+
"migrations-must-be-checked-in":
|
|
36
|
+
"Add a drizzle/ or migrations/ folder with generated SQL migration files when using Drizzle ORM.",
|
|
37
|
+
|
|
14
38
|
// Source text
|
|
15
39
|
"no-eslint-disable-comments":
|
|
16
40
|
"Remove `// eslint-disable` comments — they hide warnings. Fix the underlying violation or refactor the code.",
|
|
@@ -25,6 +49,18 @@ export const META_RULE_DOCS: Record<string, string> = {
|
|
|
25
49
|
"tsconfig-strict":
|
|
26
50
|
"Enable all strict mode flags in tsconfig.json (strict: true or all strict flags individually).",
|
|
27
51
|
|
|
52
|
+
"tsconfig-recommended-flags":
|
|
53
|
+
"Enable useUnknownInCatchVariables, erasableSyntaxOnly, exactOptionalPropertyTypes, verbatimModuleSyntax, and noPropertyAccessFromIndexSignature in tsconfig.json compilerOptions.",
|
|
54
|
+
|
|
55
|
+
"next-proxy-over-middleware":
|
|
56
|
+
"Migrate middleware.ts to proxy.ts for Next.js 16 early request interception.",
|
|
57
|
+
|
|
58
|
+
"next-instrumentation-present":
|
|
59
|
+
"Add instrumentation.ts with registerOTel for OpenTelemetry tracing in Next.js apps.",
|
|
60
|
+
|
|
61
|
+
"next-image-remote-patterns-no-wildcards":
|
|
62
|
+
"Remove `**` hostname wildcards from next.config remotePatterns — allowlist specific image hostnames.",
|
|
63
|
+
|
|
28
64
|
// Testing
|
|
29
65
|
"test-sibling-required":
|
|
30
66
|
"Add a test file for each source file; follow naming conventions (foo.ts → foo.test.ts or foo.spec.ts).",
|
|
@@ -38,4 +74,16 @@ export const META_RULE_DOCS: Record<string, string> = {
|
|
|
38
74
|
|
|
39
75
|
"workflow-timeout-required":
|
|
40
76
|
"Add a timeout-minutes setting to each GitHub Actions job to prevent hanging workflows.",
|
|
77
|
+
|
|
78
|
+
"workflow-permissions-explicit":
|
|
79
|
+
"Add a top-level permissions: block or job-level permissions to every GitHub Actions workflow.",
|
|
80
|
+
|
|
81
|
+
"workflow-permissions-least-privilege":
|
|
82
|
+
"Avoid workflow-level contents: write or id-token: write — scope write permissions to the job that needs them.",
|
|
83
|
+
|
|
84
|
+
"no-pull-request-target-untrusted-checkout":
|
|
85
|
+
"Do not combine pull_request_target with checkout of the PR head ref — use pull_request or checkout the base ref.",
|
|
86
|
+
|
|
87
|
+
"no-github-context-in-shell":
|
|
88
|
+
"Pass github.event values through env: instead of interpolating them directly in run: shell scripts.",
|
|
41
89
|
};
|
|
@@ -154,6 +154,156 @@ const RULE_DOCS: Record<string, IRuleDoc> = {
|
|
|
154
154
|
bad: "<button onClick={() => doThing(id)} />",
|
|
155
155
|
good: "const onClickRow = useCallback(() => doThing(id), [id]); <button onClick={onClickRow} />",
|
|
156
156
|
},
|
|
157
|
+
"tsforge/no-throw-literal": {
|
|
158
|
+
what: "Throw `Error` instances, not string or number literals.",
|
|
159
|
+
bad: "throw 'Unauthorized';",
|
|
160
|
+
good: "throw new Error('Unauthorized');",
|
|
161
|
+
},
|
|
162
|
+
"tsforge/no-react-fc": {
|
|
163
|
+
what: "Do not use React.FC — type props on the function parameter.",
|
|
164
|
+
bad: "const Button: React.FC<IButtonProps> = ({ onClick }) => <button onClick={onClick} />;",
|
|
165
|
+
good: "function Button({ onClick }: IButtonProps) { return <button onClick={onClick} />; }",
|
|
166
|
+
},
|
|
167
|
+
"tsforge/no-component-invocation": {
|
|
168
|
+
what: "Render components as JSX, not function calls.",
|
|
169
|
+
bad: "<div>{Header()}</div>",
|
|
170
|
+
good: "<div><Header /></div>",
|
|
171
|
+
},
|
|
172
|
+
"tsforge/no-nested-component": {
|
|
173
|
+
what: "Declare components at module scope, not inside another component.",
|
|
174
|
+
bad: "function App() { function Inner() { return <span />; } return <Inner />; }",
|
|
175
|
+
good: "function Inner() { return <span />; } function App() { return <Inner />; }",
|
|
176
|
+
},
|
|
177
|
+
"tsforge/dangerous-html-requires-sanitize": {
|
|
178
|
+
what: "Sanitize HTML before dangerouslySetInnerHTML — import DOMPurify.",
|
|
179
|
+
bad: "<div dangerouslySetInnerHTML={{ __html: rawHtml }} />",
|
|
180
|
+
good: "import DOMPurify from 'isomorphic-dompurify'; <div dangerouslySetInnerHTML={{ __html: DOMPurify.sanitize(rawHtml) }} />",
|
|
181
|
+
},
|
|
182
|
+
"tsforge/no-child-process-exec": {
|
|
183
|
+
what: "Do not use child_process.exec/execSync — shell execution enables command injection.",
|
|
184
|
+
bad: "import { exec } from 'child_process'; exec(`rm -rf ${dir}`);",
|
|
185
|
+
good: "import { execFile } from 'child_process'; execFile('rm', ['-rf', dir], callback);",
|
|
186
|
+
},
|
|
187
|
+
"tsforge/no-spawn-with-shell": {
|
|
188
|
+
what: "Do not pass `{ shell: true }` to spawn/spawnSync.",
|
|
189
|
+
bad: "spawn('sh', ['-c', cmd], { shell: true });",
|
|
190
|
+
good: "spawn('node', ['script.js', arg]);",
|
|
191
|
+
},
|
|
192
|
+
"tsforge/no-dynamic-regexp": {
|
|
193
|
+
what: "Do not build RegExp from runtime input — ReDoS risk.",
|
|
194
|
+
bad: "const re = new RegExp(userPattern);",
|
|
195
|
+
good: "const re = /^fixed-pattern$/;",
|
|
196
|
+
},
|
|
197
|
+
"tsforge/no-inner-html-assignment": {
|
|
198
|
+
what: "Do not assign to innerHTML — XSS risk in vanilla DOM code.",
|
|
199
|
+
bad: "el.innerHTML = userHtml;",
|
|
200
|
+
good: "el.textContent = userText;",
|
|
201
|
+
},
|
|
202
|
+
"tsforge/catch-must-handle": {
|
|
203
|
+
what: "Catch blocks must log, rethrow, or return a typed error — not silently mask failure.",
|
|
204
|
+
bad: "catch (e) { return null; }",
|
|
205
|
+
good: "catch (e) { logger.error(e); throw e; }",
|
|
206
|
+
},
|
|
207
|
+
"tsforge/no-react-in-services": {
|
|
208
|
+
what: "Service/data modules must not import React — keep business logic decoupled from UI.",
|
|
209
|
+
bad: "import { useMemo } from 'react'; // in src/services/users.ts",
|
|
210
|
+
good: "Move React hooks to components; keep services as plain TypeScript.",
|
|
211
|
+
},
|
|
212
|
+
"tsforge/no-anonymous-useEffect": {
|
|
213
|
+
what: "Pass a named function to useEffect for debuggable stack traces.",
|
|
214
|
+
bad: "useEffect(() => { sync(); }, [id]);",
|
|
215
|
+
good: "useEffect(function syncOnIdChange() { sync(); }, [id]);",
|
|
216
|
+
},
|
|
217
|
+
"tsforge/no-derived-state-in-effect": {
|
|
218
|
+
what: "Do not set local state inside useEffect when the value can be derived during render.",
|
|
219
|
+
bad: "useEffect(() => { setFullName(first + ' ' + last); }, [first, last]);",
|
|
220
|
+
good: "const fullName = `${first} ${last}`;",
|
|
221
|
+
},
|
|
222
|
+
"tsforge/no-internal-api-fetch": {
|
|
223
|
+
what: "Server Components must not fetch the app's own /api routes.",
|
|
224
|
+
bad: "await fetch('/api/users');",
|
|
225
|
+
good: "import { listUsers } from '@/services/users'; const users = await listUsers();",
|
|
226
|
+
},
|
|
227
|
+
"tsforge/await-dynamic-request-apis": {
|
|
228
|
+
what: "Await Next.js dynamic request APIs in Server Components.",
|
|
229
|
+
bad: "const jar = cookies();",
|
|
230
|
+
good: "const jar = await cookies();",
|
|
231
|
+
},
|
|
232
|
+
"tsforge/error-boundary-require-use-client": {
|
|
233
|
+
what: "error.tsx and global-error.tsx must be Client Components.",
|
|
234
|
+
bad: "export default function Error() { return <div />; }",
|
|
235
|
+
good: "'use client'; export default function Error() { return <div />; }",
|
|
236
|
+
},
|
|
237
|
+
"tsforge/no-html-img-element": {
|
|
238
|
+
what: "Prefer next/image over raw img elements.",
|
|
239
|
+
bad: "<img src='/hero.jpg' alt='hero' />",
|
|
240
|
+
good: "import Image from 'next/image'; <Image src='/hero.jpg' alt='hero' width={800} height={400} />",
|
|
241
|
+
},
|
|
242
|
+
"tsforge/no-sensitive-next-public-env": {
|
|
243
|
+
what: "NEXT_PUBLIC_* vars are exposed in the client bundle — never use for secrets.",
|
|
244
|
+
bad: "process.env.NEXT_PUBLIC_STRIPE_SECRET",
|
|
245
|
+
good: "process.env.STRIPE_SECRET_KEY // server-only, no NEXT_PUBLIC prefix",
|
|
246
|
+
},
|
|
247
|
+
"tsforge/prefer-lazy-use-state-init": {
|
|
248
|
+
what: "Use lazy useState when parsing localStorage on mount.",
|
|
249
|
+
bad: "useState(JSON.parse(localStorage.getItem('cfg') ?? '{}'))",
|
|
250
|
+
good: "useState(() => JSON.parse(localStorage.getItem('cfg') ?? '{}'))",
|
|
251
|
+
},
|
|
252
|
+
"tsforge/no-auth-token-in-storage": {
|
|
253
|
+
what: "Never store auth tokens in localStorage/sessionStorage.",
|
|
254
|
+
bad: "localStorage.setItem('auth_token', token);",
|
|
255
|
+
good: "Set an httpOnly session cookie on the server instead.",
|
|
256
|
+
},
|
|
257
|
+
"tsforge/fetch-must-check-ok": {
|
|
258
|
+
what: "Check response.ok before calling .json() on fetch results.",
|
|
259
|
+
bad: "const data = await fetch(url).then(r => r.json());",
|
|
260
|
+
good: "const res = await fetch(url); if (!res.ok) { throw new Error('fetch failed'); } const data = await res.json();",
|
|
261
|
+
},
|
|
262
|
+
"tsforge/json-parse-must-validate": {
|
|
263
|
+
what: "Parse external JSON through a schema library, not bare JSON.parse.",
|
|
264
|
+
bad: "const body = JSON.parse(raw);",
|
|
265
|
+
good: "const body = UserSchema.parse(JSON.parse(raw));",
|
|
266
|
+
},
|
|
267
|
+
"tsforge/no-unsafe-boundary-cast": {
|
|
268
|
+
what: "Do not cast untrusted parsed input with `as` — validate at the boundary.",
|
|
269
|
+
bad: "const user = (await req.json()) as IUser;",
|
|
270
|
+
good: "const user = UserSchema.parse(await req.json());",
|
|
271
|
+
},
|
|
272
|
+
"tsforge/no-user-controlled-redirect": {
|
|
273
|
+
what: "Redirect URLs must be string literals or allowlisted helpers — not user input.",
|
|
274
|
+
bad: "redirect(searchParams.get('next')!);",
|
|
275
|
+
good: "redirect('/dashboard');",
|
|
276
|
+
},
|
|
277
|
+
"tsforge/no-user-controlled-fetch-url": {
|
|
278
|
+
what: "fetch/axios URLs must be literals or pass through an allowlisted URL builder.",
|
|
279
|
+
bad: "await fetch(userSuppliedUrl);",
|
|
280
|
+
good: "await fetch('https://api.example.com/v1/status');",
|
|
281
|
+
},
|
|
282
|
+
"tsforge/no-prototype-polluting-merge": {
|
|
283
|
+
what: "Do not merge request body/query/params into objects wholesale.",
|
|
284
|
+
bad: "Object.assign(config, req.body);",
|
|
285
|
+
good: "const name = UserSchema.parse(req.body).name; config.name = name;",
|
|
286
|
+
},
|
|
287
|
+
"tsforge/server-only-modules-import-server-only": {
|
|
288
|
+
what: "Server-only modules importing DB/env must include `import 'server-only'`.",
|
|
289
|
+
bad: "import { db } from '@/lib/db'; // in lib/admin.ts",
|
|
290
|
+
good: "import 'server-only'; import { db } from '@/lib/db';",
|
|
291
|
+
},
|
|
292
|
+
"tsforge/server-action-requires-authz-and-validation": {
|
|
293
|
+
what: "Server actions must validate input and call authz before mutations.",
|
|
294
|
+
bad: "'use server'; export async function deleteUser(id: string) { await db.delete(users).where(eq(users.id, id)); }",
|
|
295
|
+
good: "'use server'; export async function deleteUser(raw: unknown) { const user = await requireUser(); const { id } = IdSchema.parse(raw); await authorize(user, id); ... }",
|
|
296
|
+
},
|
|
297
|
+
"tsforge/require-route-schema": {
|
|
298
|
+
what: "Fastify routes need a schema object with input validation.",
|
|
299
|
+
bad: "fastify.post('/users', async () => ({ ok: true }));",
|
|
300
|
+
good: "fastify.post('/users', { schema: { body: UserSchema } }, async () => ({ ok: true }));",
|
|
301
|
+
},
|
|
302
|
+
"tsforge/require-plugin-name": {
|
|
303
|
+
what: "fastify-plugin wrappers need a name option.",
|
|
304
|
+
bad: "export default fp(dbPlugin);",
|
|
305
|
+
good: "export default fp(dbPlugin, { name: 'db-connector', fastify: '5.x' });",
|
|
306
|
+
},
|
|
157
307
|
};
|
|
158
308
|
|
|
159
309
|
/**
|