oxlint-plugin-react-doctor 0.9.13-dev.fd23edc → 0.9.13-dev.ff7dd67
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.
|
@@ -18,7 +18,7 @@ import{parseSync as e,visitorKeys as t}from"oxc-parser";import{KEYS as n}from"es
|
|
|
18
18
|
`).slice(0,-a.length+a.search(jf));Yf.test(ie)||n.push({message:`HTML is injected from a dynamic-looking source, which can become XSS if the value is user-controlled or unsanitized.`,line:i+1,column:a.search(/\S/)+1})}return n}}),xp=e=>!(!W(e)||sl.test(e)||ll.test(e)),Sp=/firebase|firestore|supabase|\b(?:setDoc|addDoc)\s*\(/i,Cp=H({id:`firebase-client-owned-authz-field`,title:`Client writes authorization field`,severity:`error`,recommendation:`Derive authority fields on the server or enforce them in Firebase/Supabase rules; never trust client-provided owner, org, or role values.`,scan:G({shouldScan:e=>xp(e.relativePath)&&(Sp.test(e.content)||Sp.test(e.relativePath)),pattern:/(?:\b(?:setDoc|updateDoc|addDoc)\s*\(|(?:\b(?:firebase|firestore|getFirestore)\b|\bcollection\s*\(|\.collection\s*\()[\s\S]{0,500}\.(?:set|update|add)\s*\()(?:[^;'"`]|'[^'\n]*'|"[^"\n]*"|`[^`]*`){0,700}\b(?:ownerId|ownerID|creatorId|creatorID|providerId|providerID|orgId|orgID|tenantId|tenantID|workspaceId|workspaceID|ghostOrg|role|roles|isAdmin)\b/i,message:`Client code writes an ownership, tenant, or role field that should be server-owned and immutable.`})}),wp=H({id:`firebase-permissive-rules`,title:`Permissive Firebase security rule`,severity:`error`,recommendation:"Bind every read/write to `request.auth.uid`, immutable ownership, and tenant membership instead of treating sign-in as authorization.",scan:G({shouldScan:e=>Dl(e.relativePath),pattern:/allow\s+(?:read|write|create|update|delete|list|get|read,\s*write)\s*:\s*if\s+(?:true|request\.auth\s*!=\s*null)\s*;?/i,message:`Firebase rules grant broad access to everyone or to any signed-in user, which is the Chattr/Firewreck failure mode.`})}),Tp=H({id:`firebase-query-filter-as-auth`,title:`Firestore query filter used as authorization`,severity:`warn`,recommendation:"Make sure Firestore rules compare the requested document against `request.auth.uid` and trusted membership data.",scan:G({shouldScan:e=>xp(e.relativePath),pattern:/\.where\s*\(\s*["'](?:uid|userId|userID|ownerId|ownerID|orgId|orgID|tenantId|tenantID|role)["']\s*,\s*["']==["']/i,message:`Firestore query code filters by an auth-shaped field; filtering is not authorization unless rules enforce the same boundary.`})}),Ep=/api\.github\.com|github\.com|gitlab\.com|bitbucket\.org/gi,Dp=/\$\{([^}]*)\}/g,Op=/\b(?:params|searchParams|query|req|request|input|payload)\s*[.[]|\buntrusted|\bdecodeURI\w*/,kp=/encodeURIComponent\s*\(/,Ap=/^\s*[\w$]+\.toString\(\)\s*$/,jp=H({id:`git-provider-url-injection-risk`,title:`Git provider URL built from interpolation`,severity:`warn`,recommendation:`Validate owner, repo, org, and branch identifiers against strict slugs and build URLs with URL/path encoders instead of raw interpolation.`,scan:e=>{if(!W(e.relativePath))return[];let t=[];for(let n of e.content.matchAll(Ep)){let r=e.content.slice(n.index,n.index+200),i=r.indexOf("`"),a=i>=0?r.slice(0,i):r;if(!Array.from(a.matchAll(Dp)).some(e=>{let t=e[1]??``;return Op.test(t)&&!kp.test(t)&&!Ap.test(t)}))continue;let o=U(e.content,n.index);t.push({message:`GitHub/GitLab/Bitbucket URL construction interpolates path components that may be attacker-controlled.`,line:o.line,column:o.column});break}return t}}),Mp=/child_process|childProcess|execa|subprocess|Deno\.run/,Np=/(?:\b(?:eval|new\s+Function|vm\.runIn\w*)|(?<![.\w$])(?:exec(?:File)?(?:Sync)?|spawn(?:Sync)?)|\b(?:child_process|childProcess|cp)\.(?:exec|spawn)\w*)\s*\([^;]{0,200}(?<!["'])\b(?:exif|metadata|manifest|preset|plugin|upload|drop(?:ped|s)?\b|archive|zip|unzip|untar)(?!\w*["'])/,Pp=/(?:\b(?:eval|new\s+Function|vm\.runIn\w*)|\b(?:child_process|childProcess|cp)\.(?:exec|spawn)\w*)\s*\([^;]{0,200}(?<!["'])\b(?:exif|metadata|manifest|preset|plugin|upload|drop(?:ped|s)?\b|archive|zip|unzip|untar)(?!\w*["'])/,Fp=`Imported metadata, uploads, or plugin manifests appear to reach code execution.`,Ip=G({shouldScan:()=>!0,pattern:Np,ignoreStringLiterals:!0,message:Fp}),Lp=G({shouldScan:()=>!0,pattern:Pp,ignoreStringLiterals:!0,message:Fp}),Rp=H({id:`import-metadata-execution-risk`,title:`Imported metadata reaches code execution`,severity:`error`,recommendation:`Parse imported metadata as data with strict schemas; do not evaluate EXIF, manifests, presets, dropped files, or archives.`,scan:e=>W(e.relativePath)?Mp.test(e.content)?Ip(e):Lp(e):[]}),zp=/createHash\s*\(\s*["'](?:md5|sha1)["']|\bmd5\s*\(/gi,Bp=/\b(?:password|token|secret|signature|signing|auth|credential|session|cookie|csrf|api.?key)\b/i,Vp=/(?<!cipher\.)\bcreate(?:Cipher|Decipher)\s*\(/,Hp=/\bcreate(?:Cipher|Decipher)iv\s*\(\s*["'](?:des|des3|des-?ede3?|rc4|rc2|bf|blowfish)\b/i,Up=/\b(?:DES|RC4|Blowfish)\b/,Wp=/\b(?:cipher|decipher|encrypt|decrypt|crypto)\b/i,Gp=/[A-Za-z_$][\w$.]{0,100}signature[\w$]*(?:\([^)]*\))?\s*(?:===?|!==?)\s*[A-Za-z_$][\w$.]*(?:\([^)]*\))?|[A-Za-z_$][\w$.]{0,100}(?:\([^)]*\))?\s*(?:===?|!==?)\s*[A-Za-z_$][\w$.]{0,100}signature[\w$]*(?:\([^)]*\))?/i,Kp=/(?:===?|!==?)\s*[A-Z](?:[a-z]|[A-Z0-9_]*\b(?!\s*[.(]))|^[A-Z](?:[a-z]|[A-Z0-9_]*\b(?!\s*[.(]))[\w$.]*(?:\([^)]*\))?\s*(?:===?|!==?)/,qp=/signature(?:Method|Type|Status|Algorithm|Kind|Mode|Version)\b/i,Jp=/(?:===?|!==?)\s*(?:true|false|null|undefined)\b/,Yp=/\.(?:length|size|byteLength)\s*(?:===?|!==?)|\.(?:length|size|byteLength)\s*$/,Xp=/\bcrypto\b|createH(?:mac|ash)|createSign|createVerify|\bsubtle\b|\bhmac\b|\bdigest\b|\bsha-?(?:1|256|384|512)\b|\bmd5\b|\bwebhook|x-(?:hub-)?signature/i,Zp=/\.[cm]?[jt]sx$/i,Qp=/timingSafeEqual|timing.?safe/i,$p=/gravatar|digest[-_ ]?auth|oauth[-_ ]?1|pkcs#?12|smime|\b_id\b|\betag\b|checksum|cache[-_ ]?key|fingerprint/i,em=/token|secret|password|nonce|salt|csrf|credential|otp/i,tm=/(?:focus|render|refresh|remount|redraw|animation|layout|cache|update)[-_]?nonce/i,nm=/Math\.random\s*\(/g,rm=/createHash|md5|cipher|encrypt|decrypt|crypto|signature|Math\.random/i,im=(e,t,n,r)=>{for(let i of e.matchAll(t)){let t=e.slice(Math.max(0,i.index-250),i.index+250);if(n.test(t)&&!r?.test(t))return i.index}return-1},am=(e,t,n,r)=>{for(let i of e.matchAll(t)){let t=e.lastIndexOf(`
|
|
19
19
|
`,i.index)+1,a=e.indexOf(`
|
|
20
20
|
`,i.index),o=a<0?e.length:a,s=e.slice(t,o);if(!r.test(s)&&n.test(s))return i.index}return-1},om=H({id:`insecure-crypto-risk`,title:`Weak cryptography in security context`,severity:`warn`,recommendation:"Use modern primitives, `crypto.randomBytes` / Web Crypto randomness, and timing-safe comparisons for signatures, digests, tokens, and auth material.",scan:e=>{if(!W(e.relativePath)||dl.test(e.relativePath)||$p.test(e.relativePath)||!rm.test(e.content))return[];let t=zl(e),n=im(t,zp,Bp,$p);if(n<0&&(n=t.search(Hp)),n<0&&(n=t.search(Vp)),n<0&&Wp.test(t)&&(n=t.search(Up)),n<0&&!Qp.test(t)&&!Zp.test(e.relativePath)&&Xp.test(t)){let e=Gp.exec(t);e!==null&&!Kp.test(e[0])&&!qp.test(e[0])&&!Jp.test(e[0])&&!Yp.test(e[0])&&(n=e.index)}if(n<0&&(n=am(t,nm,em,tm)),n<0)return[];let r=U(t,n);return[{message:`Code uses weak hashes, deprecated ciphers, timing-unsafe comparisons, or Math.random in a security-shaped context.`,line:r.line,column:r.column}]}}),sm=`(?<![A-Za-z0-9])(?:session|sess|sid|connect\\.sid|auth|jwt|access[_-]?token|refresh[_-]?token|id[_-]?token)(?![A-Za-z0-9])`,cm=`[\`"'][^\`"']*?${sm}[^\`"']*[\`"']`,lm=RegExp(`(?:\\.cookies\\.set|cookies\\(\\s*\\)\\.set|\\.cookie)\\s*\\(\\s*${cm}`,`gi`),um=/httpOnly\s*:\s*false\b/i,dm=/"(?:[^"\\]|\\.)*"|'(?:[^'\\]|\\.)*'|`(?:[^`\\]|\\.)*`/g,fm=e=>{let t=e.split(``),n=0,r=null;for(;n<e.length;){let i=e[n];if(r!==null){if(i===`\\`){n+=2;continue}i===r?r=null:i!==`
|
|
21
|
-
`&&(t[n]=` `),n+=1;continue}(i===`"`||i===`'`||i==="`")&&(r=i),n+=1}return t.join(``)},pm=/cookie\s*:\s*\{/gi,mm=RegExp(`document\\.cookie\\s*=\\s*[\`"'][^\`"'=;]*?${sm}[^\`"'=;]*=`,`gi`),hm=e=>{if(e.trim().length===0)return 0;let t=0,n=null,r=1;for(let i=0;i<e.length;i+=1){let a=e[i];if(n!==null){a===`\\`?i+=1:a===n&&(n=null);continue}a===`"`||a===`'`||a==="`"?n=a:a===`(`||a===`[`||a===`{`?t+=1:a===`)`||a===`]`||a===`}`?--t:a===`,`&&t===0&&(r+=1)}return r},gm=(e,t,n,r,i)=>{t.lastIndex=0;for(let a=t.exec(e);a!==null;a=t.exec(e)){if(!r(a.index,a[0]))continue;let t=U(e,a.index);i.push({message:n,line:t.line,column:t.column})}},_m=H({id:`insecure-session-cookie`,title:`Auth cookie missing HttpOnly protection`,severity:`warn`,recommendation:"Set auth/session cookies server-side with `httpOnly: true`, `secure: true`, and `sameSite`. Cookies set via `document.cookie` or with `httpOnly: false` are readable by any XSS payload and can be stolen.",scan:e=>{if(!W(e.relativePath))return[];let t=zl(e);if(!/cookie/i.test(t))return[];let n=[],r=`An auth/session cookie is exposed to JavaScript (set via document.cookie, with httpOnly: false, or without cookie options), letting an XSS payload steal it.`;lm.lastIndex=0;for(let e=lm.exec(t);e!==null;e=lm.exec(t)){let i=e.index+e[0].lastIndexOf(`(`),a=gd(t,i);if(a<0)continue;let o=t.slice(i+1,a),s=hm(o)<3,c=o.replace(dm,``);if(!s&&!um.test(c))continue;let l=U(t,e.index);n.push({message:r,line:l.line,column:l.column})}let i=fm(t);pm.lastIndex=0;for(let e=pm.exec(i);e!==null;e=pm.exec(i)){let t=e.index+e[0].length-1,a=gd(i,t),o=a>=0?i.slice(t,a):i.slice(t,t+400);if(!um.test(o))continue;let s=U(i,e.index);n.push({message:r,line:s.line,column:s.column})}return gm(t,mm,r,()=>!0,n),n}}),vm=/\b(?:alg|algorithms?)\s*:\s*\[?\s*["'`]none["'`]/gi,ym=(e,t)=>{let n=null,r=[];for(let i=0;i<t;i+=1){let t=e[i];if(n==="`"){t===`\\`?i+=1:t==="`"?n=null:t===`$`&&e[i+1]===`{`&&(r.push(0),n=null,i+=1);continue}if(n!==null){t===`\\`?i+=1:t===n&&(n=null);continue}if(t===`"`||t===`'`||t==="`")n=t;else if(r.length>0){let e=r.length-1;t===`{`?r[e]+=1:t===`}`&&(r[e]===0?(r.pop(),n="`"):--r[e])}}return n!==null},bm=H({id:`jwt-insecure-verification`,title:`JWT verified with the 'none' algorithm`,severity:`error`,recommendation:"Never accept the `none` algorithm; it disables signature verification and lets any forged token through. Pin the real algorithm(s) explicitly (`jwt.verify(token, key, { algorithms: ['RS256'] })`).",scan:e=>{if(!W(e.relativePath))return[];let t=zl(e);if(!/\bjwt\b|jsonwebtoken|\bjose\b/i.test(t))return[];let n=[];vm.lastIndex=0;for(let e=vm.exec(t);e!==null;e=vm.exec(t)){if(ym(t,e.index))continue;let r=U(t,e.index);n.push({message:`JWT is configured with the 'none' algorithm, which disables signature verification, so any forged token is accepted.`,line:r.line,column:r.column})}return n}}),xm=/PRIVATE KEY|SSH_PRIVATE_KEY|GPG_PRIVATE_KEY|DEPLOY_KEY|SIGNING_KEY/i,Sm=H({id:`key-lifecycle-risk`,title:`Long-lived key material in repository`,severity:`error`,committedFilesOnly:!0,recommendation:`Remove private keys from source, rotate exposed credentials, prefer short-lived deploy credentials, and document revocation/expiry for release keys.`,scan:G({shouldScan:e=>!cl.test(e.relativePath)&&!fl.test(e.relativePath)&&xm.test(e.content),pattern:/(?<!(?:placeholder|example|sample|dummy|fake)[\s\S]{0,40})-----BEGIN (?:RSA |EC |OPENSSH |DSA )?PRIVATE KEY-----(?:\s|\\r|\\n)*[A-Za-z0-9+/=][A-Za-z0-9+/=\s]{38,}(?![^-]{0,160}\.\.\.)|\b(?:SSH_PRIVATE_KEY|GPG_PRIVATE_KEY|DEPLOY_KEY|SIGNING_KEY)\b\s*[:=]\s*["'][^"'\n]{16,}["']/i,message:`Private or long-lived release key material appears in the repository.`})}),Cm=H({id:`local-rpc-native-bridge-risk`,title:`Weak localhost native bridge boundary`,severity:`warn`,recommendation:`Use exact origin allowlists after URL parsing, per-request nonces, narrow methods, and never expose install/update commands to arbitrary web pages.`,scan:G({shouldScan:e=>W(e.relativePath),pattern:/\b(?:Access-Control-Allow-Origin|websocket|WebSocket|(?:127\.0\.0\.1|localhost)(?=[\s\S]{0,700}\b(?:UpdateApp|InstallApp)\b)|(?:127\.0\.0\.1|localhost)[\s\S]{0,350}\b(?:onmessage|message|request|req)\b)\b[\s\S]{0,700}(?:\b(?:UpdateApp|InstallApp|child_process)\b|(?<![.\w$])(?:exec(?:File)?(?:Sync)?|spawn(?:Sync)?)\s*\()/i,message:`Code appears to bridge browser code to localhost/native capabilities with weak origin or update/install checks.`})}),wm=/\bfrom\s+["']@modelcontextprotocol\/sdk[^"']*["']|\bMcpServer\b|\bMcpAgent\b/,Tm=/@modelcontextprotocol\/sdk|\bMcpServer\b|\bMcpAgent\b/,Em=/\bserver\.\s*tool\s*\(|\bregisterTool\s*\(|\bsetRequestHandler\s*\(\s*CallToolRequestSchema/,Dm=/\b(?:tool|registerTool|setRequestHandler)\b/,Om=H({id:`mcp-tool-capability-risk`,title:`MCP tool exposes dangerous capability`,severity:`warn`,recommendation:`MCP tool calls run with the connecting client's authority. Validate inputs, enforce per-tool authorization, and avoid raw filesystem/shell/network access where possible.`,scan:G({shouldScan:e=>W(e.relativePath)&&Tm.test(e.content)&&Dm.test(e.content)&&gl.test(e.content),pattern:Em,requireAll:[wm,gl],ignoreStringLiterals:!0,message:`An MCP tool/resource/prompt handler appears to expose file, shell, network, or code-execution capability.`})}),km=H({id:`mdx-ssr-execution-risk`,title:`Server-rendered MDX can execute code`,severity:`warn`,recommendation:`Use a constrained compiler for untrusted content, disable expressions/raw HTML, sandbox renderers, and avoid caching attacker-controlled output across tenants.`,scan:G({shouldScan:e=>W(e.relativePath),pattern:/(?:@mdx-js\/mdx|next-mdx-remote|\b(?:MDXRemote|compileMDX|evaluateMdx)\b)[\s\S]{0,700}\b(?:repo\b|customer|tenant|user[-_]?(?:content|markdown|mdx|input|provided|generated|submitted)|untrusted|searchParams|req\.|request\.|fetch\s*\(|prisma\.|db\.|database|rehypeRaw|allowDangerousHtml)/i,message:`MDX/markdown rendering code may evaluate user or repository content during SSR or static generation.`})}),Am=H({id:`nosql-injection-risk`,title:`NoSQL query accepts operator-shaped input`,severity:`warn`,recommendation:"Coerce scalar fields before querying, reject operator keys from client input, and avoid `$where` or request-derived regexes.",scan:G({shouldScan:e=>Sl(e.relativePath,ol),pattern:/\$where\s*['"]?\s*:\s*(?:f?['"`][^'"`]{0,200}\$\{|function|f['"])|\.find\s*\(\s*JSON\.parse\s*\(\s*(?:req|request)\.|\.aggregate\s*\(\s*\[?\s*\{[^}]{0,400}\$where|\bnew\s+RegExp\s*\(\s*(?:req|request)\.|\$regex['"]?\s*:\s*(?:req|request)\./i,message:"Code appears to pass raw JSON, regex, or `$where` style input into a NoSQL query."})}),jm=[...nu.filter(e=>!e.test(`service_role`)),ru],Mm=H({id:`package-metadata-secret`,title:`Secret-like package metadata`,severity:`warn`,recommendation:`Keep secrets out of package metadata and generated reports; they are often published to registries, logs, or browser artifacts.`,scan:e=>{if(!e.relativePath.endsWith(`package.json`))return[];let t=Au(e.content)??jm.find(t=>t.test(e.content));if(t===void 0)return[];let n=tl(e.content,t);return[{message:`Package metadata contains secret-like values or public env secret names.`,line:n.line,column:n.column}]}}),Nm=H({id:`path-traversal-risk`,title:`Filesystem path uses caller input`,severity:`warn`,recommendation:`Resolve paths against a fixed base directory, reject traversal after normalization, and map user-visible identifiers to server-owned paths.`,scan:G({shouldScan:e=>W(e.relativePath)&&!sd(e.relativePath),pattern:/\b(?:readFile|readFileSync|writeFile|writeFileSync)\s*\(\s*(?:req\.|request\.|params\.|query\.|body\.|parsed\.|`[^`]*(?<![-./\w$'"`])(?:req\.|request\.|params\.|query\.|body\.))|\bpath\.(?:join|resolve)\s*\([^)]*(?<![-./\w$'"`])(?<!basename\(\s{0,4})(?:req\.|request\.|params\.|query\.|body\.|parsed\.)/,message:`Filesystem access appears to use request, query, params, or body data as part of the path.`})}),Pm=/\b(?:repoUrl|updateUrl|UpdateApp|InstallApp|auto.?updater?|installer|curl(?!\s+(?:-T\b|--upload-file\b))|wget)\b[\s\S]{0,250}(?:\.(?:zip|exe|dmg|appimage|msi|deb|rpm)\b|\.tar\.gz\b|\|\s*(?:bash|sh)\b)/i,Fm=/\b(?:curl|wget)\b(?=(?:[^\r\n]|\\\r?\n)*https?:\/\/)(?:[^\r\n]|\\\r?\n)*?\|\s*(?:bash|sh)\b/i,Im=/sha(?:256|512|1)sum|--checksum|checksum=|EXPECTED_SHA|gpg\s+--verify|\.sha(?:256|512)\b/i,Lm=/\b(?:child_process|childProcess|execa|os\.system|subprocess\.|Deno\.run|autoUpdater|electron-updater)\b|\b(?:exec(?:File)?(?:Sync)?|spawn(?:Sync)?)\s*\(/,Rm=/.\/\.github\/workflows\//i,zm=/(?:^|\/)[\w-]+[.@-]\d+\.\d+\.\d+(?:[-.][\w.]+)?\//,Bm=e=>bl(e)&&!Rm.test(e)&&!zm.test(e)&&!dl.test(e),Vm=H({id:`plugin-update-trust-risk`,title:`Plugin or updater trust boundary risk`,severity:`warn`,recommendation:`Require signed updates/plugins, pin trusted repositories, verify hashes before execution, and keep custom repository installs behind explicit warnings.`,scan:e=>{if(!W(e.relativePath)&&!Bm(e.relativePath))return[];let t=zl(e),n=Fm.test(t);if(!n&&!Pm.test(t))return[];let r=n?Fm:Pm;if(Im.test(t)||il.test(e.relativePath)&&!Lm.test(t))return[];let i=tl(t,r);return[{message:`Code appears to download, install, update, or execute plugin/updater content across a trust boundary.`,line:i.line,column:i.column}]}}),Hm=/origin(?!al)|\.source\s*[!=]==?/i,Um=/\b(?:event|e|evt|msg|message)\.data\b/,Wm=/\b(?:const|let|var)\s+[^;=]*=\s*$/,Gm=/port\d?\b|worker|channel|broadcast|socket|\bws\b|\bsse\b|eventsource|^_?self\.|^source\./i,Km=String.raw`=\s*new\s+(?:EventSource|WebSocket|Worker|SharedWorker|BroadcastChannel|MessageChannel)\b`,qm=(e,t)=>{let n=/^[\w$]+/.exec(e)?.[0];return n===void 0?!1:RegExp(`(?<![\\w$.])${K(n)}\\s*${Km}`).test(t)},Jm=(e,t)=>{let n=/^[\w$]+/.exec(e)?.[0];return n===void 0?!1:RegExp(`(?:^|[(,{;]\\s*)${K(n)}\\s*[?!]?\\s*:\\s*(?:Worker|SharedWorker|MessagePort|BroadcastChannel|WebSocket|EventSource)\\b`,`m`).test(t)},Ym=/worker/i,Xm=(e,t)=>{let n=J(t),r=_d(t);return n<0||r<0?``:e.slice(n,r)},Zm=(e,t)=>{if(t.type===`CallExpression`){let n=y(t.callee)?Xm(e,t.callee):``;if(!n.endsWith(`addEventListener`))return null;let r=t.arguments[0];return y(r)&&r.type===`Literal`&&r.value===`message`?n:null}if(t.type===`AssignmentExpression`&&y(t.left)){let n=Xm(e,t.left);return n.endsWith(`.onmessage`)?n:null}return null},Qm=H({id:`postmessage-origin-risk`,title:`postMessage handler without origin check`,severity:`warn`,recommendation:"Validate `event.origin` against an exact allowlist before using `event.data`, especially when an iframe or parent window can be attacker-controlled.",scan:e=>{if(!W(e.relativePath)||Ym.test(e.relativePath))return[];let t=e.content.includes(`onmessage`),n=e.content.includes(`addEventListener`)&&(e.content.includes(`message`)||e.content.includes(`\\`));if(!t&&!n)return[];let r=Ut({filename:e.absolutePath,sourceText:e.content,shouldAttachParentReferences:!1});if(r===null)return[];let i=[];return k(r,t=>{let n=Zm(e.content,t);if(n===null||Gm.test(n)||qm(n,e.content)||Jm(n,e.content))return;let r=Xm(e.content,t),a=r.search(Um);if(a<0)return;let o=r.search(Hm);if(o>=0&&(Wm.test(r.slice(0,a))||o<a))return;let s=U(e.content,J(t));i.push({message:`A message event handler reads cross-window messages without an obvious origin check.`,line:s.line,column:s.column})}),i}}),$m=/(?:^|\/)(?:locales?|i18n|lang|langs|translations?)\//i,eh=/(?:^|\/)(?:\.env(?:\.[^/]*)?|(?:debug|crash|trace|stack[-_]?trace|report|dump|phpinfo)(?:[-_.][^/]*)?\.(?:txt|log|json|html?)|[^/]+\.log)$/i,th=e=>yl(e,ml.test(e))&&!$m.test(e)&&eh.test(e),nh=H({id:`public-debug-artifact`,title:`Public debug artifact`,severity:`warn`,recommendation:`Remove debug artifacts from public output; logs and dumps often reveal source paths, internal routes, tokens, or environment snapshots.`,scan:e=>{if(!th(e.relativePath))return[];let t={message:`A browser-reachable debug, log, dump, report, or env artifact is present.`,line:1,column:1};return[nu.some(t=>t.test(e.content))?{...t,severity:`error`}:t]}}),rh=/(?:^|\/)docs?\//i,ih=H({id:`public-env-secret-name`,title:`Secret-like public env variable`,severity:`warn`,recommendation:`Public env prefixes are inlined into browser bundles. Rename public values to non-secret names, and keep tokens, passwords, private keys, and service-role credentials server-only.`,scan:e=>{if(!xp(e.relativePath)||rh.test(e.relativePath))return[];let t=Au(e.content);if(t===void 0)return[];let n=tl(e.content,t);return[{message:`Client code references a public env variable whose name looks like a secret or privileged credential.`,line:n.line,column:n.column}]}}),ah=`(?:"[^"\\n]{0,200}"|'[^'\\n]{0,200}'|\`[^\`$]{0,200}\`)`,oh=`\\s*\\(?\\s*(?:${ah}|(?:[\\w\$]+(?:\\.[\\w\$]+)*\\.escape(?:Id|Literal|Identifier)|(?:[\\w\$]+\\.)*(?:connection|conn|client|pool|db|mysql|sqlstring|knex)\\.escape)\\s*\\([^()]{0,200}\\))\\s*\\)?`,sh=H({id:`raw-sql-injection-risk`,title:`Raw SQL built outside parameter binding`,severity:`warn`,recommendation:`Keep user input in driver parameters or ORM bind variables. Avoid unsafe/raw SQL helpers and string interpolation for queries.`,scan:G({shouldScan:e=>cd(e.relativePath),pattern:[/\$queryRawUnsafe\s*\(/,/\$executeRawUnsafe\s*\(/,/\bPrisma\.raw\s*\((?!\s*(?:["'][^"'\n]*["']\s*[,)]|`[^`$]*`))/,/\bsql\.\s*(?:raw|unsafe)\s*\((?!\s*(?:["'][^"'\n]*["']\s*[,)]|`[^`$]*`))/,/\b(?:client|pool|conn)\.query\s*\(\s*['"`]\s*(?:SELECT|INSERT|UPDATE|DELETE)\b[^)]{0,400}\$\{(?!\s*[\w$.]*(?:sanitiz|escape|quote)[\w$]*\s*\()/i,RegExp(`\\.query\\s*\\(\\s*${ah}(?:\\s*\\+${oh})*\\s*\\+(?!${oh})`,`i`),/\.(?:where|orderBy|having)Raw\s*\((?!\s*(?:["'][^"'\n]*["']\s*[,)]|`[^`$]*`))/,/\bcursor\.execute\s*\(\s*f['"]/,/\bcursor\.execute\s*\(\s*(?:"[^"]{0,400}"|'[^']{0,400}')\s*(?:%|\.format\s*\(|\+)/,/\b(?:engine|session)\.execute\s*\(\s*(?:text\s*\(\s*)?f['"]/,/\$[\w]+->(?:query|exec|prepare|executeQuery|executeStatement|createQuery|createNativeQuery)\s*\(\s*(?:"[^"]{0,400}"|'[^']{0,400}')\s*\.\s*\$/,/mysqli_query\s*\(\s*[^,]+,\s*(?:"[^"]{0,400}"|'[^']{0,400}')\s*\.\s*\$/],message:`Code uses a raw SQL escape hatch or string-built query shape that can bypass parameter binding.`})}),ch=e=>rl.test(e)||/(?:^|\/)\.npmrc$/.test(e)||/(?:^|\/)[^/]*(?:credential|credentials|service-account|serviceAccount|firebase-admin|google-service-account|gcp-service-account)[^/]*\.(?:json|env|pem|key)$/i.test(e),lh=e=>/(?:^|\/)\.env(?:\.[^./]+)*\.(?:example|sample|template|dist|defaults?)$|(?:^|\/)[^/]*(?:example|sample|template)[^/]*\.(?:env|json|pem|key)$/i.test(e),uh=H({id:`repository-secret-file`,title:`Secret file checked into repository`,severity:`error`,committedFilesOnly:!0,recommendation:`Remove committed env files, service-account credentials, npm auth tokens, and webhook URLs; rotate exposed values and keep only redacted examples in source.`,scan:e=>{if(!ch(e.relativePath)||lh(e.relativePath)||cl.test(e.relativePath))return[];let t=nu.find(t=>t.test(e.content))??Au(e.content);if(t===void 0)return[];let n=tl(e.content,t);return[{message:`A repository credential/config file contains secret-looking values.`,line:n.line,column:n.column}]}}),dh=`(?:req|request|ctx\\.req|ctx\\.request)\\.(?:body|query|params)|await\\s+(?:req|request)\\.json\\(\\s*\\)`,fh=H({id:`request-body-mass-assignment`,title:`Request input spread without field allowlist`,severity:`warn`,recommendation:"Assign explicit, allowlisted fields (or validate with a strict schema and no `.passthrough()`) instead of spreading/merging request input. Otherwise the client can set ownership, role, or price columns (mass assignment) or pollute the prototype.",scan:G({shouldScan:e=>W(e.relativePath),pattern:[RegExp(`\\.\\.\\.\\s*(?:${dh})`,`i`),RegExp(`(?:Object\\.assign\\s*\\(|_\\.(?:merge|mergeWith|defaultsDeep)\\s*\\(|(?:^|[^.\\w])(?:merge|defaultsDeep)\\s*\\()[\\s\\S]{0,80}?(?:${dh})`,`i`)],message:`Request input is spread or merged into an object without a field allowlist, enabling mass assignment (client-set owner/role/price fields) or prototype pollution.`})}),ph=H({id:`secret-in-fallback`,title:`Hardcoded secret fallback for env var`,severity:`error`,recommendation:"Remove the literal fallback and fail closed (throw when the variable is unset). The hardcoded value is a committed secret, and the `??`/`||` default makes the app run with it in any environment that forgot to set the var.",scan:G({shouldScan:e=>W(e.relativePath),pattern:/\bprocess\.env\.(?!(?:(?:(?:NEXT|EXPO|GATSBY|NUXT|REACT_APP|VITE)_)?PUBLIC_[A-Z0-9_]*(?<!_SECRET)(?<!_PRIVATE_KEY)(?<!_PASSWORD)(?<!_PASSWD)|[A-Z0-9_]*(?:PUBLISHABLE|ANON)_KEY)(?![A-Z0-9_]))[A-Z][A-Z0-9_]*(?:SECRET|TOKEN|PASSWORD|PASSWD|PRIVATE_KEY|API_?KEY|APIKEY|ACCESS_KEY|CLIENT_SECRET|CREDENTIAL|SIGNING_KEY|ENCRYPTION_KEY|WEBHOOK_SECRET|SERVICE_ROLE)[A-Z0-9_]*(?<!_(?:NAME|HEADER|ENDPOINT|URL|URI|ID|PREFIX|SUFFIX|PARAM|PARAMS|FIELD|ISSUER|AUDIENCE|ALGORITHM|ALG|REGION|BUCKET|HOST|HOSTNAME|PORT|PATH|VERSION|SCOPE|TYPE|FORMAT|EXPIRY|TTL))\s*(?:\?\?|\|\|)\s*(["'`])(?![0-9]+["'`])(?![a-z][a-z0-9]*(?:[_-][a-z0-9]+)*[_-](?:token|secret|key|password|passwd|credential)s?["'`])(?![\w-]{0,12}0{8,}["'`])(?!test test test test test test test test test test test junk["'`])(?!(?:changeme|change[_-]?me|placeholder|your[_-]|example|sample|dummy|development|local|todo|replace[_-]?me|https?:\/\/|x{3,}|\*{3,}))[^"'`\n]{8,}\1/i,message:`A secret env var has a hardcoded string fallback: the literal is a committed secret and the app fails open (uses it) when the variable is unset.`})}),mh=(e,t)=>{if(!x(e,`Program`))return!1;for(let n of e.body){if(!x(n,`ExpressionStatement`)||n.directive===void 0)return!1;if(n.directive===t)return!0}return!1},hh=(e,t=`source.tsx`)=>{let n=Ut({filename:t,sourceText:e,shouldAttachParentReferences:!1});return n===null?!1:mh(n,`use server`)},gh=G({shouldScan:e=>xp(e.relativePath),pattern:/\b(?:ownerId|ownerID|creatorId|creatorID|userId|userID|uid|providerId|providerID|orgId|orgID|tenantId|tenantID|teamId|teamID|workspaceId|workspaceID|ghostOrg|role|roles|isAdmin|admin)\b/,requireAll:[/\b(?:supabase\b|\.from\s*\(\s*["'][^"']+["']\s*\))[\s\S]{0,700}\b(?:insert|upsert|update)\s*\(\s*(?:\{|\[?\s*\{)[\s\S]{0,700}\b(?:ownerId|creatorId|userId|orgId|tenantId|role|isAdmin)\b/i],message:`Client Supabase code appears to write user, tenant, owner, or role fields that should be enforced by RLS.`}),_h=H({id:`supabase-client-owned-authz-field`,title:`Client writes Supabase authorization field`,severity:`error`,recommendation:"Use RLS policies based on `auth.uid()` and server-owned membership rows; do not trust client-provided owner, org, or role columns.",scan:e=>{let t=gh(e);return t.length===0?t:hh(e.content,e.relativePath)?[]:t}}),vh=e=>/(?:^|\/)supabase\/(?:migrations|schemas)\//.test(e),yh=e=>e.endsWith(`.sql`)||vh(e),bh=/^\$[A-Za-z_]?\w*\$/,xh=new Set([`do`,`plpgsql`,`sql`,`plpython3u`,`plpythonu`,`plperl`,`plperlu`,`plv8`]),Sh=(e,t)=>{let n=t-1;for(;n>=0&&/\s/.test(e[n]??``);)--n;let r=n;for(;r>=0&&/[A-Za-z0-9_]/.test(e[r]??``);)--r;return e.slice(r+1,n+1).toLowerCase()},Ch=(e,t,n,r)=>{let i=n,a=!1;for(;i<r;){let n=e[i];if(n===`;`){a=!1,i+=1;continue}if(/[A-Za-z_]/.test(n)){let t=i;for(;t<r&&/[A-Za-z0-9_]/.test(e[t]??``);)t+=1;e.slice(i,t).toLowerCase()===`execute`&&(a=!0),i=t;continue}if(n===`'`){let n=a;for(n||(t[i]=` `),i+=1;i<r;){if(e[i]===`'`){if(e[i+1]===`'`){n||(t[i]=` `,t[i+1]=` `),i+=2;continue}n||(t[i]=` `),i+=1;break}!n&&e[i]!==`
|
|
21
|
+
`&&(t[n]=` `),n+=1;continue}(i===`"`||i===`'`||i==="`")&&(r=i),n+=1}return t.join(``)},pm=/cookie\s*:\s*\{/gi,mm=RegExp(`document\\.cookie\\s*=\\s*[\`"'][^\`"'=;]*?${sm}[^\`"'=;]*=`,`gi`),hm=e=>{if(e.trim().length===0)return 0;let t=0,n=null,r=1;for(let i=0;i<e.length;i+=1){let a=e[i];if(n!==null){a===`\\`?i+=1:a===n&&(n=null);continue}a===`"`||a===`'`||a==="`"?n=a:a===`(`||a===`[`||a===`{`?t+=1:a===`)`||a===`]`||a===`}`?--t:a===`,`&&t===0&&(r+=1)}return r},gm=(e,t,n,r,i)=>{t.lastIndex=0;for(let a=t.exec(e);a!==null;a=t.exec(e)){if(!r(a.index,a[0]))continue;let t=U(e,a.index);i.push({message:n,line:t.line,column:t.column})}},_m=H({id:`insecure-session-cookie`,title:`Auth cookie missing HttpOnly protection`,severity:`warn`,recommendation:"Set auth/session cookies server-side with `httpOnly: true`, `secure: true`, and `sameSite`. Cookies set via `document.cookie` or with `httpOnly: false` are readable by any XSS payload and can be stolen.",scan:e=>{if(!W(e.relativePath))return[];let t=zl(e);if(!/cookie/i.test(t))return[];let n=[],r=`An auth/session cookie is exposed to JavaScript (set via document.cookie, with httpOnly: false, or without cookie options), letting an XSS payload steal it.`;lm.lastIndex=0;for(let e=lm.exec(t);e!==null;e=lm.exec(t)){let i=e.index+e[0].lastIndexOf(`(`),a=gd(t,i);if(a<0)continue;let o=t.slice(i+1,a),s=hm(o)<3,c=o.replace(dm,``);if(!s&&!um.test(c))continue;let l=U(t,e.index);n.push({message:r,line:l.line,column:l.column})}let i=fm(t);pm.lastIndex=0;for(let e=pm.exec(i);e!==null;e=pm.exec(i)){let t=e.index+e[0].length-1,a=gd(i,t),o=a>=0?i.slice(t,a):i.slice(t,t+400);if(!um.test(o))continue;let s=U(i,e.index);n.push({message:r,line:s.line,column:s.column})}return gm(t,mm,r,()=>!0,n),n}}),vm=/\b(?:alg|algorithms?)\s*:\s*\[?\s*["'`]none["'`]/gi,ym=(e,t)=>{let n=null,r=[];for(let i=0;i<t;i+=1){let t=e[i];if(n==="`"){t===`\\`?i+=1:t==="`"?n=null:t===`$`&&e[i+1]===`{`&&(r.push(0),n=null,i+=1);continue}if(n!==null){t===`\\`?i+=1:t===n&&(n=null);continue}if(t===`"`||t===`'`||t==="`")n=t;else if(r.length>0){let e=r.length-1;t===`{`?r[e]+=1:t===`}`&&(r[e]===0?(r.pop(),n="`"):--r[e])}}return n!==null},bm=H({id:`jwt-insecure-verification`,title:`JWT verified with the 'none' algorithm`,severity:`error`,recommendation:"Never accept the `none` algorithm; it disables signature verification and lets any forged token through. Pin the real algorithm(s) explicitly (`jwt.verify(token, key, { algorithms: ['RS256'] })`).",scan:e=>{if(!W(e.relativePath))return[];let t=zl(e);if(!/\bjwt\b|jsonwebtoken|\bjose\b/i.test(t))return[];let n=[];vm.lastIndex=0;for(let e=vm.exec(t);e!==null;e=vm.exec(t)){if(ym(t,e.index))continue;let r=U(t,e.index);n.push({message:`JWT is configured with the 'none' algorithm, which disables signature verification, so any forged token is accepted.`,line:r.line,column:r.column})}return n}}),xm=/PRIVATE KEY|SSH_PRIVATE_KEY|GPG_PRIVATE_KEY|DEPLOY_KEY|SIGNING_KEY/i,Sm=H({id:`key-lifecycle-risk`,title:`Long-lived key material in repository`,severity:`error`,committedFilesOnly:!0,recommendation:`Remove private keys from source, rotate exposed credentials, prefer short-lived deploy credentials, and document revocation/expiry for release keys.`,scan:G({shouldScan:e=>!cl.test(e.relativePath)&&!fl.test(e.relativePath)&&xm.test(e.content),pattern:/(?<!(?:placeholder|example|sample|dummy|fake)[\s\S]{0,40})-----BEGIN (?:RSA |EC |OPENSSH |DSA )?PRIVATE KEY-----(?:\s|\\r|\\n)*[A-Za-z0-9+/=][A-Za-z0-9+/=\s]{38,}(?![^-]{0,160}\.\.\.)|\b(?:SSH_PRIVATE_KEY|GPG_PRIVATE_KEY|DEPLOY_KEY|SIGNING_KEY)\b\s*[:=]\s*["'][^"'\n]{16,}["']/i,message:`Private or long-lived release key material appears in the repository.`})}),Cm=H({id:`local-rpc-native-bridge-risk`,title:`Weak localhost native bridge boundary`,severity:`warn`,recommendation:`Use exact origin allowlists after URL parsing, per-request nonces, narrow methods, and never expose install/update commands to arbitrary web pages.`,scan:G({shouldScan:e=>W(e.relativePath),pattern:/\b(?:Access-Control-Allow-Origin|websocket|WebSocket|(?:127\.0\.0\.1|localhost)(?=[\s\S]{0,700}\b(?:UpdateApp|InstallApp)\b)|(?:127\.0\.0\.1|localhost)[\s\S]{0,350}\b(?:onmessage|message|request|req)\b)\b[\s\S]{0,700}(?:\b(?:UpdateApp|InstallApp|child_process)\b|(?<![.\w$])(?:exec(?:File)?(?:Sync)?|spawn(?:Sync)?)\s*\()/i,message:`Code appears to bridge browser code to localhost/native capabilities with weak origin or update/install checks.`})}),wm=/\bfrom\s+["']@modelcontextprotocol\/sdk[^"']*["']|\bMcpServer\b|\bMcpAgent\b/,Tm=/@modelcontextprotocol\/sdk|\bMcpServer\b|\bMcpAgent\b/,Em=/\bserver\.\s*tool\s*\(|\bregisterTool\s*\(|\bsetRequestHandler\s*\(\s*CallToolRequestSchema/,Dm=/\b(?:tool|registerTool|setRequestHandler)\b/,Om=H({id:`mcp-tool-capability-risk`,title:`MCP tool exposes dangerous capability`,severity:`warn`,recommendation:`MCP tool calls run with the connecting client's authority. Validate inputs, enforce per-tool authorization, and avoid raw filesystem/shell/network access where possible.`,scan:G({shouldScan:e=>W(e.relativePath)&&Tm.test(e.content)&&Dm.test(e.content)&&gl.test(e.content),pattern:Em,requireAll:[wm,gl],ignoreStringLiterals:!0,message:`An MCP tool/resource/prompt handler appears to expose file, shell, network, or code-execution capability.`})}),km=H({id:`mdx-ssr-execution-risk`,title:`Server-rendered MDX can execute code`,severity:`warn`,recommendation:`Use a constrained compiler for untrusted content, disable expressions/raw HTML, sandbox renderers, and avoid caching attacker-controlled output across tenants.`,scan:G({shouldScan:e=>W(e.relativePath),pattern:/(?:@mdx-js\/mdx|next-mdx-remote|\b(?:MDXRemote|compileMDX|evaluateMdx)\b)[\s\S]{0,700}\b(?:repo\b|customer|tenant|user[-_]?(?:content|markdown|mdx|input|provided|generated|submitted)|untrusted|searchParams|req\.|request\.|fetch\s*\(|prisma\.|db\.|database|rehypeRaw|allowDangerousHtml)/i,message:`MDX/markdown rendering code may evaluate user or repository content during SSR or static generation.`})}),Am=H({id:`nosql-injection-risk`,title:`NoSQL query accepts operator-shaped input`,severity:`warn`,recommendation:"Coerce scalar fields before querying, reject operator keys from client input, and avoid `$where` or request-derived regexes.",scan:G({shouldScan:e=>Sl(e.relativePath,ol),pattern:/\$where\s*['"]?\s*:\s*(?:f?['"`][^'"`]{0,200}\$\{|function|f['"])|\.find\s*\(\s*JSON\.parse\s*\(\s*(?:req|request)\.|\.aggregate\s*\(\s*\[?\s*\{[^}]{0,400}\$where|\bnew\s+RegExp\s*\(\s*(?:req|request)\.|\$regex['"]?\s*:\s*(?:req|request)\./i,message:"Code appears to pass raw JSON, regex, or `$where` style input into a NoSQL query."})}),jm=[...nu.filter(e=>!e.test(`service_role`)),ru],Mm=H({id:`package-metadata-secret`,title:`Secret-like package metadata`,severity:`warn`,recommendation:`Keep secrets out of package metadata and generated reports; they are often published to registries, logs, or browser artifacts.`,scan:e=>{if(!e.relativePath.endsWith(`package.json`))return[];let t=Au(e.content)??jm.find(t=>t.test(e.content));if(t===void 0)return[];let n=tl(e.content,t);return[{message:`Package metadata contains secret-like values or public env secret names.`,line:n.line,column:n.column}]}}),Nm=H({id:`path-traversal-risk`,title:`Filesystem path uses caller input`,severity:`warn`,recommendation:`Resolve paths against a fixed base directory, reject traversal after normalization, and map user-visible identifiers to server-owned paths.`,scan:G({shouldScan:e=>W(e.relativePath)&&!sd(e.relativePath),pattern:/\b(?:readFile|readFileSync|writeFile|writeFileSync)\s*\(\s*(?:req\.|request\.|params\.|query\.|body\.|parsed\.|`[^`]*(?<![-./\w$'"`])(?:req\.|request\.|params\.|query\.|body\.))|\bpath\.(?:join|resolve)\s*\([^)]*(?<![-./\w$'"`])(?<!basename\(\s{0,4})(?:req\.|request\.|params\.|query\.|body\.|parsed\.)/,message:`Filesystem access appears to use request, query, params, or body data as part of the path.`})}),Pm=/\b(?:repoUrl|updateUrl|UpdateApp|InstallApp|auto.?updater?|installer|curl(?!\s+(?:-T\b|--upload-file\b))|wget)\b[\s\S]{0,250}(?:\.(?:zip|exe|dmg|appimage|msi|deb|rpm)\b|\.tar\.gz\b|\|\s*(?:bash|sh)\b)/i,Fm=/\b(?:curl|wget)\b(?=(?:[^\r\n]|\\\r?\n)*https?:\/\/)(?:[^\r\n]|\\\r?\n)*?\|\s*(?:bash|sh)\b/i,Im=/sha(?:256|512|1)sum|--checksum|checksum=|EXPECTED_SHA|gpg\s+--verify|\.sha(?:256|512)\b/i,Lm=/\b(?:child_process|childProcess|execa|os\.system|subprocess\.|Deno\.run|autoUpdater|electron-updater)\b|\b(?:exec(?:File)?(?:Sync)?|spawn(?:Sync)?)\s*\(/,Rm=/.\/\.github\/workflows\//i,zm=/(?:^|\/)[\w-]+[.@-]\d+\.\d+\.\d+(?:[-.][\w.]+)?\//,Bm=e=>bl(e)&&!Rm.test(e)&&!zm.test(e)&&!dl.test(e),Vm=H({id:`plugin-update-trust-risk`,title:`Plugin or updater trust boundary risk`,severity:`warn`,recommendation:`Require signed updates/plugins, pin trusted repositories, verify hashes before execution, and keep custom repository installs behind explicit warnings.`,scan:e=>{if(!W(e.relativePath)&&!Bm(e.relativePath))return[];let t=zl(e),n=Fm.test(t);if(!n&&!Pm.test(t))return[];let r=n?Fm:Pm;if(Im.test(t)||il.test(e.relativePath)&&!Lm.test(t))return[];let i=tl(t,r);return[{message:`Code appears to download, install, update, or execute plugin/updater content across a trust boundary.`,line:i.line,column:i.column}]}}),Hm=/origin(?!al)|\.source\s*[!=]==?/i,Um=/\b(?:event|e|evt|msg|message)\.data\b/,Wm=/\b(?:const|let|var)\s+[^;=]*=\s*$/,Gm=/port\d?\b|worker|channel|broadcast|socket|\bws\b|\bsse\b|eventsource|^_?self\.|^source\./i,Km=String.raw`=\s*new\s+(?:EventSource|WebSocket|Worker|SharedWorker|BroadcastChannel|MessageChannel)\b`,qm=(e,t)=>{let n=/^[\w$]+/.exec(e)?.[0];return n===void 0?!1:RegExp(`(?<![\\w$.])${K(n)}\\s*${Km}`).test(t)},Jm=(e,t)=>{let n=/^[\w$]+/.exec(e)?.[0];return n===void 0?!1:RegExp(`(?:^|[(,{;]\\s*)${K(n)}\\s*[?!]?\\s*:\\s*(?:Worker|SharedWorker|MessagePort|BroadcastChannel|WebSocket|EventSource)\\b`,`m`).test(t)},Ym=/worker/i,Xm=(e,t)=>{let n=J(t),r=_d(t);return n<0||r<0?``:e.slice(n,r)},Zm=(e,t)=>{if(t.type===`CallExpression`){let n=y(t.callee)?Xm(e,t.callee):``;if(!n.endsWith(`addEventListener`))return null;let r=t.arguments[0];return y(r)&&r.type===`Literal`&&r.value===`message`?n:null}if(t.type===`AssignmentExpression`&&y(t.left)){let n=Xm(e,t.left);return n.endsWith(`.onmessage`)?n:null}return null},Qm=H({id:`postmessage-origin-risk`,title:`postMessage handler without origin check`,severity:`warn`,recommendation:"Validate `event.origin` against an exact allowlist before using `event.data`, especially when an iframe or parent window can be attacker-controlled.",scan:e=>{if(!W(e.relativePath)||Ym.test(e.relativePath))return[];let t=e.content.includes(`onmessage`),n=e.content.includes(`addEventListener`)&&(e.content.includes(`message`)||e.content.includes(`\\`));if(!t&&!n)return[];let r=Ut({filename:e.absolutePath,sourceText:e.content,shouldAttachParentReferences:!1});if(r===null)return[];let i=[];return k(r,t=>{let n=Zm(e.content,t);if(n===null||Gm.test(n)||qm(n,e.content)||Jm(n,e.content))return;let r=Xm(e.content,t),a=r.search(Um);if(a<0)return;let o=r.search(Hm);if(o>=0&&(Wm.test(r.slice(0,a))||o<a))return;let s=U(e.content,J(t));i.push({message:`A message event handler reads cross-window messages without an obvious origin check.`,line:s.line,column:s.column})}),i}}),$m=/(?:^|\/)(?:locales?|i18n|lang|langs|translations?)\//i,eh=/(?:^|\/)(?:\.env(?:\.[^/]*)?|(?:debug|crash|trace|stack[-_]?trace|report|dump|phpinfo)(?:[-_.][^/]*)?\.(?:txt|log|json|html?)|[^/]+\.log)$/i,th=e=>yl(e,ml.test(e))&&!$m.test(e)&&eh.test(e),nh=H({id:`public-debug-artifact`,title:`Public debug artifact`,severity:`warn`,recommendation:`Remove debug artifacts from public output; logs and dumps often reveal source paths, internal routes, tokens, or environment snapshots.`,scan:e=>{if(!th(e.relativePath))return[];let t={message:`A browser-reachable debug, log, dump, report, or env artifact is present.`,line:1,column:1};return[nu.some(t=>t.test(e.content))?{...t,severity:`error`}:t]}}),rh=/(?:^|\/)docs?\//i,ih=H({id:`public-env-secret-name`,title:`Secret-like public env variable`,severity:`warn`,recommendation:`Public env prefixes are inlined into browser bundles. Rename public values to non-secret names, and keep tokens, passwords, private keys, and service-role credentials server-only.`,scan:e=>{if(!xp(e.relativePath)||rh.test(e.relativePath))return[];let t=Au(e.content);if(t===void 0)return[];let n=tl(e.content,t);return[{message:`Client code references a public env variable whose name looks like a secret or privileged credential.`,line:n.line,column:n.column}]}}),ah=`(?:"[^"\\n]{0,200}"|'[^'\\n]{0,200}'|\`[^\`$]{0,200}\`)`,oh=`\\s*\\(?\\s*(?:${ah}|(?:[\\w\$]+(?:\\.[\\w\$]+)*\\.escape(?:Id|Literal|Identifier)|(?:[\\w\$]+\\.)*(?:connection|conn|client|pool|db|mysql|sqlstring|knex)\\.escape)\\s*\\([^()]{0,200}\\))\\s*\\)?`,sh=H({id:`raw-sql-injection-risk`,title:`Raw SQL built outside parameter binding`,severity:`warn`,recommendation:`Keep user input in driver parameters or ORM bind variables. Avoid unsafe/raw SQL helpers and string interpolation for queries.`,scan:G({shouldScan:e=>cd(e.relativePath),pattern:[/\$queryRawUnsafe\s*\(/,/\$executeRawUnsafe\s*\(/,/\bPrisma\.raw\s*\((?!\s*(?:["'][^"'\n]*["']\s*[,)]|`[^`$]*`))/,/\bsql\.\s*(?:raw|unsafe)\s*\((?!\s*(?:["'][^"'\n]*["']\s*[,)]|`[^`$]*`))/,/\b(?:client|pool|conn)\.query\s*\(\s*['"`]\s*(?:SELECT|INSERT|UPDATE|DELETE)\b[^)]{0,400}\$\{(?!\s*[\w$.]*(?:sanitiz|escape|quote)[\w$]*\s*\()/i,RegExp(`\\.query\\s*\\(\\s*${ah}(?:\\s*\\+${oh})*\\s*\\+(?!${oh})`,`i`),/\.(?:where|orderBy|having)Raw\s*\((?!\s*(?:["'][^"'\n]*["']\s*[,)]|`[^`$]*`))/,/\bcursor\.execute\s*\(\s*f['"]/,/\bcursor\.execute\s*\(\s*(?:"[^"]{0,400}"|'[^']{0,400}')\s*(?:%|\.format\s*\(|\+)/,/\b(?:engine|session)\.execute\s*\(\s*(?:text\s*\(\s*)?f['"]/,/\$[\w]+->(?:query|exec|prepare|executeQuery|executeStatement|createQuery|createNativeQuery)\s*\(\s*(?:"[^"]{0,400}"|'[^']{0,400}')\s*\.\s*\$/,/mysqli_query\s*\(\s*[^,]+,\s*(?:"[^"]{0,400}"|'[^']{0,400}')\s*\.\s*\$/],message:`Code uses a raw SQL escape hatch or string-built query shape that can bypass parameter binding.`})}),ch=e=>rl.test(e)||/(?:^|\/)\.npmrc$/.test(e)||/(?:^|\/)[^/]*(?:credential|credentials|service-account|serviceAccount|firebase-admin|google-service-account|gcp-service-account)[^/]*\.(?:json|env|pem|key)$/i.test(e),lh=e=>/(?:^|\/)\.env(?:\.[^./]+)*\.(?:example|sample|template|dist|defaults?)$|(?:^|\/)[^/]*(?:example|sample|template)[^/]*\.(?:env|json|pem|key)$/i.test(e),uh=H({id:`repository-secret-file`,title:`Secret file checked into repository`,severity:`error`,committedFilesOnly:!0,recommendation:`Remove committed env files, service-account credentials, npm auth tokens, and webhook URLs; rotate exposed values and keep only redacted examples in source.`,scan:e=>{if(!ch(e.relativePath)||lh(e.relativePath)||cl.test(e.relativePath))return[];let t=nu.find(t=>t.test(e.content))??Au(e.content);if(t===void 0)return[];let n=tl(e.content,t);return[{message:`A repository credential/config file contains secret-looking values.`,line:n.line,column:n.column}]}}),dh=`(?:req|request|ctx\\.req|ctx\\.request)\\.(?:body|query|params)|await\\s+(?:req|request)\\.json\\(\\s*\\)`,fh=H({id:`request-body-mass-assignment`,title:`Request input spread without field allowlist`,severity:`warn`,recommendation:"Assign explicit, allowlisted fields (or validate with a strict schema and no `.passthrough()`) instead of spreading/merging request input. Otherwise the client can set ownership, role, or price columns (mass assignment) or pollute the prototype.",scan:G({shouldScan:e=>W(e.relativePath),pattern:[RegExp(`\\.\\.\\.\\s*(?:${dh})`,`i`),RegExp(`(?:Object\\.assign\\s*\\(|_\\.(?:merge|mergeWith|defaultsDeep)\\s*\\(|(?:^|[^.\\w])(?:merge|defaultsDeep)\\s*\\()[\\s\\S]{0,80}?(?:${dh})`,`i`)],message:`Request input is spread or merged into an object without a field allowlist, enabling mass assignment (client-set owner/role/price fields) or prototype pollution.`})}),ph=H({id:`secret-in-fallback`,title:`Hardcoded secret fallback for env var`,severity:`error`,recommendation:"Remove the literal fallback and fail closed (throw when the variable is unset). The hardcoded value is a committed secret, and the `??`/`||` default makes the app run with it in any environment that forgot to set the var.",scan:G({shouldScan:e=>W(e.relativePath),pattern:/\bprocess\.env\.(?!(?:(?:(?:NEXT|EXPO|GATSBY|NUXT|REACT_APP|VITE)_)?PUBLIC_[A-Z0-9_]*(?<!_SECRET)(?<!_PRIVATE_KEY)(?<!_PASSWORD)(?<!_PASSWD)|[A-Z0-9_]*(?:PUBLISHABLE|ANON)_KEY)(?![A-Z0-9_]))[A-Z][A-Z0-9_]*(?:SECRET|TOKEN|PASSWORD|PASSWD|PRIVATE_KEY|API_?KEY|APIKEY|ACCESS_KEY|CLIENT_SECRET|CREDENTIAL|SIGNING_KEY|ENCRYPTION_KEY|WEBHOOK_SECRET|SERVICE_ROLE)[A-Z0-9_]*(?<!_(?:NAME|HEADER|ENDPOINT|URL|URI|ID|PREFIX|SUFFIX|PARAM|PARAMS|FIELD|ISSUER|AUDIENCE|ALGORITHM|ALG|REGION|BUCKET|HOST|HOSTNAME|PORT|PATH|VERSION|SCOPE|TYPE|FORMAT|EXPIRY|TTL))\s*(?:\?\?|\|\|)\s*(["'`])(?![0-9]+["'`])(?![a-z][a-z0-9]*(?:[_-][a-z0-9]+)*[_-](?:token|secret|key|password|passwd|credential)s?["'`])(?![\w-]{0,12}0{8,}["'`])(?!test test test test test test test test test test test junk["'`])(?!(?:changeme|change[_-]?me|placeholder|your[_-]|example|sample|dummy|development|local|todo|replace[_-]?me|https?:\/\/|x{3,}|\*{3,}))[^"'`\n]{8,}\1/i,message:`A secret env var has a hardcoded string fallback: the literal is a committed secret and the app fails open (uses it) when the variable is unset.`})}),mh=(e,t)=>{let n=null;if(x(e,`Program`)?n=e.body:(x(e,`FunctionDeclaration`)||x(e,`FunctionExpression`)||x(e,`ArrowFunctionExpression`))&&x(e.body,`BlockStatement`)&&(n=e.body.body),n===null)return!1;for(let e of n){if(!x(e,`ExpressionStatement`)||e.directive===void 0)return!1;if(e.directive===t)return!0}return!1},hh=(e,t=`source.tsx`)=>{let n=Ut({filename:t,sourceText:e,shouldAttachParentReferences:!1});return n===null?!1:mh(n,`use server`)},gh=G({shouldScan:e=>xp(e.relativePath),pattern:/\b(?:ownerId|ownerID|creatorId|creatorID|userId|userID|uid|providerId|providerID|orgId|orgID|tenantId|tenantID|teamId|teamID|workspaceId|workspaceID|ghostOrg|role|roles|isAdmin|admin)\b/,requireAll:[/\b(?:supabase\b|\.from\s*\(\s*["'][^"']+["']\s*\))[\s\S]{0,700}\b(?:insert|upsert|update)\s*\(\s*(?:\{|\[?\s*\{)[\s\S]{0,700}\b(?:ownerId|creatorId|userId|orgId|tenantId|role|isAdmin)\b/i],message:`Client Supabase code appears to write user, tenant, owner, or role fields that should be enforced by RLS.`}),_h=H({id:`supabase-client-owned-authz-field`,title:`Client writes Supabase authorization field`,severity:`error`,recommendation:"Use RLS policies based on `auth.uid()` and server-owned membership rows; do not trust client-provided owner, org, or role columns.",scan:e=>{let t=gh(e);return t.length===0?t:hh(e.content,e.relativePath)?[]:t}}),vh=e=>/(?:^|\/)supabase\/(?:migrations|schemas)\//.test(e),yh=e=>e.endsWith(`.sql`)||vh(e),bh=/^\$[A-Za-z_]?\w*\$/,xh=new Set([`do`,`plpgsql`,`sql`,`plpython3u`,`plpythonu`,`plperl`,`plperlu`,`plv8`]),Sh=(e,t)=>{let n=t-1;for(;n>=0&&/\s/.test(e[n]??``);)--n;let r=n;for(;r>=0&&/[A-Za-z0-9_]/.test(e[r]??``);)--r;return e.slice(r+1,n+1).toLowerCase()},Ch=(e,t,n,r)=>{let i=n,a=!1;for(;i<r;){let n=e[i];if(n===`;`){a=!1,i+=1;continue}if(/[A-Za-z_]/.test(n)){let t=i;for(;t<r&&/[A-Za-z0-9_]/.test(e[t]??``);)t+=1;e.slice(i,t).toLowerCase()===`execute`&&(a=!0),i=t;continue}if(n===`'`){let n=a;for(n||(t[i]=` `),i+=1;i<r;){if(e[i]===`'`){if(e[i+1]===`'`){n||(t[i]=` `,t[i+1]=` `),i+=2;continue}n||(t[i]=` `),i+=1;break}!n&&e[i]!==`
|
|
22
22
|
`&&(t[i]=` `),i+=1}continue}if(n===`"`){for(i+=1;i<r;){if(e[i]===`"`){if(e[i+1]===`"`){i+=2;continue}i+=1;break}i+=1}continue}if(n===`-`&&e[i+1]===`-`){for(;i<r&&e[i]!==`
|
|
23
23
|
`;)t[i]=` `,i+=1;continue}if(n===`/`&&e[i+1]===`*`){for(;i<r;){if(e[i]===`*`&&e[i+1]===`/`){t[i]=` `,t[i+1]=` `,i+=2;break}e[i]!==`
|
|
24
24
|
`&&(t[i]=` `),i+=1}continue}i+=1}},wh=e=>{let t=e.split(``),n=0;for(;n<e.length;){let r=e[n];if(r===`-`&&e[n+1]===`-`){for(;n<e.length&&e[n]!==`
|
|
@@ -28,4 +28,4 @@ import{parseSync as e,visitorKeys as t}from"oxc-parser";import{KEYS as n}from"es
|
|
|
28
28
|
`&&(t[r]=` `);n=o;continue}}if(r===`"`){for(n+=1;n<e.length;){if(e[n]===`"`){if(e[n+1]===`"`){n+=2;continue}n+=1;break}n+=1}continue}n+=1}return t.join(``)},Th=/disable\s+row\s+level\s+security/i,Eh=/auth\.role\(\)\s*=\s*["']service_role["']/i,Dh=/create\s+policy/gi,Oh=/;|create\s+policy/i,kh=/\b(?:using|with\s+check)\s*\(\s*true\s*\)/i,Ah=/\bfor\s+select\b/i,jh=/\bto\s+([\s\S]+?)(?=\s+(?:using|with\s+check|as|for)\b|;|$)/i,Mh=new Set([`service_role`,`postgres`,`supabase_admin`]),Nh=e=>{let t=jh.exec(e);if(t===null)return!1;let n=t[1].split(`,`).map(e=>e.trim().replace(/["'`]/g,``).toLowerCase()).filter(Boolean);return n.length>0&&n.every(e=>Mh.has(e))},Ph=(e,t)=>Eh.test(t)?!0:!kh.test(e)||Ah.test(e)?!1:!Nh(e),Fh=H({id:`supabase-rls-policy-risk`,title:`Permissive Supabase RLS policy`,severity:`error`,recommendation:"Keep public-read policies explicit, but gate inserts, updates, deletes, and service-role bypasses behind `auth.uid()` plus trusted tenant membership.",scan:e=>{if(!yh(e.relativePath))return[];let t=wh(e.content),n=t.search(Th);Dh.lastIndex=0;for(let r=Dh.exec(t);r!==null;r=Dh.exec(t)){let i=r.index+r[0].length,a=t.slice(i).search(Oh),o=a<0?t.length:i+a;if(Ph(t.slice(r.index,o),e.content.slice(r.index,o))){(n<0||r.index<n)&&(n=r.index);break}}if(n<0)return[];let{line:r,column:i}=U(t,n);return[{message:`Supabase policy SQL disables RLS, permits writes broadly, or references a service-role bypass.`,line:r,column:i}]}}),Ih=/create\s+(?:unlogged\s+)?table\s+(?:if\s+not\s+exists\s+)?(?!["`]?(?:auth|storage|realtime|vault|extensions|graphql|graphql_public|pgbouncer|net|supabase_functions|supabase_migrations|cron|pgsodium|pgmq|information_schema|pg_catalog|pg_temp|private|internal)["`]?\s*\.)(?:["`]?public["`]?\s*\.\s*)?["`]?([A-Za-z_][\w$]*)["`]?(?:\s*\(|\s+as\b)/gi,Lh=/alter\s+table\s+(?:if\s+exists\s+)?(?:only\s+)?(?:["`]?public["`]?\s*\.\s*)?["`]?([A-Za-z_][\w$]*)["`]?\s+(?:force\s+)?enable\s+row\s+level\s+security/gi,Rh=/\benable\s+row\s+level\s+security\b/gi,zh=e=>{let t=new Map,n=0;for(let r of e.matchAll(Lh)){let e=r[1];e!==void 0&&(n+=1,t.set(e.toLowerCase(),r.index))}return{lastEnableIndexByTable:t,staticEnableCount:n}},Bh=(e,t)=>(e.match(Rh)?.length??0)>t,Vh=H({id:`supabase-table-missing-rls`,title:`Supabase table created without Row Level Security`,severity:`error`,recommendation:"Enable RLS in the same migration (`alter table <name> enable row level security;`) and add `auth.uid()`-scoped policies for select/insert/update/delete. A public table without RLS is fully readable and writable with the public anon key.",scan:e=>{if(!vh(e.relativePath))return[];let t=wh(e.content);if(!/create\s+(?:unlogged\s+)?table/i.test(t))return[];let n=[],{lastEnableIndexByTable:r,staticEnableCount:i}=zh(t);if(Bh(t,i))return[];Ih.lastIndex=0;for(let e=Ih.exec(t);e!==null;e=Ih.exec(t)){let i=e[1];if(i===void 0)continue;let a=r.get(i.toLowerCase());if(a!==void 0&&a>=e.index)continue;let o=U(t,e.index);n.push({message:`Supabase migration creates a public table but never enables Row Level Security, leaving every row exposed to the anon key.`,line:o.line,column:o.column})}return n}}),Hh=H({id:`svg-filter-clickjacking-risk`,title:`SVG-filtered iframe clickjacking primitive`,severity:`warn`,recommendation:"Avoid filtering cross-origin iframes. Use `frame-ancestors` on sensitive pages and keep SVG filters away from embedded privileged UI.",scan:G({shouldScan:e=>W(e.relativePath),pattern:/<iframe\b(?:=>|[^>]){0,300}\bfilter\s*:\s*["']?url\(#|filter\s*:\s*url\(#[\s\S]{0,700}<iframe\b|<fe(?:DisplacementMap|ColorMatrix|Composite|Tile|Morphology)\b[\s\S]{0,700}<iframe\b/i,message:`An iframe is rendered through an SVG/CSS filter, which can support advanced clickjacking or visual exfiltration tricks.`})}),Uh=e=>W(e)?sl.test(e)?!0:/(?:^|\/)(?:middleware|route)\.[cm]?[jt]sx?$/.test(e):!1,Wh=H({id:`tenant-static-proxy-risk`,title:`Tenant-controlled static asset proxy`,severity:`warn`,recommendation:`Bind tenant identity to the trusted host or authenticated org, canonicalize after decoding, reject traversal, and never let one tenant choose another tenant's asset prefix.`,scan:G({shouldScan:e=>Uh(e.relativePath),pattern:/\b(?:fetch|path\.join|getObject\w*|GetObjectCommand|getSignedUrl|createReadStream)\s*\([^;]{0,200}(?:\$\{[^}]{0,100}\b(?:tenant|subdomain|workspace|hostPattern|(?<!\.)organization(?:Id|Slug)?)\b|\b(?:tenant|subdomain|workspace)(?:Id|Slug|Name)?\b\s*[,)+\].])/i,message:`Route code appears to compose tenant or subdomain input into a static/CDN/object-store fetch path.`})}),Gh=[/dangerouslySetInnerHTML\s*=\s*\{\{\s*__html\s*:[\s\S]{0,300}?\bJSON\.stringify\s*\(/gi,/<script\b[^>]*>(?:(?!<\/script>)[\s\S]){0,300}?\bJSON\.stringify\s*\(/gi],Kh=/\$\{|["'`]\s*\+\s*$/,qh=/^\s*\{(?!\{)/,Jh=/"(?:[^"\\\n]|\\.)*"|'(?:[^'\\\n]|\\.)*'/g,Yh=/\b(?:true|false|null)\b|[\d.]+(?:e[+-]?\d+)?/gi,Xh=e=>{if(e.includes("`")||e.includes("${"))return!1;let t=e.replace(Jh,``).replace(Yh,``);return!/[A-Za-z_$]/.test(t)},Zh=/^[\s)]*\.replace\s*\([^)]*(?:\\u003[cC]|<|<)/,Qh=/(?:\b(?:escapeHtml|escapeJSON|escapeJson|htmlEscape|jsesc)|(?<![.\w])(?:serialize|serializeJavascript|devalue|uneval|superjson))\s*\(\s*$/i,$h=/\bJSON\.stringify\s*\($/i,eg=H({id:`unsafe-json-in-html`,title:`Unescaped JSON in HTML or script sink`,severity:`warn`,recommendation:'JSON.stringify does not HTML-escape, so a `<\/script>` (or `<`) in the data breaks out and becomes XSS. Use an HTML-safe serializer (serialize-javascript, devalue) or escape `<`, `>`, and `&`, or pass data via a JSON `<script type="application/json">` read with JSON.parse.',scan:e=>{if(!W(e.relativePath))return[];let t=zl(e);if(!t.includes(`JSON.stringify`))return[];let n=[],r=new Set;for(let[e,i]of Gh.entries()){i.lastIndex=0;for(let a=i.exec(t);a!==null;a=i.exec(t)){let i=a[0].replace($h,``);if(Qh.test(i))continue;if(e===1){let e=t.indexOf(`>`,a.index),n=a[0].search(/\bJSON\.stringify\s*\($/i);if(e>=0&&n>=0){let r=t.slice(e+1,a.index+n);if(!Kh.test(r)||qh.test(r))continue}}let o=a.index+a[0].length-1,s=gd(t,o);if(s>=0){let e=t.slice(s+1,s+1+160);if(Zh.test(e)||Xh(t.slice(o+1,s)))continue}if(r.has(a.index))continue;r.add(a.index);let c=U(t,a.index);n.push({message:"JSON.stringify is embedded in HTML/script markup without HTML-escaping; data containing `<\/script>` or `<` breaks out and becomes XSS.",line:c.line,column:c.column})}}return n}}),tg=/(?:(?<![.\w$])fetch|\baxios\.\s*(?:get|post|put|delete|head)|\bgot|\bgot\.\s*(?:get|post))\s*\(\s*([^,)]+)/,ng=/\b(?:url|targetUrl|callbackUrl|redirectUrl|webhookUrl|companyUrl|websiteUrl|domainUrl|imageUrl|fetchUrl|next|return_to|returnTo|destination|location)\b/i,rg=/\breq\.|\brequest\.(?:query|body|params|nextUrl)|\bsearchParams\b|\bparams\.|\bbody\.|\bquery\./,ig=/\bredirect\s*:\s*["'](?:manual|error)["']/,ag=/\b(params|body|query)\./g,og=(e,t)=>RegExp(`(?:const|let|var)\\s+${e}\\s*(?::[^=;\\n]{0,80})?=\\s*new\\s+URLSearchParams\\b`).test(t),sg=(e,t)=>{let n=e.replace(ag,(e,n)=>og(n,t)?``:e);return rg.test(n)},cg=(e,t)=>{if(sg(e,t))return!0;let n=/^[\w$]+$/.exec(e.trim());if(n===null)return!1;let r=RegExp(`(?:const|let|var)[^=;\\n]{0,80}\\b${n[0]}\\b[^=;\\n]{0,80}=([^;\\n]*)`,`g`);for(let e of t.matchAll(r)){let n=e[1]??``;if(/\breq\.|\brequest\.|\bsearchParams\b|\$_(?:GET|POST|REQUEST)/.test(n)||sg(n,t))return!0}return!1},lg=H({id:`untrusted-redirect-following`,title:`Server fetch follows redirects for caller-shaped URL`,severity:`warn`,recommendation:'Use `redirect: "manual"` or equivalent and re-validate every redirect target before following it to avoid SSRF redirect bypasses.',scan:e=>{if(!Uh(e.relativePath)||!tg.test(e.content))return[];let t=[],n=e.content.split(`
|
|
29
29
|
`);for(let r=0;r<n.length;r+=1){let i=n[r]??``,a=i.match(tg),o=a?.[1]??``;if(!a||!ng.test(o)||!cg(o,e.content))continue;let s=n.slice(r,r+5).join(`
|
|
30
30
|
`);ig.test(s)||t.push({message:`Server-side fetch code appears to follow redirects for a URL shaped like caller-controlled input.`,line:r+1,column:i.search(/\S/)+1})}return t}}),ug=H({id:`url-prefilled-privileged-action`,title:`URL pre-fills a privileged action`,severity:`warn`,recommendation:`Require server-side validation and explicit confirmation for URL-sourced invite, role, permission, redirect, or sharing parameters.`,scan:G({shouldScan:e=>xp(e.relativePath),pattern:/(?<!(?:safe|valid|sanitiz|relativ|allowlist|whitelist|parse|normaliz)[\w$]*\(\s*(?:new\s+)?(?:[\w$]+\s*\.\s*){0,4})\b(?:searchParams|useSearchParams\s*\(\s*\)|URLSearchParams\s*\([^)]{0,120}\))(?:[?!])?\.get(?:All)?\s*\(\s*["'](?:userstoinvite|role|permission|sharingaction|invite|admin|next|continue|returnTo|redirect_uri|callbackUrl)["']|\bsearchParams\.(?:userstoinvite|role|permission|sharingaction|invite|admin|returnTo|redirect_uri|callbackUrl)\b/i,message:`Client code reads sensitive action state from the URL, which can pre-fill invites, roles, redirects, or sharing flows with attacker values.`})}),dg=/(?:^|\/)[^/]*webhook[^/]*\/|(?:^|\/)[^/]*webhook[^/]*\.[cm]?[jt]s$|\bwebhook\b/i,fg=/\b(?:export\s+(?:async\s+)?function\s+POST|export\s+const\s+(?:POST|handler|webhook)|webhookHandler|webhookRoute)\b/i,pg=RegExp(`verifySignature|verify.*signature|verify\\w*(?:Webhook|Auth)|constructEvent|createHmac|timingSafeEqual|svix|webhookSecret|stripe\\.webhooks|["'][\\w-]*signature["']|\\b[A-Za-z]{0,40}(?:verif|valid|check|assert|authenticat|compare|guard)[A-Za-z]{0,40}(?:secret|signature|hmac|webhook|digest)[A-Za-z]{0,40}\\s*\\(`,`i`),mg=/webhook[\s_-]?ur[il]\w*/gi,hg=/process\.env\.\w*WEBHOOK_URL|\b(?:send|post|dispatch|publish|notify)\w*Webhook/,gg=/\b(?:req|request)\b/,_g=/\/\/[^\n]*|\/\*[\s\S]*?\*\/|"(?:\\.|[^"\\\n])*"|'(?:\\.|[^'\\\n])*'|`(?:\\.|[^`\\])*`/g,vg=H({id:`webhook-signature-risk`,title:`Webhook handler lacks signature verification`,severity:`warn`,recommendation:`Verify provider signatures before parsing or acting on webhook bodies. Use provider SDK helpers or HMAC verification with timing-safe comparison.`,scan:G({shouldScan:e=>{if(!W(e.relativePath)||hg.test(e.content))return!1;let t=e.content.replace(_g,``).replace(mg,``);return dg.test(e.relativePath)||dg.test(t)},pattern:fg,requireAll:[gg],suppressWhen:pg,message:`Webhook handler code does not show an obvious signature verification step.`})}),yg=/\.[cm]?[jt]sx?$/i,bg=/\.d\.[cm]?[jt]s$/i,xg=/\.mdx$/i,Sg=/\.html?$/i,Cg=new Set([`.angular`,`.astro`,`.cache`,`.contentlayer`,`.docusaurus`,`.expo`,`.git`,`.next`,`.nuxt`,`.output`,`.svelte-kit`,`.turbo`,`.vercel`,`build`,`coverage`,`dist`,`node_modules`,`out`,`storybook-static`]),wg=new WeakMap,Tg=e=>{let t=wg.get(e);if(t)return t;let n=Ie(e);return wg.set(e,n),n},Eg=e=>{let t=[],n=[],a=[e],o=!1;for(;a.length>0;){let e=a.pop();if(!e)continue;let s;try{s=r.readdirSync(e,{withFileTypes:!0})}catch{return null}for(let r of s){let s=i.join(e,r.name),c=Cg.has(r.name)||r.name.startsWith(`.`)&&r.name!==`.dumi`&&r.name!==`.storybook`;if(!(r.isSymbolicLink()&&c)){if(r.isSymbolicLink())return null;if(r.isDirectory()){if(c)continue;a.push(s);continue}if(!(!r.isFile()||Ic(s))){if(xg.test(r.name)){o=!0;continue}if(Sg.test(r.name)){n.push(Wi(s));continue}yg.test(r.name)&&(bg.test(r.name)||t.push(Wi(s)))}}}}return{sourceFilePaths:t,htmlFilePaths:n,hasOpaqueMdxConsumerSurface:o}},Dg=(e,t)=>{for(let n of e){let e;try{e=r.readFileSync(n,`utf8`)}catch{return!0}for(let n of t)if(e.includes(`"${n}"`)||e.includes(`'${n}'`))return!0}return!1},Og=e=>{if(x(e,`ImportDeclaration`))return mc(e)?null:typeof e.source.value==`string`?e.source.value:null;if(x(e,`ExportNamedDeclaration`))return e.exportKind===`type`||pc(e.specifiers,`ExportSpecifier`,`exportKind`)?null:e.source&&typeof e.source.value==`string`?e.source.value:null;if(x(e,`ExportAllDeclaration`))return e.exportKind===`type`?null:typeof e.source.value==`string`?e.source.value:null;if(x(e,`ImportExpression`))return x(e.source,`Literal`)&&typeof e.source.value==`string`?e.source.value:null;if(x(e,`CallExpression`)&&x(e.callee,`Identifier`)&&e.callee.name===`require`&&e.arguments.length===1){let t=e.arguments[0];return t&&x(t,`Literal`)&&typeof t.value==`string`?t.value:null}return null},kg=(e,t,n,r)=>{k(e.programNode,i=>{let a=Og(i);if(!a)return;let o=An(e.filePath,a);if(!o){r.add(a);return}let s=Wi(o);n.set(i,s);let c=t.get(s)??new Set;c.add(e),t.set(s,c)})},Ag=({rootDirectory:e,currentFilePath:t,currentProgramNode:n,currentScopes:r,requiredModuleSources:i=[],knownModuleSources:a})=>{if(i.length>0&&a!==void 0&&!i.some(e=>a.includes(e)))return null;let o=Eg(e);if(!o||i.length>0&&a===void 0&&!Dg(o.sourceFilePaths,i))return null;let s=new Map;for(let e of o.sourceFilePaths){if(e===t){s.set(e,{filePath:e,programNode:n,scopes:r});continue}let i=Gt(e);if(!i||!x(i,`Program`))return null;s.set(e,{filePath:e,programNode:i,scopes:Tg(i)})}let c=new Map,l=new WeakMap,u=new Set;for(let e of s.values())kg(e,c,l,u);return{modulesByFilePath:s,htmlFilePaths:o.htmlFilePaths,consumerModulesByFilePath:c,resolvedSourcePathByNode:l,unresolvedRuntimeSources:u,hasOpaqueMdxConsumerSurface:o.hasOpaqueMdxConsumerSurface}},jg=e=>{if(!e)return null;let t=D(e);return!x(t,`Literal`)||typeof t.value!=`boolean`?null:t.value},Mg=(e,t,n={})=>{let r=n.maximumConstAliases===void 0?4:n.maximumConstAliases,i=n.shouldFoldStaticConditions??!1,a=n.shouldKeepKnownValues??!1,o=[],s=[{expression:e,remainingConstAliases:r,resolvingSymbols:new Set}];for(;s.length>0;){let e=s.pop();if(!e)continue;let n=D(e.expression);if(x(n,`Literal`)){if(typeof n.value!=`string`){if(a)continue;return null}o.push(n.value);continue}if(x(n,`TemplateLiteral`)){let e=ka(n);if(e===null){if(a)continue;return null}o.push(e);continue}if(x(n,`ConditionalExpression`)){let t=i?jg(n.test):null;if(t!==null){s.push({expression:t?n.consequent:n.alternate,remainingConstAliases:e.remainingConstAliases,resolvingSymbols:e.resolvingSymbols});continue}s.push({expression:n.alternate,remainingConstAliases:e.remainingConstAliases,resolvingSymbols:new Set(e.resolvingSymbols)}),s.push({expression:n.consequent,remainingConstAliases:e.remainingConstAliases,resolvingSymbols:new Set(e.resolvingSymbols)});continue}if(a&&x(n,`LogicalExpression`)&&(n.operator===`??`||n.operator===`||`)){s.push({expression:n.right,remainingConstAliases:e.remainingConstAliases,resolvingSymbols:new Set(e.resolvingSymbols)}),s.push({expression:n.left,remainingConstAliases:e.remainingConstAliases,resolvingSymbols:new Set(e.resolvingSymbols)});continue}if(x(n,`Identifier`)){let r=t.referenceFor(n)?.resolvedSymbol;if(!r||r.kind!==`const`||!r.initializer||!x(r.declarationNode,`VariableDeclarator`)||r.declarationNode.id!==r.bindingIdentifier||e.remainingConstAliases===0||e.resolvingSymbols.has(r)){if(a)continue;return null}e.resolvingSymbols.add(r),s.push({expression:r.initializer,remainingConstAliases:e.remainingConstAliases===null?null:e.remainingConstAliases-1,resolvingSymbols:e.resolvingSymbols});continue}if(!a)return null}return o.length>0?o:null},Ng=(e,t,n={})=>Mg(e,t,n),Pg=(e,t)=>Mg(e,t,{shouldKeepKnownValues:!0}),Fg=(e,t,n,r,i=!1)=>{let a=e.value;return a?x(a,`Literal`)?typeof a.value==`string`?[a.value]:null:x(a,`JSXExpressionContainer`)?Ng(a.expression,t,{maximumConstAliases:n,shouldFoldStaticConditions:r,shouldKeepKnownValues:i}):null:null},Ig=(e,t)=>Fg(e,t,4,!1),Lg=(e,t)=>Fg(e,t,null,!0),Rg=(e,t)=>Fg(e,t,4,!1,!0),zg=(e,t)=>{let n=i.relative(t,e);return n===``||!n.startsWith(`..${i.sep}`)&&n!==`..`&&!i.isAbsolute(n)},Bg={attribute:null,expression:null,isPresent:!1,isUnknown:!1},Vg={attribute:null,expression:null,isPresent:!1,isUnknown:!0},Hg=(e,t,n)=>{for(let r=e.properties.length-1;r>=0;--r){let i=e.properties[r];if(!i)return Vg;if(x(i,`SpreadElement`)){if(!x(i.argument,`ObjectExpression`))return Vg;let e=Hg(i.argument,t,n);if(e.isPresent||e.isUnknown)return e;continue}if(!x(i,`Property`))return Vg;let a=M(i,{allowComputedString:!0});if(!a)return Vg;if((n?a:a.toLowerCase())===t)return{attribute:null,expression:i.value,isPresent:!0,isUnknown:!1}}return Bg},Ug=(e,t,n=!0)=>{let r=n?t:t.toLowerCase();for(let t=e.length-1;t>=0;--t){let i=e[t];if(!i)return Vg;if(x(i,`JSXSpreadAttribute`)){if(!x(i.argument,`ObjectExpression`))return Vg;let e=Hg(i.argument,r,n);if(e.isPresent||e.isUnknown)return e;continue}if(!x(i,`JSXAttribute`))continue;let a=Io(i.name);if((n?a:a?.toLowerCase())===r)return{attribute:i,expression:null,isPresent:!0,isUnknown:!1}}return Bg},Wg=/(?<attributeName>[^\s"'<>/=]+)(?:\s*=\s*(?:"(?<doubleQuoted>[^"]*)"|'(?<singleQuoted>[^']*)'|(?<unquoted>[^\s"'=<>`]+)))?/g,Gg=/<!--[\s\S]*?-->|(?<skippedContentOpeningTag><(?<skippedContentTag>script|style|textarea|title|xmp|iframe|noembed|noframes|plaintext|template)\b(?:"[^"]*"|'[^']*'|[^'"<>])*?>)[\s\S]*?(?:<\/\k<skippedContentTag>\s*>|$)|(?<openingTag><[A-Za-z][\w:-]*(?:"[^"]*"|'[^']*'|[^'"<>])*>)/gi,Kg=new Map,qg=(e,t,n)=>{if(!e.isPresent)return;let r=e.attribute?Rg(e.attribute,t):e.expression?Pg(e.expression,t):null;for(let e of r??[]){let t=e?.trim();t&&n.add(t)}},Jg=(e,t,n)=>{for(let r of e.properties){if(x(r,`SpreadElement`)){x(r.argument,`ObjectExpression`)&&Jg(r.argument,t,n);continue}if(x(r,`Property`)&&M(r,{allowComputedString:!0})?.toLowerCase()===`id`)for(let e of Pg(r.value,t)??[]){let t=e.trim();t&&n.add(t)}}},Yg=e=>{let t=e.parent;for(x(t,`JSXElement`)&&t.openingElement===e&&(t=t.parent);t;){if(x(t,`JSXElement`)&&$o(t.openingElement)===`template`)return!0;t=t.parent}return!1},Xg=(e,t,n)=>{k(e,e=>{if(!x(e,`JSXOpeningElement`)||Yg(e))return;let r=Ug(e.attributes,`id`,!1);if(r.isUnknown){for(let r of e.attributes)qg(Ug([r],`id`,!1),t,n),x(r,`JSXSpreadAttribute`)&&x(r.argument,`ObjectExpression`)&&Jg(r.argument,t,n);return}qg(r,t,n)})},Zg=(e,t)=>{for(let n of e.matchAll(Gg)){let e=n.groups?.skippedContentOpeningTag??n.groups?.openingTag;if(e)for(let n of e.matchAll(Wg)){if(n.groups?.attributeName?.toLowerCase()!==`id`)continue;let e=n.groups?.doubleQuoted??n.groups?.singleQuoted??n.groups?.unquoted??``;e.trim()&&t.add(e.trim())}}},Qg=e=>{if(!i.isAbsolute(e.currentFilePath))return null;let t=e.configuredRootDirectory?Wi(e.configuredRootDirectory):null;return!t||!zg(e.currentFilePath,t)?null:t},$g=()=>{Kg.clear()},e_=e=>{let t=new Set;Xg(e.currentProgramNode,e.currentScopes,t);let n=Qg(e);if(!n)return t;if(Kg.has(n)){let e=Kg.get(n);return e?new Set([...e,...t]):null}let i=Ag({rootDirectory:n,currentFilePath:Wi(e.currentFilePath),currentProgramNode:e.currentProgramNode,currentScopes:e.currentScopes});if(!i)return Kg.set(n,null),null;let a=new Set;for(let e of i.modulesByFilePath.values())Xg(e.programNode,e.scopes,a);try{for(let e of i.htmlFilePaths)Zg(r.readFileSync(e,`utf8`),a)}catch{return Kg.set(n,null),null}return Kg.set(n,a),new Set([...a,...t])},t_=()=>{Mn(),vt(),mn(),$g(),kn()},n_=e=>nl.test(e)||rl.test(e),r_=e=>{let t=ml.test(e);return ch(e)||yh(e)||Dl(e)||bl(e)?{bucket:`priority`,isGeneratedBundleByName:t}:yl(e,t)?{bucket:`artifact`,isGeneratedBundleByName:t}:n_(e)?{bucket:`other`,isGeneratedBundleByName:t}:null},i_=(e,t)=>t||n_(e)||bl(e)||ch(e),a_=[`nextjs`,`astro`,`vite`,`cra`,`remix`,`gatsby`,`expo`,`react-native`,`tanstack-start`,`preact`,`unknown`];export{vd as $,Je as $i,Hi as $n,Ir as $r,mc as $t,Nm as A,l as Aa,ft as Ai,ka as An,oi as Ar,Cu as At,Rp as B,Ze as Bi,aa as Bn,Zr as Br,El as Bt,fh as C,p as Ca,jt as Ci,ho as Cn,Mi as Cr,yu as Ct,nh as D,f as Da,bt as Di,Ua as Dn,ri as Dr,Su as Dt,ih as E,u as Ea,yt as Ei,_o as En,vi as Er,Ou as Et,Cm as F,at as Fi,_a as Fn,ei as Fr,hu as Ft,bp as G,He as Gi,na as Gn,Rr as Gr,Uc as Gt,Tp as H,et as Hi,ea as Hn,Jr as Hr,Wc as Ht,Sm as I,nt as Ii,ga as In,$r as Ir,uu as It,zd as J,Re as Ji,P as Jn,Vr as Jr,Bc as Jt,Xd as K,Xe as Ki,Xi as Kn,zr as Kr,qc as Kt,bm as L,it as Li,ca as Ln,Qr as Lr,pu as Lt,Am as M,o as Ma,ct as Mi,Sa as Mn,ci as Mr,gu as Mt,km as N,ut as Ni,Ca as Nn,di as Nr,Tu as Nt,Qm as O,h as Oa,xt as Oi,ja as On,ui as Or,vu as Ot,Om as P,rt as Pi,I as Pn,qr as Pr,mu as Pt,yd as Q,qe as Qi,Li as Qn,Hr as Qr,pc as Qt,_m as R,Ge as Ri,sa as Rn,Gr as Rr,Wl as Rt,ph as S,m as Sa,Mt as Si,mo as Sn,yi as Sr,Du as St,sh as T,d as Ta,kt as Ti,po as Tn,fi as Tr,fu as Tt,wp as U,Ke as Ui,ra as Un,Yr as Ur,Gc as Ut,jp as V,Qe as Vi,ia as Vn,ti as Vr,H as Vt,Cp as W,Ue as Wi,ta as Wn,Xr as Wr,Kc as Wt,wd as X,ze as Xi,qi as Xn,Wr as Xr,Rc as Xt,Cd as Y,tt as Yi,Ji as Yn,Br as Yr,Ic as Yt,Sd as Z,Be as Zi,Wi as Zn,Ur as Zr,Sc as Zt,Hh as _,ce as _a,zt as _i,vo as _n,mi as _r,lu as _t,e_ as a,b as aa,$n as ai,os as an,Ti as ar,Xu as at,_h as b,de as ba,Lt as bi,fo as bn,ni as br,Eu as bt,Lg as c,pe as ca,Ln as ci,es as cn,hi as cr,Gl as ct,Ag as d,le as da,Pn as di,Yo as dn,Ai as dr,Xl as dt,$e as ea,kr as ei,fc as en,zi as er,J as et,vg as f,v as fa,Fn as fi,zo as fn,Di as fr,ql as ft,Wh as g,ie as ga,Gt as gi,bo as gn,Ci as gr,$l as gt,eg as h,ue as ha,pn as hi,Mo as hn,si as hr,Ql as ht,t_ as i,x as ia,er as ii,Us as in,_i as ir,ad as it,Mm as j,s as ja,st as ji,wa as jn,xi as jr,wu as jt,Vm as k,c as ka,E as ki,L as kn,li as kr,_u as kt,Ig as l,fe as la,k as li,$o as ln,ji as lr,Yl as lt,lg as m,ne as ma,An as mi,Fo as mn,Ei as mr,Kl as mt,r_ as n,Le as na,j as ni,uc as nn,M as nr,md as nt,Ug as o,y as oa,A as oi,ns as on,Si as or,Yu as ot,ug as p,te as pa,O as pi,Io as pn,Oi as pr,Zl as pt,Yd as q,We as qi,F as qn,Lr as qr,Jc as qt,i_ as r,he as ra,tr as ri,dc as rn,wi as rr,od as rt,zg as s,me as sa,Zn as si,ts as sn,bi as sr,Jl as st,a_ as t,Ie as ta,hr as ti,lc as tn,N as tr,hd as tt,jg as u,se as ua,In as ui,Xo as un,ki as ur,eu as ut,Vh as v,ae as va,Rt as vi,z as vn,gi as vr,xu as vt,uh as w,ee as wa,Ot as wi,uo as wn,ii as wr,tu as wt,mh as x,g as xa,Nt as xi,go as xn,ai as xr,du as xt,Fh as y,oe as ya,D as yi,yo as yn,pi as yr,bu as yt,om as z,Ye as zi,oa as zn,Kr as zr,Ul as zt};
|
|
31
|
-
//# sourceMappingURL=capability-
|
|
31
|
+
//# sourceMappingURL=capability-BOohHFgq.js.map
|
package/dist/core.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import{A as e,Aa as t,B as n,Bt as r,C as i,D as a,E as o,F as s,G as c,H as l,I as u,L as d,M as f,Ma as ee,Mi as p,N as m,Ni as te,O as h,Oa as ne,P as g,R as _,Rt as v,S as y,T as b,U as x,V as S,W as C,_ as w,at as T,b as E,ca as D,f as O,g as k,h as A,i as j,it as M,j as N,ja as P,ji as F,k as I,ka as re,m as L,n as ie,nn as ae,nt as R,ot as z,p as B,r as V,rn as H,rt as U,t as W,tn as G,tt as K,v as q,w as J,y as Y,z as X,zt as Z}from"./capability-C-pV36er.js";var oe=[{key:`react-doctor/active-static-asset`,id:`active-static-asset`,source:`react-doctor`,originallyExternal:!1,rule:{id:`active-static-asset`,title:`Executable SVG exposure`,severity:`warn`,recommendation:"Prefer `<img>` for SVG images; if SVG must be served directly, use attachment disposition and a CSP that blocks scripts and objects.",category:`Security`,framework:`global`,tags:[`security-scan`],isScanRule:!0}},{key:`react-doctor/activity-wraps-effect-heavy-subtree`,id:`activity-wraps-effect-heavy-subtree`,source:`react-doctor`,originallyExternal:!1,rule:{id:`activity-wraps-effect-heavy-subtree`,title:`Activity wraps an effect-heavy subtree`,severity:`warn`,recommendation:"Check what's under `<Activity>`. Every hide and show rebuilds every Effect inside from scratch. Move subscriptions and effect-driven setState out of the Activity, or load the data above it.",category:`Bugs`,framework:`global`,requires:[`react`,`react:19.2`],isScanRule:!1}},{key:`react-doctor/advanced-event-handler-refs`,id:`advanced-event-handler-refs`,source:`react-doctor`,originallyExternal:!1,rule:{id:`advanced-event-handler-refs`,title:`Listener re-subscribes on every handler change`,severity:`warn`,recommendation:"Store the handler in a ref and have the listener read `handlerRef.current()`. The subscription stays put while the latest handler still runs.",category:`Performance`,framework:`global`,requires:[`react`],tags:[`test-noise`],isScanRule:!1}},{key:`react-doctor/agent-tool-capability-risk`,id:`agent-tool-capability-risk`,source:`react-doctor`,originallyExternal:!1,rule:{id:`agent-tool-capability-risk`,title:`Agent tool exposes dangerous capability`,severity:`warn`,recommendation:`Treat tool inputs as prompt-injection controlled. Validate arguments, scope permissions per call, and avoid exposing shell/file/network primitives directly to agents.`,category:`Security`,framework:`global`,tags:[`security-scan`],isScanRule:!0}},{key:`react-doctor/alt-text`,id:`alt-text`,source:`react-doctor`,originallyExternal:!0,rule:{id:`alt-text`,title:`Image missing alt text`,severity:`error`,recommendation:"Give every meaningful image an `alt`, `aria-label`, or `aria-labelledby`.",category:`Accessibility`,framework:`global`,requires:[`react`],tags:[`react-jsx-only`],isScanRule:!1}},{key:`react-doctor/anchor-ambiguous-text`,id:`anchor-ambiguous-text`,source:`react-doctor`,originallyExternal:!0,rule:{id:`anchor-ambiguous-text`,title:`Ambiguous link text`,severity:`warn`,recommendation:`Name where a link goes. Avoid 'click here', 'learn more', and 'link'.`,category:`Accessibility`,framework:`global`,requires:[`react`],tags:[`react-jsx-only`],isScanRule:!1}},{key:`react-doctor/anchor-has-content`,id:`anchor-has-content`,source:`react-doctor`,originallyExternal:!0,rule:{id:`anchor-has-content`,title:`Anchor has no content`,severity:`warn`,recommendation:`Put readable text inside every link.`,category:`Accessibility`,framework:`global`,requires:[`react`],tags:[`react-jsx-only`],isScanRule:!1}},{key:`react-doctor/anchor-is-valid`,id:`anchor-is-valid`,source:`react-doctor`,originallyExternal:!0,rule:{id:`anchor-is-valid`,title:`Anchor used as a button`,severity:`warn`,recommendation:"Give links a real destination. Use `<button>` for in-page actions.",category:`Accessibility`,framework:`global`,requires:[`react`],tags:[`react-jsx-only`],isScanRule:!1}},{key:`react-doctor/anchor-target-exists`,id:`anchor-target-exists`,source:`react-doctor`,originallyExternal:!1,rule:{id:`anchor-target-exists`,title:`Fragment link target is missing`,severity:`warn`,recommendation:`Add an element with the referenced id, or update the fragment link to point to an existing target.`,category:`Accessibility`,framework:`global`,requires:[`react`],isScanRule:!1}},{key:`react-doctor/aria-activedescendant-has-tabindex`,id:`aria-activedescendant-has-tabindex`,source:`react-doctor`,originallyExternal:!0,rule:{id:`aria-activedescendant-has-tabindex`,title:`aria-activedescendant missing tabindex`,severity:`warn`,recommendation:"Add `tabIndex` so keyboard users can reach elements that use `aria-activedescendant`.",category:`Accessibility`,framework:`global`,requires:[`react`],tags:[`react-jsx-only`],isScanRule:!1}},{key:`react-doctor/aria-braille-equivalent`,id:`aria-braille-equivalent`,source:`react-doctor`,originallyExternal:!0,rule:{id:`aria-braille-equivalent`,title:`Braille-only accessible description`,severity:`warn`,recommendation:`Pair braille-specific labels and role descriptions with an equivalent accessible name or role description.`,category:`Accessibility`,framework:`global`,requires:[`react`],defaultEnabled:!1,isScanRule:!1}},{key:`react-doctor/aria-props`,id:`aria-props`,source:`react-doctor`,originallyExternal:!0,rule:{id:`aria-props`,title:`Invalid ARIA attribute`,severity:`error`,recommendation:"Only use `aria-*` attributes that actually exist.",category:`Accessibility`,framework:`global`,requires:[`react`],tags:[`react-jsx-only`],isScanRule:!1}},{key:`react-doctor/aria-proptypes`,id:`aria-proptypes`,source:`react-doctor`,originallyExternal:!0,rule:{id:`aria-proptypes`,title:`Invalid ARIA attribute value`,severity:`error`,recommendation:"Give each `aria-*` attribute the kind of value it expects.",category:`Accessibility`,framework:`global`,requires:[`react`],tags:[`react-jsx-only`],isScanRule:!1}},{key:`react-doctor/aria-role`,id:`aria-role`,source:`react-doctor`,originallyExternal:!0,rule:{id:`aria-role`,title:`Invalid ARIA role`,severity:`error`,recommendation:`Use a real, non-abstract ARIA role so assistive tech can expose the element correctly.`,category:`Accessibility`,framework:`global`,requires:[`react`],tags:[`react-jsx-only`],isScanRule:!1}},{key:`react-doctor/aria-unsupported-elements`,id:`aria-unsupported-elements`,source:`react-doctor`,originallyExternal:!0,rule:{id:`aria-unsupported-elements`,title:`ARIA on unsupported element`,severity:`error`,recommendation:"Do not put `role` or `aria-*` on reserved HTML elements.",category:`Accessibility`,framework:`global`,requires:[`react`],tags:[`react-jsx-only`],isScanRule:!1}},{key:`react-doctor/artifact-baas-authority-surface`,id:`artifact-baas-authority-surface`,source:`react-doctor`,originallyExternal:!1,rule:{id:`artifact-baas-authority-surface`,title:`BaaS authority map shipped in browser artifact`,severity:`warn`,recommendation:`Client BaaS config is often public, but shipped collection names plus owner, role, tenant, or admin fields give attackers a precise authorization map. Verify rules/RLS enforce every boundary server-side.`,category:`Security`,framework:`global`,tags:[`security-scan`],isScanRule:!0}},{key:`react-doctor/artifact-env-leak`,id:`artifact-env-leak`,source:`react-doctor`,originallyExternal:!1,rule:{id:`artifact-env-leak`,title:`Server env leaked to browser artifact`,severity:`error`,recommendation:`Treat public env prefixes as publication, not secrecy; keep secret env vars server-only and rebuild after rotating leaked keys.`,category:`Security`,framework:`global`,tags:[`security-scan`],isScanRule:!0}},{key:`react-doctor/artifact-secret-leak`,id:`artifact-secret-leak`,source:`react-doctor`,originallyExternal:!1,rule:{id:`artifact-secret-leak`,title:`Secret shipped in browser artifact`,severity:`error`,recommendation:`Remove the secret from client bundles/static assets, rotate it, and route privileged service calls through server-only code.`,category:`Security`,framework:`global`,tags:[`security-scan`],isScanRule:!0}},{key:`react-doctor/async-await-in-loop`,id:`async-await-in-loop`,source:`react-doctor`,originallyExternal:!1,rule:{id:`async-await-in-loop`,title:`await inside a loop`,severity:`warn`,recommendation:"Collect the items, then use `await Promise.all(items.map(...))` so independent work runs at the same time",category:`Performance`,framework:`global`,tags:[`test-noise`],isScanRule:!1}},{key:`react-doctor/async-defer-await`,id:`async-defer-await`,source:`react-doctor`,originallyExternal:!1,rule:{id:`async-defer-await`,title:`await before an early-return guard`,severity:`warn`,recommendation:"Move the `await` below the early-return guard so the skip path stays fast and avoids unnecessary async work.",category:`Performance`,framework:`global`,requires:[`react`],tags:[`test-noise`],isScanRule:!1}},{key:`react-doctor/async-parallel`,id:`async-parallel`,source:`react-doctor`,originallyExternal:!1,rule:{id:`async-parallel`,title:`Independent awaits run sequentially`,severity:`warn`,recommendation:"Use `const [a, b] = await Promise.all([fetchA(), fetchB()])` so independent calls run at the same time",category:`Performance`,framework:`global`,tags:[`test-noise`],isScanRule:!1}},{key:`react-doctor/auth-token-in-web-storage`,id:`auth-token-in-web-storage`,source:`react-doctor`,originallyExternal:!1,rule:{id:`auth-token-in-web-storage`,title:`Auth token in web storage`,severity:`warn`,recommendation:"Don't persist auth tokens (JWTs, access/refresh tokens, secrets) in `localStorage`/`sessionStorage`; they're readable by any XSS. Use an `HttpOnly` cookie set by the server.",category:`Security`,framework:`global`,isScanRule:!1}},{key:`react-doctor/autocomplete-valid`,id:`autocomplete-valid`,source:`react-doctor`,originallyExternal:!0,rule:{id:`autocomplete-valid`,title:`Invalid autocomplete value`,severity:`warn`,recommendation:"Use a valid autofill token in `autoComplete` so browsers can fill the right field reliably.",category:`Accessibility`,framework:`global`,requires:[`react`],tags:[`react-jsx-only`],isScanRule:!1}},{key:`react-doctor/base-ui-dialog-popup-requires-title`,id:`base-ui-dialog-popup-requires-title`,source:`react-doctor`,originallyExternal:!1,rule:{id:`base-ui-dialog-popup-requires-title`,title:`Base UI dialog popup without a title`,severity:`warn`,recommendation:`Give every Base UI Dialog.Popup and AlertDialog.Popup a Title part (visually hidden when the design shows no heading) or name the dialog with aria-label.`,category:`Accessibility`,framework:`global`,requires:[`react`,`base-ui`],isScanRule:!1}},{key:`react-doctor/base-ui-field-requires-label`,id:`base-ui-field-requires-label`,source:`react-doctor`,originallyExternal:!1,rule:{id:`base-ui-field-requires-label`,title:`Base UI field control without a label`,severity:`warn`,recommendation:`Give every Field.Root that wraps a Field.Control a Field.Label (visually hidden when the design shows no label) or name the control directly with aria-label.`,category:`Accessibility`,framework:`global`,requires:[`react`,`base-ui`],isScanRule:!1}},{key:`react-doctor/base-ui-tabs-tab-requires-list`,id:`base-ui-tabs-tab-requires-list`,source:`react-doctor`,originallyExternal:!1,rule:{id:`base-ui-tabs-tab-requires-list`,title:`Base UI tab is outside Tabs.List`,severity:`warn`,recommendation:`Render each Base UI Tabs.Tab inside Tabs.List so the tablist role and arrow-key focus handling cover every tab.`,category:`Bugs`,framework:`global`,requires:[`base-ui`],matchByOccurrence:!0,isScanRule:!1}},{key:`react-doctor/build-pipeline-secret-boundary`,id:`build-pipeline-secret-boundary`,source:`react-doctor`,originallyExternal:!1,rule:{id:`build-pipeline-secret-boundary`,title:`Build pipeline runs code near secrets`,severity:`warn`,recommendation:`Run dependency installs with scripts disabled before exposing secrets, isolate untrusted build code, and move signing/deploy authority into a narrow privileged step.`,category:`Security`,framework:`global`,tags:[`security-scan`],isScanRule:!0}},{key:`react-doctor/button-has-type`,id:`button-has-type`,source:`react-doctor`,originallyExternal:!0,rule:{id:`button-has-type`,title:`Button missing explicit type`,severity:`warn`,recommendation:'Set an explicit button `type` so plain buttons do not submit forms by accident: `type="button"`, `"submit"`, or `"reset"`.',category:`Bugs`,framework:`global`,requires:[`react`],isScanRule:!1}},{key:`react-doctor/checked-requires-onchange-or-readonly`,id:`checked-requires-onchange-or-readonly`,source:`react-doctor`,originallyExternal:!0,rule:{id:`checked-requires-onchange-or-readonly`,title:`Checked input without onChange`,severity:`warn`,recommendation:"Add `onChange`, `readOnly`, or `defaultChecked` so React knows whether the checkbox is editable, display-only, or uncontrolled.",category:`Bugs`,framework:`global`,requires:[`react`],isScanRule:!1}},{key:`react-doctor/circular-dependency`,id:`circular-dependency`,source:`react-doctor`,originallyExternal:!1,rule:{id:`circular-dependency`,title:`Runtime import cycle`,severity:`warn`,recommendation:`Break the runtime cycle by extracting shared code into a lower-level module or inverting one dependency.`,category:`Maintainability`,framework:`global`,tags:[`project-analysis`],defaultEnabled:!1,isScanRule:!1,isProjectRule:!0}},{key:`react-doctor/class-component-missing-component-will-unmount-teardown`,id:`class-component-missing-component-will-unmount-teardown`,source:`react-doctor`,originallyExternal:!1,rule:{id:`class-component-missing-component-will-unmount-teardown`,title:`Class component acquires a resource with no teardown`,severity:`warn`,recommendation:"Release listeners and timers acquired in `componentDidMount`/`constructor` by adding a `componentWillUnmount` that removes them (or use MobX `disposeOnUnmount`).",category:`Bugs`,framework:`global`,requires:[`react`],isScanRule:!1}},{key:`react-doctor/click-events-have-key-events`,id:`click-events-have-key-events`,source:`react-doctor`,originallyExternal:!0,rule:{id:`click-events-have-key-events`,title:`Click handler missing keyboard handler`,severity:`warn`,recommendation:"Pair `onClick` with a key handler so keyboard users can trigger it.",category:`Accessibility`,framework:`global`,requires:[`react`],tags:[`react-jsx-only`],isScanRule:!1}},{key:`react-doctor/clickjacking-redirect-risk`,id:`clickjacking-redirect-risk`,source:`react-doctor`,originallyExternal:!1,rule:{id:`clickjacking-redirect-risk`,title:`Redirect or frame boundary risk`,severity:`warn`,recommendation:"Allowlist redirect origins/paths, set `frame-ancestors` for privileged pages, and avoid URL-prefilled privileged dialogs.",category:`Security`,framework:`global`,tags:[`security-scan`],isScanRule:!0}},{key:`react-doctor/client-localstorage-no-version`,id:`client-localstorage-no-version`,source:`react-doctor`,originallyExternal:!1,rule:{id:`client-localstorage-no-version`,title:`Unversioned localStorage key`,severity:`warn`,recommendation:`Put a version in the storage key (e.g. "myKey:v1"). If you change the data shape later, old saved data can be ignored instead of crashing the app.`,category:`Bugs`,framework:`global`,requires:[`react`],tags:[`test-noise`],isScanRule:!1}},{key:`react-doctor/client-passive-event-listeners`,id:`client-passive-event-listeners`,source:`react-doctor`,originallyExternal:!1,rule:{id:`client-passive-event-listeners`,title:`Non-passive scroll listener`,severity:`warn`,recommendation:"Add `{ passive: true }` as the third argument: `addEventListener('touchmove', handler, { passive: true })`. Only do this if the handler doesn't call `event.preventDefault()`, since passive listeners ignore it (which breaks pull-to-refresh, custom gestures, and nested scrolling).",category:`Performance`,framework:`global`,requires:[`react`],tags:[`test-noise`],isScanRule:!1}},{key:`react-doctor/command-execution-input-risk`,id:`command-execution-input-risk`,source:`react-doctor`,originallyExternal:!1,rule:{id:`command-execution-input-risk`,title:`Command execution uses caller-shaped input`,severity:`error`,recommendation:`Avoid shell execution for caller-controlled values. Use fixed commands, argument arrays, strict allowlists, and no shell interpolation.`,category:`Security`,framework:`global`,tags:[`security-scan`],isScanRule:!0}},{key:`react-doctor/context-provider-value-from-unmemoized-local-literal`,id:`context-provider-value-from-unmemoized-local-literal`,source:`react-doctor`,originallyExternal:!1,rule:{id:`context-provider-value-from-unmemoized-local-literal`,title:`Context value from an unmemoized local literal`,severity:`warn`,recommendation:`Memoize the context value in component scope so consumers do not redraw every render. For mapped providers, extract each item into a child component and memoize there.`,category:`Performance`,framework:`global`,requires:[`react`],disabledWhen:[`react-compiler`],tags:[`react-jsx-only`,`test-noise`],isScanRule:!1}},{key:`react-doctor/control-has-associated-label`,id:`control-has-associated-label`,source:`react-doctor`,originallyExternal:!0,rule:{id:`control-has-associated-label`,title:`Control missing accessible label`,severity:`warn`,recommendation:`Give every interactive control a label screen readers can read.`,category:`Accessibility`,framework:`global`,requires:[`react`],tags:[`react-jsx-only`],isScanRule:!1}},{key:`react-doctor/cors-cookie-trust-risk`,id:`cors-cookie-trust-risk`,source:`react-doctor`,originallyExternal:!1,rule:{id:`cors-cookie-trust-risk`,title:`Broad cookie or credentialed CORS trust`,severity:`warn`,recommendation:`Keep auth cookies host-only and HttpOnly, avoid credentialed CORS for less-trusted docs/vendor origins, and isolate documentation domains from app sessions.`,category:`Security`,framework:`global`,tags:[`security-scan`],isScanRule:!0}},{key:`react-doctor/dangerous-html-sink`,id:`dangerous-html-sink`,source:`react-doctor`,originallyExternal:!1,rule:{id:`dangerous-html-sink`,title:`HTML injection sink with dynamic content`,severity:`warn`,recommendation:`Prefer rendering structured React nodes. If HTML is required, sanitize with a well-reviewed sanitizer and keep the trust boundary close to the sink.`,category:`Security`,framework:`global`,tags:[`security-scan`],isScanRule:!0}},{key:`react-doctor/data-table-requires-accessible-name`,id:`data-table-requires-accessible-name`,source:`react-doctor`,originallyExternal:!1,rule:{id:`data-table-requires-accessible-name`,title:`Data table has no accessible name`,severity:`warn`,recommendation:`Give data tables a concise caption, or reference an existing visible title with aria-labelledby.`,category:`Accessibility`,framework:`global`,requires:[`react`],defaultEnabled:!1,isScanRule:!1}},{key:`react-doctor/debounce-no-cleanup`,id:`debounce-no-cleanup`,source:`react-doctor`,originallyExternal:!1,rule:{id:`debounce-no-cleanup`,title:`Memoized debounce never cancelled on unmount`,severity:`warn`,recommendation:"A debounced/throttled callback holds a pending timer that still fires after unmount, so add `useEffect(() => () => debounced.cancel(), [debounced])` to cancel the trailing invocation when the component tears down.",category:`Bugs`,framework:`global`,requires:[`react`],isScanRule:!1}},{key:`react-doctor/design-no-em-dash-in-jsx-text`,id:`design-no-em-dash-in-jsx-text`,source:`react-doctor`,originallyExternal:!1,rule:{id:`design-no-em-dash-in-jsx-text`,title:`Em dash in JSX text`,severity:`warn`,recommendation:`Replace em dashes in UI text with commas, colons, semicolons, or parentheses so the copy reads less like AI output.`,category:`Maintainability`,framework:`global`,requires:[`react`],tags:[`design`,`test-noise`],defaultEnabled:!1,isScanRule:!1}},{key:`react-doctor/design-no-redundant-padding-axes`,id:`design-no-redundant-padding-axes`,source:`react-doctor`,originallyExternal:!1,rule:{id:`design-no-redundant-padding-axes`,title:`Redundant padding axes`,severity:`warn`,recommendation:"Collapse matching padding axes to `p-N` so duplicated classes do not make spacing harder to scan; keep split axes only when breakpoints differ.",category:`Maintainability`,framework:`global`,requires:[`react`],tags:[`design`,`test-noise`],defaultEnabled:!1,isScanRule:!1}},{key:`react-doctor/design-no-redundant-size-axes`,id:`design-no-redundant-size-axes`,source:`react-doctor`,originallyExternal:!1,rule:{id:`design-no-redundant-size-axes`,title:`Redundant width and height axes`,severity:`warn`,recommendation:"Collapse matching width and height to `size-N` so duplicated classes do not make layout harder to scan.",category:`Maintainability`,framework:`global`,requires:[`react`,`tailwind:3.4`],tags:[`design`,`test-noise`],defaultEnabled:!1,isScanRule:!1}},{key:`react-doctor/design-no-space-on-flex-children`,id:`design-no-space-on-flex-children`,source:`react-doctor`,originallyExternal:!1,rule:{id:`design-no-space-on-flex-children`,title:`space-* utility on flex children`,severity:`warn`,recommendation:"Use `gap-*` on the flex or grid parent. `space-x-*` and `space-y-*` leave gaps when a child is hidden, miss spacing on wrapped lines, and don't flip in right-to-left layouts.",category:`Maintainability`,framework:`global`,requires:[`react`],tags:[`design`,`test-noise`],defaultEnabled:!1,isScanRule:!1}},{key:`react-doctor/design-no-three-period-ellipsis`,id:`design-no-three-period-ellipsis`,source:`react-doctor`,originallyExternal:!1,rule:{id:`design-no-three-period-ellipsis`,title:`Three dots instead of ellipsis`,severity:`warn`,recommendation:`Use the real ellipsis character ("…") so UI labels look polished and consistent instead of like three separate periods.`,category:`Maintainability`,framework:`global`,requires:[`react`],tags:[`design`,`test-noise`],defaultEnabled:!1,isScanRule:!1}},{key:`react-doctor/design-no-vague-button-label`,id:`design-no-vague-button-label`,source:`react-doctor`,originallyExternal:!1,rule:{id:`design-no-vague-button-label`,title:`Vague button label`,severity:`warn`,recommendation:`Name the action: "Save changes" instead of "Continue", "Send invite" instead of "Submit". The label is the button's accessible name.`,category:`Accessibility`,framework:`global`,requires:[`react`],tags:[`design`,`test-noise`],defaultEnabled:!1,isScanRule:!1}},{key:`react-doctor/details-requires-summary`,id:`details-requires-summary`,source:`react-doctor`,originallyExternal:!1,rule:{id:`details-requires-summary`,title:`Details disclosure has no explicit summary`,severity:`warn`,recommendation:`Make a meaningful summary the first child of details so users can understand and operate the disclosure.`,category:`Accessibility`,framework:`global`,requires:[`react`],defaultEnabled:!1,isScanRule:!1}},{key:`react-doctor/dialog-has-accessible-name`,id:`dialog-has-accessible-name`,source:`react-doctor`,originallyExternal:!1,rule:{id:`dialog-has-accessible-name`,title:`Dialog without accessible name`,severity:`warn`,recommendation:'Give every `<dialog>` / `role="dialog"` an accessible name with `aria-label` or `aria-labelledby` (referencing the dialog\'s title element).',category:`Accessibility`,framework:`global`,requires:[`react`],isScanRule:!1}},{key:`react-doctor/display-name`,id:`display-name`,source:`react-doctor`,originallyExternal:!0,rule:{id:`display-name`,title:`Component missing display name`,severity:`warn`,recommendation:"Give each component a `displayName` so DevTools shows a clear name.",category:`Maintainability`,framework:`global`,requires:[`react`],defaultEnabled:!1,isScanRule:!1}},{key:`react-doctor/duplicate-jsx-subtree`,id:`duplicate-jsx-subtree`,source:`react-doctor`,originallyExternal:!1,rule:{id:`duplicate-jsx-subtree`,title:`Duplicated JSX structure`,severity:`warn`,recommendation:`Extract a shared component when the repeated JSX trees represent the same UI concept and should evolve together.`,category:`Maintainability`,framework:`global`,tags:[`project-analysis`],isScanRule:!1,isProjectRule:!0}},{key:`react-doctor/effect-listener-cleanup-mismatch`,id:`effect-listener-cleanup-mismatch`,source:`react-doctor`,originallyExternal:!1,rule:{id:`effect-listener-cleanup-mismatch`,title:`Effect cleanup does not match its event listener`,severity:`error`,recommendation:"Pass the same callback binding and capture flag to `addEventListener` and `removeEventListener`, or abort the registration's local AbortController during cleanup.",category:`Bugs`,framework:`global`,requires:[`react`],isScanRule:!1}},{key:`react-doctor/effect-listener-cleanup-reference-mismatch`,id:`effect-listener-cleanup-reference-mismatch`,source:`react-doctor`,originallyExternal:!1,rule:{id:`effect-listener-cleanup-reference-mismatch`,title:`Effect cleanup removes the wrong listener reference`,severity:`error`,recommendation:`Removal APIs match by reference identity, so the second inline function passed to the remove call can never equal the one you added; hoist the handler into a single named const (or useCallback) and pass that same reference to both the add and remove calls.`,category:`Bugs`,framework:`global`,requires:[`react`],isScanRule:!1}},{key:`react-doctor/effect-needs-cleanup`,id:`effect-needs-cleanup`,source:`react-doctor`,originallyExternal:!1,rule:{id:`effect-needs-cleanup`,title:`Effect subscription or timer never cleaned up`,severity:`error`,recommendation:"Return a cleanup function that stops the subscription or timer: `return () => target.removeEventListener(name, handler)` for listeners, `return () => clearInterval(id)` or `clearTimeout(id)` for timers, `return () => observer.disconnect()` for observers, `return () => socket.close()` for connections, or `return unsubscribe` if the subscribe call already gave you one.",category:`Bugs`,framework:`global`,requires:[`react`],tags:[`test-noise`],isScanRule:!1}},{key:`react-doctor/effect-observer-needs-disconnect`,id:`effect-observer-needs-disconnect`,source:`react-doctor`,originallyExternal:!1,rule:{id:`effect-observer-needs-disconnect`,title:`Observer created in an effect never disconnected`,severity:`error`,recommendation:"Return a cleanup function that calls `observer.disconnect()` (or `observer.unobserve(node)`) so the observer stops firing callbacks against detached nodes after unmount instead of leaking on every mount.",category:`Bugs`,framework:`global`,requires:[`react`],isScanRule:!1}},{key:`react-doctor/effect-raf-loop-needs-cancel`,id:`effect-raf-loop-needs-cancel`,source:`react-doctor`,originallyExternal:!1,rule:{id:`effect-raf-loop-needs-cancel`,title:`requestAnimationFrame loop never cancelled`,severity:`warn`,recommendation:"Store the frame id and return a cleanup that calls `cancelAnimationFrame(id)` so self-scheduling work cannot continue after unmount.",category:`Bugs`,framework:`global`,requires:[`react`],isScanRule:!1}},{key:`react-doctor/effect-remove-listener-inline-handler`,id:`effect-remove-listener-inline-handler`,source:`react-doctor`,originallyExternal:!1,rule:{id:`effect-remove-listener-inline-handler`,title:`removeEventListener called with a fresh inline handler`,severity:`error`,recommendation:"Removal APIs match the listener by reference equality, so a fresh inline arrow, function expression, or `.bind(...)` result can never equal the registered handler; hoist the handler into a named const and pass that same reference to both the add and remove calls.",category:`Bugs`,framework:`global`,requires:[`react`],tags:[`test-noise`],isScanRule:!1}},{key:`react-doctor/empty-table-header`,id:`empty-table-header`,source:`react-doctor`,originallyExternal:!0,rule:{id:`empty-table-header`,title:`Table header has no accessible text`,severity:`warn`,recommendation:`Give every table header concise text or an explicit accessible name.`,category:`Accessibility`,framework:`global`,requires:[`react`],defaultEnabled:!1,isScanRule:!1}},{key:`react-doctor/exhaustive-deps`,id:`exhaustive-deps`,source:`react-doctor`,originallyExternal:!0,rule:{id:`exhaustive-deps`,title:`Missing effect dependencies`,severity:`warn`,recommendation:`Don't blindly add missing dependencies. Read the hook callback first.
|
|
1
|
+
import{A as e,Aa as t,B as n,Bt as r,C as i,D as a,E as o,F as s,G as c,H as l,I as u,L as d,M as f,Ma as ee,Mi as p,N as m,Ni as te,O as h,Oa as ne,P as g,R as _,Rt as v,S as y,T as b,U as x,V as S,W as C,_ as w,at as T,b as E,ca as D,f as O,g as k,h as A,i as j,it as M,j as N,ja as P,ji as F,k as I,ka as re,m as L,n as ie,nn as ae,nt as R,ot as z,p as B,r as V,rn as H,rt as U,t as W,tn as G,tt as K,v as q,w as J,y as Y,z as X,zt as Z}from"./capability-BOohHFgq.js";var oe=[{key:`react-doctor/active-static-asset`,id:`active-static-asset`,source:`react-doctor`,originallyExternal:!1,rule:{id:`active-static-asset`,title:`Executable SVG exposure`,severity:`warn`,recommendation:"Prefer `<img>` for SVG images; if SVG must be served directly, use attachment disposition and a CSP that blocks scripts and objects.",category:`Security`,framework:`global`,tags:[`security-scan`],isScanRule:!0}},{key:`react-doctor/activity-wraps-effect-heavy-subtree`,id:`activity-wraps-effect-heavy-subtree`,source:`react-doctor`,originallyExternal:!1,rule:{id:`activity-wraps-effect-heavy-subtree`,title:`Activity wraps an effect-heavy subtree`,severity:`warn`,recommendation:"Check what's under `<Activity>`. Every hide and show rebuilds every Effect inside from scratch. Move subscriptions and effect-driven setState out of the Activity, or load the data above it.",category:`Bugs`,framework:`global`,requires:[`react`,`react:19.2`],isScanRule:!1}},{key:`react-doctor/advanced-event-handler-refs`,id:`advanced-event-handler-refs`,source:`react-doctor`,originallyExternal:!1,rule:{id:`advanced-event-handler-refs`,title:`Listener re-subscribes on every handler change`,severity:`warn`,recommendation:"Store the handler in a ref and have the listener read `handlerRef.current()`. The subscription stays put while the latest handler still runs.",category:`Performance`,framework:`global`,requires:[`react`],tags:[`test-noise`],isScanRule:!1}},{key:`react-doctor/agent-tool-capability-risk`,id:`agent-tool-capability-risk`,source:`react-doctor`,originallyExternal:!1,rule:{id:`agent-tool-capability-risk`,title:`Agent tool exposes dangerous capability`,severity:`warn`,recommendation:`Treat tool inputs as prompt-injection controlled. Validate arguments, scope permissions per call, and avoid exposing shell/file/network primitives directly to agents.`,category:`Security`,framework:`global`,tags:[`security-scan`],isScanRule:!0}},{key:`react-doctor/alt-text`,id:`alt-text`,source:`react-doctor`,originallyExternal:!0,rule:{id:`alt-text`,title:`Image missing alt text`,severity:`error`,recommendation:"Give every meaningful image an `alt`, `aria-label`, or `aria-labelledby`.",category:`Accessibility`,framework:`global`,requires:[`react`],tags:[`react-jsx-only`],isScanRule:!1}},{key:`react-doctor/anchor-ambiguous-text`,id:`anchor-ambiguous-text`,source:`react-doctor`,originallyExternal:!0,rule:{id:`anchor-ambiguous-text`,title:`Ambiguous link text`,severity:`warn`,recommendation:`Name where a link goes. Avoid 'click here', 'learn more', and 'link'.`,category:`Accessibility`,framework:`global`,requires:[`react`],tags:[`react-jsx-only`],isScanRule:!1}},{key:`react-doctor/anchor-has-content`,id:`anchor-has-content`,source:`react-doctor`,originallyExternal:!0,rule:{id:`anchor-has-content`,title:`Anchor has no content`,severity:`warn`,recommendation:`Put readable text inside every link.`,category:`Accessibility`,framework:`global`,requires:[`react`],tags:[`react-jsx-only`],isScanRule:!1}},{key:`react-doctor/anchor-is-valid`,id:`anchor-is-valid`,source:`react-doctor`,originallyExternal:!0,rule:{id:`anchor-is-valid`,title:`Anchor used as a button`,severity:`warn`,recommendation:"Give links a real destination. Use `<button>` for in-page actions.",category:`Accessibility`,framework:`global`,requires:[`react`],tags:[`react-jsx-only`],isScanRule:!1}},{key:`react-doctor/anchor-target-exists`,id:`anchor-target-exists`,source:`react-doctor`,originallyExternal:!1,rule:{id:`anchor-target-exists`,title:`Fragment link target is missing`,severity:`warn`,recommendation:`Add an element with the referenced id, or update the fragment link to point to an existing target.`,category:`Accessibility`,framework:`global`,requires:[`react`],isScanRule:!1}},{key:`react-doctor/aria-activedescendant-has-tabindex`,id:`aria-activedescendant-has-tabindex`,source:`react-doctor`,originallyExternal:!0,rule:{id:`aria-activedescendant-has-tabindex`,title:`aria-activedescendant missing tabindex`,severity:`warn`,recommendation:"Add `tabIndex` so keyboard users can reach elements that use `aria-activedescendant`.",category:`Accessibility`,framework:`global`,requires:[`react`],tags:[`react-jsx-only`],isScanRule:!1}},{key:`react-doctor/aria-braille-equivalent`,id:`aria-braille-equivalent`,source:`react-doctor`,originallyExternal:!0,rule:{id:`aria-braille-equivalent`,title:`Braille-only accessible description`,severity:`warn`,recommendation:`Pair braille-specific labels and role descriptions with an equivalent accessible name or role description.`,category:`Accessibility`,framework:`global`,requires:[`react`],defaultEnabled:!1,isScanRule:!1}},{key:`react-doctor/aria-props`,id:`aria-props`,source:`react-doctor`,originallyExternal:!0,rule:{id:`aria-props`,title:`Invalid ARIA attribute`,severity:`error`,recommendation:"Only use `aria-*` attributes that actually exist.",category:`Accessibility`,framework:`global`,requires:[`react`],tags:[`react-jsx-only`],isScanRule:!1}},{key:`react-doctor/aria-proptypes`,id:`aria-proptypes`,source:`react-doctor`,originallyExternal:!0,rule:{id:`aria-proptypes`,title:`Invalid ARIA attribute value`,severity:`error`,recommendation:"Give each `aria-*` attribute the kind of value it expects.",category:`Accessibility`,framework:`global`,requires:[`react`],tags:[`react-jsx-only`],isScanRule:!1}},{key:`react-doctor/aria-role`,id:`aria-role`,source:`react-doctor`,originallyExternal:!0,rule:{id:`aria-role`,title:`Invalid ARIA role`,severity:`error`,recommendation:`Use a real, non-abstract ARIA role so assistive tech can expose the element correctly.`,category:`Accessibility`,framework:`global`,requires:[`react`],tags:[`react-jsx-only`],isScanRule:!1}},{key:`react-doctor/aria-unsupported-elements`,id:`aria-unsupported-elements`,source:`react-doctor`,originallyExternal:!0,rule:{id:`aria-unsupported-elements`,title:`ARIA on unsupported element`,severity:`error`,recommendation:"Do not put `role` or `aria-*` on reserved HTML elements.",category:`Accessibility`,framework:`global`,requires:[`react`],tags:[`react-jsx-only`],isScanRule:!1}},{key:`react-doctor/artifact-baas-authority-surface`,id:`artifact-baas-authority-surface`,source:`react-doctor`,originallyExternal:!1,rule:{id:`artifact-baas-authority-surface`,title:`BaaS authority map shipped in browser artifact`,severity:`warn`,recommendation:`Client BaaS config is often public, but shipped collection names plus owner, role, tenant, or admin fields give attackers a precise authorization map. Verify rules/RLS enforce every boundary server-side.`,category:`Security`,framework:`global`,tags:[`security-scan`],isScanRule:!0}},{key:`react-doctor/artifact-env-leak`,id:`artifact-env-leak`,source:`react-doctor`,originallyExternal:!1,rule:{id:`artifact-env-leak`,title:`Server env leaked to browser artifact`,severity:`error`,recommendation:`Treat public env prefixes as publication, not secrecy; keep secret env vars server-only and rebuild after rotating leaked keys.`,category:`Security`,framework:`global`,tags:[`security-scan`],isScanRule:!0}},{key:`react-doctor/artifact-secret-leak`,id:`artifact-secret-leak`,source:`react-doctor`,originallyExternal:!1,rule:{id:`artifact-secret-leak`,title:`Secret shipped in browser artifact`,severity:`error`,recommendation:`Remove the secret from client bundles/static assets, rotate it, and route privileged service calls through server-only code.`,category:`Security`,framework:`global`,tags:[`security-scan`],isScanRule:!0}},{key:`react-doctor/async-await-in-loop`,id:`async-await-in-loop`,source:`react-doctor`,originallyExternal:!1,rule:{id:`async-await-in-loop`,title:`await inside a loop`,severity:`warn`,recommendation:"Collect the items, then use `await Promise.all(items.map(...))` so independent work runs at the same time",category:`Performance`,framework:`global`,tags:[`test-noise`],isScanRule:!1}},{key:`react-doctor/async-defer-await`,id:`async-defer-await`,source:`react-doctor`,originallyExternal:!1,rule:{id:`async-defer-await`,title:`await before an early-return guard`,severity:`warn`,recommendation:"Move the `await` below the early-return guard so the skip path stays fast and avoids unnecessary async work.",category:`Performance`,framework:`global`,requires:[`react`],tags:[`test-noise`],isScanRule:!1}},{key:`react-doctor/async-parallel`,id:`async-parallel`,source:`react-doctor`,originallyExternal:!1,rule:{id:`async-parallel`,title:`Independent awaits run sequentially`,severity:`warn`,recommendation:"Use `const [a, b] = await Promise.all([fetchA(), fetchB()])` so independent calls run at the same time",category:`Performance`,framework:`global`,tags:[`test-noise`],isScanRule:!1}},{key:`react-doctor/auth-token-in-web-storage`,id:`auth-token-in-web-storage`,source:`react-doctor`,originallyExternal:!1,rule:{id:`auth-token-in-web-storage`,title:`Auth token in web storage`,severity:`warn`,recommendation:"Don't persist auth tokens (JWTs, access/refresh tokens, secrets) in `localStorage`/`sessionStorage`; they're readable by any XSS. Use an `HttpOnly` cookie set by the server.",category:`Security`,framework:`global`,isScanRule:!1}},{key:`react-doctor/autocomplete-valid`,id:`autocomplete-valid`,source:`react-doctor`,originallyExternal:!0,rule:{id:`autocomplete-valid`,title:`Invalid autocomplete value`,severity:`warn`,recommendation:"Use a valid autofill token in `autoComplete` so browsers can fill the right field reliably.",category:`Accessibility`,framework:`global`,requires:[`react`],tags:[`react-jsx-only`],isScanRule:!1}},{key:`react-doctor/base-ui-dialog-popup-requires-title`,id:`base-ui-dialog-popup-requires-title`,source:`react-doctor`,originallyExternal:!1,rule:{id:`base-ui-dialog-popup-requires-title`,title:`Base UI dialog popup without a title`,severity:`warn`,recommendation:`Give every Base UI Dialog.Popup and AlertDialog.Popup a Title part (visually hidden when the design shows no heading) or name the dialog with aria-label.`,category:`Accessibility`,framework:`global`,requires:[`react`,`base-ui`],isScanRule:!1}},{key:`react-doctor/base-ui-field-requires-label`,id:`base-ui-field-requires-label`,source:`react-doctor`,originallyExternal:!1,rule:{id:`base-ui-field-requires-label`,title:`Base UI field control without a label`,severity:`warn`,recommendation:`Give every Field.Root that wraps a Field.Control a Field.Label (visually hidden when the design shows no label) or name the control directly with aria-label.`,category:`Accessibility`,framework:`global`,requires:[`react`,`base-ui`],isScanRule:!1}},{key:`react-doctor/base-ui-tabs-tab-requires-list`,id:`base-ui-tabs-tab-requires-list`,source:`react-doctor`,originallyExternal:!1,rule:{id:`base-ui-tabs-tab-requires-list`,title:`Base UI tab is outside Tabs.List`,severity:`warn`,recommendation:`Render each Base UI Tabs.Tab inside Tabs.List so the tablist role and arrow-key focus handling cover every tab.`,category:`Bugs`,framework:`global`,requires:[`base-ui`],matchByOccurrence:!0,isScanRule:!1}},{key:`react-doctor/build-pipeline-secret-boundary`,id:`build-pipeline-secret-boundary`,source:`react-doctor`,originallyExternal:!1,rule:{id:`build-pipeline-secret-boundary`,title:`Build pipeline runs code near secrets`,severity:`warn`,recommendation:`Run dependency installs with scripts disabled before exposing secrets, isolate untrusted build code, and move signing/deploy authority into a narrow privileged step.`,category:`Security`,framework:`global`,tags:[`security-scan`],isScanRule:!0}},{key:`react-doctor/button-has-type`,id:`button-has-type`,source:`react-doctor`,originallyExternal:!0,rule:{id:`button-has-type`,title:`Button missing explicit type`,severity:`warn`,recommendation:'Set an explicit button `type` so plain buttons do not submit forms by accident: `type="button"`, `"submit"`, or `"reset"`.',category:`Bugs`,framework:`global`,requires:[`react`],isScanRule:!1}},{key:`react-doctor/checked-requires-onchange-or-readonly`,id:`checked-requires-onchange-or-readonly`,source:`react-doctor`,originallyExternal:!0,rule:{id:`checked-requires-onchange-or-readonly`,title:`Checked input without onChange`,severity:`warn`,recommendation:"Add `onChange`, `readOnly`, or `defaultChecked` so React knows whether the checkbox is editable, display-only, or uncontrolled.",category:`Bugs`,framework:`global`,requires:[`react`],isScanRule:!1}},{key:`react-doctor/circular-dependency`,id:`circular-dependency`,source:`react-doctor`,originallyExternal:!1,rule:{id:`circular-dependency`,title:`Runtime import cycle`,severity:`warn`,recommendation:`Break the runtime cycle by extracting shared code into a lower-level module or inverting one dependency.`,category:`Maintainability`,framework:`global`,tags:[`project-analysis`],defaultEnabled:!1,isScanRule:!1,isProjectRule:!0}},{key:`react-doctor/class-component-missing-component-will-unmount-teardown`,id:`class-component-missing-component-will-unmount-teardown`,source:`react-doctor`,originallyExternal:!1,rule:{id:`class-component-missing-component-will-unmount-teardown`,title:`Class component acquires a resource with no teardown`,severity:`warn`,recommendation:"Release listeners and timers acquired in `componentDidMount`/`constructor` by adding a `componentWillUnmount` that removes them (or use MobX `disposeOnUnmount`).",category:`Bugs`,framework:`global`,requires:[`react`],isScanRule:!1}},{key:`react-doctor/click-events-have-key-events`,id:`click-events-have-key-events`,source:`react-doctor`,originallyExternal:!0,rule:{id:`click-events-have-key-events`,title:`Click handler missing keyboard handler`,severity:`warn`,recommendation:"Pair `onClick` with a key handler so keyboard users can trigger it.",category:`Accessibility`,framework:`global`,requires:[`react`],tags:[`react-jsx-only`],isScanRule:!1}},{key:`react-doctor/clickjacking-redirect-risk`,id:`clickjacking-redirect-risk`,source:`react-doctor`,originallyExternal:!1,rule:{id:`clickjacking-redirect-risk`,title:`Redirect or frame boundary risk`,severity:`warn`,recommendation:"Allowlist redirect origins/paths, set `frame-ancestors` for privileged pages, and avoid URL-prefilled privileged dialogs.",category:`Security`,framework:`global`,tags:[`security-scan`],isScanRule:!0}},{key:`react-doctor/client-localstorage-no-version`,id:`client-localstorage-no-version`,source:`react-doctor`,originallyExternal:!1,rule:{id:`client-localstorage-no-version`,title:`Unversioned localStorage key`,severity:`warn`,recommendation:`Put a version in the storage key (e.g. "myKey:v1"). If you change the data shape later, old saved data can be ignored instead of crashing the app.`,category:`Bugs`,framework:`global`,requires:[`react`],tags:[`test-noise`],isScanRule:!1}},{key:`react-doctor/client-passive-event-listeners`,id:`client-passive-event-listeners`,source:`react-doctor`,originallyExternal:!1,rule:{id:`client-passive-event-listeners`,title:`Non-passive scroll listener`,severity:`warn`,recommendation:"Add `{ passive: true }` as the third argument: `addEventListener('touchmove', handler, { passive: true })`. Only do this if the handler doesn't call `event.preventDefault()`, since passive listeners ignore it (which breaks pull-to-refresh, custom gestures, and nested scrolling).",category:`Performance`,framework:`global`,requires:[`react`],tags:[`test-noise`],isScanRule:!1}},{key:`react-doctor/command-execution-input-risk`,id:`command-execution-input-risk`,source:`react-doctor`,originallyExternal:!1,rule:{id:`command-execution-input-risk`,title:`Command execution uses caller-shaped input`,severity:`error`,recommendation:`Avoid shell execution for caller-controlled values. Use fixed commands, argument arrays, strict allowlists, and no shell interpolation.`,category:`Security`,framework:`global`,tags:[`security-scan`],isScanRule:!0}},{key:`react-doctor/context-provider-value-from-unmemoized-local-literal`,id:`context-provider-value-from-unmemoized-local-literal`,source:`react-doctor`,originallyExternal:!1,rule:{id:`context-provider-value-from-unmemoized-local-literal`,title:`Context value from an unmemoized local literal`,severity:`warn`,recommendation:`Memoize the context value in component scope so consumers do not redraw every render. For mapped providers, extract each item into a child component and memoize there.`,category:`Performance`,framework:`global`,requires:[`react`],disabledWhen:[`react-compiler`],tags:[`react-jsx-only`,`test-noise`],isScanRule:!1}},{key:`react-doctor/control-has-associated-label`,id:`control-has-associated-label`,source:`react-doctor`,originallyExternal:!0,rule:{id:`control-has-associated-label`,title:`Control missing accessible label`,severity:`warn`,recommendation:`Give every interactive control a label screen readers can read.`,category:`Accessibility`,framework:`global`,requires:[`react`],tags:[`react-jsx-only`],isScanRule:!1}},{key:`react-doctor/cors-cookie-trust-risk`,id:`cors-cookie-trust-risk`,source:`react-doctor`,originallyExternal:!1,rule:{id:`cors-cookie-trust-risk`,title:`Broad cookie or credentialed CORS trust`,severity:`warn`,recommendation:`Keep auth cookies host-only and HttpOnly, avoid credentialed CORS for less-trusted docs/vendor origins, and isolate documentation domains from app sessions.`,category:`Security`,framework:`global`,tags:[`security-scan`],isScanRule:!0}},{key:`react-doctor/dangerous-html-sink`,id:`dangerous-html-sink`,source:`react-doctor`,originallyExternal:!1,rule:{id:`dangerous-html-sink`,title:`HTML injection sink with dynamic content`,severity:`warn`,recommendation:`Prefer rendering structured React nodes. If HTML is required, sanitize with a well-reviewed sanitizer and keep the trust boundary close to the sink.`,category:`Security`,framework:`global`,tags:[`security-scan`],isScanRule:!0}},{key:`react-doctor/data-table-requires-accessible-name`,id:`data-table-requires-accessible-name`,source:`react-doctor`,originallyExternal:!1,rule:{id:`data-table-requires-accessible-name`,title:`Data table has no accessible name`,severity:`warn`,recommendation:`Give data tables a concise caption, or reference an existing visible title with aria-labelledby.`,category:`Accessibility`,framework:`global`,requires:[`react`],defaultEnabled:!1,isScanRule:!1}},{key:`react-doctor/debounce-no-cleanup`,id:`debounce-no-cleanup`,source:`react-doctor`,originallyExternal:!1,rule:{id:`debounce-no-cleanup`,title:`Memoized debounce never cancelled on unmount`,severity:`warn`,recommendation:"A debounced/throttled callback holds a pending timer that still fires after unmount, so add `useEffect(() => () => debounced.cancel(), [debounced])` to cancel the trailing invocation when the component tears down.",category:`Bugs`,framework:`global`,requires:[`react`],isScanRule:!1}},{key:`react-doctor/design-no-em-dash-in-jsx-text`,id:`design-no-em-dash-in-jsx-text`,source:`react-doctor`,originallyExternal:!1,rule:{id:`design-no-em-dash-in-jsx-text`,title:`Em dash in JSX text`,severity:`warn`,recommendation:`Replace em dashes in UI text with commas, colons, semicolons, or parentheses so the copy reads less like AI output.`,category:`Maintainability`,framework:`global`,requires:[`react`],tags:[`design`,`test-noise`],defaultEnabled:!1,isScanRule:!1}},{key:`react-doctor/design-no-redundant-padding-axes`,id:`design-no-redundant-padding-axes`,source:`react-doctor`,originallyExternal:!1,rule:{id:`design-no-redundant-padding-axes`,title:`Redundant padding axes`,severity:`warn`,recommendation:"Collapse matching padding axes to `p-N` so duplicated classes do not make spacing harder to scan; keep split axes only when breakpoints differ.",category:`Maintainability`,framework:`global`,requires:[`react`],tags:[`design`,`test-noise`],defaultEnabled:!1,isScanRule:!1}},{key:`react-doctor/design-no-redundant-size-axes`,id:`design-no-redundant-size-axes`,source:`react-doctor`,originallyExternal:!1,rule:{id:`design-no-redundant-size-axes`,title:`Redundant width and height axes`,severity:`warn`,recommendation:"Collapse matching width and height to `size-N` so duplicated classes do not make layout harder to scan.",category:`Maintainability`,framework:`global`,requires:[`react`,`tailwind:3.4`],tags:[`design`,`test-noise`],defaultEnabled:!1,isScanRule:!1}},{key:`react-doctor/design-no-space-on-flex-children`,id:`design-no-space-on-flex-children`,source:`react-doctor`,originallyExternal:!1,rule:{id:`design-no-space-on-flex-children`,title:`space-* utility on flex children`,severity:`warn`,recommendation:"Use `gap-*` on the flex or grid parent. `space-x-*` and `space-y-*` leave gaps when a child is hidden, miss spacing on wrapped lines, and don't flip in right-to-left layouts.",category:`Maintainability`,framework:`global`,requires:[`react`],tags:[`design`,`test-noise`],defaultEnabled:!1,isScanRule:!1}},{key:`react-doctor/design-no-three-period-ellipsis`,id:`design-no-three-period-ellipsis`,source:`react-doctor`,originallyExternal:!1,rule:{id:`design-no-three-period-ellipsis`,title:`Three dots instead of ellipsis`,severity:`warn`,recommendation:`Use the real ellipsis character ("…") so UI labels look polished and consistent instead of like three separate periods.`,category:`Maintainability`,framework:`global`,requires:[`react`],tags:[`design`,`test-noise`],defaultEnabled:!1,isScanRule:!1}},{key:`react-doctor/design-no-vague-button-label`,id:`design-no-vague-button-label`,source:`react-doctor`,originallyExternal:!1,rule:{id:`design-no-vague-button-label`,title:`Vague button label`,severity:`warn`,recommendation:`Name the action: "Save changes" instead of "Continue", "Send invite" instead of "Submit". The label is the button's accessible name.`,category:`Accessibility`,framework:`global`,requires:[`react`],tags:[`design`,`test-noise`],defaultEnabled:!1,isScanRule:!1}},{key:`react-doctor/details-requires-summary`,id:`details-requires-summary`,source:`react-doctor`,originallyExternal:!1,rule:{id:`details-requires-summary`,title:`Details disclosure has no explicit summary`,severity:`warn`,recommendation:`Make a meaningful summary the first child of details so users can understand and operate the disclosure.`,category:`Accessibility`,framework:`global`,requires:[`react`],defaultEnabled:!1,isScanRule:!1}},{key:`react-doctor/dialog-has-accessible-name`,id:`dialog-has-accessible-name`,source:`react-doctor`,originallyExternal:!1,rule:{id:`dialog-has-accessible-name`,title:`Dialog without accessible name`,severity:`warn`,recommendation:'Give every `<dialog>` / `role="dialog"` an accessible name with `aria-label` or `aria-labelledby` (referencing the dialog\'s title element).',category:`Accessibility`,framework:`global`,requires:[`react`],isScanRule:!1}},{key:`react-doctor/display-name`,id:`display-name`,source:`react-doctor`,originallyExternal:!0,rule:{id:`display-name`,title:`Component missing display name`,severity:`warn`,recommendation:"Give each component a `displayName` so DevTools shows a clear name.",category:`Maintainability`,framework:`global`,requires:[`react`],defaultEnabled:!1,isScanRule:!1}},{key:`react-doctor/duplicate-jsx-subtree`,id:`duplicate-jsx-subtree`,source:`react-doctor`,originallyExternal:!1,rule:{id:`duplicate-jsx-subtree`,title:`Duplicated JSX structure`,severity:`warn`,recommendation:`Extract a shared component when the repeated JSX trees represent the same UI concept and should evolve together.`,category:`Maintainability`,framework:`global`,tags:[`project-analysis`],isScanRule:!1,isProjectRule:!0}},{key:`react-doctor/effect-listener-cleanup-mismatch`,id:`effect-listener-cleanup-mismatch`,source:`react-doctor`,originallyExternal:!1,rule:{id:`effect-listener-cleanup-mismatch`,title:`Effect cleanup does not match its event listener`,severity:`error`,recommendation:"Pass the same callback binding and capture flag to `addEventListener` and `removeEventListener`, or abort the registration's local AbortController during cleanup.",category:`Bugs`,framework:`global`,requires:[`react`],isScanRule:!1}},{key:`react-doctor/effect-listener-cleanup-reference-mismatch`,id:`effect-listener-cleanup-reference-mismatch`,source:`react-doctor`,originallyExternal:!1,rule:{id:`effect-listener-cleanup-reference-mismatch`,title:`Effect cleanup removes the wrong listener reference`,severity:`error`,recommendation:`Removal APIs match by reference identity, so the second inline function passed to the remove call can never equal the one you added; hoist the handler into a single named const (or useCallback) and pass that same reference to both the add and remove calls.`,category:`Bugs`,framework:`global`,requires:[`react`],isScanRule:!1}},{key:`react-doctor/effect-needs-cleanup`,id:`effect-needs-cleanup`,source:`react-doctor`,originallyExternal:!1,rule:{id:`effect-needs-cleanup`,title:`Effect subscription or timer never cleaned up`,severity:`error`,recommendation:"Return a cleanup function that stops the subscription or timer: `return () => target.removeEventListener(name, handler)` for listeners, `return () => clearInterval(id)` or `clearTimeout(id)` for timers, `return () => observer.disconnect()` for observers, `return () => socket.close()` for connections, or `return unsubscribe` if the subscribe call already gave you one.",category:`Bugs`,framework:`global`,requires:[`react`],tags:[`test-noise`],isScanRule:!1}},{key:`react-doctor/effect-observer-needs-disconnect`,id:`effect-observer-needs-disconnect`,source:`react-doctor`,originallyExternal:!1,rule:{id:`effect-observer-needs-disconnect`,title:`Observer created in an effect never disconnected`,severity:`error`,recommendation:"Return a cleanup function that calls `observer.disconnect()` (or `observer.unobserve(node)`) so the observer stops firing callbacks against detached nodes after unmount instead of leaking on every mount.",category:`Bugs`,framework:`global`,requires:[`react`],isScanRule:!1}},{key:`react-doctor/effect-raf-loop-needs-cancel`,id:`effect-raf-loop-needs-cancel`,source:`react-doctor`,originallyExternal:!1,rule:{id:`effect-raf-loop-needs-cancel`,title:`requestAnimationFrame loop never cancelled`,severity:`warn`,recommendation:"Store the frame id and return a cleanup that calls `cancelAnimationFrame(id)` so self-scheduling work cannot continue after unmount.",category:`Bugs`,framework:`global`,requires:[`react`],isScanRule:!1}},{key:`react-doctor/effect-remove-listener-inline-handler`,id:`effect-remove-listener-inline-handler`,source:`react-doctor`,originallyExternal:!1,rule:{id:`effect-remove-listener-inline-handler`,title:`removeEventListener called with a fresh inline handler`,severity:`error`,recommendation:"Removal APIs match the listener by reference equality, so a fresh inline arrow, function expression, or `.bind(...)` result can never equal the registered handler; hoist the handler into a named const and pass that same reference to both the add and remove calls.",category:`Bugs`,framework:`global`,requires:[`react`],tags:[`test-noise`],isScanRule:!1}},{key:`react-doctor/empty-table-header`,id:`empty-table-header`,source:`react-doctor`,originallyExternal:!0,rule:{id:`empty-table-header`,title:`Table header has no accessible text`,severity:`warn`,recommendation:`Give every table header concise text or an explicit accessible name.`,category:`Accessibility`,framework:`global`,requires:[`react`],defaultEnabled:!1,isScanRule:!1}},{key:`react-doctor/exhaustive-deps`,id:`exhaustive-deps`,source:`react-doctor`,originallyExternal:!0,rule:{id:`exhaustive-deps`,title:`Missing effect dependencies`,severity:`warn`,recommendation:`Don't blindly add missing dependencies. Read the hook callback first.
|
|
2
2
|
|
|
3
3
|
Bad:
|
|
4
4
|
useEffect(() => {
|