@nifrajs/cli 4.0.2 → 5.0.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.
Files changed (323) hide show
  1. package/README.md +3 -0
  2. package/dist/agent-files.d.ts +107 -0
  3. package/dist/agent-files.d.ts.map +1 -0
  4. package/dist/agent-files.js +162 -0
  5. package/dist/agent-files.js.map +1 -0
  6. package/dist/app-files.d.ts +6 -0
  7. package/dist/app-files.d.ts.map +1 -1
  8. package/dist/app-files.js +27 -2
  9. package/dist/app-files.js.map +1 -1
  10. package/dist/assure-hydration.d.ts.map +1 -1
  11. package/dist/assure-hydration.js +7 -2
  12. package/dist/assure-hydration.js.map +1 -1
  13. package/dist/capabilities-tool.d.ts.map +1 -1
  14. package/dist/capabilities-tool.js +13 -3
  15. package/dist/capabilities-tool.js.map +1 -1
  16. package/dist/check-diagnostics.d.ts +20 -2
  17. package/dist/check-diagnostics.d.ts.map +1 -1
  18. package/dist/check-diagnostics.js +54 -7
  19. package/dist/check-diagnostics.js.map +1 -1
  20. package/dist/check-scan.d.ts +75 -22
  21. package/dist/check-scan.d.ts.map +1 -1
  22. package/dist/check-scan.js +1475 -310
  23. package/dist/check-scan.js.map +1 -1
  24. package/dist/check.d.ts +2 -2
  25. package/dist/check.d.ts.map +1 -1
  26. package/dist/check.js +192 -65
  27. package/dist/check.js.map +1 -1
  28. package/dist/cli.d.ts +7 -0
  29. package/dist/cli.d.ts.map +1 -1
  30. package/dist/cli.js +112 -5
  31. package/dist/cli.js.map +1 -1
  32. package/dist/client-module.d.ts +18 -0
  33. package/dist/client-module.d.ts.map +1 -0
  34. package/dist/client-module.js +49 -0
  35. package/dist/client-module.js.map +1 -0
  36. package/dist/command-catalog.d.ts +16 -5
  37. package/dist/command-catalog.d.ts.map +1 -1
  38. package/dist/command-catalog.js +133 -29
  39. package/dist/command-catalog.js.map +1 -1
  40. package/dist/component-check.d.ts +13 -0
  41. package/dist/component-check.d.ts.map +1 -0
  42. package/dist/component-check.js +59 -0
  43. package/dist/component-check.js.map +1 -0
  44. package/dist/contract-proof.d.ts +1 -1
  45. package/dist/contract-proof.d.ts.map +1 -1
  46. package/dist/contract-proof.js +2 -24
  47. package/dist/contract-proof.js.map +1 -1
  48. package/dist/contracts.d.ts +19 -0
  49. package/dist/contracts.d.ts.map +1 -1
  50. package/dist/contracts.js +50 -3
  51. package/dist/contracts.js.map +1 -1
  52. package/dist/db-tool.d.ts.map +1 -1
  53. package/dist/db-tool.js +4 -3
  54. package/dist/db-tool.js.map +1 -1
  55. package/dist/dev-bun-config.d.ts +5 -1
  56. package/dist/dev-bun-config.d.ts.map +1 -1
  57. package/dist/dev-bun-config.js +44 -11
  58. package/dist/dev-bun-config.js.map +1 -1
  59. package/dist/diagnostics.d.ts +3 -0
  60. package/dist/diagnostics.d.ts.map +1 -1
  61. package/dist/diagnostics.js.map +1 -1
  62. package/dist/diff-tool.d.ts +9 -8
  63. package/dist/diff-tool.d.ts.map +1 -1
  64. package/dist/diff-tool.js +28 -24
  65. package/dist/diff-tool.js.map +1 -1
  66. package/dist/doctor.d.ts +13 -0
  67. package/dist/doctor.d.ts.map +1 -1
  68. package/dist/doctor.js +23 -15
  69. package/dist/doctor.js.map +1 -1
  70. package/dist/env-file.d.ts +6 -0
  71. package/dist/env-file.d.ts.map +1 -1
  72. package/dist/env-file.js +6 -0
  73. package/dist/env-file.js.map +1 -1
  74. package/dist/fix-recipes.d.ts.map +1 -1
  75. package/dist/fix-recipes.js +21 -2
  76. package/dist/fix-recipes.js.map +1 -1
  77. package/dist/hand-built-manifest.d.ts +27 -0
  78. package/dist/hand-built-manifest.d.ts.map +1 -0
  79. package/dist/hand-built-manifest.js +124 -0
  80. package/dist/hand-built-manifest.js.map +1 -0
  81. package/dist/hooks-report.d.ts +23 -0
  82. package/dist/hooks-report.d.ts.map +1 -0
  83. package/dist/hooks-report.js +104 -0
  84. package/dist/hooks-report.js.map +1 -0
  85. package/dist/init-agents.d.ts +2 -2
  86. package/dist/init-agents.d.ts.map +1 -1
  87. package/dist/init-agents.js +17 -11
  88. package/dist/init-agents.js.map +1 -1
  89. package/dist/internal/nifra-project.d.ts +41 -0
  90. package/dist/internal/nifra-project.d.ts.map +1 -0
  91. package/dist/internal/nifra-project.js +183 -0
  92. package/dist/internal/nifra-project.js.map +1 -0
  93. package/dist/internal/project-specifiers.d.ts +16 -0
  94. package/dist/internal/project-specifiers.d.ts.map +1 -0
  95. package/dist/internal/project-specifiers.js +396 -0
  96. package/dist/internal/project-specifiers.js.map +1 -0
  97. package/dist/internal/script-kind.d.ts.map +1 -1
  98. package/dist/internal/script-kind.js +2 -1
  99. package/dist/internal/script-kind.js.map +1 -1
  100. package/dist/internal/source-facts.d.ts +2 -0
  101. package/dist/internal/source-facts.d.ts.map +1 -1
  102. package/dist/internal/source-facts.js +11 -0
  103. package/dist/internal/source-facts.js.map +1 -1
  104. package/dist/internal/typescript-import.d.ts +1 -1
  105. package/dist/internal/typescript-import.d.ts.map +1 -1
  106. package/dist/internal/typescript-import.js +5 -3
  107. package/dist/internal/typescript-import.js.map +1 -1
  108. package/dist/introspect.d.ts +6 -6
  109. package/dist/introspect.d.ts.map +1 -1
  110. package/dist/introspect.js +55 -24
  111. package/dist/introspect.js.map +1 -1
  112. package/dist/levels-tool.d.ts.map +1 -1
  113. package/dist/levels-tool.js +93 -15
  114. package/dist/levels-tool.js.map +1 -1
  115. package/dist/load.d.ts +31 -8
  116. package/dist/load.d.ts.map +1 -1
  117. package/dist/load.js +37 -7
  118. package/dist/load.js.map +1 -1
  119. package/dist/mcp-context.d.ts +5 -5
  120. package/dist/mcp-context.d.ts.map +1 -1
  121. package/dist/mcp-context.js +2 -2
  122. package/dist/mcp-context.js.map +1 -1
  123. package/dist/mcp-exec.d.ts +12 -4
  124. package/dist/mcp-exec.d.ts.map +1 -1
  125. package/dist/mcp-exec.js +50 -13
  126. package/dist/mcp-exec.js.map +1 -1
  127. package/dist/mcp-http.js +1 -1
  128. package/dist/mcp-io.d.ts +17 -0
  129. package/dist/mcp-io.d.ts.map +1 -1
  130. package/dist/mcp-io.js +40 -0
  131. package/dist/mcp-io.js.map +1 -1
  132. package/dist/mcp-isolate.d.ts.map +1 -1
  133. package/dist/mcp-isolate.js +3 -2
  134. package/dist/mcp-isolate.js.map +1 -1
  135. package/dist/mcp-project-child.js +1 -1
  136. package/dist/mcp-project-child.js.map +1 -1
  137. package/dist/mcp-root.d.ts.map +1 -1
  138. package/dist/mcp-root.js +50 -10
  139. package/dist/mcp-root.js.map +1 -1
  140. package/dist/mcp-ws.d.ts.map +1 -1
  141. package/dist/mcp-ws.js +3 -1
  142. package/dist/mcp-ws.js.map +1 -1
  143. package/dist/mcp.d.ts.map +1 -1
  144. package/dist/mcp.js +8 -2
  145. package/dist/mcp.js.map +1 -1
  146. package/dist/migrate-layout.d.ts +7 -0
  147. package/dist/migrate-layout.d.ts.map +1 -1
  148. package/dist/migrate-layout.js +778 -64
  149. package/dist/migrate-layout.js.map +1 -1
  150. package/dist/output-schemas.d.ts +94 -0
  151. package/dist/output-schemas.d.ts.map +1 -0
  152. package/dist/output-schemas.js +864 -0
  153. package/dist/output-schemas.js.map +1 -0
  154. package/dist/port.d.ts.map +1 -1
  155. package/dist/port.js +3 -2
  156. package/dist/port.js.map +1 -1
  157. package/dist/project-facts.d.ts +15 -4
  158. package/dist/project-facts.d.ts.map +1 -1
  159. package/dist/project-facts.js +1 -0
  160. package/dist/project-facts.js.map +1 -1
  161. package/dist/recipes/5.0.0.d.ts +11 -0
  162. package/dist/recipes/5.0.0.d.ts.map +1 -0
  163. package/dist/recipes/5.0.0.js +34 -0
  164. package/dist/recipes/5.0.0.js.map +1 -0
  165. package/dist/recipes/index.d.ts +7 -0
  166. package/dist/recipes/index.d.ts.map +1 -1
  167. package/dist/recipes/index.js +2 -0
  168. package/dist/recipes/index.js.map +1 -1
  169. package/dist/replay.d.ts.map +1 -1
  170. package/dist/replay.js +12 -3
  171. package/dist/replay.js.map +1 -1
  172. package/dist/review.d.ts +3 -1
  173. package/dist/review.d.ts.map +1 -1
  174. package/dist/review.js +38 -7
  175. package/dist/review.js.map +1 -1
  176. package/dist/rules/codes.d.ts +10 -0
  177. package/dist/rules/codes.d.ts.map +1 -1
  178. package/dist/rules/codes.js +10 -0
  179. package/dist/rules/codes.js.map +1 -1
  180. package/dist/rules/comment-markers.d.ts +16 -7
  181. package/dist/rules/comment-markers.d.ts.map +1 -1
  182. package/dist/rules/comment-markers.js +58 -19
  183. package/dist/rules/comment-markers.js.map +1 -1
  184. package/dist/rules/contract-visibility.d.ts +3 -0
  185. package/dist/rules/contract-visibility.d.ts.map +1 -0
  186. package/dist/rules/contract-visibility.js +337 -0
  187. package/dist/rules/contract-visibility.js.map +1 -0
  188. package/dist/rules/data-guard.d.ts +7 -0
  189. package/dist/rules/data-guard.d.ts.map +1 -1
  190. package/dist/rules/data-guard.js +319 -37
  191. package/dist/rules/data-guard.js.map +1 -1
  192. package/dist/rules/layout.d.ts +4 -0
  193. package/dist/rules/layout.d.ts.map +1 -0
  194. package/dist/rules/layout.js +19 -0
  195. package/dist/rules/layout.js.map +1 -0
  196. package/dist/rules/legacy.d.ts.map +1 -1
  197. package/dist/rules/legacy.js +158 -43
  198. package/dist/rules/legacy.js.map +1 -1
  199. package/dist/rules/pages.d.ts +24 -0
  200. package/dist/rules/pages.d.ts.map +1 -1
  201. package/dist/rules/pages.js +221 -3
  202. package/dist/rules/pages.js.map +1 -1
  203. package/dist/rules/routes.d.ts +6 -0
  204. package/dist/rules/routes.d.ts.map +1 -1
  205. package/dist/rules/routes.js +111 -16
  206. package/dist/rules/routes.js.map +1 -1
  207. package/dist/rules/secrets.js +2 -2
  208. package/dist/rules/secrets.js.map +1 -1
  209. package/dist/rules/security.d.ts +12 -1
  210. package/dist/rules/security.d.ts.map +1 -1
  211. package/dist/rules/security.js +281 -68
  212. package/dist/rules/security.js.map +1 -1
  213. package/dist/rules/zones.d.ts +13 -0
  214. package/dist/rules/zones.d.ts.map +1 -1
  215. package/dist/rules/zones.js +192 -40
  216. package/dist/rules/zones.js.map +1 -1
  217. package/dist/scaffold.d.ts.map +1 -1
  218. package/dist/scaffold.js +6 -4
  219. package/dist/scaffold.js.map +1 -1
  220. package/dist/sdk.d.ts.map +1 -1
  221. package/dist/sdk.js +14 -5
  222. package/dist/sdk.js.map +1 -1
  223. package/dist/stylex-migrate.d.ts.map +1 -1
  224. package/dist/stylex-migrate.js +163 -7
  225. package/dist/stylex-migrate.js.map +1 -1
  226. package/dist/test-tool.d.ts.map +1 -1
  227. package/dist/test-tool.js +12 -3
  228. package/dist/test-tool.js.map +1 -1
  229. package/dist/typecheck-coverage.d.ts +20 -0
  230. package/dist/typecheck-coverage.d.ts.map +1 -0
  231. package/dist/typecheck-coverage.js +129 -0
  232. package/dist/typecheck-coverage.js.map +1 -0
  233. package/dist/upgrade.d.ts +69 -0
  234. package/dist/upgrade.d.ts.map +1 -1
  235. package/dist/upgrade.js +420 -77
  236. package/dist/upgrade.js.map +1 -1
  237. package/dist/verification-plan.d.ts.map +1 -1
  238. package/dist/verification-plan.js +11 -0
  239. package/dist/verification-plan.js.map +1 -1
  240. package/dist/version-drift.d.ts +22 -0
  241. package/dist/version-drift.d.ts.map +1 -0
  242. package/dist/version-drift.js +42 -0
  243. package/dist/version-drift.js.map +1 -0
  244. package/dist/web-app-options.d.ts +1 -1
  245. package/dist/web-app-options.d.ts.map +1 -1
  246. package/dist/web-app-options.js +1 -1
  247. package/dist/web-app-options.js.map +1 -1
  248. package/dist/work-graph.d.ts.map +1 -1
  249. package/dist/work-graph.js +20 -8
  250. package/dist/work-graph.js.map +1 -1
  251. package/docs/examples.json +54 -6
  252. package/docs/llms-full.txt +1539 -138
  253. package/docs/types.json +1456 -206
  254. package/package.json +11 -12
  255. package/src/agent-files.ts +203 -0
  256. package/src/app-files.ts +34 -4
  257. package/src/assure-hydration.ts +15 -2
  258. package/src/capabilities-tool.ts +15 -3
  259. package/src/check-diagnostics.ts +73 -9
  260. package/src/check-scan.ts +1773 -381
  261. package/src/check.ts +237 -76
  262. package/src/cli.ts +141 -5
  263. package/src/client-module.ts +61 -0
  264. package/src/command-catalog.ts +166 -37
  265. package/src/component-check.ts +79 -0
  266. package/src/contract-proof.ts +3 -25
  267. package/src/contracts.ts +60 -3
  268. package/src/db-tool.ts +5 -2
  269. package/src/dev-bun-config.ts +51 -10
  270. package/src/diagnostics.ts +3 -0
  271. package/src/diff-tool.ts +33 -26
  272. package/src/doctor.ts +43 -21
  273. package/src/env-file.ts +7 -0
  274. package/src/fix-recipes.ts +25 -2
  275. package/src/hand-built-manifest.ts +144 -0
  276. package/src/hooks-report.ts +135 -0
  277. package/src/init-agents.ts +17 -11
  278. package/src/internal/nifra-project.ts +191 -0
  279. package/src/internal/project-specifiers.ts +424 -0
  280. package/src/internal/script-kind.ts +2 -1
  281. package/src/internal/source-facts.ts +25 -0
  282. package/src/internal/typescript-import.ts +10 -3
  283. package/src/introspect.ts +65 -27
  284. package/src/levels-tool.ts +117 -20
  285. package/src/load.ts +78 -14
  286. package/src/mcp-context.ts +7 -7
  287. package/src/mcp-exec.ts +60 -17
  288. package/src/mcp-http.ts +1 -1
  289. package/src/mcp-io.ts +41 -0
  290. package/src/mcp-isolate.ts +9 -2
  291. package/src/mcp-project-child.ts +1 -1
  292. package/src/mcp-root.ts +58 -8
  293. package/src/mcp-ws.ts +4 -2
  294. package/src/mcp.ts +8 -3
  295. package/src/migrate-layout.ts +885 -63
  296. package/src/output-schemas.ts +1084 -0
  297. package/src/port.ts +3 -2
  298. package/src/project-facts.ts +17 -3
  299. package/src/recipes/5.0.0.ts +35 -0
  300. package/src/recipes/index.ts +10 -0
  301. package/src/replay.ts +14 -3
  302. package/src/review.ts +39 -10
  303. package/src/rules/codes.ts +10 -0
  304. package/src/rules/comment-markers.ts +57 -21
  305. package/src/rules/contract-visibility.ts +383 -0
  306. package/src/rules/data-guard.ts +351 -42
  307. package/src/rules/layout.ts +22 -0
  308. package/src/rules/legacy.ts +184 -51
  309. package/src/rules/pages.ts +238 -3
  310. package/src/rules/routes.ts +124 -16
  311. package/src/rules/secrets.ts +2 -2
  312. package/src/rules/security.ts +352 -71
  313. package/src/rules/zones.ts +205 -38
  314. package/src/scaffold.ts +6 -4
  315. package/src/sdk.ts +14 -5
  316. package/src/stylex-migrate.ts +134 -7
  317. package/src/test-tool.ts +18 -3
  318. package/src/typecheck-coverage.ts +134 -0
  319. package/src/upgrade.ts +487 -71
  320. package/src/verification-plan.ts +24 -0
  321. package/src/version-drift.ts +67 -0
  322. package/src/web-app-options.ts +2 -2
  323. package/src/work-graph.ts +17 -12
@@ -1,8 +1,14 @@
1
1
  import type * as TSApi from "typescript"
2
+ import { BACKEND_APP_FILE } from "../app-files.ts"
2
3
  import { isBrowserSource } from "../check-scan.ts"
3
4
  import { type Diagnostic, diagnostic } from "../diagnostics.ts"
5
+ import { createWebAppProbe } from "../internal/nifra-project.ts"
4
6
  import { scriptKindOf } from "../internal/script-kind.ts"
5
- import { commentBlockHasMarker } from "./comment-markers.ts"
7
+ import {
8
+ commentBlockExpectation,
9
+ commentBlockHasMarker,
10
+ MISSING_EXPECTATION_REASON,
11
+ } from "./comment-markers.ts"
6
12
  import type { CheckRule, SourceIndex } from "./index.ts"
7
13
  import { loadRuleTypeScript } from "./typescript.ts"
8
14
 
@@ -25,35 +31,64 @@ function didNotRun(code: string, title: string): readonly Diagnostic[] {
25
31
 
26
32
  const SECRET = /(?:token|secret|apiKey|api_key|signature|hmac|password)/i
27
33
  const PII = /(?:email|phone|ssn|password|token|authorization)/i
28
- const REVIEWED = "@nifra-gate-reviewed"
34
+ // Retired in 5.0.0: it silenced any security finding without a reason. It now silences nothing and
35
+ // only makes the finding name the form that replaced it.
36
+ const RETIRED_REVIEW_MARKER = "@nifra-gate-reviewed"
29
37
  const parseCache = new WeakMap<
30
38
  SourceIndex,
31
39
  Map<string, { readonly tree: TSApi.SourceFile; readonly lines: readonly string[] }>
32
40
  >()
33
41
 
34
- // The marker counts anywhere in the contiguous comment block above the finding (or trailing on its
35
- // line) - a human writing the multi-line justification the hatch asks for must not un-suppress the
36
- // finding by growing the comment past two lines.
37
- function hasReview(lines: readonly string[], line: number): boolean {
38
- return commentBlockHasMarker(lines, line, REVIEWED)
42
+ /**
43
+ * The reason a `nifra-expect <code>: <reason>` comment on or above `line` gives for a security
44
+ * finding. Every security hatch needs the reason: a comment without one silences nothing.
45
+ */
46
+ function expectedReason(lines: readonly string[], line: number, code: string): string | undefined {
47
+ return commentBlockExpectation(lines, line, [code])?.reason
39
48
  }
40
49
 
41
- function reviewedEvidence(lines: readonly string[], line: number): readonly string[] {
42
- return hasReview(lines, line) ? [REVIEWED] : []
50
+ /** The evidence a silenced finding carries: the comment that silenced it, reason included. */
51
+ function expectationEvidence(
52
+ lines: readonly string[],
53
+ line: number,
54
+ code: string,
55
+ ): readonly string[] {
56
+ const reason = expectedReason(lines, line, code)
57
+ return reason === undefined ? [] : [`nifra-expect ${code}: ${reason}`]
43
58
  }
44
59
 
60
+ /** An unsilenced finding's message, plus why a hatch comment written for it does not count. */
61
+ function unexpectedMessage(
62
+ message: string,
63
+ lines: readonly string[],
64
+ line: number,
65
+ code: string,
66
+ ): string {
67
+ if (commentBlockExpectation(lines, line, [code]) !== undefined)
68
+ return `${message}; ${MISSING_EXPECTATION_REASON}`
69
+ if (commentBlockHasMarker(lines, line, RETIRED_REVIEW_MARKER))
70
+ return `${message}; ${RETIRED_REVIEW_MARKER} no longer silences a finding - after review, write // nifra-expect ${code}: <reason>`
71
+ return message
72
+ }
73
+
74
+ const parentOf = (dir: string): string => dir.slice(0, Math.max(dir.lastIndexOf("/"), 0))
75
+
45
76
  /**
46
- * NF-S002 severity by file role. Server-side code compares real secret material - a timing oracle
47
- * there is exploitable, so it fails the gate. Browser code (a route's frontend half, `frontend/`)
48
- * compares values the client already holds, so the same shape is advisory rather than a gate
49
- * failure. `shared/` also runs on the server, and a file that cannot be classified is treated as
50
- * server - fail closed.
77
+ * Whether NF-S002 may treat `file` as browser code, which compares values the client already holds,
78
+ * so a finding there is advisory. It must be browser code by path (a route's frontend half,
79
+ * `frontend/`) and sit inside a nifra web app: a `routes/` folder in an API-only project or a package
80
+ * without a web dependency runs on the server. `shared/` also runs on the server, and a file that
81
+ * cannot be classified is treated as server - fail closed.
51
82
  */
52
- function secretComparisonSeverity(file: string): "error" | "warn" {
83
+ export function provenBrowserFile(
84
+ root: string,
85
+ file: string,
86
+ inWebApp: (dir: string) => boolean = createWebAppProbe(root),
87
+ ): boolean {
53
88
  const path = file.replaceAll("\\", "/")
54
- if (!isBrowserSource(path) || /\.server\.[cm]?[tj]sx?$/.test(path)) return "error"
55
- if (/\.shared\.[^/]+$/.test(path) || /(?:^|\/)shared\//.test(path)) return "error"
56
- return "warn"
89
+ if (!isBrowserSource(path) || /\.server\.[cm]?[tj]sx?$/.test(path)) return false
90
+ if (/\.shared\.[^/]+$/.test(path) || /(?:^|\/)shared\//.test(path)) return false
91
+ return inWebApp(parentOf(path))
57
92
  }
58
93
 
59
94
  const CONFIRMATION_NAME =
@@ -61,11 +96,11 @@ const CONFIRMATION_NAME =
61
96
 
62
97
  /** Client form confirmation compares two values already held by the browser, not secret material. */
63
98
  function isClientConfirmationPair(
64
- file: string,
99
+ browser: boolean,
65
100
  left: string | undefined,
66
101
  right: string | undefined,
67
102
  ): boolean {
68
- if (secretComparisonSeverity(file) !== "warn") return false
103
+ if (!browser) return false
69
104
  const names = [left, right]
70
105
  return (
71
106
  names.some((name) => name?.toLowerCase() === "password") &&
@@ -102,13 +137,16 @@ function secretName(ts: typeof TSApi, node: TSApi.Node): string | undefined {
102
137
  * Presence checks (`token === undefined`, `secret == null`, `apiKey !== ""`) and `typeof` guards are
103
138
  * not equality over secret material - rewriting them to a timing-safe comparison would break the code.
104
139
  * A comparison against a numeric literal (`signature !== 1`, `signatureLength !== 64`) is a
105
- * length/version/discriminant check, never a secret: secrets are strings or bytes, so a number on
106
- * either side proves this is not the compare a timing oracle applies to.
140
+ * length/version/discriminant check, and one against `true`/`false` (`persistRefreshToken === true`)
141
+ * is a flag check: secrets are strings or bytes, so a number or a boolean on either side proves this
142
+ * is not the compare a timing oracle applies to.
107
143
  */
108
144
  function isPresenceComparison(ts: typeof TSApi, node: TSApi.BinaryExpression): boolean {
109
145
  for (const side of [node.left, node.right]) {
110
146
  if (ts.isIdentifier(side) && side.text === "undefined") return true
111
147
  if (side.kind === ts.SyntaxKind.NullKeyword) return true
148
+ if (side.kind === ts.SyntaxKind.TrueKeyword || side.kind === ts.SyntaxKind.FalseKeyword)
149
+ return true
112
150
  if (ts.isStringLiteralLike(side) && side.text === "") return true
113
151
  if (ts.isTypeOfExpression(side)) return true
114
152
  if (ts.isVoidExpression(side)) return true
@@ -149,10 +187,143 @@ interface SecretComparison {
149
187
  readonly line: number
150
188
  }
151
189
 
190
+ // The last word of a name that states a fact about a secret rather than holding it: `tokenType`,
191
+ // `SECRET_CAP_SQLSTATE`, `apiKeyIds`, `apiKeyHeaderName`. Exact words only, so the secret word anywhere
192
+ // else still matches (`tokenValue`, `passwordHash`, `apiKeyHeader`). `code`, `url`, `path` and `prefix`
193
+ // stay out: an OTP, a capability URL and a prefix compare all leak secret material.
194
+ const NON_MATERIAL_WORD: ReadonlySet<string> = new Set([
195
+ "type",
196
+ "kind",
197
+ "name",
198
+ "id",
199
+ "ids",
200
+ "length",
201
+ "len",
202
+ "count",
203
+ "size",
204
+ "mode",
205
+ "policy",
206
+ "version",
207
+ "sqlstate",
208
+ "status",
209
+ "enabled",
210
+ "required",
211
+ "ttl",
212
+ "expiry",
213
+ "expires",
214
+ ])
215
+
216
+ function lastWord(name: string): string {
217
+ const words = name
218
+ .replace(/([a-z0-9])([A-Z])/g, "$1 $2")
219
+ .replace(/([A-Z]+)([A-Z][a-z])/g, "$1 $2")
220
+ .split(/[\s_$-]+/)
221
+ return (words.findLast((word) => word !== "") ?? "").toLowerCase()
222
+ }
223
+
224
+ function isSecretName(name: string | undefined): boolean {
225
+ return name !== undefined && SECRET.test(name) && !NON_MATERIAL_WORD.has(lastWord(name))
226
+ }
227
+
228
+ /** Whether `name` binds a declaration in `statements` itself (not in a nested scope). */
229
+ function declaresIn(
230
+ ts: typeof TSApi,
231
+ statements: TSApi.NodeArray<TSApi.Statement>,
232
+ name: string,
233
+ ): boolean {
234
+ return statements.some((statement) => {
235
+ if (ts.isVariableStatement(statement))
236
+ return statement.declarationList.declarations.some((d) => bindsName(ts, d.name, name))
237
+ if (ts.isFunctionDeclaration(statement) || ts.isClassDeclaration(statement))
238
+ return statement.name?.text === name
239
+ return false
240
+ })
241
+ }
242
+
243
+ function bindsName(ts: typeof TSApi, binding: TSApi.BindingName, name: string): boolean {
244
+ if (ts.isIdentifier(binding)) return binding.text === name
245
+ return binding.elements.some(
246
+ (element) => !ts.isOmittedExpression(element) && bindsName(ts, element.name, name),
247
+ )
248
+ }
249
+
250
+ /**
251
+ * Whether `id` names a plain parameter of an enclosing function, not a value the file builds: the
252
+ * nearest declaration of the name, walking out, must be that parameter. A loop or catch variable, a
253
+ * local or a destructured parameter in between disproves it.
254
+ */
255
+ function isParameterRef(ts: typeof TSApi, id: TSApi.Identifier): boolean {
256
+ for (let node: TSApi.Node | undefined = id.parent; node !== undefined; node = node.parent) {
257
+ if (ts.isFunctionLike(node)) {
258
+ for (const parameter of node.parameters) {
259
+ if (ts.isIdentifier(parameter.name)) {
260
+ if (parameter.name.text === id.text) return true
261
+ } else if (bindsName(ts, parameter.name, id.text)) return false
262
+ }
263
+ }
264
+ if ((ts.isBlock(node) || ts.isSourceFile(node)) && declaresIn(ts, node.statements, id.text))
265
+ return false
266
+ if (
267
+ (ts.isForStatement(node) || ts.isForOfStatement(node) || ts.isForInStatement(node)) &&
268
+ node.initializer !== undefined &&
269
+ ts.isVariableDeclarationList(node.initializer) &&
270
+ node.initializer.declarations.some((d) => bindsName(ts, d.name, id.text))
271
+ )
272
+ return false
273
+ if (
274
+ ts.isCatchClause(node) &&
275
+ node.variableDeclaration !== undefined &&
276
+ bindsName(ts, node.variableDeclaration.name, id.text)
277
+ )
278
+ return false
279
+ }
280
+ return false
281
+ }
282
+
283
+ const CONFIG_KEY = /^[A-Z][A-Z0-9_]*$/
284
+ const CONTEXT_NAMES: ReadonlySet<string> = new Set(["c", "ctx", "context"])
285
+
286
+ /**
287
+ * Whether `node` reads deployment configuration: `process.env.X`, `Bun.env.X`, `import.meta.env.X`,
288
+ * `Deno.env.get("X")`, `c.env.UPPER_SNAKE` (also `ctx`/`context`), or `env.X`, where `c` and `env` must
289
+ * be parameters of an enclosing function, never values the file builds. Comparing two such reads
290
+ * involves no request value, so it is no timing oracle (a startup "keys must differ" check).
291
+ */
292
+ function isConfigRead(ts: typeof TSApi, node: TSApi.Expression): boolean {
293
+ if (ts.isCallExpression(node)) {
294
+ const callee = node.expression
295
+ const [key] = node.arguments
296
+ return (
297
+ ts.isPropertyAccessExpression(callee) &&
298
+ callee.name.text === "get" &&
299
+ ts.isPropertyAccessExpression(callee.expression) &&
300
+ callee.expression.name.text === "env" &&
301
+ ts.isIdentifier(callee.expression.expression) &&
302
+ callee.expression.expression.text === "Deno" &&
303
+ node.arguments.length === 1 &&
304
+ key !== undefined &&
305
+ ts.isStringLiteralLike(key)
306
+ )
307
+ }
308
+ let key: string
309
+ if (ts.isPropertyAccessExpression(node)) key = node.name.text
310
+ else if (ts.isElementAccessExpression(node) && ts.isStringLiteralLike(node.argumentExpression))
311
+ key = node.argumentExpression.text
312
+ else return false
313
+ const object = node.expression
314
+ if (ts.isIdentifier(object)) return object.text === "env" && isParameterRef(ts, object)
315
+ if (!ts.isPropertyAccessExpression(object) || object.name.text !== "env") return false
316
+ const owner = object.expression
317
+ if (ts.isMetaProperty(owner)) return owner.name.text === "meta"
318
+ if (!ts.isIdentifier(owner)) return false
319
+ if (owner.text === "process" || owner.text === "Bun") return true
320
+ return CONTEXT_NAMES.has(owner.text) && CONFIG_KEY.test(key) && isParameterRef(ts, owner)
321
+ }
322
+
152
323
  function secretComparisons(
153
324
  ts: typeof TSApi,
154
- file: string,
155
325
  tree: TSApi.SourceFile,
326
+ browser: boolean,
156
327
  ): readonly SecretComparison[] {
157
328
  const operators = [
158
329
  ts.SyntaxKind.EqualsEqualsEqualsToken,
@@ -170,9 +341,9 @@ function secretComparisons(
170
341
  const left = secretName(ts, node.left)
171
342
  const right = secretName(ts, node.right)
172
343
  if (
173
- ((left !== undefined && SECRET.test(left)) ||
174
- (right !== undefined && SECRET.test(right))) &&
175
- !isClientConfirmationPair(file, left, right)
344
+ (isSecretName(left) || isSecretName(right)) &&
345
+ !isClientConfirmationPair(browser, left, right) &&
346
+ !(isConfigRead(ts, node.left) && isConfigRead(ts, node.right))
176
347
  ) {
177
348
  const line = tree.getLineAndCharacterOfPosition(node.getStart(tree)).line + 1
178
349
  found.push({ node, left, right, line })
@@ -203,11 +374,19 @@ function timingSafeHelper(file: string): string {
203
374
  * Rewrite every unreviewed NF-S002 comparison in one file to `nifraTimingSafeEqual(left, right)` and
204
375
  * add the helper once. Sites come from the syntax tree, not a reported line: the helper lands above
205
376
  * them, so a line number a check run reported no longer names the same statement after the first fix.
377
+ * `browser` is {@link provenBrowserFile} for the file, so the fix rewrites exactly what check reported.
206
378
  */
207
- export function rewriteSecretComparisons(ts: typeof TSApi, file: string, source: string): string {
379
+ export function rewriteSecretComparisons(
380
+ ts: typeof TSApi,
381
+ file: string,
382
+ source: string,
383
+ browser: boolean,
384
+ ): string {
208
385
  const tree = parse(ts, file, source)
209
386
  const lines = tree.text.split("\n")
210
- const sites = secretComparisons(ts, file, tree).filter((site) => !hasReview(lines, site.line))
387
+ const sites = secretComparisons(ts, tree, browser).filter(
388
+ (site) => expectedReason(lines, site.line, "NF-S002") === undefined,
389
+ )
211
390
  // A comparison nested inside another (`(token === a) === flag`) is left for the next run, so every
212
391
  // edit below covers a disjoint range and applying them from the end keeps earlier offsets valid.
213
392
  const outer = sites.filter(
@@ -246,22 +425,32 @@ export const secretComparisonRule: CheckRule = {
246
425
  const ts = loaded.compiler
247
426
  if (ts === undefined) return didNotRun("NF-S002", "Non-constant-time secret comparison scan")
248
427
  const findings: Diagnostic[] = []
428
+ const webDirs = createWebAppProbe(ctx.root)
249
429
  for (const file of ctx.project.source.files) {
250
430
  const parsed = parsedFile(ts, ctx.project.source, file)
251
431
  if (parsed === undefined) continue
252
432
  const { tree, lines } = parsed
253
- for (const { left, right, line } of secretComparisons(ts, file, tree)) {
254
- const reviewed = hasReview(lines, line)
433
+ const browser = provenBrowserFile(ctx.root, file, webDirs)
434
+ for (const { left, right, line } of secretComparisons(ts, tree, browser)) {
435
+ const reviewed = expectedReason(lines, line, "NF-S002") !== undefined
255
436
  findings.push(
256
437
  diagnostic({
257
438
  code: "NF-S002",
258
- severity: reviewed ? "info" : secretComparisonSeverity(file),
439
+ severity: reviewed ? "info" : browser ? "warn" : "error",
259
440
  file,
260
441
  line,
261
442
  message: reviewed
262
- ? "secret comparison is explicitly marked as reviewed"
263
- : "secret-like values must use a length check and timing-safe comparison",
264
- evidence: [left ?? right ?? "secret comparison", ...reviewedEvidence(lines, line)],
443
+ ? "secret comparison is expected here"
444
+ : unexpectedMessage(
445
+ "secret-like values must use a length check and timing-safe comparison",
446
+ lines,
447
+ line,
448
+ "NF-S002",
449
+ ),
450
+ evidence: [
451
+ left ?? right ?? "secret comparison",
452
+ ...expectationEvidence(lines, line, "NF-S002"),
453
+ ],
265
454
  ...(reviewed
266
455
  ? {}
267
456
  : {
@@ -303,7 +492,7 @@ export const piiLogRule: CheckRule = {
303
492
  const name = nameOf(ts, arg)
304
493
  if (name !== undefined && PII.test(name)) {
305
494
  const line = tree.getLineAndCharacterOfPosition(node.getStart(tree)).line + 1
306
- const reviewed = hasReview(lines, line)
495
+ const reviewed = expectedReason(lines, line, "NF-S003") !== undefined
307
496
  findings.push(
308
497
  diagnostic({
309
498
  code: "NF-S003",
@@ -311,9 +500,14 @@ export const piiLogRule: CheckRule = {
311
500
  file,
312
501
  line,
313
502
  message: reviewed
314
- ? "sensitive value in log call is explicitly marked as reviewed"
315
- : "PII-shaped values must not be passed directly to log calls",
316
- evidence: [name, ...reviewedEvidence(lines, line)],
503
+ ? "sensitive value in log call is expected here"
504
+ : unexpectedMessage(
505
+ "PII-shaped values must not be passed directly to log calls",
506
+ lines,
507
+ line,
508
+ "NF-S003",
509
+ ),
510
+ evidence: [name, ...expectationEvidence(lines, line, "NF-S003")],
317
511
  verify: "nifra check --lints-only",
318
512
  }),
319
513
  )
@@ -353,6 +547,71 @@ function functionName(ts: typeof TSApi, fn: TSApi.Node): string | undefined {
353
547
  return undefined
354
548
  }
355
549
 
550
+ /**
551
+ * A gate's name. `can` only names a gate in camelCase (`canEdit`, `canAccess`) - matching it
552
+ * case-insensitively swept in `canonicalize…`, `cancel…`, `candidate…`, none of them authorization
553
+ * decisions. `ensure`/`verify`/`check`/`validate` name one only before what a gate decides on
554
+ * (`verifyToken`, `ensureLoggedIn`, `checkPermission`), so `ensureDir` and `checkHealth` are not gates.
555
+ */
556
+ const GATE_NAMES = [
557
+ /^(?:require|assert|authori[sz]e|authenticate)/i,
558
+ /^can[A-Z]/,
559
+ /.Guard$/,
560
+ /^(?:ensure|verify|check|validate)(?:Auth|LoggedIn|Login|Session|Token|Jwt|JWT|Signature|Webhook|Hmac|HMAC|Password|Credential|Permission|Access|Role|Admin|Owner|Scope|Csrf|CSRF|ApiKey|Origin|Tenant|User|Member)/,
561
+ ]
562
+
563
+ /** Helpers whose whole job is to fail (`fail(…)`, `deny(…)`, `reject(…)`, …). */
564
+ const FAILING_HELPERS = new Set(["fail", "deny", "reject", "forbid", "unauthorized", "abort"])
565
+
566
+ /**
567
+ * Whether a gate's catch block fails closed: it throws, returns `false`, a `new Response(…)` or a
568
+ * `deny…(…)` call, or hands the failure to a {@link FAILING_HELPERS} helper. The last form is why
569
+ * conformance/invariant asserts that funnel every error through a `never`-returning `fail()` are not
570
+ * fail-open - the scanner reads no types, so it trusts the helper by name, the same trade the SQL and
571
+ * gate-name heuristics already make. Read from the syntax, so a "throw" in a comment or a string counts
572
+ * for nothing, and a nested function's statements are not the catch's own.
573
+ */
574
+ function failsClosed(ts: typeof TSApi, block: TSApi.Block): boolean {
575
+ let closed = false
576
+ const visit = (node: TSApi.Node): void => {
577
+ if (closed || ts.isFunctionLike(node)) return
578
+ if (
579
+ ts.isThrowStatement(node) ||
580
+ (ts.isReturnStatement(node) &&
581
+ node.expression !== undefined &&
582
+ isDenial(ts, node.expression)) ||
583
+ (ts.isCallExpression(node) &&
584
+ ts.isIdentifier(node.expression) &&
585
+ FAILING_HELPERS.has(node.expression.text))
586
+ ) {
587
+ closed = true
588
+ return
589
+ }
590
+ ts.forEachChild(node, visit)
591
+ }
592
+ visit(block)
593
+ return closed
594
+ }
595
+
596
+ function isDenial(ts: typeof TSApi, expression: TSApi.Expression): boolean {
597
+ let value = expression
598
+ while (
599
+ ts.isParenthesizedExpression(value) ||
600
+ ts.isAwaitExpression(value) ||
601
+ ts.isAsExpression(value) ||
602
+ ts.isSatisfiesExpression(value)
603
+ )
604
+ value = value.expression
605
+ if (value.kind === ts.SyntaxKind.FalseKeyword) return true
606
+ if (ts.isNewExpression(value))
607
+ return ts.isIdentifier(value.expression) && value.expression.text === "Response"
608
+ return (
609
+ ts.isCallExpression(value) &&
610
+ ts.isIdentifier(value.expression) &&
611
+ value.expression.text.startsWith("deny")
612
+ )
613
+ }
614
+
356
615
  export const failOpenGateRule: CheckRule = {
357
616
  code: "NF-S001",
358
617
  title: "Fail-open gate",
@@ -371,25 +630,10 @@ export const failOpenGateRule: CheckRule = {
371
630
  let parent: TSApi.Node | undefined = node.parent
372
631
  while (parent !== undefined && !ts.isFunctionLike(parent)) parent = parent.parent
373
632
  const name = parent === undefined ? undefined : functionName(ts, parent)
374
- // `can` only names a gate in camelCase (`canEdit`, `canAccess`) - matching it
375
- // case-insensitively swept in `canonicalize…`, `cancel…`, `candidate…`, none of them
376
- // authorization decisions. The other roots stay case-insensitive.
377
- const gateName =
378
- name !== undefined &&
379
- (/^(?:require|assert|authorize)/i.test(name) || /^can[A-Z]/.test(name))
380
- if (name !== undefined && gateName) {
381
- const text = node.block.getText(tree)
382
- // A catch fails closed when it rethrows, returns an explicit denial, or hands the failure
383
- // to a helper whose whole job is to fail (`fail(…)`, `deny(…)`, `reject(…)`, …). The last
384
- // form is why conformance/invariant asserts that funnel every error through a
385
- // `never`-returning `fail()` are not fail-open - the scanner reads no types, so it trusts
386
- // the helper by name, the same trade the SQL and gate-name heuristics already make.
387
- if (
388
- !/\b(?:throw|return\s+(?:false|new\s+Response|deny))/s.test(text) &&
389
- !/(?:^|[^.\w])(?:fail|deny|reject|forbid|unauthorized|abort)\s*\(/s.test(text)
390
- ) {
633
+ if (name !== undefined && GATE_NAMES.some((gate) => gate.test(name))) {
634
+ if (!failsClosed(ts, node.block)) {
391
635
  const line = tree.getLineAndCharacterOfPosition(node.getStart(tree)).line + 1
392
- const reviewed = hasReview(lines, line)
636
+ const reviewed = expectedReason(lines, line, "NF-S001") !== undefined
393
637
  findings.push(
394
638
  diagnostic({
395
639
  code: "NF-S001",
@@ -397,12 +641,17 @@ export const failOpenGateRule: CheckRule = {
397
641
  file,
398
642
  line,
399
643
  message: reviewed
400
- ? "gate catch block is explicitly marked as reviewed"
401
- : "gate catch blocks must rethrow or return an explicit denial",
644
+ ? "gate catch block is expected here"
645
+ : unexpectedMessage(
646
+ "gate catch blocks must rethrow or return an explicit denial",
647
+ lines,
648
+ line,
649
+ "NF-S001",
650
+ ),
402
651
  evidence: [
403
652
  name,
404
653
  "catch block has no denial or rethrow",
405
- ...reviewedEvidence(lines, line),
654
+ ...expectationEvidence(lines, line, "NF-S001"),
406
655
  ],
407
656
  verify: "nifra check --lints-only",
408
657
  }),
@@ -469,7 +718,7 @@ export const corsOriginPredicateRule: CheckRule = {
469
718
  : false
470
719
  if (ignoresOrigin) {
471
720
  const line = tree.getLineAndCharacterOfPosition(prop.getStart(tree)).line + 1
472
- const reviewed = hasReview(lines, line)
721
+ const reviewed = expectedReason(lines, line, "NF-S004") !== undefined
473
722
  findings.push(
474
723
  diagnostic({
475
724
  code: "NF-S004",
@@ -477,9 +726,17 @@ export const corsOriginPredicateRule: CheckRule = {
477
726
  file,
478
727
  line,
479
728
  message: reviewed
480
- ? "constant CORS origin predicate is explicitly marked as reviewed"
481
- : "CORS origin predicate never inspects the origin - it allows every origin; list explicit origins or use the argument",
482
- evidence: ["origin predicate", ...reviewedEvidence(lines, line)],
729
+ ? "constant CORS origin predicate is expected here"
730
+ : unexpectedMessage(
731
+ "CORS origin predicate never inspects the origin - it allows every origin; list explicit origins or use the argument",
732
+ lines,
733
+ line,
734
+ "NF-S004",
735
+ ),
736
+ evidence: [
737
+ "origin predicate",
738
+ ...expectationEvidence(lines, line, "NF-S004"),
739
+ ],
483
740
  verify: "nifra check --lints-only",
484
741
  }),
485
742
  )
@@ -520,7 +777,7 @@ export const externalRedirectRule: CheckRule = {
520
777
  prop.initializer.kind === ts.SyntaxKind.TrueKeyword
521
778
  ) {
522
779
  const line = tree.getLineAndCharacterOfPosition(node.getStart(tree)).line + 1
523
- const reviewed = hasReview(lines, line)
780
+ const reviewed = expectedReason(lines, line, "NF-S005") !== undefined
524
781
  findings.push(
525
782
  diagnostic({
526
783
  code: "NF-S005",
@@ -528,9 +785,14 @@ export const externalRedirectRule: CheckRule = {
528
785
  file,
529
786
  line,
530
787
  message: reviewed
531
- ? "external redirect is explicitly marked as reviewed"
532
- : "redirect opts out of the same-origin default - verify the target can never be derived from request input (open-redirect risk)",
533
- evidence: ["external: true", ...reviewedEvidence(lines, line)],
788
+ ? "external redirect is expected here"
789
+ : unexpectedMessage(
790
+ "redirect opts out of the same-origin default - verify the target can never be derived from request input (open-redirect risk)",
791
+ lines,
792
+ line,
793
+ "NF-S005",
794
+ ),
795
+ evidence: ["external: true", ...expectationEvidence(lines, line, "NF-S005")],
534
796
  verify: "nifra check --lints-only",
535
797
  }),
536
798
  )
@@ -574,7 +836,7 @@ export const assuranceEscapeHatchRule: CheckRule = {
574
836
  const consequence = name !== undefined ? ESCAPE_HATCHES[name] : undefined
575
837
  if (name !== undefined && consequence !== undefined) {
576
838
  const line = tree.getLineAndCharacterOfPosition(node.getStart(tree)).line + 1
577
- const reviewed = hasReview(lines, line)
839
+ const reviewed = expectedReason(lines, line, "NF-S006") !== undefined
578
840
  findings.push(
579
841
  diagnostic({
580
842
  code: "NF-S006",
@@ -582,9 +844,9 @@ export const assuranceEscapeHatchRule: CheckRule = {
582
844
  file,
583
845
  line,
584
846
  message: reviewed
585
- ? `${name} escape hatch is explicitly marked as reviewed`
586
- : `${name}: ${consequence}`,
587
- evidence: [name, ...reviewedEvidence(lines, line)],
847
+ ? `${name} escape hatch is expected here`
848
+ : unexpectedMessage(`${name}: ${consequence}`, lines, line, "NF-S006"),
849
+ evidence: [name, ...expectationEvidence(lines, line, "NF-S006")],
588
850
  verify: "nifra check --lints-only",
589
851
  }),
590
852
  )
@@ -658,6 +920,24 @@ export const unprefixedSecureCookieRule: CheckRule = {
658
920
  },
659
921
  }
660
922
 
923
+ /** Reads the backend the contract-lock check loaded; this rule runs no project code of its own. */
924
+ export const inputBeforeAuthenticationRule: CheckRule = {
925
+ code: "NF-S008",
926
+ title: "Authenticated route validates input before authentication",
927
+ async scan(ctx) {
928
+ return (ctx.project.check.contracts.inputBeforeAuth ?? []).map((route) =>
929
+ diagnostic({
930
+ code: "NF-S008",
931
+ severity: "warn",
932
+ file: BACKEND_APP_FILE,
933
+ message: `${route} authenticates in a hook that runs after its declared input is validated, so an unauthenticated caller reaches the body parser and reads the validation issues. Authenticate with \`app.authenticate()\` (the first-party auth middleware already does), or set \`validationOrder: "auth-before-validation"\` on the route so its hooks run first`,
934
+ evidence: [route],
935
+ verify: "nifra check --lints-only",
936
+ }),
937
+ )
938
+ },
939
+ }
940
+
661
941
  export const securityRules = Object.freeze([
662
942
  failOpenGateRule,
663
943
  secretComparisonRule,
@@ -666,4 +946,5 @@ export const securityRules = Object.freeze([
666
946
  externalRedirectRule,
667
947
  assuranceEscapeHatchRule,
668
948
  unprefixedSecureCookieRule,
949
+ inputBeforeAuthenticationRule,
669
950
  ])