@vibecodiq/cli 0.4.0 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +18 -18
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -1,19 +1,19 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import{createHash as
|
|
3
|
-
`).length;
|
|
4
|
-
`);for(let c=0;c<a.length;c++)t.test(a[c])&&i.push({file:
|
|
5
|
-
`);for(let a=0;a<o.length;a++){let c=o[a].trim();if(!(c.startsWith("#")||!c)){for(let l of t)if(l.test(c)){i.push({file:r,line:a+1,content:c.split("=")[0]});break}}}}return i.length>0?{result:"FAIL",message:`Secret keys exposed via public prefix (${i.length} found)`,evidence:i.map(r=>`${r.file}:${r.line} \u2192 ${r.content}`)}:n.length===0?{result:"UNKNOWN",message:"No .env files found to check"}:{result:"PASS",message:"No secrets found with NEXT_PUBLIC_/VITE_/EXPO_PUBLIC_ prefix"}}};var _e={id:"AUTH-06",name:"Protected routes redirect unauthenticated users",module:"auth",layer:"L2",priority:"P1",description:"Middleware or route guard must redirect unauthenticated users to /login for protected pages.",fixCost:100,fixSize:"M",async run(e){let t=e.files.find(i=>/^middleware\.(ts|js)$/.test(i));if(!t)return(await e.grepFiles(/redirect.*login|redirect.*auth|redirect.*sign/i)).length>0?{result:"PASS",message:"Route guards with login redirect detected"}:{result:"FAIL",message:"No middleware.ts and no route guards found \u2014 unauthenticated users can access protected pages",evidence:["Missing: middleware.ts or per-page auth redirect"]};let n;try{n=await e.readFile(t)}catch{return{result:"UNKNOWN",message:"Could not read middleware file"}}return/redirect.*login|redirect.*auth|NextResponse.*redirect/i.test(n)?{result:"PASS",message:"Middleware redirects unauthenticated users to login"}:{result:"FAIL",message:"Middleware exists but no login redirect detected",evidence:[`${t} \u2014 no redirect to /login or /auth`]}}};var ke={id:"AUTH-07",name:"Session tokens in httpOnly cookies",module:"auth",layer:"L2",priority:"P2",description:"Tokens in localStorage are accessible via XSS. httpOnly cookies prevent JavaScript access to session tokens.",fixCost:100,fixSize:"M",async run(e){let t=/localStorage.*(?:token|session|jwt|access_token)|sessionStorage.*(?:token|session|jwt)/i,n=/httpOnly|cookie.*session|supabase.*ssr|@supabase\/ssr/i,i=await e.grepFiles(t),r=await e.grepFiles(n),s=i.filter(o=>!o.content.trimStart().startsWith("//"));return s.length>0&&r.length===0?{result:"FAIL",message:`Session tokens stored in localStorage (${s.length} location${s.length>1?"s":""})`,evidence:s.slice(0,3).map(o=>`${o.file}:${o.line} \u2192 ${o.content.substring(0,120)}`)}:r.length>0?{result:"PASS",message:"httpOnly cookies or @supabase/ssr detected for session management"}:{result:"UNKNOWN",message:"No explicit token storage pattern detected"}}};var Ie={id:"AUTH-08",name:"Password hashing (if custom auth)",module:"auth",layer:"L2",priority:"P0",description:"Passwords must be hashed with bcrypt/argon2/scrypt. N/A if using Supabase Auth (handles hashing internally).",fixCost:100,fixSize:"M",async run(e){let t=/supabase|@supabase|createBrowserClient|createServerClient/i;if((await e.grepFiles(t)).length>0)return{result:"N/A",message:"Supabase Auth handles password hashing internally (bcrypt/Argon2)"};let i=/bcrypt|argon2|scrypt|pbkdf2/i,r=/password.*=.*req\.body|password.*=.*body\.|plaintext|md5.*password|sha1.*password/i,s=await e.grepFiles(i),o=await e.grepFiles(r);return o.length>0&&s.length===0?{result:"FAIL",message:"Password handling without secure hashing detected",evidence:o.slice(0,3).map(a=>`${a.file}:${a.line} \u2192 ${a.content.substring(0,120)}`)}:s.length>0?{result:"PASS",message:"Secure password hashing detected (bcrypt/argon2/scrypt)"}:{result:"UNKNOWN",message:"No custom auth password handling detected"}}};var Pe={id:"AUTH-09",name:"Rate limiting on auth endpoints",module:"auth",layer:"L2",priority:"P1",description:"Login/register endpoints without rate limiting are vulnerable to brute force attacks.",fixCost:100,fixSize:"M",async run(e){let t=e.files.filter(o=>/auth|login|register|sign/i.test(o)),n=/rateLimit|rate.limit|throttle|limiter|too.many.requests|429|upstash/i;return t.length===0?{result:"UNKNOWN",message:"No auth endpoint files found"}:(await e.grepFiles(n,t)).length>0?{result:"PASS",message:"Rate limiting detected on auth endpoints"}:(await e.grepFiles(n)).length>0?{result:"PASS",message:"Rate limiting detected in codebase (verify it covers auth endpoints)"}:(await e.grepFiles(/supabase.*auth|@supabase/i)).length>0?{result:"PASS",message:"Supabase Auth has built-in rate limiting for auth endpoints"}:{result:"FAIL",message:"No rate limiting found on auth endpoints",evidence:["Missing: rate limiting middleware on login/register routes"]}}};var we={id:"AUTH-10",name:"Profile sync trigger with safe search_path",module:"auth",layer:"L2",priority:"P1",description:"handle_new_user() trigger must use SECURITY DEFINER with restricted search_path to prevent privilege escalation.",fixCost:100,fixSize:"M",async run(e){let t=e.files.filter(b);if(t.length===0)return{result:"UNKNOWN",message:"No SQL migration files found"};let n=/handle_new_user|on_auth_user_created|AFTER\s+INSERT\s+ON\s+auth\.users/i,i=/SECURITY\s+DEFINER/i,r=/search_path|SET\s+search_path/i;if((await e.grepFiles(n)).length===0)return{result:"FAIL",message:"No profile sync trigger found (handle_new_user or equivalent)",evidence:["Missing trigger: new auth.users rows won't sync to profiles table"]};for(let o of t){let a;try{a=await e.readFile(o)}catch{continue}if(n.test(a)){let c=i.test(a),l=r.test(a);return c&&!l?{result:"FAIL",message:"Profile trigger uses SECURITY DEFINER without restricted search_path",evidence:[`${o} \u2014 SECURITY DEFINER without SET search_path = '' (privilege escalation risk)`]}:c&&l?{result:"PASS",message:"Profile sync trigger found with SECURITY DEFINER and restricted search_path"}:{result:"PASS",message:"Profile sync trigger found"}}}return{result:"PASS",message:"Profile sync trigger detected"}}};var Le={id:"AUTH-11",name:"Client/server auth separation",module:"auth",layer:"L2",priority:"P0",description:"Separate Supabase clients for browser and server. One shared client leaks service_role to browser or uses anon key on server.",fixCost:100,fixSize:"M",async run(e){let t=/createBrowserClient|createClient.*browser/i,n=/createServerClient|createClient.*server/i,i=/createClient\s*\(/,r=await e.grepFiles(t),s=await e.grepFiles(n),o=await e.grepFiles(i);if(r.length>0&&s.length>0)return{result:"PASS",message:"Separate browser and server Supabase clients detected"};if(r.length>0||s.length>0)return{result:"PASS",message:"Dedicated Supabase client pattern detected (@supabase/ssr)"};if(o.length>0){let a=o.filter(c=>/supabase|createClient/.test(c.content));if(a.length>0)return{result:"FAIL",message:"Single generic createClient() used \u2014 no client/server separation",evidence:a.slice(0,3).map(c=>`${c.file}:${c.line} \u2192 ${c.content.substring(0,120)}`)}}return{result:"UNKNOWN",message:"No Supabase client initialization found"}}};var Ne={id:"AUTH-12",name:"Auth environment variables present",module:"auth",layer:"L2",priority:"P2",description:"Missing auth env vars = hardcoded keys or broken auth in production.",fixCost:100,fixSize:"M",async run(e){let t=e.files.filter(E);if(t.length===0)return{result:"UNKNOWN",message:"No .env files found"};let n=["SUPABASE_URL","SUPABASE_ANON_KEY"],i=[];for(let o of t){let a;try{a=await e.readFile(o)}catch{continue}for(let c of n)a.includes(c)&&!i.includes(c)&&i.push(c)}if((await e.grepFiles(/supabase/i)).length===0)return{result:"UNKNOWN",message:"No Supabase references found"};let s=n.filter(o=>!i.includes(o));return s.length>0?{result:"FAIL",message:`Missing auth env vars: ${s.join(", ")}`,evidence:s.map(o=>`${o} not found in any .env file`)}:{result:"PASS",message:"Auth environment variables configured"}}};var Re={id:"AUTH-13",name:"getUser() not getSession() for server-side auth",module:"auth",layer:"L2",priority:"P0",description:"getSession() reads JWT without server verification. Use getUser() for auth decisions.",fixCost:100,fixSize:"M",async run(e){let t=/\.auth\.getSession\s*\(/,n=/\.auth\.getUser\s*\(/,i=await e.grepFiles(t),r=await e.grepFiles(n);if(i.length===0&&r.length===0)return{result:"UNKNOWN",message:"No getSession/getUser calls found \u2014 Supabase Auth may not be used"};let s=i.filter(l=>w(l.file)),o=r.filter(l=>w(l.file));if(s.length>0&&o.length===0)return{result:"FAIL",message:`Server-side code uses getSession() without getUser() (${s.length} location${s.length>1?"s":""})`,evidence:s.map(l=>`${l.file}:${l.line} \u2192 ${l.content.substring(0,120)}`)};if(s.length>0&&o.length>0)return{result:"FAIL",message:"Server code uses both getSession() and getUser() \u2014 getSession() in server context is insecure",evidence:s.map(l=>`${l.file}:${l.line} \u2192 ${l.content.substring(0,120)}`)};let a=i.filter(l=>!w(l.file)&&!/supabase\/functions\//i.test(l.file)),c=r.filter(l=>!w(l.file)&&!/supabase\/functions\//i.test(l.file));return a.length>0&&c.length===0&&o.length===0?{result:"FAIL",message:`Client code uses getSession() (${a.length} location${a.length>1?"s":""}) but getUser() never called \u2014 auth relies on unverified JWT`,evidence:a.slice(0,3).map(l=>`${l.file}:${l.line} \u2192 ${l.content.substring(0,120)}`)}:o.length>0||c.length>0?{result:"PASS",message:"Auth uses getUser() for verification"}:{result:"PASS",message:"getUser() is used for auth verification"}}};var Ee={id:"AUTH-14",name:"No eval() or dangerouslySetInnerHTML with user data",module:"auth",layer:"L2",priority:"P0",description:"eval() and dangerouslySetInnerHTML enable XSS attacks \u2014 session theft and account takeover.",fixCost:100,fixSize:"M",async run(e){let t=/\beval\s*\(/,n=/dangerouslySetInnerHTML/,i=await e.grepFiles(t),r=await e.grepFiles(n),s=i.filter(c=>!(c.content.trimStart().startsWith("//")||c.content.trimStart().startsWith("*")||c.file.includes("node_modules"))),o=r.filter(c=>!(c.content.trimStart().startsWith("//")||c.file.includes("node_modules"))),a=[...s,...o];return a.length>0?{result:"FAIL",message:`Unsafe code patterns found (${s.length} eval, ${o.length} dangerouslySetInnerHTML)`,evidence:a.slice(0,5).map(c=>`${c.file}:${c.line} \u2192 ${c.content.substring(0,120)}`)}:{result:"PASS",message:"No eval() or dangerouslySetInnerHTML found"}}};var Fe={id:"AUTH-15",name:"CORS configuration",module:"auth",layer:"L2",priority:"P2",description:"Access-Control-Allow-Origin: * with credentials allows any website to read auth cookies and data.",fixCost:100,fixSize:"M",async run(e){let t=/Access-Control-Allow-Origin.*\*|cors.*origin.*\*|origin:\s*['"]?\*/i,n=/credentials.*true|allowCredentials|Access-Control-Allow-Credentials/i,i=await e.grepFiles(t),r=await e.grepFiles(n);return i.length>0&&r.length>0?{result:"FAIL",message:"CORS wildcard (*) used with credentials \u2014 any website can read auth data",evidence:i.slice(0,3).map(s=>`${s.file}:${s.line} \u2192 ${s.content.substring(0,120)}`)}:i.length>0?{result:"FAIL",message:"CORS wildcard (*) detected \u2014 consider restricting to specific origins",evidence:i.slice(0,3).map(s=>`${s.file}:${s.line} \u2192 ${s.content.substring(0,120)}`)}:{result:"PASS",message:"No dangerous CORS wildcard configuration detected"}}};var Te={id:"AUTH-16",name:"Token/session expiration configured",module:"auth",layer:"L2",priority:"P2",description:"Infinite sessions mean a stolen token works forever. Configure JWT expiration and session timeouts.",fixCost:100,fixSize:"M",async run(e){let t=/expiresIn|maxAge|session.*expir|jwt.*expir|SESSION_EXPIRY|JWT_EXPIRY|token.*expir/i;return(await e.grepFiles(t)).length>0?{result:"PASS",message:"Token/session expiration configured"}:(await e.grepFiles(/supabase/i)).length>0?{result:"PASS",message:"Supabase Auth has default JWT expiry (3600s)"}:{result:"FAIL",message:"No token/session expiration configuration found",evidence:["Missing: expiresIn, maxAge, or session expiry configuration"]}}};var xe={id:"AUTH-17",name:"Storage bucket RLS",module:"auth",layer:"L2",priority:"P0",description:"Supabase storage buckets must have RLS on storage.objects. Without it, all files are publicly accessible.",fixCost:100,fixSize:"M",async run(e){let t=e.files.filter(b);if(t.length===0)return{result:"UNKNOWN",message:"No SQL migration files found"};let n=/storage\.buckets|create.*bucket|INSERT.*storage\.buckets/i,i=/storage\.objects.*ENABLE.*ROW.*LEVEL|RLS.*storage\.objects|POLICY.*storage\.objects/i,r=await e.grepFiles(n,t),s=await e.grepFiles(i,t);return r.length===0?(await e.grepFiles(/supabase.*storage|storage\.from\(/i)).length===0?{result:"N/A",message:"No Supabase storage usage detected"}:{result:"UNKNOWN",message:"Storage used in code but no bucket creation in migrations"}:s.length>0?{result:"PASS",message:"Storage bucket RLS policies detected"}:{result:"FAIL",message:"Storage buckets created without RLS on storage.objects \u2014 files may be publicly accessible",evidence:r.slice(0,3).map(o=>`${o.file}:${o.line} \u2192 ${o.content.substring(0,120)}`)}}};var De={id:"AUTH-18",name:"RBAC in app_metadata not user_metadata",module:"auth",layer:"L2",priority:"P0",description:"Roles in user_metadata are user-editable. Use app_metadata for RBAC (server-only).",fixCost:100,fixSize:"M",async run(e){let t=/user_meta_?data.*role|raw_user_meta_?data.*role|user\.user_metadata.*role/i,n=/app_meta_?data.*role|user\.app_metadata.*role/i,i=/updateUser\s*\(\s*\{[\s\S]*?data\s*:\s*\{[\s\S]*?role/,r=await e.grepFiles(t),s=await e.grepFiles(n),o=await e.grepFiles(i);return r.length>0?{result:"FAIL",message:`Role stored in user_metadata (user-editable) \u2014 ${r.length} location${r.length>1?"s":""}`,evidence:r.map(a=>`${a.file}:${a.line} \u2192 ${a.content.substring(0,120)}`)}:o.length>0?{result:"FAIL",message:"Role set via updateUser() data field (user-editable user_metadata)",evidence:o.map(a=>`${a.file}:${a.line} \u2192 ${a.content.substring(0,120)}`)}:s.length>0?{result:"PASS",message:"Role stored in app_metadata (server-only, not user-editable)"}:{result:"UNKNOWN",message:"No role/RBAC references found in codebase"}}};var Ue={id:"AUTH-19",name:"Multi-tenancy data isolation",module:"auth",layer:"L2",priority:"P1",description:"RLS policies must include tenant_id or organization_id to prevent cross-tenant data access.",fixCost:100,fixSize:"M",async run(e){let t=e.files.filter(b),n=/tenant_id|organization_id|org_id|team_id|workspace_id/i,i=await e.grepFiles(n,t.length>0?t:void 0),r=await e.grepFiles(n);return i.length===0&&r.length===0?{result:"N/A",message:"No multi-tenancy pattern detected (single-tenant app)"}:(await e.grepFiles(/POLICY[\s\S]*?tenant_id|POLICY[\s\S]*?org_id|POLICY[\s\S]*?organization_id/i,t)).length>0?{result:"PASS",message:"RLS policies include tenant isolation"}:{result:"FAIL",message:"Multi-tenant schema detected but RLS policies don't include tenant_id filtering",evidence:["Tenant columns exist but RLS policies may allow cross-tenant data access"]}}};var Me={id:"AUTH-20",name:"OAuth domain restriction",module:"auth",layer:"L2",priority:"P1",description:"Social login (Google, GitHub) should restrict allowed email domains to prevent unauthorized access.",fixCost:100,fixSize:"M",async run(e){let t=/signInWithOAuth|signIn.*provider|google|github.*login|oauth/i,n=/allowedDomain|domain.*restrict|email.*domain|hd=|hosted_domain/i,i=await e.grepFiles(t);return i.length===0?{result:"N/A",message:"No OAuth/social login detected"}:(await e.grepFiles(n)).length>0?{result:"PASS",message:"OAuth domain restriction detected"}:{result:"FAIL",message:"OAuth login enabled without domain restriction \u2014 any Google/GitHub account can sign in",evidence:i.slice(0,3).map(s=>`${s.file}:${s.line} \u2192 ${s.content.substring(0,120)}`)}}};var Oe={id:"AUTH-21",name:"Force dynamic on auth routes",module:"auth",layer:"L2",priority:"P1",description:"Auth routes must use force-dynamic to prevent ISR cache serving wrong user data.",fixCost:100,fixSize:"M",async run(e){let t=e.files.filter(s=>/auth|login|register|signup|sign-in|sign-up/i.test(s)&&(s.includes("page.")||s.includes("route."))&&(s.endsWith(".ts")||s.endsWith(".tsx")||s.endsWith(".js")||s.endsWith(".jsx")));if(t.length===0)return{result:"UNKNOWN",message:"No auth route/page files found"};let n=/export\s+const\s+dynamic\s*=\s*['"]force-dynamic['"]/,i=/unstable_noStore|noStore|revalidate\s*=\s*0/,r=[];for(let s of t){let o;try{o=await e.readFile(s)}catch{continue}!n.test(o)&&!i.test(o)&&s.includes("page.")&&r.push(s)}return r.length>0?{result:"FAIL",message:`${r.length} auth page${r.length>1?"s":""} without force-dynamic \u2014 may serve cached auth state`,evidence:r.slice(0,5).map(s=>`${s} \u2014 missing export const dynamic = 'force-dynamic'`)}:{result:"PASS",message:"Auth routes use force-dynamic or equivalent"}}};var We={id:"AUTH-22",name:"CSRF protection on Route Handlers",module:"auth",layer:"L2",priority:"P2",description:"Server Actions have built-in CSRF protection (Next.js 14+). Route Handlers do NOT \u2014 they need explicit CSRF tokens or SameSite cookies.",fixCost:100,fixSize:"M",async run(e){let t=e.files.filter(o=>/route\.(ts|js)$/i.test(o));if(t.length===0)return{result:"UNKNOWN",message:"No Route Handler files found"};let n=[];for(let o of t){let a;try{a=await e.readFile(o)}catch{continue}/export\s+(?:async\s+)?function\s+(?:POST|PUT|PATCH|DELETE)/i.test(a)&&n.push(o)}if(n.length===0)return{result:"PASS",message:"No mutation Route Handlers found (only GET)"};let i=/csrf|csurf|csrfToken|SameSite|x-csrf|anti.?forgery/i;return(await e.grepFiles(i)).length>0?{result:"PASS",message:"CSRF protection detected"}:(await e.grepFiles(/["']use server["']/i)).length>0&&n.length<=2?{result:"PASS",message:"Mutations use Server Actions (built-in CSRF protection)"}:{result:"FAIL",message:`${n.length} mutation Route Handler${n.length>1?"s":""} without CSRF protection`,evidence:n.slice(0,3).map(o=>`${o} \u2014 POST/PUT/PATCH/DELETE without CSRF token`)}}};var je={id:"AUTH-23",name:"Email verification required",module:"auth",layer:"L2",priority:"P2",description:"Without email verification, anyone can sign up with any email \u2014 spam accounts and impersonation.",fixCost:100,fixSize:"M",async run(e){let t=/email_confirmed_at|emailConfirmed|verifyEmail|confirmEmail|email.*verif|verification.*email/i;return(await e.grepFiles(t)).length>0?{result:"PASS",message:"Email verification check detected"}:(await e.grepFiles(/supabase/i)).length>0?{result:"PASS",message:"Supabase Auth has configurable email verification (check dashboard settings)"}:{result:"FAIL",message:"No email verification enforcement found",evidence:["Missing: email_confirmed_at check or equivalent verification flow"]}}};var Be={id:"AUTH-24",name:"Account enumeration prevention",module:"auth",layer:"L2",priority:"P2",description:"Login/register error messages must not reveal whether an email exists. Consistent messages prevent user enumeration.",fixCost:100,fixSize:"M",async run(e){let t=/email.*not.*found|user.*not.*found|no.*account.*with|email.*already.*registered|email.*already.*exists|account.*already.*exists/i,i=(await e.grepFiles(t)).filter(r=>!r.content.trimStart().startsWith("//")&&!r.content.trimStart().startsWith("*"));return i.length>0?{result:"FAIL",message:`Account enumeration possible \u2014 error messages reveal email existence (${i.length} location${i.length>1?"s":""})`,evidence:i.slice(0,3).map(r=>`${r.file}:${r.line} \u2192 ${r.content.substring(0,120)}`)}:{result:"PASS",message:"No account enumeration patterns detected in error messages"}}};var He={id:"AUTH-25",name:"Refresh token reuse detection",module:"auth",layer:"L2",priority:"P2",description:"Token rotation prevents stolen refresh tokens from being reused. Supabase supports this via config.",fixCost:100,fixSize:"M",async run(e){let t=/token.*rotation|refresh.*token.*reuse|reuse.*detection|GOTRUE_SECURITY_REFRESH_TOKEN_REUSE_INTERVAL/i;return(await e.grepFiles(t)).length>0?{result:"PASS",message:"Refresh token rotation/reuse detection configured"}:(await e.grepFiles(/supabase/i)).length>0?{result:"PASS",message:"Supabase Auth has built-in refresh token rotation (check dashboard config)"}:{result:"UNKNOWN",message:"No refresh token configuration detected"}}};var ze={id:"AUTH-26",name:"Sign-out revokes server session",module:"auth",layer:"L2",priority:"P2",description:"Sign-out must revoke the server session (scope: 'global'), not just clear client cookies.",fixCost:100,fixSize:"M",async run(e){let t=/signOut|sign_out|logout|log_out/i,n=/scope.*global|global.*scope/i,i=await e.grepFiles(t);return i.length===0?{result:"UNKNOWN",message:"No sign-out implementation found"}:(await e.grepFiles(n)).length>0?{result:"PASS",message:"Sign-out uses global scope (revokes all sessions)"}:{result:"FAIL",message:"Sign-out found but no global scope \u2014 sessions may persist on other devices",evidence:i.slice(0,2).map(s=>`${s.file}:${s.line} \u2192 ${s.content.substring(0,120)}`)}}};var qe={id:"AUTH-27",name:"Email link poisoning mitigation",module:"auth",layer:"L2",priority:"P2",description:"SITE_URL and REDIRECT_ALLOW_LIST must be configured to prevent email link redirect attacks.",fixCost:100,fixSize:"M",async run(e){let t=/SITE_URL|GOTRUE_SITE_URL/i,n=/REDIRECT_ALLOW_LIST|ADDITIONAL_REDIRECT_URLS|redirect.*allow/i,i=await e.grepFiles(t),r=await e.grepFiles(n);return i.length>0&&r.length>0?{result:"PASS",message:"SITE_URL and REDIRECT_ALLOW_LIST configured"}:i.length>0?{result:"PASS",message:"SITE_URL configured (check REDIRECT_ALLOW_LIST in Supabase dashboard)"}:(await e.grepFiles(/supabase/i)).length===0?{result:"N/A",message:"No Supabase usage detected"}:{result:"FAIL",message:"No SITE_URL or REDIRECT_ALLOW_LIST found \u2014 email magic links may redirect to attacker domains",evidence:["Missing: SITE_URL and REDIRECT_ALLOW_LIST in env configuration"]}}};var Ke={id:"AUTH-28",name:"Realtime presence authorization",module:"auth",layer:"L2",priority:"P2",description:"Supabase Realtime Presence channels must have authorization. Without it, any user can see who's online.",fixCost:100,fixSize:"M",async run(e){let t=/realtime|presence|channel.*subscribe|supabase.*channel/i,n=await e.grepFiles(t);if(n.length===0)return{result:"N/A",message:"No Supabase Realtime/Presence usage detected"};let i=/authorized|RLS.*realtime|realtime.*auth|channel.*auth/i;return(await e.grepFiles(i)).length>0?{result:"PASS",message:"Realtime channel authorization detected"}:{result:"FAIL",message:"Realtime/Presence channels found without authorization",evidence:n.slice(0,3).map(s=>`${s.file}:${s.line} \u2192 ${s.content.substring(0,120)}`)}}};var Ge={id:"BIL-01",name:"Stripe secret key not in client code",module:"billing",layer:"L2",priority:"P0",description:"sk_live_/sk_test_ in client code = full Stripe API access for any visitor",fixCost:100,fixSize:"M",async run(e){let t=/sk_live_[a-zA-Z0-9]{20,}|sk_test_[a-zA-Z0-9]{20,}/,n=/STRIPE_SECRET|NEXT_PUBLIC_.*STRIPE.*SECRET|VITE_.*STRIPE.*SECRET/i,r=(await e.grepFiles(t)).filter(l=>!E(l.file)&&!l.content.trimStart().startsWith("#")&&!l.content.trimStart().startsWith("//"));if(r.length>0)return{result:"FAIL",message:`Hardcoded Stripe secret key found in source code (${r.length} location${r.length>1?"s":""})`,evidence:r.map(l=>`${l.file}:${l.line} \u2192 sk_***_[REDACTED]`)};let s=await e.grepFiles(n),o=s.filter(l=>/NEXT_PUBLIC_|VITE_|EXPO_PUBLIC_/i.test(l.content)&&/STRIPE.*SECRET/i.test(l.content));if(o.length>0)return{result:"FAIL",message:"Stripe secret key exposed via NEXT_PUBLIC_/VITE_ prefix",evidence:o.map(l=>`${l.file}:${l.line} \u2192 ${l.content.split("=")[0]}`)};let a=s.filter(l=>v(l.file)&&/STRIPE_SECRET/i.test(l.content)&&!l.file.includes("/api/")&&!l.file.includes("route."));return a.length>0?{result:"FAIL",message:"Stripe secret key referenced in client-side file",evidence:a.map(l=>`${l.file}:${l.line} \u2192 ${l.content.substring(0,120)}`)}:s.filter(l=>/STRIPE_SECRET/i.test(l.content)).length>0?{result:"PASS",message:"Stripe secret key found only in server-side/env files"}:{result:"UNKNOWN",message:"No Stripe secret key references found \u2014 Stripe may not be used"}}};var Ve={id:"BIL-02",name:"Webhook signature verification",module:"billing",layer:"L2",priority:"P0",aliases:["ADM-15"],description:"Stripe webhook handler must verify signature via constructEvent(). Without it, anyone can send fake webhooks.",fixCost:100,fixSize:"M",async run(e){let t=/webhook/i,n=e.files.filter(c=>t.test(c));if(n.length===0)return{result:"UNKNOWN",message:"No webhook handler files found"};let i=/constructEvent|constructEventAsync|webhooks\.construct/,r=/stripe-signature|Stripe-Signature|STRIPE_WEBHOOK_SECRET|webhook.*secret/i,s=await e.grepFiles(i,n),o=await e.grepFiles(r,n);if(s.length>0){for(let c of n){let l;try{l=await e.readFile(c)}catch{continue}let p=/constructEvent|constructEventAsync|webhooks\.construct/.test(l),f=/JSON\.parse\s*\(.*body/i.test(l),S=/if\s*\(\s*(?:webhook_?[Ss]ecret|STRIPE_WEBHOOK_SECRET|secret)/i.test(l)||/if\s*\(\s*!?\s*webhook_?[Ss]ecret/i.test(l);if(p&&f&&S)return{result:"FAIL",message:"Webhook signature verification is conditional \u2014 JSON.parse fallback bypasses constructEvent() when secret is missing",evidence:[`${c} \u2014 constructEvent() inside conditional, JSON.parse(body) fallback allows unverified webhooks`]}}return{result:"PASS",message:"Webhook handler uses constructEvent() for signature verification"}}return o.length>0?{result:"PASS",message:"Webhook handler references signature verification"}:(await e.grepFiles(/stripe|Stripe/,n)).length>0?{result:"FAIL",message:"Stripe webhook handler found WITHOUT signature verification",evidence:n.map(c=>`${c} \u2014 no constructEvent() or signature check`)}:{result:"UNKNOWN",message:"Webhook files found but no Stripe references \u2014 may not be Stripe webhooks"}}};var Ye={id:"BIL-03",name:"Raw body preservation in webhook",module:"billing",layer:"L2",priority:"P0",description:"Webhook signature verification requires raw request body. req.json() breaks it \u2014 use req.text() or bodyParser: false.",fixCost:100,fixSize:"M",async run(e){let t=e.files.filter(a=>/webhook/i.test(a));if(t.length===0)return{result:"UNKNOWN",message:"No webhook handler files found"};let n=/request\.text\(\)|req\.text\(\)|bodyParser\s*:\s*false|getRawBody|raw\s*:\s*true|rawBody/,i=/request\.json\(\)|req\.body(?!\s*Parser)|JSON\.parse/,r=await e.grepFiles(n,t),s=await e.grepFiles(i,t);return r.length>0?{result:"PASS",message:"Webhook handler preserves raw body for signature verification"}:(await e.grepFiles(/stripe|constructEvent/i,t)).length===0?{result:"UNKNOWN",message:"Webhook files found but no Stripe references"}:s.length>0?{result:"FAIL",message:"Webhook handler uses req.json()/req.body instead of raw body \u2014 signature verification will fail",evidence:s.slice(0,3).map(a=>`${a.file}:${a.line} \u2192 ${a.content.substring(0,120)}`)}:{result:"FAIL",message:"Stripe webhook handler found but no raw body preservation detected",evidence:t.map(a=>`${a} \u2014 missing request.text() or bodyParser: false`)}}};var Xe={id:"BIL-04",name:"Idempotent webhook processing",module:"billing",layer:"L2",priority:"P1",description:"Stripe retries webhooks by design. Without idempotency checks, duplicate credits and double subscriptions occur.",fixCost:100,fixSize:"M",async run(e){let t=e.files.filter(s=>/webhook/i.test(s));if(t.length===0)return{result:"UNKNOWN",message:"No webhook handler files found"};let n=/event\.id|event_id|idempoten|UNIQUE.*event|duplicate.*check|processed.*events|already.*processed/i,i=await e.grepFiles(n,t);return(await e.grepFiles(/stripe|constructEvent/i,t)).length===0?{result:"UNKNOWN",message:"No Stripe webhook handler found"}:i.length>0?{result:"PASS",message:"Webhook idempotency check detected"}:{result:"FAIL",message:"Stripe webhook handler has no idempotency check \u2014 duplicate processing possible",evidence:["No event.id tracking or duplicate check in webhook handler"]}}};var Qe={id:"BIL-05",name:"Subscription state machine",module:"billing",layer:"L2",priority:"P1",description:"Stripe has 8 subscription states. Handling only active/canceled causes access issues for past_due, trialing, incomplete, unpaid.",fixCost:100,fixSize:"M",async run(e){let t=/past_due|trialing|incomplete|unpaid/i,n=/active|canceled/i,i=await e.grepFiles(t),r=await e.grepFiles(n);return i.length>=2?{result:"PASS",message:"Multiple subscription states handled beyond active/canceled"}:r.length>0&&i.length===0?{result:"FAIL",message:"Only active/canceled states handled \u2014 missing past_due, trialing, incomplete, unpaid",evidence:["Subscription state machine is incomplete \u2014 users may lose access incorrectly"]}:(await e.grepFiles(/subscription|stripe/i)).length===0?{result:"UNKNOWN",message:"No subscription handling found"}:{result:"FAIL",message:"Subscription code found but no explicit state handling",evidence:["Missing: past_due, trialing, incomplete, unpaid state handling"]}}};var Je={id:"BIL-06",name:"Entitlement/plan limit checking",module:"billing",layer:"L2",priority:"P1",description:"Stripe doesn't track usage limits. Without app-side entitlement checks, free users access paid features.",fixCost:100,fixSize:"M",async run(e){let t=/checkPlan|checkEntitle|planLimit|featureGate|subscription.*check|plan.*limit|canAccess|hasFeature|isSubscribed|entitlement/i;return(await e.grepFiles(t)).length>0?{result:"PASS",message:"Entitlement/plan limit checking detected"}:(await e.grepFiles(/subscription|stripe.*plan|pricing/i)).length===0?{result:"UNKNOWN",message:"No subscription/pricing code found"}:{result:"FAIL",message:"Subscription code exists but no entitlement/plan limit checks found",evidence:["Missing: checkPlanLimit, checkEntitlement, featureGate, or equivalent"]}}};var Ze={id:"BIL-07",name:"Customer \u2194 User sync",module:"billing",layer:"L2",priority:"P1",description:"Every user must map to exactly one Stripe customer. Missing sync = orphaned customers, broken subscription lookups.",fixCost:100,fixSize:"M",async run(e){let t=/stripe_customer_id|stripeCustomerId|customer_id.*stripe/i,n=/customers\.create|createCustomer|stripe.*customer/i,i=e.files.filter(b),r=await e.grepFiles(t,i.length>0?i:void 0),s=await e.grepFiles(n);return r.length>0?{result:"PASS",message:"stripe_customer_id found in database schema \u2014 user-customer sync exists"}:s.length>0?{result:"PASS",message:"Stripe customer creation found in code"}:(await e.grepFiles(/stripe/i)).length>0?{result:"FAIL",message:"Stripe is used but no stripe_customer_id in schema and no customer creation logic",evidence:["Missing: stripe_customer_id column in users/profiles table"]}:{result:"UNKNOWN",message:"No Stripe references found"}}};var et={id:"BIL-08",name:"Webhook returns 200 for unknown events",module:"billing",layer:"L2",priority:"P1",description:"Returning 400/500 for unhandled events triggers Stripe retry loops and eventual endpoint deactivation.",fixCost:100,fixSize:"M",async run(e){let t=e.files.filter(o=>/webhook/i.test(o));if(t.length===0)return{result:"UNKNOWN",message:"No webhook handler files found"};let n=/else\s*\{[\s\S]*?(?:return.*(?:4\d\d|5\d\d)|throw|NextResponse.*(?:4\d\d|5\d\d))/i,i=/default\s*:[\s\S]*?(?:200|ok|return\s+new\s+Response)/i,r=await e.grepFiles(n,t),s=await e.grepFiles(i,t);return r.length>0?{result:"FAIL",message:"Webhook handler returns error for unknown events \u2014 will trigger Stripe retry loop",evidence:r.slice(0,3).map(o=>`${o.file}:${o.line} \u2192 ${o.content.substring(0,120)}`)}:s.length>0?{result:"PASS",message:"Webhook handler returns 200 for unhandled events"}:{result:"UNKNOWN",message:"Could not determine webhook default response behavior"}}};var tt={id:"BIL-09",name:"No client-side billing state as source of truth",module:"billing",layer:"L2",priority:"P1",description:"Subscription state in localStorage/React state can be manipulated. Server must be source of truth.",fixCost:100,fixSize:"M",async run(e){let t=[],n=/localStorage.*(?:subscription|plan|billing)|sessionStorage.*(?:subscription|plan)|useState.*(?:subscription|isPro|isPaid|plan)/i,r=(await e.grepFiles(n)).filter(p=>v(p.file));for(let p of r)t.push(`${p.file}:${p.line} \u2192 ${p.content.substring(0,120)}`);let s=/\.from\s*\(\s*['"](?:subscriptions|plans|credits|billing)['"]\s*\)\s*\.\s*select/i,a=(await e.grepFiles(s)).filter(p=>v(p.file));for(let p of a)t.push(`${p.file}:${p.line} \u2192 ${p.content.substring(0,120)}`);let c=/(?:useSubscription|useBilling|usePlan)\b/,l=e.files.filter(p=>c.test(p)||/hook/i.test(p));for(let p of l){let f;try{f=await e.readFile(p)}catch{continue}let S=/from\s*\(\s*['"](?:subscriptions|plans|credits)['"]\s*\)/.test(f),A=/isPro|isPaid|plan\s*===|plan\s*!==|plan\s*==|plan\s*!=/.test(f);S&&A&&t.push(`${p} \u2192 Client hook reads billing table and derives plan/isPro locally`)}return t.length>0?{result:"FAIL",message:`Client-side billing state as source of truth (${t.length} location${t.length>1?"s":""})`,evidence:t.slice(0,5)}:{result:"PASS",message:"No client-side billing state as source of truth detected"}}};var it={id:"BIL-10",name:"Reconciliation mechanism",module:"billing",layer:"L2",priority:"P2",description:"Even good webhook handling drifts 1-2x/month. A reconciliation job comparing Stripe vs DB prevents silent revenue loss.",fixCost:100,fixSize:"M",async run(e){let t=/reconcil|sync.*stripe|stripe.*sync|cron.*billing|billing.*cron|verify.*subscription|subscription.*verify/i,n=e.files.filter(s=>/reconcil/i.test(s)),i=await e.grepFiles(t);return n.length>0||i.length>0?{result:"PASS",message:"Reconciliation mechanism detected"}:(await e.grepFiles(/stripe/i)).length===0?{result:"UNKNOWN",message:"No Stripe references found"}:{result:"FAIL",message:"No billing reconciliation mechanism found",evidence:["Missing: reconciliation script/job to compare Stripe state vs DB state"]}}};var st={id:"BIL-11",name:"Cancellation handling",module:"billing",layer:"L2",priority:"P1",description:"Explicit cancel flow: server-side cancellation + webhook processing + DB update + access revocation.",fixCost:100,fixSize:"M",async run(e){let t=/cancel.*subscription|subscription.*cancel|customer\.subscription\.deleted|cancelAt|cancel_at_period_end/i,n=await e.grepFiles(t);return n.length>=2?{result:"PASS",message:"Cancellation handling detected in multiple locations"}:n.length===1?{result:"PASS",message:"Cancellation handling detected"}:(await e.grepFiles(/subscription|stripe/i)).length===0?{result:"UNKNOWN",message:"No subscription code found"}:{result:"FAIL",message:"No cancellation handling found \u2014 users may retain access after canceling or be charged after canceling",evidence:["Missing: cancel subscription flow, subscription.deleted webhook handler"]}}};var nt={id:"BIL-12",name:"Stripe env vars configured",module:"billing",layer:"L2",priority:"P2",description:"Missing Stripe env vars = billing won't work in production or keys get hardcoded.",fixCost:100,fixSize:"M",async run(e){let t=e.files.filter(E);if(t.length===0)return{result:"UNKNOWN",message:"No .env files found"};let n=["STRIPE_SECRET_KEY","STRIPE_WEBHOOK_SECRET","STRIPE_PUBLISHABLE_KEY"],i=[],r=[];for(let o of t){let a;try{a=await e.readFile(o)}catch{continue}for(let c of n)a.includes(c)&&!i.includes(c)&&i.push(c)}for(let o of n)i.includes(o)||r.push(o);return(await e.grepFiles(/stripe/i)).length===0?{result:"UNKNOWN",message:"No Stripe references found"}:r.length>0?{result:"FAIL",message:`Missing Stripe env vars: ${r.join(", ")}`,evidence:r.map(o=>`${o} not found in any .env file`)}:{result:"PASS",message:"All required Stripe env vars configured"}}};var rt={id:"BIL-13",name:"Error handling in payment flows",module:"billing",layer:"L2",priority:"P2",description:"Missing error handling around Stripe API calls = white screen on payment failure, no diagnostics.",fixCost:100,fixSize:"M",async run(e){let t=/stripe\.\w+\.\w+\(|checkout\.sessions\.create|subscriptions\.create|customers\.create/i,n=/try\s*\{|\.catch\s*\(|catch\s*\(/,i=e.files.filter(o=>/api|route|action|server/i.test(o)),r=await e.grepFiles(t,i.length>0?i:void 0);return r.length===0?{result:"UNKNOWN",message:"No Stripe API calls found"}:(await e.grepFiles(n,i.length>0?i:void 0)).length>0?{result:"PASS",message:"Error handling found in payment flow files"}:{result:"FAIL",message:"Stripe API calls found without error handling",evidence:r.slice(0,3).map(o=>`${o.file}:${o.line} \u2192 ${o.content.substring(0,120)}`)}}};var ot={id:"BIL-14",name:"Checkout flow is server-initiated",module:"billing",layer:"L2",priority:"P0",description:"Checkout session must be created on the server. Client-side creation requires secret key in browser.",fixCost:100,fixSize:"M",async run(e){let t=/checkout\.sessions\.create|createCheckoutSession/,n=await e.grepFiles(t);if(n.length===0)return{result:"UNKNOWN",message:"No Checkout session creation found"};let i=n.filter(s=>v(s.file)&&!w(s.file));for(let s of i){let o;try{o=await e.readFile(s.file)}catch{continue}if(/["']use client["']/.test(o))return{result:"FAIL",message:"Checkout session created in client component \u2014 secret key required in browser",evidence:[`${s.file}:${s.line} \u2192 ${s.content.substring(0,120)}`]}}return n.filter(s=>w(s.file)).length>0?{result:"PASS",message:"Checkout session created server-side"}:{result:"PASS",message:"Checkout session creation found (not in client component)"}}};var at={id:"BIL-15",name:"Stripe Price ID tampering prevention",module:"billing",layer:"L2",priority:"P0",description:"Price ID from client request must be validated server-side against an allowlist. Client can send any price_id.",fixCost:100,fixSize:"M",async run(e){let t=/checkout\.sessions\.create|createCheckoutSession/;if((await e.grepFiles(t)).length===0)return{result:"UNKNOWN",message:"No Stripe Checkout session creation found"};let i=/req\.body.*price|req\.json.*price|request\.json.*price|body\.price|priceId|price_id/i,r=/allowedPrices|ALLOWED_PRICES|validPrices|PRICE_IDS|priceWhitelist|priceLookup|PLANS\[|plans\[|PRICES\[|prices\./i,s=await e.grepFiles(i),o=await e.grepFiles(r);return s.length>0&&o.length===0?{result:"FAIL",message:"Price ID accepted from client without server-side validation",evidence:s.slice(0,3).map(a=>`${a.file}:${a.line} \u2192 ${a.content.substring(0,120)}`)}:o.length>0?{result:"PASS",message:"Price ID validation/allowlist detected"}:{result:"PASS",message:"Checkout session creation found with no client price input detected"}}};var ct={id:"BIL-16",name:"Never fulfill on success_url",module:"billing",layer:"L2",priority:"P0",description:"Fulfillment (DB writes, access grants) must happen via webhook, not on the success redirect page.",fixCost:100,fixSize:"M",async run(e){let t=[/success/i,/thank/i,/payment.*confirm/i],n=e.files.filter(s=>(s.includes("page.")||s.includes("index."))&&t.some(a=>a.test(s)));if(n.length===0)return{result:"UNKNOWN",message:"No success/thank-you pages found"};let i=/\.insert\(|\.update\(|\.upsert\(|createSubscription|grantAccess|activateUser|fulfillOrder|UPDATE.*SET|INSERT.*INTO/i,r=[];for(let s of n){let o;try{o=await e.readFile(s)}catch{continue}let a=o.split(`
|
|
6
|
-
`);for(let c=0;c<a.length;c++)i.test(a[c])&&r.push({file:
|
|
7
|
-
`).length,Mi=A[1].toUpperCase();o.push(`${p}:${q} \u2192 RLS ${Mi} policy on ${l} allows user writes (auth.uid() = user_id)`)}}}if(o.length>0)return{result:"FAIL",message:`Billing table${s.size>1?"s":""} (${[...s].join(", ")}) allow user writes via RLS \u2014 subscription fraud risk`,evidence:o.slice(0,5)};let a=/\.from\s*\(\s*['"](?:subscriptions|plans|credits|billing)['"]\s*\)\s*\.\s*(?:update|insert|upsert)/i,c=await e.grepFiles(a);return c.length>0?{result:"FAIL",message:`Client-side writes to billing table detected (${c.length} location${c.length>1?"s":""})`,evidence:c.slice(0,3).map(l=>`${l.file}:${l.line} \u2192 ${l.content.substring(0,120)}`)}:{result:"PASS",message:"No user-writable billing tables detected"}}};var yt={id:"ADM-01",name:"Admin endpoints have server-side auth",module:"admin",layer:"L2",priority:"P0",description:"Every /admin or /api/admin endpoint must verify admin role server-side. Server Actions are public POST endpoints.",fixCost:100,fixSize:"M",async run(e){let t=e.files.filter(r=>/api\/admin|app\/api\/admin/i.test(r)&&(r.endsWith(".ts")||r.endsWith(".js")));if(t.length===0)return{result:"UNKNOWN",message:"No admin API route handlers found"};let n=/requireAdmin|role.*admin|isAdmin|checkPermission|requireRole|app_metadata.*admin|admin.*guard/i,i=[];for(let r of t){let s;try{s=await e.readFile(r)}catch{continue}n.test(s)||i.push(r)}return i.length>0?{result:"FAIL",message:`${i.length} admin API route${i.length>1?"s":""} without admin role verification`,evidence:i.slice(0,5).map(r=>`${r} \u2014 no admin role check detected`)}:{result:"PASS",message:`All ${t.length} admin API routes have admin role verification`}}};var At={id:"ADM-02",name:"Admin routes not accessible without auth",module:"admin",layer:"L2",priority:"P0",description:"Admin pages and API must return 401/403 without valid admin token. Middleware alone is insufficient (CVE-2025-29927).",fixCost:100,fixSize:"M",async run(e){let t=e.files.filter(o=>/app\/admin\/.*page\.|pages\/admin\//i.test(o)||/(?:pages|views|routes)\/Admin[A-Z][^/]*\.(tsx|ts|jsx|js)$/.test(o)||/(?:pages|views|routes)\/admin-[^/]*\.(tsx|ts|jsx|js)$/.test(o));if(t.length===0)return{result:"UNKNOWN",message:"No admin pages found"};let n=/getUser|getSession|requireAuth|requireAdmin|redirect.*login|redirect.*auth|middleware|auth\(\)|checkPermission/i,i=[];for(let o of t){let a;try{a=await e.readFile(o)}catch{continue}n.test(a)||i.push(o)}let r=e.files.some(o=>/middleware\.(ts|js)$/i.test(o)),s=!1;if(r){let o=e.files.find(a=>/middleware\.(ts|js)$/i.test(a));if(o)try{let a=await e.readFile(o);s=/admin/i.test(a)}catch{}}return i.length>0&&!s?{result:"FAIL",message:`${i.length} admin page${i.length>1?"s":""} without auth guard (no middleware coverage either)`,evidence:i.slice(0,5).map(o=>`${o} \u2014 no auth guard detected`)}:i.length>0&&s?{result:"PASS",message:`Admin pages protected via middleware (${t.length} pages). Note: add per-route auth for defense-in-depth.`}:{result:"PASS",message:`All ${t.length} admin pages have auth guards`}}};var bt={id:"ADM-03",name:"No client-side-only role checks",module:"admin",layer:"L2",priority:"P1",description:"Role checks in JSX ({isAdmin && <Panel/>}) without server-side enforcement are bypassable via dev tools.",fixCost:100,fixSize:"M",async run(e){let t=/isAdmin|is_admin|role.*admin|admin.*role/i,n=/requireAdmin|requireRole|checkPermission|app_metadata.*admin/i,r=(await e.grepFiles(t)).filter(o=>v(o.file)&&!w(o.file));return r.length===0?{result:"PASS",message:"No client-side-only role checks detected"}:(await e.grepFiles(n)).length>0?{result:"PASS",message:"Client-side role checks backed by server-side enforcement"}:{result:"FAIL",message:`Client-side role checks found without server-side enforcement (${r.length} location${r.length>1?"s":""})`,evidence:r.slice(0,3).map(o=>`${o.file}:${o.line} \u2192 ${o.content.substring(0,120)}`)}}};var $t={id:"ADM-04",name:"Audit log for admin actions",module:"admin",layer:"L2",priority:"P1",description:"Admin operations (delete user, change role, modify data) must be logged. Required for SOC 2 compliance.",fixCost:100,fixSize:"M",async run(e){let t=/audit.*log|admin.*log|action.*log|createAuditEntry|logAdminAction|activity.*log/i,n=/audit_log|admin_log|activity_log/i,i=await e.grepFiles(t),r=e.files.filter(b),s=await e.grepFiles(n,r.length>0?r:void 0);return i.length>0||s.length>0?{result:"PASS",message:"Audit logging detected for admin actions"}:(await e.grepFiles(/admin/i)).length===0?{result:"UNKNOWN",message:"No admin functionality detected"}:{result:"FAIL",message:"No audit logging found for admin actions",evidence:["Missing: audit_log table or logAdminAction function"]}}};var vt={id:"ADM-05",name:"RBAC beyond binary admin",module:"admin",layer:"L2",priority:"P2",description:"Binary admin/user model means every admin can do everything. Granular roles limit blast radius.",fixCost:100,fixSize:"M",async run(e){let t=e.files.filter(b),n=/RBAC|permission|capability|role.*check|roles.*table|user_roles/i,i=/isAdmin|is_admin|boolean.*admin/i,r=await e.grepFiles(n),s=await e.grepFiles(/role.*enum|roles.*table|permissions.*table/i,t.length>0?t:void 0);if(r.length>0||s.length>0)return{result:"PASS",message:"RBAC or granular permissions model detected"};let o=await e.grepFiles(i);return o.length>0?{result:"FAIL",message:"Binary admin model (isAdmin boolean) \u2014 no granular permissions",evidence:o.slice(0,3).map(a=>`${a.file}:${a.line} \u2192 ${a.content.substring(0,120)}`)}:{result:"UNKNOWN",message:"No admin role model detected"}}};var Ct={id:"ADM-06",name:"Safe impersonation (if exists)",module:"admin",layer:"L2",priority:"P1",description:"If admin can 'login as user', the action must be logged with admin ID preserved. Replacing admin session = invisible abuse.",fixCost:100,fixSize:"M",async run(e){let t=/impersonat|loginAs|actAs|switchUser|act_as|login_as/i,n=await e.grepFiles(t);if(n.length===0)return{result:"N/A",message:"No impersonation functionality detected"};let i=/audit|log.*admin|admin.*log|logAction|track/i;return(await e.grepFiles(i)).length>0?{result:"PASS",message:"Impersonation exists with audit logging"}:{result:"FAIL",message:"Impersonation found without audit logging \u2014 admin actions will appear as user actions",evidence:n.slice(0,3).map(s=>`${s.file}:${s.line} \u2192 ${s.content.substring(0,120)}`)}}};var _t={id:"ADM-07",name:"UUIDs not sequential IDs",module:"admin",layer:"L2",priority:"P1",description:"Sequential integer IDs enable enumeration attacks (IDOR). Use UUIDs for all user-facing primary keys.",fixCost:100,fixSize:"M",async run(e){let t=e.files.filter(b);if(t.length===0)return{result:"UNKNOWN",message:"No SQL migration files found"};let n=/(?:SERIAL|BIGSERIAL|INTEGER\s+PRIMARY\s+KEY\s+(?:AUTO_INCREMENT|GENERATED))/i,i=/UUID\s+(?:PRIMARY\s+KEY\s+)?DEFAULT\s+(?:gen_random_uuid|uuid_generate)/i,r=await e.grepFiles(n,t),s=await e.grepFiles(i,t),o=r.filter(a=>!/migration|schema_migration|_prisma/i.test(a.content));return o.length>0&&s.length===0?{result:"FAIL",message:`Sequential IDs found without UUID usage (${o.length} table${o.length>1?"s":""})`,evidence:o.slice(0,5).map(a=>`${a.file}:${a.line} \u2192 ${a.content.substring(0,120)}`)}:s.length>0?{result:"PASS",message:"UUID primary keys detected in schema"}:{result:"UNKNOWN",message:"No primary key definitions found in migrations"}}};var kt={id:"ADM-08",name:"No unprotected debug/admin routes",module:"admin",layer:"L2",priority:"P0",description:"Debug, internal, and admin routes must have auth guards. Hidden URLs are not security.",fixCost:100,fixSize:"M",async run(e){let t=[/app\/admin\//,/pages\/admin\//,/app\/internal\//,/app\/debug\//,/api\/admin\//,/api\/debug\//,/api\/internal\//,/api\/graphql/,/api\/seed/,/api\/reset/,/api\/test/,/supabase\/functions\/.*(?:seed|admin|debug|reset|test)/],n=e.files.filter(s=>t.some(o=>o.test(s)));if(n.length===0)return{result:"PASS",message:"No admin/debug/internal routes detected"};let i=/requireAdmin|requireAuth|getUser|getSession|verifyToken|isAdmin|checkPermission|requireRole|auth\(\)|middleware/i,r=[];for(let s of n){if(!s.endsWith(".ts")&&!s.endsWith(".tsx")&&!s.endsWith(".js")&&!s.endsWith(".jsx"))continue;let o;try{o=await e.readFile(s)}catch{continue}i.test(o)||r.push(s)}return r.length>0?{result:"FAIL",message:`${r.length} admin/debug route${r.length>1?"s":""} without auth check`,evidence:r.map(s=>`${s} \u2014 no auth guard detected`)}:{result:"PASS",message:`All ${n.length} admin/debug routes have auth checks`}}};var It={id:"ADM-09",name:"Destructive ops require extra authorization",module:"admin",layer:"L2",priority:"P2",description:"Bulk delete, data wipe, billing override should require confirmation step or elevated permission.",fixCost:100,fixSize:"M",async run(e){let t=/deleteAll|bulkDelete|wipeData|truncate|DROP\s+TABLE|removeAll|destroyAll|purge/i,n=/confirm|double.*auth|re.?authenticate|verification.*step|two.*step/i,r=(await e.grepFiles(t)).filter(o=>/admin/i.test(o.file));return r.length===0?{result:"N/A",message:"No bulk destructive admin operations detected"}:(await e.grepFiles(n)).length>0?{result:"PASS",message:"Confirmation/extra auth detected for destructive operations"}:{result:"FAIL",message:"Destructive admin operations without extra authorization",evidence:r.slice(0,3).map(o=>`${o.file}:${o.line} \u2192 ${o.content.substring(0,120)}`)}}};var Pt={id:"ADM-10",name:"Admin code separated from user app",module:"admin",layer:"L2",priority:"P2",description:"Admin code in separate directories prevents user app bugs from affecting admin, and vice versa.",fixCost:100,fixSize:"M",async run(e){let t=e.files.some(i=>/^app\/admin\/|^domains\/admin\/|^src\/admin\//i.test(i)),n=e.files.filter(i=>/admin/i.test(i)&&!/(app|domains|src)\/admin\//i.test(i));return t?{result:"PASS",message:"Admin code in dedicated directory (app/admin/ or domains/admin/)"}:n.length>0?{result:"FAIL",message:"Admin code scattered across user-facing directories",evidence:n.slice(0,5).map(i=>i)}:{result:"N/A",message:"No admin code detected"}}};var wt={id:"ADM-11",name:"No hardcoded admin credentials",module:"admin",layer:"L2",priority:"P0",description:"Hardcoded admin passwords, tokens, or emails in source code = anyone with repo access is admin",fixCost:100,fixSize:"M",async run(e){let t=[/admin.*password\s*[:=]\s*["']/i,/admin.*token\s*[:=]\s*["']/i,/admin.*secret\s*[:=]\s*["']/i,/password\s*[:=]\s*["'](?:admin|password|123456|secret)/i,/DEFAULT_ADMIN_PASSWORD/i,/ADMIN_PASSWORD\s*[:=]/i,/seed.*admin.*password/i],n=[];for(let r of t){let o=(await e.grepFiles(r)).filter(a=>!(E(a.file)||a.content.trimStart().startsWith("//")||a.content.trimStart().startsWith("#")||a.content.trimStart().startsWith("*")||a.file.includes("test")||a.file.includes("spec")));n.push(...o)}let i=[...new Map(n.map(r=>[`${r.file}:${r.line}`,r])).values()];return i.length>0?{result:"FAIL",message:`Hardcoded admin credentials found (${i.length} location${i.length>1?"s":""})`,evidence:i.map(r=>`${r.file}:${r.line} \u2192 ${r.content.substring(0,80).replace(/["'][^"']{8,}["']/g,'"[REDACTED]"')}`)}:{result:"PASS",message:"No hardcoded admin credentials found in source code"}}};var Lt={id:"ADM-12",name:"Admin error handling",module:"admin",layer:"L2",priority:"P2",description:"Admin API errors must not leak stack traces, SQL queries, or internal schema details.",fixCost:100,fixSize:"M",async run(e){let t=e.files.filter(o=>/admin.*route|api.*admin/i.test(o));if(t.length===0)return{result:"UNKNOWN",message:"No admin API routes found"};let n=/try\s*\{|\.catch\s*\(|catch\s*\(/,i=/stack|trace|SQL|query.*error|\.message/i,r=!1,s=[];for(let o of t){let a;try{a=await e.readFile(o)}catch{continue}n.test(a)&&(r=!0);let c=a.split(`
|
|
8
|
-
`);for(let l=0;l<c.length;l++)/error\.stack|error\.message|err\.stack|JSON\.stringify.*error/i.test(c[l])&&s.push({file:o,line:l+1,content:c[l].trim()})}return s.length>0?{result:"FAIL",message:"Admin API may leak error details (stack traces, error messages)",evidence:s.slice(0,3).map(o=>`${o.file}:${o.line} \u2192 ${o.content.substring(0,120)}`)}:r?{result:"PASS",message:"Admin error handling present without obvious information leaks"}:{result:"FAIL",message:"No error handling in admin API routes",evidence:t.slice(0,3).map(o=>`${o} \u2014 no try/catch`)}}};var Nt={id:"ADM-13",name:"MFA requirement for admin roles",module:"admin",layer:"L2",priority:"P0",description:"Admin auth must require MFA/AAL2. Compromised admin password without MFA = total takeover.",fixCost:100,fixSize:"M",async run(e){let t=/mfa|aal2|getAuthenticatorAssuranceLevel|totp|authenticator|multi.?factor|two.?factor/i,n=e.files.filter(s=>/admin/i.test(s));if(n.length===0)return{result:"UNKNOWN",message:"No admin files found"};let i=await e.grepFiles(t,n),r=await e.grepFiles(t);return i.length>0?{result:"PASS",message:"MFA/AAL2 enforcement detected in admin code"}:r.length>0?{result:"PASS",message:"MFA implementation detected in codebase (verify it covers admin routes)"}:{result:"FAIL",message:"No MFA/AAL2 enforcement found for admin roles",evidence:["No references to mfa, aal2, totp, or authenticator in admin code"]}}};var Rt={id:"ADM-14",name:"Rate limiting on admin endpoints",module:"admin",layer:"L2",priority:"P1",description:"Admin API endpoints without rate limiting are vulnerable to brute force and DoS attacks.",fixCost:100,fixSize:"M",async run(e){let t=e.files.filter(s=>/api.*admin|admin.*route/i.test(s));if(t.length===0)return{result:"UNKNOWN",message:"No admin API endpoints found"};let n=/rateLimit|rate.limit|throttle|limiter|upstash|redis.*limit|too.many.requests|429/i;return(await e.grepFiles(n,t)).length>0?{result:"PASS",message:"Rate limiting detected on admin endpoints"}:(await e.grepFiles(n)).length>0?{result:"PASS",message:"Rate limiting detected in codebase (verify it covers admin endpoints)"}:{result:"FAIL",message:"No rate limiting found on admin endpoints",evidence:["Missing: rate limiting middleware on admin API routes"]}}};var Et={id:"ADM-16",name:"Separate admin session timeouts",module:"admin",layer:"L2",priority:"P1",description:"Admin sessions should have shorter timeouts than user sessions to limit session hijacking window.",fixCost:100,fixSize:"M",async run(e){let t=/admin.*timeout|admin.*expir|admin.*maxAge|admin.*session.*duration|session.*admin.*short/i;return(await e.grepFiles(t)).length>0?{result:"PASS",message:"Differentiated admin session timeout detected"}:(await e.grepFiles(/admin/i)).length===0?{result:"UNKNOWN",message:"No admin functionality detected"}:{result:"FAIL",message:"No separate admin session timeout \u2014 admin sessions use same duration as user sessions",evidence:["Missing: shorter session timeout for admin roles"]}}};var Ft={id:"ADM-18",name:"Admin action notification/alerting",module:"admin",layer:"L2",priority:"P2",description:"Critical admin actions should trigger notifications (Slack, email) so compromised admin accounts are detected quickly.",fixCost:100,fixSize:"M",async run(e){let t=/slack.*webhook|sendSlack|sendEmail.*admin|notify.*admin|alert.*admin|admin.*notify|admin.*alert|webhook.*notify/i,n=e.files.filter(s=>/admin/i.test(s));if(n.length===0)return{result:"UNKNOWN",message:"No admin functionality detected"};let i=await e.grepFiles(t,n),r=await e.grepFiles(t);return i.length>0||r.length>0?{result:"PASS",message:"Admin action notifications/alerting detected"}:{result:"FAIL",message:"No notification/alerting for admin actions \u2014 compromised admin goes undetected",evidence:["Missing: Slack webhook, email alert, or notification for critical admin actions"]}}};var Tt={id:"ADM-19",name:"CSRF protection for admin mutations",module:"admin",layer:"L2",priority:"P1",description:"Admin mutation endpoints need CSRF protection. If victim is admin, CSRF compromises entire app.",fixCost:100,fixSize:"M",async run(e){let t=e.files.filter(s=>/admin.*route\.(ts|js)$|api.*admin/i.test(s));if(t.length===0)return{result:"UNKNOWN",message:"No admin route handlers found"};let n=/csrf|csurf|csrfToken|SameSite|x-csrf|anti.?forgery/i;return(await e.grepFiles(n)).length>0?{result:"PASS",message:"CSRF protection detected"}:(await e.grepFiles(/["']use server["']/i)).length>0?{result:"PASS",message:"Server Actions used (built-in CSRF protection in Next.js 14+)"}:{result:"FAIL",message:"No CSRF protection on admin mutation routes",evidence:t.slice(0,3).map(s=>`${s} \u2014 no CSRF token or SameSite cookie`)}}};var xt={id:"ADM-20",name:"Data export controls",module:"admin",layer:"L2",priority:"P1",description:"Admin bulk export/download must have authorization and logging. Compromised admin can exfiltrate all user data.",fixCost:100,fixSize:"M",async run(e){let t=/export.*csv|downloadCSV|bulk.*fetch|dump.*data|export.*users|export.*data/i,n=await e.grepFiles(t);if(n.length===0)return{result:"N/A",message:"No bulk data export functionality detected"};let i=/requireAdmin|requireAuth|getUser|checkPermission/i,r=/audit|log.*export|log.*download/i,s=[...new Set(n.map(c=>c.file))],o=await e.grepFiles(i,s),a=await e.grepFiles(r,s);return o.length>0&&a.length>0?{result:"PASS",message:"Data export has auth and logging"}:o.length>0?{result:"PASS",message:"Data export has auth (consider adding export logging)"}:{result:"FAIL",message:"Data export without auth/logging \u2014 PII exfiltration risk",evidence:n.slice(0,3).map(c=>`${c.file}:${c.line} \u2192 ${c.content.substring(0,120)}`)}}};var Dt={id:"ADM-21",name:"Admin provisioning control",module:"admin",layer:"L2",priority:"P1",description:"Admin role must not be self-assignable. AI tools often generate 'isFirstUser \u2192 admin' or open role selection on signup.",fixCost:100,fixSize:"M",async run(e){let t=/role\s*=\s*['"]admin['"]|isFirstUser|first.*user.*admin|role.*select|self.*assign.*admin/i,n=e.files.filter(a=>/signup|register|onboard/i.test(a)),i=await e.grepFiles(t),s=[...n.length>0?await e.grepFiles(t,n):[],...i.filter(a=>/signup|register|onboard/i.test(a.file))],o=[...new Map(s.map(a=>[`${a.file}:${a.line}`,a])).values()];return o.length>0?{result:"FAIL",message:"Self-assign admin pattern detected in signup/onboarding flow",evidence:o.slice(0,3).map(a=>`${a.file}:${a.line} \u2192 ${a.content.substring(0,120)}`)}:{result:"PASS",message:"No self-assign admin pattern detected in signup flow"}}};var Ut={id:"ADM-22",name:"Edge Function authentication",module:"admin",layer:"L2",priority:"P0",description:"Supabase Edge Functions must verify the Authorization header or user session. Unprotected Edge Functions can be called by anyone with the public anon key, enabling privilege escalation (e.g. seed-admin).",fixCost:100,fixSize:"M",async run(e){let t=e.files.filter(r=>/supabase\/functions\/[^/]+\/index\.(ts|js)$/i.test(r));if(t.length===0)return{result:"PASS",message:"No Supabase Edge Functions found"};let n=/auth\.getUser|authorization.*header|req\.headers\.get\s*\(\s*['"]authorization['"]\)|verifyJWT|supabaseClient\.auth|createClient.*serviceRole/i,i=[];for(let r of t){let s=await e.readFile(r);n.test(s)||i.push(r)}return i.length>0?{result:"FAIL",message:`${i.length} Edge Function${i.length>1?"s":""} without auth verification`,evidence:i.slice(0,5).map(r=>`${r} \u2192 No Authorization header check or auth.getUser() call`)}:{result:"PASS",message:`All ${t.length} Edge Functions have auth checks`}}};var Oi=["/auth/","middleware.ts","middleware.js","/domains/auth/"],Wi=/\b(getUser|createServerClient|supabase\.auth|currentUser|clerkMiddleware|useUser|getServerSession|useSession|authOptions|getSession)\b/;function ji(e){return Oi.some(t=>e.includes(t))}function Bi(e){return e.includes("node_modules")||e.includes(".next/")||e.includes("test")||e.includes("spec")||e.includes(".d.ts")||e.includes("types.ts")||e.includes("types.js")}var Mt={id:"DRIFT-AUTH-01",name:"Auth logic spreading outside auth directory",module:"auth",layer:"L2",priority:"P1",category:"drift",description:"Auth patterns (getUser, createServerClient, etc.) found outside auth directories indicate module boundary drift.",fixCost:100,fixSize:"M",async run(e){let n=(await e.grepFiles(Wi)).filter(i=>!(Bi(i.file)||i.content.trimStart().startsWith("//")||i.content.trimStart().startsWith("*")||ji(i.file)));return n.length>3?{result:"FAIL",message:`Auth logic found in ${n.length} locations outside auth directories \u2014 module boundary is leaking`,evidence:n.slice(0,5).map(i=>`${i.file}:${i.line} \u2192 ${i.content.substring(0,100)}`)}:n.length>0?{result:"PASS",message:`Minor auth references outside auth dir (${n.length}) \u2014 within acceptable range`}:{result:"PASS",message:"Auth logic is contained within auth directories"}}};var Hi=/\b(getUser|createServerClient|supabase\.auth|currentUser|clerkMiddleware|auth\(\)|getServerSession|NextAuth)\b/,Ot={id:"DRIFT-AUTH-02",name:"Duplicate auth middleware files",module:"auth",layer:"L2",priority:"P1",category:"drift",description:"Multiple middleware files with auth logic indicate fragmented auth \u2014 a common AI generation pattern.",fixCost:100,fixSize:"M",async run(e){let t=e.files.filter(i=>(i.includes("middleware.ts")||i.includes("middleware.js"))&&!i.includes("node_modules")&&!i.includes(".next/")),n=[];for(let i of t)try{let r=await e.readFile(i);Hi.test(r)&&n.push(i)}catch{continue}return n.length>1?{result:"FAIL",message:`${n.length} middleware files contain auth logic \u2014 auth should have a single entry point`,evidence:n.map(i=>i)}:n.length===1?{result:"PASS",message:`Single auth middleware found: ${n[0]}`}:{result:"UNKNOWN",message:"No middleware with auth logic found"}}};var zi=/\b(getUser|createServerClient|supabase\.auth|currentUser|auth\(\)|getServerSession|requireAuth|withAuth|isAuthenticated)\b/,Wt={id:"DRIFT-AUTH-03",name:"New API routes without auth checks",module:"auth",layer:"L2",priority:"P0",category:"drift",description:"API route handlers without auth verification \u2014 any visitor can call unprotected endpoints.",fixCost:100,fixSize:"M",async run(e){let t=e.files.filter(i=>i.includes("/api/")&&(i.endsWith("route.ts")||i.endsWith("route.js"))&&!i.includes("node_modules")&&!i.includes(".next/")&&!i.includes("/api/auth/")&&!i.includes("/api/webhook")&&!i.includes("/api/health")&&!i.includes("/api/public"));if(t.length===0)return{result:"UNKNOWN",message:"No API route files found"};let n=[];for(let i of t)try{let r=await e.readFile(i);zi.test(r)||n.push(i)}catch{continue}return n.length>0?{result:"FAIL",message:`${n.length} of ${t.length} API routes have no auth verification`,evidence:n.slice(0,5).map(i=>i)}:{result:"PASS",message:`All ${t.length} API routes have auth checks`}}};var qi=/\b(isAdmin|role\s*===?\s*['"`]admin['"`]|user\.role|userRole|hasRole)\b/,Ki=/\b(getUser|createServerClient|supabase\.auth|currentUser|getServerSession|requireAdmin|checkAdmin)\b/,jt={id:"DRIFT-AUTH-04",name:"Client-side auth bypass \u2014 role check without server verification",module:"auth",layer:"L2",priority:"P1",category:"drift",description:"Role checks in JSX/client code without server-side verification \u2014 UI-only gates can be bypassed.",fixCost:100,fixSize:"M",async run(e){let n=(await e.grepFiles(qi)).filter(s=>s.content.trimStart().startsWith("//")||s.file.includes("node_modules")||s.file.includes(".next/")?!1:v(s.file));if(n.length===0)return{result:"PASS",message:"No client-side role checks found"};let r=(await e.grepFiles(Ki,["**/api/**","**/server/**","**/actions.*"])).some(s=>!s.file.includes("node_modules"));return n.length>0&&!r?{result:"FAIL",message:`${n.length} client-side role checks found but no server-side role verification \u2014 admin UI can be bypassed`,evidence:n.slice(0,5).map(s=>`${s.file}:${s.line} \u2192 ${s.content.substring(0,100)}`)}:{result:"PASS",message:`Client-side role checks (${n.length}) backed by server-side verification`}}};var Gi=/localStorage\.(getItem|setItem|removeItem)\s*\(\s*['"`](token|jwt|session|auth|access_token|refresh_token|user)/,Bt={id:"DRIFT-AUTH-05",name:"Auth tokens stored in localStorage",module:"auth",layer:"L2",priority:"P1",category:"drift",description:"localStorage for auth tokens is vulnerable to XSS \u2014 use httpOnly cookies or secure session management.",fixCost:100,fixSize:"M",async run(e){let n=(await e.grepFiles(Gi)).filter(i=>!(i.content.trimStart().startsWith("//")||i.file.includes("node_modules")||i.file.includes(".next/")));return n.length>0?{result:"FAIL",message:`Auth tokens stored in localStorage (${n.length} location${n.length>1?"s":""}) \u2014 vulnerable to XSS`,evidence:n.slice(0,5).map(i=>`${i.file}:${i.line} \u2192 ${i.content.substring(0,100)}`)}:{result:"PASS",message:"No localStorage auth token usage found"}}};var Vi=["/billing/","/stripe/","/webhook/","/payment/","/subscription/","/domains/billing/"],Yi=/\b(stripe|Stripe|createCheckout|price_id|priceId|subscription_id|subscriptionId|checkout\.sessions|customer\.subscriptions)\b/;function Xi(e){return Vi.some(t=>e.includes(t))}function Qi(e){return e.includes("node_modules")||e.includes(".next/")||e.includes("test")||e.includes("spec")||e.includes(".d.ts")||e.includes("types.ts")||e.includes("types.js")||e.includes("package.json")||e.includes("package-lock")||e.includes("pnpm-lock")||e.includes(".env")}var Ht={id:"DRIFT-BIL-01",name:"Billing logic spreading outside billing directory",module:"billing",layer:"L2",priority:"P1",category:"drift",description:"Stripe/payment imports outside billing directories indicate module boundary drift.",fixCost:100,fixSize:"M",async run(e){let n=(await e.grepFiles(Yi)).filter(i=>!(Qi(i.file)||i.content.trimStart().startsWith("//")||i.content.trimStart().startsWith("*")||i.content.trimStart().startsWith("#")||Xi(i.file)));return n.length>3?{result:"FAIL",message:`Billing/Stripe logic found in ${n.length} locations outside billing directories \u2014 module boundary is leaking`,evidence:n.slice(0,5).map(i=>`${i.file}:${i.line} \u2192 ${i.content.substring(0,100)}`)}:n.length>0?{result:"PASS",message:`Minor billing references outside billing dir (${n.length}) \u2014 within acceptable range`}:{result:"PASS",message:"Billing logic is contained within billing directories"}}};var Ji=/\b(webhook|stripe.*event|event\.type|constructEvent)\b/i,zt={id:"DRIFT-BIL-02",name:"Duplicate webhook handler endpoints",module:"billing",layer:"L2",priority:"P0",category:"drift",description:"Multiple webhook endpoints create race conditions and duplicate event processing.",fixCost:100,fixSize:"M",async run(e){let t=e.files.filter(i=>!i.includes("node_modules")&&!i.includes(".next/")&&!i.includes("test")&&(i.includes("webhook")||i.includes("stripe"))&&(i.endsWith(".ts")||i.endsWith(".js")||i.endsWith(".py"))),n=[];for(let i of t)try{let r=await e.readFile(i);Ji.test(r)&&n.push(i)}catch{continue}return n.length>1?{result:"FAIL",message:`${n.length} webhook handler files found \u2014 should be exactly 1`,evidence:n}:n.length===1?{result:"PASS",message:`Single webhook handler: ${n[0]}`}:{result:"UNKNOWN",message:"No webhook handler files found \u2014 Stripe webhooks may not be configured"}}};var Zi=/\b(subscription|plan|isPro|isFreeTier|isPaid|currentPlan|userPlan|hasSubscription)\b/,qt={id:"DRIFT-BIL-03",name:"Client-side subscription check without server verification",module:"billing",layer:"L2",priority:"P1",category:"drift",description:"Plan/subscription checks in client code without server-side verification \u2014 users can bypass paywalls.",fixCost:100,fixSize:"M",async run(e){let n=(await e.grepFiles(Zi)).filter(s=>s.content.trimStart().startsWith("//")||s.file.includes("node_modules")||s.file.includes(".next/")||s.file.includes("test")||s.file.includes("types")?!1:v(s.file)&&!s.file.includes("/api/"));if(n.length===0)return{result:"PASS",message:"No client-side subscription checks found"};let r=(await e.grepFiles(/\b(subscription|check.*plan|check.*limit|entitlement|getSubscription)\b/i,["**/api/**","**/server/**","**/actions.*","**/billing/**"])).some(s=>!s.file.includes("node_modules"));return n.length>0&&!r?{result:"FAIL",message:`${n.length} client-side subscription checks but no server-side plan verification \u2014 paywall can be bypassed`,evidence:n.slice(0,5).map(s=>`${s.file}:${s.line} \u2192 ${s.content.substring(0,100)}`)}:{result:"PASS",message:`Client-side subscription checks (${n.length}) backed by server-side verification`}}};var es=/(?:price|amount|cost|fee)\s*[:=]\s*(\d{2,}(?:\.\d{2})?|['"`]\$?\d+)/i,ts=/(?:amount|price|unit_amount)\s*[:=]\s*\d{3,}/,Kt={id:"DRIFT-BIL-04",name:"Hardcoded prices instead of plan configuration",module:"billing",layer:"L2",priority:"P2",category:"drift",description:"Hardcoded dollar amounts or price values should come from plan configuration or Stripe metadata.",fixCost:100,fixSize:"M",async run(e){let t=await e.grepFiles(es),n=await e.grepFiles(ts),r=[...t,...n].filter(s=>!(s.content.trimStart().startsWith("//")||s.content.trimStart().startsWith("*")||s.file.includes("node_modules")||s.file.includes(".next/")||s.file.includes("test")||s.file.includes("package.json")||s.file.includes(".lock")||s.file.includes(".css")||s.file.includes("migration")));return r.length>3?{result:"FAIL",message:`${r.length} hardcoded price/amount values found \u2014 prices should come from plan config or Stripe`,evidence:r.slice(0,5).map(s=>`${s.file}:${s.line} \u2192 ${s.content.substring(0,100)}`)}:r.length>0?{result:"PASS",message:`Minor hardcoded amounts found (${r.length}) \u2014 review recommended`}:{result:"PASS",message:"No hardcoded price values detected"}}};var is=/\b(canAccess|hasFeature|featureEnabled|isEnabled|featureFlag|canUse)\b/,ss=/\b(checkLimit|checkSubscription|checkPlan|entitlement|billingGuard|requirePlan|checkQuota)\b/,Gt={id:"DRIFT-BIL-05",name:"Feature access without billing verification",module:"billing",layer:"L2",priority:"P2",category:"drift",description:"New features with access gates but no billing/entitlement check \u2014 free users can access paid features.",fixCost:100,fixSize:"M",async run(e){let n=(await e.grepFiles(is)).filter(s=>!(s.content.trimStart().startsWith("//")||s.file.includes("node_modules")||s.file.includes(".next/")||s.file.includes("test")));if(n.length===0)return{result:"UNKNOWN",message:"No feature gate patterns found \u2014 may not use feature flags"};let r=(await e.grepFiles(ss)).some(s=>!s.file.includes("node_modules")&&!s.file.includes(".next/"));return n.length>0&&!r?{result:"FAIL",message:`${n.length} feature gate(s) found but no billing/entitlement verification \u2014 paid features may be accessible for free`,evidence:n.slice(0,5).map(s=>`${s.file}:${s.line} \u2192 ${s.content.substring(0,100)}`)}:{result:"PASS",message:`Feature gates (${n.length}) backed by billing verification`}}};var ns=["/admin/","/domains/admin/"],rs=/\b(isAdmin|requireAdmin|checkAdmin|adminGuard|role\s*===?\s*['"`]admin['"`]|user_role|superAdmin|isSuperAdmin)\b/;function os(e){return ns.some(t=>e.includes(t))}function as(e){return e.includes("node_modules")||e.includes(".next/")||e.includes("test")||e.includes("spec")||e.includes(".d.ts")||e.includes("types.ts")||e.includes("types.js")||e.includes("migration")||e.includes(".sql")}var Vt={id:"DRIFT-ADM-01",name:"Admin logic spreading outside admin directory",module:"admin",layer:"L2",priority:"P1",category:"drift",description:"Admin permission patterns found outside admin directories indicate module boundary drift.",fixCost:100,fixSize:"M",async run(e){let n=(await e.grepFiles(rs)).filter(i=>!(as(i.file)||i.content.trimStart().startsWith("//")||i.content.trimStart().startsWith("*")||os(i.file)));return n.length>3?{result:"FAIL",message:`Admin logic found in ${n.length} locations outside admin directories \u2014 module boundary is leaking`,evidence:n.slice(0,5).map(i=>`${i.file}:${i.line} \u2192 ${i.content.substring(0,100)}`)}:n.length>0?{result:"PASS",message:`Minor admin references outside admin dir (${n.length}) \u2014 within acceptable range`}:{result:"PASS",message:"Admin logic is contained within admin directories"}}};var cs=/\b(requireAdmin|checkAdmin|adminGuard|isAdmin|role\s*===?\s*['"`]admin['"`]|isSuperAdmin)\b/,Yt={id:"DRIFT-ADM-02",name:"New admin routes without permission guards",module:"admin",layer:"L2",priority:"P0",category:"drift",description:"Admin route handlers without permission checks \u2014 any authenticated user can access admin features.",fixCost:100,fixSize:"M",async run(e){let t=e.files.filter(i=>i.includes("/admin/")&&!i.includes("node_modules")&&!i.includes(".next/")&&!i.includes("test")&&(i.endsWith("route.ts")||i.endsWith("route.js")||i.endsWith("page.tsx")||i.endsWith("page.jsx")||i.endsWith("page.ts")));if(t.length===0)return{result:"UNKNOWN",message:"No admin route files found"};let n=[];for(let i of t)try{let r=await e.readFile(i);cs.test(r)||n.push(i)}catch{continue}return n.length>0?{result:"FAIL",message:`${n.length} of ${t.length} admin routes have no permission guard`,evidence:n.slice(0,5).map(i=>i)}:{result:"PASS",message:`All ${t.length} admin routes have permission guards`}}};var Xt=/\b(logAuditEvent|auditLog|audit_log|createAuditEntry|logAdminAction|insertAuditLog)\b/,Qt=/\b(DELETE|PUT|PATCH|POST)\b/,Jt={id:"DRIFT-ADM-03",name:"Admin mutations without audit logging",module:"admin",layer:"L2",priority:"P1",category:"drift",description:"Admin mutation endpoints (POST/PUT/DELETE) without audit log calls \u2014 no trail of admin actions.",fixCost:100,fixSize:"M",async run(e){let t=e.files.filter(i=>i.includes("/admin/")&&i.includes("/api/")&&!i.includes("node_modules")&&!i.includes(".next/")&&!i.includes("test")&&(i.endsWith("route.ts")||i.endsWith("route.js")));if(t.length===0){let i=e.files.filter(s=>s.includes("/admin/")&&!s.includes("node_modules")&&!s.includes(".next/")&&(s.includes("action")||s.includes("service")||s.includes("handler"))&&(s.endsWith(".ts")||s.endsWith(".js")||s.endsWith(".py")));if(i.length===0)return{result:"UNKNOWN",message:"No admin API routes or action files found"};let r=[];for(let s of i)try{let o=await e.readFile(s);Qt.test(o)&&!Xt.test(o)&&r.push(s)}catch{continue}return r.length>0?{result:"FAIL",message:`${r.length} admin action file(s) with mutations but no audit logging`,evidence:r.slice(0,5)}:{result:"PASS",message:"Admin action files have audit logging"}}let n=[];for(let i of t)try{let r=await e.readFile(i);Qt.test(r)&&!Xt.test(r)&&n.push(i)}catch{continue}return n.length>0?{result:"FAIL",message:`${n.length} of ${t.length} admin API routes have mutations without audit logging`,evidence:n.slice(0,5)}:{result:"PASS",message:`All ${t.length} admin API routes have audit logging`}}};var Zt={id:"ENV-01",name:".env.example exists",module:"foundation",layer:"L3",priority:"P1",description:"Project should have .env.example documenting required environment variables",fixCost:50,fixSize:"S",async run(e){return e.files.some(i=>i===".env.example"||i.endsWith("/.env.example"))?{result:"PASS",message:".env.example found"}:e.files.some(i=>i===".env"||i===".env.local"||i.endsWith("/.env")||i.endsWith("/.env.local"))?{result:"FAIL",message:"Project uses .env files but has no .env.example for documentation",evidence:["Create .env.example listing all required vars (without secret values)"]}:{result:"UNKNOWN",message:"No .env files found \u2014 project may not use environment variables"}}};var ei={id:"ENV-02",name:"No secrets in committed .env",module:"foundation",layer:"L3",priority:"P0",description:"Committed .env files must not contain actual secret values \u2014 only .env.example with placeholders",fixCost:100,fixSize:"M",async run(e){let t=e.files.filter(s=>{let o=s.split("/").pop()||"";return(o===".env"||o===".env.local"||o===".env.production")&&!o.includes("example")});if(t.length===0)return{result:"PASS",message:"No committed .env files found"};let n=/^[A-Z_]+=(?:sk_live_|sk_test_|whsec_|sbp_|eyJ|ghp_|gho_|AKIA|supabase.*service_role)/m,i=/^[A-Z_]+=.{20,}/m,r=[];for(let s of t){let o;try{o=await e.readFile(s)}catch{continue}if(n.test(o))r.push(`${s} \u2014 contains secret key patterns`);else if(i.test(o)){let a=o.split(`
|
|
9
|
-
`).filter(c=>/^[A-Z_]+=.{20,}/.test(c)&&!c.startsWith("#")&&!c.includes("your_")&&!c.includes("xxx"));a.length>2&&r.push(`${
|
|
10
|
-
`);for(let a=0;a<o.length;a++){let c=o[a].trim();c.startsWith("//")||c.startsWith("*")||c.startsWith("#")||
|
|
11
|
-
`),
|
|
12
|
-
`),i=
|
|
13
|
-
`);e(""),e(`${m}${"\u2500".repeat(62)}${u}`),e(` ${m}Static source-code analysis focused on high-confidence patterns.${u}`),e(` ${m}Not a complete security audit. Some findings may need manual review.${u}`),e(` ${m}https://vibecodiq.com/scan | https://asastandard.org${u}`),e("")}function
|
|
14
|
-
`);e(""),e(` ${R}\u26A0 API unavailable. Showing local results only.${u}`),e(` ${m}Run again later for Trust Score + fix prompts.${u}`),e(` ${m}Status: https://status.vibecodiq.com${u}`),e("")}import
|
|
15
|
-
`);var
|
|
16
|
-
`),
|
|
17
|
-
`);var
|
|
18
|
-
`).length;c>80&&
|
|
19
|
-
`,{mode:384}),console.log(""),console.log(` ${
|
|
2
|
+
import{createHash as zs}from"crypto";import bi from"path";var ge={id:"ARCH-01",name:"Business logic in domains/",module:"architecture",layer:"L1",priority:"P0",description:"Supabase/DB calls in pages or components indicate business logic outside domains/",fixCost:250,fixSize:"L",async run(e){let t=/supabase\.(from|auth|rpc|storage)\b|\.from\(\s*['"`]/,s=[],i=e.files.filter(n=>(n.includes("/pages/")||n.includes("/app/"))&&!n.includes("/api/")&&!n.includes("route.")&&!n.includes("layout.")&&!n.includes("globals.")&&(n.endsWith(".ts")||n.endsWith(".tsx")||n.endsWith(".js")||n.endsWith(".jsx"))),r=e.files.filter(n=>n.includes("/components/")&&(n.endsWith(".ts")||n.endsWith(".tsx")||n.endsWith(".js")||n.endsWith(".jsx")));for(let n of[...i,...r]){let o;try{o=await e.readFile(n)}catch{continue}t.test(o)&&s.push(n)}return s.length>0?{result:"FAIL",message:`Supabase/DB calls found in pages/components (${s.length} file${s.length>1?"s":""})`,evidence:s.slice(0,5)}:{result:"PASS",message:"No business logic detected in pages/components"}}};var he={id:"ARCH-02",name:"domains/ directory exists",module:"architecture",layer:"L1",priority:"P0",description:"Projects with >5 source files need a domains/ directory for organized business logic",fixCost:250,fixSize:"L",async run(e){let t=e.files.filter(r=>(r.endsWith(".ts")||r.endsWith(".tsx")||r.endsWith(".js")||r.endsWith(".jsx"))&&!r.includes("node_modules"));if(t.length<=5)return{result:"PASS",message:"Project is small \u2014 domains/ not yet required"};if(!e.files.some(r=>r.includes("domains/")||r.includes("src/domains/")))return{result:"FAIL",message:`${t.length} source files but no domains/ directory`,evidence:["Create domains/<domain>/<slice>/ for business logic"]};let i=new Set;for(let r of e.files){let n=r.match(/(?:src\/)?domains\/([^/]+)\//);n&&i.add(n[1])}return i.size===0?{result:"FAIL",message:"domains/ exists but is empty"}:{result:"PASS",message:`${i.size} domain${i.size>1?"s":""} found`}}};var Se={id:"ARCH-03",name:"No cross-domain imports",module:"architecture",layer:"L1",priority:"P1",description:"Files in one domain must not import from another domain \u2014 use shared/ for cross-domain communication",fixCost:100,fixSize:"M",async run(e){let t=e.files.filter(r=>r.includes("domains/"));if(t.length===0)return{result:"PASS",message:"No domains/ directory yet"};let s=new Set;for(let r of t){let n=r.match(/(?:src\/)?domains\/([^/]+)\//);n&&s.add(n[1])}let i=[];for(let r of t){if(!r.endsWith(".ts")&&!r.endsWith(".tsx")&&!r.endsWith(".js")&&!r.endsWith(".jsx"))continue;let n=r.match(/(?:src\/)?domains\/([^/]+)\//)?.[1];if(!n)continue;let o;try{o=await e.readFile(r)}catch{continue}for(let a of s){if(a===n)continue;new RegExp(`from.*domains/${a}|import.*domains/${a}`).test(o)&&i.push(`${r} \u2192 imports from ${a}/`)}}return i.length>0?{result:"FAIL",message:`${i.length} cross-domain import${i.length>1?"s":""} found`,evidence:i.slice(0,5)}:{result:"PASS",message:"No cross-domain imports detected"}}};var ye={id:"ARCH-04",name:"Pages are thin wrappers",module:"architecture",layer:"L1",priority:"P1",description:"Page files should be thin wrappers (<80 lines) that compose domain components",fixCost:250,fixSize:"L",async run(e){let t=e.files.filter(i=>(i.includes("/pages/")||i.match(/app\/.*\/page\.(ts|tsx|js|jsx)$/))&&(i.endsWith(".tsx")||i.endsWith(".jsx")));if(t.length===0)return{result:"PASS",message:"No page files found"};let s=[];for(let i of t){let r;try{r=await e.readFile(i)}catch{continue}let n=r.split(`
|
|
3
|
+
`).length;n>80&&s.push(`${i} (${n} lines)`)}return s.length>0?{result:"FAIL",message:`${s.length} page${s.length>1?"s":""} over 80 lines \u2014 extract logic to domains/`,evidence:s.slice(0,5)}:{result:"PASS",message:`All ${t.length} pages are thin wrappers`}}};var Ae={id:"ARCH-05",name:"shared/ has no business logic",module:"architecture",layer:"L1",priority:"P1",description:"shared/ should contain only infrastructure (DB clients, auth helpers) \u2014 not domain-specific components",fixCost:100,fixSize:"M",async run(e){let t=e.files.filter(n=>(n.includes("shared/")||n.includes("src/shared/"))&&(n.endsWith(".ts")||n.endsWith(".tsx")||n.endsWith(".js")||n.endsWith(".jsx")));if(t.length===0)return{result:"PASS",message:"No shared/ directory yet"};let s=/supabase\.(from|rpc)\(|\.insert\(|\.update\(|\.delete\(|\.select\(/,i=/TaskList|TaskForm|PricingCard|AdminUser|LoginForm|RegisterForm|InvoiceTable|OrderList|ProductCard/,r=[];for(let n of t){let o;try{o=await e.readFile(n)}catch{continue}i.test(o)?r.push(`${n} \u2014 business component name detected`):s.test(o)&&!n.includes("/db/")&&!n.includes("/auth/")&&!n.includes("/billing/")&&r.push(`${n} \u2014 direct DB operations outside infrastructure`)}return r.length>0?{result:"FAIL",message:`Business logic found in shared/ (${r.length} file${r.length>1?"s":""})`,evidence:r.slice(0,5)}:{result:"PASS",message:`shared/ contains only infrastructure (${t.length} files)`}}};async function be(e,t,s){let i=[],r=e.files;s&&s.length>0&&(r=r.filter(n=>s.some(o=>$e(n,o))));for(let n of r){let o;try{o=await e.readFile(n)}catch{continue}let a=o.split(`
|
|
4
|
+
`);for(let c=0;c<a.length;c++)t.test(a[c])&&i.push({file:n,line:c+1,content:a[c].trim()})}return i}function $e(e,t){if(t.startsWith("!"))return!$e(e,t.slice(1));if(t.startsWith("**/")){let s=t.slice(3);return e.includes(s)}if(t.endsWith("/**")){let s=t.slice(0,-3);return e.startsWith(s)}return t.includes("*")?new RegExp("^"+t.replace(/\*/g,".*")+"$").test(e):e===t||e.startsWith(t+"/")}function _(e){return e.startsWith("src/")||e.startsWith("components/")||e.startsWith("app/")||e.startsWith("pages/")||e.includes("/components/")||e.includes("/hooks/")||e.includes("/contexts/")}function w(e){return e.includes("/api/")||e.includes("route.ts")||e.includes("route.js")||e.includes("server/")||e.includes("actions.ts")||e.includes("actions.js")}function $(e){return e.includes("migration")||e.includes("supabase/")||e.endsWith(".sql")}function E(e){return(e.split("/").pop()||"").startsWith(".env")}var ve={id:"AUTH-01",name:"service_role key not in client code",module:"auth",layer:"L2",priority:"P0",aliases:["ADM-17"],description:"service_role bypasses all RLS \u2014 must never appear in client-side code or NEXT_PUBLIC_ vars",fixCost:100,fixSize:"M",async run(e){let t=/service_role|SERVICE_ROLE|SUPABASE_SERVICE_ROLE|sb_secret_/i,s=await e.grepFiles(t),i=s.filter(c=>c.content.includes("NEXT_PUBLIC_")?!0:c.file.includes("use client")?!1:!!(_(c.file)&&!c.file.includes("/api/")&&!c.file.includes("route."))),r=s.filter(c=>c.content.includes("NEXT_PUBLIC_")&&/service_role|SERVICE_ROLE/i.test(c.content)),n=[...i,...r],o=[...new Map(n.map(c=>[`${c.file}:${c.line}`,c])).values()];return o.length>0?{result:"FAIL",message:`service_role key found in client-accessible code (${o.length} location${o.length>1?"s":""})`,evidence:o.map(c=>`${c.file}:${c.line} \u2192 ${c.content.substring(0,120)}`)}:s.filter(c=>!c.content.startsWith("//")&&!c.content.startsWith("#")&&!c.content.startsWith("*")).length>0?{result:"PASS",message:"service_role key found only in server-side code"}:{result:"UNKNOWN",message:"No service_role references found \u2014 Supabase may not be used"}}};var Ce={id:"AUTH-02",name:"RLS enabled on all tables",module:"auth",layer:"L2",priority:"P0",description:"Every table must have Row Level Security enabled. Without RLS, anon key = full DB access.",fixCost:100,fixSize:"M",async run(e){let t=e.files.filter($);if(t.length===0)return{result:"UNKNOWN",message:"No SQL migration files found"};let s=/CREATE\s+TABLE\s+(?:IF\s+NOT\s+EXISTS\s+)?(?:public\.)?(\w+)/gi,i=/ALTER\s+TABLE\s+(?:public\.)?(\w+)\s+ENABLE\s+ROW\s+LEVEL\s+SECURITY/gi,r=new Set(["schema_migrations","_prisma_migrations","migrations"]),n=new Set,o=new Set;for(let c of t){let l;try{l=await e.readFile(c)}catch{continue}let p,f=new RegExp(s.source,"gi");for(;(p=f.exec(l))!==null;){let y=p[1].toLowerCase();r.has(y)||n.add(y)}let h=new RegExp(i.source,"gi");for(;(p=h.exec(l))!==null;)o.add(p[1].toLowerCase())}if(n.size===0)return{result:"UNKNOWN",message:"No CREATE TABLE statements found in migrations"};let a=[...n].filter(c=>!o.has(c));return a.length>0?{result:"FAIL",message:`${a.length} table${a.length>1?"s":""} without RLS (of ${n.size} total)`,evidence:a.map(c=>`Table "${c}" \u2014 missing ENABLE ROW LEVEL SECURITY`)}:{result:"PASS",message:`All ${n.size} tables have RLS enabled`}}};var _e={id:"AUTH-03",name:"RLS policies have WITH CHECK",module:"auth",layer:"L2",priority:"P0",description:"INSERT/UPDATE policies need WITH CHECK clause. USING alone lets users insert data owned by others.",fixCost:100,fixSize:"M",async run(e){let t=e.files.filter($);if(t.length===0)return{result:"UNKNOWN",message:"No SQL migration files found"};let s=[],i=[];for(let a of t){let c;try{c=await e.readFile(a)}catch{continue}let l=/CREATE\s+POLICY\s+"?(\w+)"?\s+ON\s+(?:public\.)?(\w+)\s+(?:AS\s+\w+\s+)?FOR\s+(INSERT|UPDATE|ALL)\b([\s\S]*?)(?=CREATE\s+POLICY|$)/gi,p;for(;(p=l.exec(c))!==null;){let f=p[1],h=p[2],y=p[3].toUpperCase(),A=p[4];(y==="INSERT"||y==="UPDATE"||y==="ALL")&&(/WITH\s+CHECK\s*\(\s*true\s*\)/i.test(A)?s.push(`${a}: policy "${f}" on ${h} \u2014 WITH CHECK (true) is permissive`):/WITH\s+CHECK/i.test(A)||i.push(`${a}: policy "${f}" on ${h} (${y}) \u2014 missing WITH CHECK`))}}let r=[...s,...i];if(r.length>0)return{result:"FAIL",message:`${r.length} RLS policy issue${r.length>1?"s":""} found`,evidence:r.slice(0,5)};let n=t.some(a=>{try{let c=e.files.includes(a)?a:"";return/CREATE\s+POLICY/i.test(c)}catch{return!1}});return(await e.grepFiles(/CREATE\s+POLICY/i)).length===0?{result:"UNKNOWN",message:"No RLS policies found in migrations"}:{result:"PASS",message:"All INSERT/UPDATE RLS policies have proper WITH CHECK clauses"}}};var ke={id:"AUTH-04",name:"Server-side auth on protected routes",module:"auth",layer:"L2",priority:"P0",description:"Every protected API route/server action must verify auth server-side. Middleware-only auth is bypassable (CVE-2025-29927).",fixCost:100,fixSize:"M",async run(e){let t=e.files.filter(n=>(n.includes("route.")||n.includes("actions."))&&w(n));if(t.length===0)return{result:"UNKNOWN",message:"No API route handlers found"};let s=/getUser|getSession|auth\(\)|verifyToken|requireAuth|requireAdmin|checkPermission|requireRole|currentUser|validateSession/i,i=/webhook|health|status|public|og|sitemap|robots|favicon|_next/i,r=[];for(let n of t){if(i.test(n))continue;let o;try{o=await e.readFile(n)}catch{continue}s.test(o)||r.push(n)}return r.length>0?{result:"FAIL",message:`${r.length} API route${r.length>1?"s":""} without server-side auth check`,evidence:r.slice(0,5).map(n=>`${n} \u2014 no auth verification detected`)}:{result:"PASS",message:`All ${t.length} API routes have server-side auth checks`}}};var Ie={id:"AUTH-05",name:"No secrets with NEXT_PUBLIC_ prefix",module:"auth",layer:"L2",priority:"P0",description:"NEXT_PUBLIC_ vars are bundled in the browser. Secrets must never use this prefix.",fixCost:100,fixSize:"M",async run(e){let t=[/NEXT_PUBLIC_.*SERVICE_ROLE/i,/NEXT_PUBLIC_.*SECRET/i,/NEXT_PUBLIC_.*DATABASE_URL/i,/NEXT_PUBLIC_.*DB_URL/i,/NEXT_PUBLIC_.*PRIVATE/i,/NEXT_PUBLIC_.*PASSWORD/i,/NEXT_PUBLIC_.*JWT_SECRET/i,/VITE_.*SERVICE_ROLE/i,/VITE_.*SECRET_KEY/i,/VITE_.*DATABASE_URL/i,/EXPO_PUBLIC_.*SERVICE_ROLE/i,/EXPO_PUBLIC_.*SECRET/i],s=e.files.filter(E),i=[];for(let r of s){let n;try{n=await e.readFile(r)}catch{continue}let o=n.split(`
|
|
5
|
+
`);for(let a=0;a<o.length;a++){let c=o[a].trim();if(!(c.startsWith("#")||!c)){for(let l of t)if(l.test(c)){i.push({file:r,line:a+1,content:c.split("=")[0]});break}}}}return i.length>0?{result:"FAIL",message:`Secret keys exposed via public prefix (${i.length} found)`,evidence:i.map(r=>`${r.file}:${r.line} \u2192 ${r.content}`)}:s.length===0?{result:"UNKNOWN",message:"No .env files found to check"}:{result:"PASS",message:"No secrets found with NEXT_PUBLIC_/VITE_/EXPO_PUBLIC_ prefix"}}};var Pe={id:"AUTH-06",name:"Protected routes redirect unauthenticated users",module:"auth",layer:"L2",priority:"P1",description:"Middleware or route guard must redirect unauthenticated users to /login for protected pages.",fixCost:100,fixSize:"M",async run(e){let t=e.files.find(i=>/^middleware\.(ts|js)$/.test(i));if(!t)return(await e.grepFiles(/redirect.*login|redirect.*auth|redirect.*sign/i)).length>0?{result:"PASS",message:"Route guards with login redirect detected"}:{result:"FAIL",message:"No middleware.ts and no route guards found \u2014 unauthenticated users can access protected pages",evidence:["Missing: middleware.ts or per-page auth redirect"]};let s;try{s=await e.readFile(t)}catch{return{result:"UNKNOWN",message:"Could not read middleware file"}}return/redirect.*login|redirect.*auth|NextResponse.*redirect/i.test(s)?{result:"PASS",message:"Middleware redirects unauthenticated users to login"}:{result:"FAIL",message:"Middleware exists but no login redirect detected",evidence:[`${t} \u2014 no redirect to /login or /auth`]}}};var we={id:"AUTH-07",name:"Session tokens in httpOnly cookies",module:"auth",layer:"L2",priority:"P2",description:"Tokens in localStorage are accessible via XSS. httpOnly cookies prevent JavaScript access to session tokens.",fixCost:100,fixSize:"M",async run(e){let t=/localStorage.*(?:token|session|jwt|access_token)|sessionStorage.*(?:token|session|jwt)/i,s=/httpOnly|cookie.*session|supabase.*ssr|@supabase\/ssr/i,i=await e.grepFiles(t),r=await e.grepFiles(s),n=i.filter(o=>!o.content.trimStart().startsWith("//"));return n.length>0&&r.length===0?{result:"FAIL",message:`Session tokens stored in localStorage (${n.length} location${n.length>1?"s":""})`,evidence:n.slice(0,3).map(o=>`${o.file}:${o.line} \u2192 ${o.content.substring(0,120)}`)}:r.length>0?{result:"PASS",message:"httpOnly cookies or @supabase/ssr detected for session management"}:{result:"UNKNOWN",message:"No explicit token storage pattern detected"}}};var Le={id:"AUTH-08",name:"Password hashing (if custom auth)",module:"auth",layer:"L2",priority:"P0",description:"Passwords must be hashed with bcrypt/argon2/scrypt. N/A if using Supabase Auth (handles hashing internally).",fixCost:100,fixSize:"M",async run(e){let t=/supabase|@supabase|createBrowserClient|createServerClient/i;if((await e.grepFiles(t)).length>0)return{result:"N/A",message:"Supabase Auth handles password hashing internally (bcrypt/Argon2)"};let i=/bcrypt|argon2|scrypt|pbkdf2/i,r=/password.*=.*req\.body|password.*=.*body\.|plaintext|md5.*password|sha1.*password/i,n=await e.grepFiles(i),o=await e.grepFiles(r);return o.length>0&&n.length===0?{result:"FAIL",message:"Password handling without secure hashing detected",evidence:o.slice(0,3).map(a=>`${a.file}:${a.line} \u2192 ${a.content.substring(0,120)}`)}:n.length>0?{result:"PASS",message:"Secure password hashing detected (bcrypt/argon2/scrypt)"}:{result:"UNKNOWN",message:"No custom auth password handling detected"}}};var Ne={id:"AUTH-09",name:"Rate limiting on auth endpoints",module:"auth",layer:"L2",priority:"P1",description:"Login/register endpoints without rate limiting are vulnerable to brute force attacks.",fixCost:100,fixSize:"M",async run(e){let t=e.files.filter(o=>/auth|login|register|sign/i.test(o)),s=/rateLimit|rate.limit|throttle|limiter|too.many.requests|429|upstash/i;return t.length===0?{result:"UNKNOWN",message:"No auth endpoint files found"}:(await e.grepFiles(s,t)).length>0?{result:"PASS",message:"Rate limiting detected on auth endpoints"}:(await e.grepFiles(s)).length>0?{result:"PASS",message:"Rate limiting detected in codebase (verify it covers auth endpoints)"}:(await e.grepFiles(/supabase.*auth|@supabase/i)).length>0?{result:"PASS",message:"Supabase Auth has built-in rate limiting for auth endpoints"}:{result:"FAIL",message:"No rate limiting found on auth endpoints",evidence:["Missing: rate limiting middleware on login/register routes"]}}};var Re={id:"AUTH-10",name:"Profile sync trigger with safe search_path",module:"auth",layer:"L2",priority:"P1",description:"handle_new_user() trigger must use SECURITY DEFINER with restricted search_path to prevent privilege escalation.",fixCost:100,fixSize:"M",async run(e){let t=e.files.filter($);if(t.length===0)return{result:"UNKNOWN",message:"No SQL migration files found"};let s=/handle_new_user|on_auth_user_created|AFTER\s+INSERT\s+ON\s+auth\.users/i,i=/SECURITY\s+DEFINER/i,r=/search_path|SET\s+search_path/i;if((await e.grepFiles(s)).length===0)return{result:"FAIL",message:"No profile sync trigger found (handle_new_user or equivalent)",evidence:["Missing trigger: new auth.users rows won't sync to profiles table"]};for(let o of t){let a;try{a=await e.readFile(o)}catch{continue}if(s.test(a)){let c=i.test(a),l=r.test(a);return c&&!l?{result:"FAIL",message:"Profile trigger uses SECURITY DEFINER without restricted search_path",evidence:[`${o} \u2014 SECURITY DEFINER without SET search_path = '' (privilege escalation risk)`]}:c&&l?{result:"PASS",message:"Profile sync trigger found with SECURITY DEFINER and restricted search_path"}:{result:"PASS",message:"Profile sync trigger found"}}}return{result:"PASS",message:"Profile sync trigger detected"}}};var Ee={id:"AUTH-11",name:"Client/server auth separation",module:"auth",layer:"L2",priority:"P0",description:"Separate Supabase clients for browser and server. One shared client leaks service_role to browser or uses anon key on server.",fixCost:100,fixSize:"M",async run(e){let t=/createBrowserClient|createClient.*browser/i,s=/createServerClient|createClient.*server/i,i=/createClient\s*\(/,r=await e.grepFiles(t),n=await e.grepFiles(s),o=await e.grepFiles(i);if(r.length>0&&n.length>0)return{result:"PASS",message:"Separate browser and server Supabase clients detected"};if(r.length>0||n.length>0)return{result:"PASS",message:"Dedicated Supabase client pattern detected (@supabase/ssr)"};if(o.length>0){let a=o.filter(c=>/supabase|createClient/.test(c.content));if(a.length>0)return{result:"FAIL",message:"Single generic createClient() used \u2014 no client/server separation",evidence:a.slice(0,3).map(c=>`${c.file}:${c.line} \u2192 ${c.content.substring(0,120)}`)}}return{result:"UNKNOWN",message:"No Supabase client initialization found"}}};var Fe={id:"AUTH-12",name:"Auth environment variables present",module:"auth",layer:"L2",priority:"P2",description:"Missing auth env vars = hardcoded keys or broken auth in production.",fixCost:100,fixSize:"M",async run(e){let t=e.files.filter(E);if(t.length===0)return{result:"UNKNOWN",message:"No .env files found"};let s=["SUPABASE_URL","SUPABASE_ANON_KEY"],i=[];for(let o of t){let a;try{a=await e.readFile(o)}catch{continue}for(let c of s)a.includes(c)&&!i.includes(c)&&i.push(c)}if((await e.grepFiles(/supabase/i)).length===0)return{result:"UNKNOWN",message:"No Supabase references found"};let n=s.filter(o=>!i.includes(o));return n.length>0?{result:"FAIL",message:`Missing auth env vars: ${n.join(", ")}`,evidence:n.map(o=>`${o} not found in any .env file`)}:{result:"PASS",message:"Auth environment variables configured"}}};var Te={id:"AUTH-13",name:"getUser() not getSession() for server-side auth",module:"auth",layer:"L2",priority:"P0",description:"getSession() reads JWT without server verification. Use getUser() for auth decisions.",fixCost:100,fixSize:"M",async run(e){let t=/\.auth\.getSession\s*\(/,s=/\.auth\.getUser\s*\(/,i=await e.grepFiles(t),r=await e.grepFiles(s);if(i.length===0&&r.length===0)return{result:"UNKNOWN",message:"No getSession/getUser calls found \u2014 Supabase Auth may not be used"};let n=i.filter(l=>w(l.file)),o=r.filter(l=>w(l.file));if(n.length>0&&o.length===0)return{result:"FAIL",message:`Server-side code uses getSession() without getUser() (${n.length} location${n.length>1?"s":""})`,evidence:n.map(l=>`${l.file}:${l.line} \u2192 ${l.content.substring(0,120)}`)};if(n.length>0&&o.length>0)return{result:"FAIL",message:"Server code uses both getSession() and getUser() \u2014 getSession() in server context is insecure",evidence:n.map(l=>`${l.file}:${l.line} \u2192 ${l.content.substring(0,120)}`)};let a=i.filter(l=>!w(l.file)&&!/supabase\/functions\//i.test(l.file)),c=r.filter(l=>!w(l.file)&&!/supabase\/functions\//i.test(l.file));return a.length>0&&c.length===0&&o.length===0?{result:"FAIL",message:`Client code uses getSession() (${a.length} location${a.length>1?"s":""}) but getUser() never called \u2014 auth relies on unverified JWT`,evidence:a.slice(0,3).map(l=>`${l.file}:${l.line} \u2192 ${l.content.substring(0,120)}`)}:o.length>0||c.length>0?{result:"PASS",message:"Auth uses getUser() for verification"}:{result:"PASS",message:"getUser() is used for auth verification"}}};var xe={id:"AUTH-14",name:"No eval() or dangerouslySetInnerHTML with user data",module:"auth",layer:"L2",priority:"P0",description:"eval() and dangerouslySetInnerHTML enable XSS attacks \u2014 session theft and account takeover.",fixCost:100,fixSize:"M",async run(e){let t=/\beval\s*\(/,s=/dangerouslySetInnerHTML/,i=await e.grepFiles(t),r=await e.grepFiles(s),n=i.filter(c=>!(c.content.trimStart().startsWith("//")||c.content.trimStart().startsWith("*")||c.file.includes("node_modules"))),o=r.filter(c=>!(c.content.trimStart().startsWith("//")||c.file.includes("node_modules"))),a=[...n,...o];return a.length>0?{result:"FAIL",message:`Unsafe code patterns found (${n.length} eval, ${o.length} dangerouslySetInnerHTML)`,evidence:a.slice(0,5).map(c=>`${c.file}:${c.line} \u2192 ${c.content.substring(0,120)}`)}:{result:"PASS",message:"No eval() or dangerouslySetInnerHTML found"}}};var De={id:"AUTH-15",name:"CORS configuration",module:"auth",layer:"L2",priority:"P2",description:"Access-Control-Allow-Origin: * with credentials allows any website to read auth cookies and data.",fixCost:100,fixSize:"M",async run(e){let t=/Access-Control-Allow-Origin.*\*|cors.*origin.*\*|origin:\s*['"]?\*/i,s=/credentials.*true|allowCredentials|Access-Control-Allow-Credentials/i,i=await e.grepFiles(t),r=await e.grepFiles(s);return i.length>0&&r.length>0?{result:"FAIL",message:"CORS wildcard (*) used with credentials \u2014 any website can read auth data",evidence:i.slice(0,3).map(n=>`${n.file}:${n.line} \u2192 ${n.content.substring(0,120)}`)}:i.length>0?{result:"FAIL",message:"CORS wildcard (*) detected \u2014 consider restricting to specific origins",evidence:i.slice(0,3).map(n=>`${n.file}:${n.line} \u2192 ${n.content.substring(0,120)}`)}:{result:"PASS",message:"No dangerous CORS wildcard configuration detected"}}};var Ue={id:"AUTH-16",name:"Token/session expiration configured",module:"auth",layer:"L2",priority:"P2",description:"Infinite sessions mean a stolen token works forever. Configure JWT expiration and session timeouts.",fixCost:100,fixSize:"M",async run(e){let t=/expiresIn|maxAge|session.*expir|jwt.*expir|SESSION_EXPIRY|JWT_EXPIRY|token.*expir/i;return(await e.grepFiles(t)).length>0?{result:"PASS",message:"Token/session expiration configured"}:(await e.grepFiles(/supabase/i)).length>0?{result:"PASS",message:"Supabase Auth has default JWT expiry (3600s)"}:{result:"FAIL",message:"No token/session expiration configuration found",evidence:["Missing: expiresIn, maxAge, or session expiry configuration"]}}};var Me={id:"AUTH-17",name:"Storage bucket RLS",module:"auth",layer:"L2",priority:"P0",description:"Supabase storage buckets must have RLS on storage.objects. Without it, all files are publicly accessible.",fixCost:100,fixSize:"M",async run(e){let t=e.files.filter($);if(t.length===0)return{result:"UNKNOWN",message:"No SQL migration files found"};let s=/storage\.buckets|create.*bucket|INSERT.*storage\.buckets/i,i=/storage\.objects.*ENABLE.*ROW.*LEVEL|RLS.*storage\.objects|POLICY.*storage\.objects/i,r=await e.grepFiles(s,t),n=await e.grepFiles(i,t);return r.length===0?(await e.grepFiles(/supabase.*storage|storage\.from\(/i)).length===0?{result:"N/A",message:"No Supabase storage usage detected"}:{result:"UNKNOWN",message:"Storage used in code but no bucket creation in migrations"}:n.length>0?{result:"PASS",message:"Storage bucket RLS policies detected"}:{result:"FAIL",message:"Storage buckets created without RLS on storage.objects \u2014 files may be publicly accessible",evidence:r.slice(0,3).map(o=>`${o.file}:${o.line} \u2192 ${o.content.substring(0,120)}`)}}};var Oe={id:"AUTH-18",name:"RBAC in app_metadata not user_metadata",module:"auth",layer:"L2",priority:"P0",description:"Roles in user_metadata are user-editable. Use app_metadata for RBAC (server-only).",fixCost:100,fixSize:"M",async run(e){let t=/user_meta_?data.*role|raw_user_meta_?data.*role|user\.user_metadata.*role/i,s=/app_meta_?data.*role|user\.app_metadata.*role/i,i=/updateUser\s*\(\s*\{[\s\S]*?data\s*:\s*\{[\s\S]*?role/,r=await e.grepFiles(t),n=await e.grepFiles(s),o=await e.grepFiles(i);return r.length>0?{result:"FAIL",message:`Role stored in user_metadata (user-editable) \u2014 ${r.length} location${r.length>1?"s":""}`,evidence:r.map(a=>`${a.file}:${a.line} \u2192 ${a.content.substring(0,120)}`)}:o.length>0?{result:"FAIL",message:"Role set via updateUser() data field (user-editable user_metadata)",evidence:o.map(a=>`${a.file}:${a.line} \u2192 ${a.content.substring(0,120)}`)}:n.length>0?{result:"PASS",message:"Role stored in app_metadata (server-only, not user-editable)"}:{result:"UNKNOWN",message:"No role/RBAC references found in codebase"}}};var We={id:"AUTH-19",name:"Multi-tenancy data isolation",module:"auth",layer:"L2",priority:"P1",description:"RLS policies must include tenant_id or organization_id to prevent cross-tenant data access.",fixCost:100,fixSize:"M",async run(e){let t=e.files.filter($),s=/tenant_id|organization_id|org_id|team_id|workspace_id/i,i=await e.grepFiles(s,t.length>0?t:void 0),r=await e.grepFiles(s);return i.length===0&&r.length===0?{result:"N/A",message:"No multi-tenancy pattern detected (single-tenant app)"}:(await e.grepFiles(/POLICY[\s\S]*?tenant_id|POLICY[\s\S]*?org_id|POLICY[\s\S]*?organization_id/i,t)).length>0?{result:"PASS",message:"RLS policies include tenant isolation"}:{result:"FAIL",message:"Multi-tenant schema detected but RLS policies don't include tenant_id filtering",evidence:["Tenant columns exist but RLS policies may allow cross-tenant data access"]}}};var je={id:"AUTH-20",name:"OAuth domain restriction",module:"auth",layer:"L2",priority:"P1",description:"Social login (Google, GitHub) should restrict allowed email domains to prevent unauthorized access.",fixCost:100,fixSize:"M",async run(e){let t=/signInWithOAuth|signIn.*provider|google|github.*login|oauth/i,s=/allowedDomain|domain.*restrict|email.*domain|hd=|hosted_domain/i,i=await e.grepFiles(t);return i.length===0?{result:"N/A",message:"No OAuth/social login detected"}:(await e.grepFiles(s)).length>0?{result:"PASS",message:"OAuth domain restriction detected"}:{result:"FAIL",message:"OAuth login enabled without domain restriction \u2014 any Google/GitHub account can sign in",evidence:i.slice(0,3).map(n=>`${n.file}:${n.line} \u2192 ${n.content.substring(0,120)}`)}}};var Be={id:"AUTH-21",name:"Force dynamic on auth routes",module:"auth",layer:"L2",priority:"P1",description:"Auth routes must use force-dynamic to prevent ISR cache serving wrong user data.",fixCost:100,fixSize:"M",async run(e){let t=e.files.filter(n=>/auth|login|register|signup|sign-in|sign-up/i.test(n)&&(n.includes("page.")||n.includes("route."))&&(n.endsWith(".ts")||n.endsWith(".tsx")||n.endsWith(".js")||n.endsWith(".jsx")));if(t.length===0)return{result:"UNKNOWN",message:"No auth route/page files found"};let s=/export\s+const\s+dynamic\s*=\s*['"]force-dynamic['"]/,i=/unstable_noStore|noStore|revalidate\s*=\s*0/,r=[];for(let n of t){let o;try{o=await e.readFile(n)}catch{continue}!s.test(o)&&!i.test(o)&&n.includes("page.")&&r.push(n)}return r.length>0?{result:"FAIL",message:`${r.length} auth page${r.length>1?"s":""} without force-dynamic \u2014 may serve cached auth state`,evidence:r.slice(0,5).map(n=>`${n} \u2014 missing export const dynamic = 'force-dynamic'`)}:{result:"PASS",message:"Auth routes use force-dynamic or equivalent"}}};var He={id:"AUTH-22",name:"CSRF protection on Route Handlers",module:"auth",layer:"L2",priority:"P2",description:"Server Actions have built-in CSRF protection (Next.js 14+). Route Handlers do NOT \u2014 they need explicit CSRF tokens or SameSite cookies.",fixCost:100,fixSize:"M",async run(e){let t=e.files.filter(o=>/route\.(ts|js)$/i.test(o));if(t.length===0)return{result:"UNKNOWN",message:"No Route Handler files found"};let s=[];for(let o of t){let a;try{a=await e.readFile(o)}catch{continue}/export\s+(?:async\s+)?function\s+(?:POST|PUT|PATCH|DELETE)/i.test(a)&&s.push(o)}if(s.length===0)return{result:"PASS",message:"No mutation Route Handlers found (only GET)"};let i=/csrf|csurf|csrfToken|SameSite|x-csrf|anti.?forgery/i;return(await e.grepFiles(i)).length>0?{result:"PASS",message:"CSRF protection detected"}:(await e.grepFiles(/["']use server["']/i)).length>0&&s.length<=2?{result:"PASS",message:"Mutations use Server Actions (built-in CSRF protection)"}:{result:"FAIL",message:`${s.length} mutation Route Handler${s.length>1?"s":""} without CSRF protection`,evidence:s.slice(0,3).map(o=>`${o} \u2014 POST/PUT/PATCH/DELETE without CSRF token`)}}};var ze={id:"AUTH-23",name:"Email verification required",module:"auth",layer:"L2",priority:"P2",description:"Without email verification, anyone can sign up with any email \u2014 spam accounts and impersonation.",fixCost:100,fixSize:"M",async run(e){let t=/email_confirmed_at|emailConfirmed|verifyEmail|confirmEmail|email.*verif|verification.*email/i;return(await e.grepFiles(t)).length>0?{result:"PASS",message:"Email verification check detected"}:(await e.grepFiles(/supabase/i)).length>0?{result:"PASS",message:"Supabase Auth has configurable email verification (check dashboard settings)"}:{result:"FAIL",message:"No email verification enforcement found",evidence:["Missing: email_confirmed_at check or equivalent verification flow"]}}};var qe={id:"AUTH-24",name:"Account enumeration prevention",module:"auth",layer:"L2",priority:"P2",description:"Login/register error messages must not reveal whether an email exists. Consistent messages prevent user enumeration.",fixCost:100,fixSize:"M",async run(e){let t=/email.*not.*found|user.*not.*found|no.*account.*with|email.*already.*registered|email.*already.*exists|account.*already.*exists/i,i=(await e.grepFiles(t)).filter(r=>!r.content.trimStart().startsWith("//")&&!r.content.trimStart().startsWith("*"));return i.length>0?{result:"FAIL",message:`Account enumeration possible \u2014 error messages reveal email existence (${i.length} location${i.length>1?"s":""})`,evidence:i.slice(0,3).map(r=>`${r.file}:${r.line} \u2192 ${r.content.substring(0,120)}`)}:{result:"PASS",message:"No account enumeration patterns detected in error messages"}}};var Ke={id:"AUTH-25",name:"Refresh token reuse detection",module:"auth",layer:"L2",priority:"P2",description:"Token rotation prevents stolen refresh tokens from being reused. Supabase supports this via config.",fixCost:100,fixSize:"M",async run(e){let t=/token.*rotation|refresh.*token.*reuse|reuse.*detection|GOTRUE_SECURITY_REFRESH_TOKEN_REUSE_INTERVAL/i;return(await e.grepFiles(t)).length>0?{result:"PASS",message:"Refresh token rotation/reuse detection configured"}:(await e.grepFiles(/supabase/i)).length>0?{result:"PASS",message:"Supabase Auth has built-in refresh token rotation (check dashboard config)"}:{result:"UNKNOWN",message:"No refresh token configuration detected"}}};var Ge={id:"AUTH-26",name:"Sign-out revokes server session",module:"auth",layer:"L2",priority:"P2",description:"Sign-out must revoke the server session (scope: 'global'), not just clear client cookies.",fixCost:100,fixSize:"M",async run(e){let t=/signOut|sign_out|logout|log_out/i,s=/scope.*global|global.*scope/i,i=await e.grepFiles(t);return i.length===0?{result:"UNKNOWN",message:"No sign-out implementation found"}:(await e.grepFiles(s)).length>0?{result:"PASS",message:"Sign-out uses global scope (revokes all sessions)"}:{result:"FAIL",message:"Sign-out found but no global scope \u2014 sessions may persist on other devices",evidence:i.slice(0,2).map(n=>`${n.file}:${n.line} \u2192 ${n.content.substring(0,120)}`)}}};var Ve={id:"AUTH-27",name:"Email link poisoning mitigation",module:"auth",layer:"L2",priority:"P2",description:"SITE_URL and REDIRECT_ALLOW_LIST must be configured to prevent email link redirect attacks.",fixCost:100,fixSize:"M",async run(e){let t=/SITE_URL|GOTRUE_SITE_URL/i,s=/REDIRECT_ALLOW_LIST|ADDITIONAL_REDIRECT_URLS|redirect.*allow/i,i=await e.grepFiles(t),r=await e.grepFiles(s);return i.length>0&&r.length>0?{result:"PASS",message:"SITE_URL and REDIRECT_ALLOW_LIST configured"}:i.length>0?{result:"PASS",message:"SITE_URL configured (check REDIRECT_ALLOW_LIST in Supabase dashboard)"}:(await e.grepFiles(/supabase/i)).length===0?{result:"N/A",message:"No Supabase usage detected"}:{result:"FAIL",message:"No SITE_URL or REDIRECT_ALLOW_LIST found \u2014 email magic links may redirect to attacker domains",evidence:["Missing: SITE_URL and REDIRECT_ALLOW_LIST in env configuration"]}}};var Ye={id:"AUTH-28",name:"Realtime presence authorization",module:"auth",layer:"L2",priority:"P2",description:"Supabase Realtime Presence channels must have authorization. Without it, any user can see who's online.",fixCost:100,fixSize:"M",async run(e){let t=/realtime|presence|channel.*subscribe|supabase.*channel/i,s=await e.grepFiles(t);if(s.length===0)return{result:"N/A",message:"No Supabase Realtime/Presence usage detected"};let i=/authorized|RLS.*realtime|realtime.*auth|channel.*auth/i;return(await e.grepFiles(i)).length>0?{result:"PASS",message:"Realtime channel authorization detected"}:{result:"FAIL",message:"Realtime/Presence channels found without authorization",evidence:s.slice(0,3).map(n=>`${n.file}:${n.line} \u2192 ${n.content.substring(0,120)}`)}}};var Xe={id:"BIL-01",name:"Stripe secret key not in client code",module:"billing",layer:"L2",priority:"P0",description:"sk_live_/sk_test_ in client code = full Stripe API access for any visitor",fixCost:100,fixSize:"M",async run(e){let t=/sk_live_[a-zA-Z0-9]{20,}|sk_test_[a-zA-Z0-9]{20,}/,s=/STRIPE_SECRET|NEXT_PUBLIC_.*STRIPE.*SECRET|VITE_.*STRIPE.*SECRET/i,r=(await e.grepFiles(t)).filter(l=>!E(l.file)&&!l.content.trimStart().startsWith("#")&&!l.content.trimStart().startsWith("//"));if(r.length>0)return{result:"FAIL",message:`Hardcoded Stripe secret key found in source code (${r.length} location${r.length>1?"s":""})`,evidence:r.map(l=>`${l.file}:${l.line} \u2192 sk_***_[REDACTED]`)};let n=await e.grepFiles(s),o=n.filter(l=>/NEXT_PUBLIC_|VITE_|EXPO_PUBLIC_/i.test(l.content)&&/STRIPE.*SECRET/i.test(l.content));if(o.length>0)return{result:"FAIL",message:"Stripe secret key exposed via NEXT_PUBLIC_/VITE_ prefix",evidence:o.map(l=>`${l.file}:${l.line} \u2192 ${l.content.split("=")[0]}`)};let a=n.filter(l=>_(l.file)&&/STRIPE_SECRET/i.test(l.content)&&!l.file.includes("/api/")&&!l.file.includes("route."));return a.length>0?{result:"FAIL",message:"Stripe secret key referenced in client-side file",evidence:a.map(l=>`${l.file}:${l.line} \u2192 ${l.content.substring(0,120)}`)}:n.filter(l=>/STRIPE_SECRET/i.test(l.content)).length>0?{result:"PASS",message:"Stripe secret key found only in server-side/env files"}:{result:"UNKNOWN",message:"No Stripe secret key references found \u2014 Stripe may not be used"}}};var Qe={id:"BIL-02",name:"Webhook signature verification",module:"billing",layer:"L2",priority:"P0",aliases:["ADM-15"],description:"Stripe webhook handler must verify signature via constructEvent(). Without it, anyone can send fake webhooks.",fixCost:100,fixSize:"M",async run(e){let t=/webhook/i,s=e.files.filter(c=>t.test(c));if(s.length===0)return{result:"UNKNOWN",message:"No webhook handler files found"};let i=/constructEvent|constructEventAsync|webhooks\.construct/,r=/stripe-signature|Stripe-Signature|STRIPE_WEBHOOK_SECRET|webhook.*secret/i,n=await e.grepFiles(i,s),o=await e.grepFiles(r,s);if(n.length>0){for(let c of s){let l;try{l=await e.readFile(c)}catch{continue}let p=/constructEvent|constructEventAsync|webhooks\.construct/.test(l),f=/JSON\.parse\s*\(.*body/i.test(l),h=/if\s*\(\s*(?:webhook_?[Ss]ecret|STRIPE_WEBHOOK_SECRET|secret)/i.test(l)||/if\s*\(\s*!?\s*webhook_?[Ss]ecret/i.test(l);if(p&&f&&h)return{result:"FAIL",message:"Webhook signature verification is conditional \u2014 JSON.parse fallback bypasses constructEvent() when secret is missing",evidence:[`${c} \u2014 constructEvent() inside conditional, JSON.parse(body) fallback allows unverified webhooks`]}}return{result:"PASS",message:"Webhook handler uses constructEvent() for signature verification"}}return o.length>0?{result:"PASS",message:"Webhook handler references signature verification"}:(await e.grepFiles(/stripe|Stripe/,s)).length>0?{result:"FAIL",message:"Stripe webhook handler found WITHOUT signature verification",evidence:s.map(c=>`${c} \u2014 no constructEvent() or signature check`)}:{result:"UNKNOWN",message:"Webhook files found but no Stripe references \u2014 may not be Stripe webhooks"}}};var Je={id:"BIL-03",name:"Raw body preservation in webhook",module:"billing",layer:"L2",priority:"P0",description:"Webhook signature verification requires raw request body. req.json() breaks it \u2014 use req.text() or bodyParser: false.",fixCost:100,fixSize:"M",async run(e){let t=e.files.filter(a=>/webhook/i.test(a));if(t.length===0)return{result:"UNKNOWN",message:"No webhook handler files found"};let s=/request\.text\(\)|req\.text\(\)|bodyParser\s*:\s*false|getRawBody|raw\s*:\s*true|rawBody/,i=/request\.json\(\)|req\.body(?!\s*Parser)|JSON\.parse/,r=await e.grepFiles(s,t),n=await e.grepFiles(i,t);return r.length>0?{result:"PASS",message:"Webhook handler preserves raw body for signature verification"}:(await e.grepFiles(/stripe|constructEvent/i,t)).length===0?{result:"UNKNOWN",message:"Webhook files found but no Stripe references"}:n.length>0?{result:"FAIL",message:"Webhook handler uses req.json()/req.body instead of raw body \u2014 signature verification will fail",evidence:n.slice(0,3).map(a=>`${a.file}:${a.line} \u2192 ${a.content.substring(0,120)}`)}:{result:"FAIL",message:"Stripe webhook handler found but no raw body preservation detected",evidence:t.map(a=>`${a} \u2014 missing request.text() or bodyParser: false`)}}};var Ze={id:"BIL-04",name:"Idempotent webhook processing",module:"billing",layer:"L2",priority:"P1",description:"Stripe retries webhooks by design. Without idempotency checks, duplicate credits and double subscriptions occur.",fixCost:100,fixSize:"M",async run(e){let t=e.files.filter(n=>/webhook/i.test(n));if(t.length===0)return{result:"UNKNOWN",message:"No webhook handler files found"};let s=/event\.id|event_id|idempoten|UNIQUE.*event|duplicate.*check|processed.*events|already.*processed/i,i=await e.grepFiles(s,t);return(await e.grepFiles(/stripe|constructEvent/i,t)).length===0?{result:"UNKNOWN",message:"No Stripe webhook handler found"}:i.length>0?{result:"PASS",message:"Webhook idempotency check detected"}:{result:"FAIL",message:"Stripe webhook handler has no idempotency check \u2014 duplicate processing possible",evidence:["No event.id tracking or duplicate check in webhook handler"]}}};var et={id:"BIL-05",name:"Subscription state machine",module:"billing",layer:"L2",priority:"P1",description:"Stripe has 8 subscription states. Handling only active/canceled causes access issues for past_due, trialing, incomplete, unpaid.",fixCost:100,fixSize:"M",async run(e){let t=/past_due|trialing|incomplete|unpaid/i,s=/active|canceled/i,i=await e.grepFiles(t),r=await e.grepFiles(s);return i.length>=2?{result:"PASS",message:"Multiple subscription states handled beyond active/canceled"}:r.length>0&&i.length===0?{result:"FAIL",message:"Only active/canceled states handled \u2014 missing past_due, trialing, incomplete, unpaid",evidence:["Subscription state machine is incomplete \u2014 users may lose access incorrectly"]}:(await e.grepFiles(/subscription|stripe/i)).length===0?{result:"UNKNOWN",message:"No subscription handling found"}:{result:"FAIL",message:"Subscription code found but no explicit state handling",evidence:["Missing: past_due, trialing, incomplete, unpaid state handling"]}}};var tt={id:"BIL-06",name:"Entitlement/plan limit checking",module:"billing",layer:"L2",priority:"P1",description:"Stripe doesn't track usage limits. Without app-side entitlement checks, free users access paid features.",fixCost:100,fixSize:"M",async run(e){let t=/checkPlan|checkEntitle|planLimit|featureGate|subscription.*check|plan.*limit|canAccess|hasFeature|isSubscribed|entitlement/i;return(await e.grepFiles(t)).length>0?{result:"PASS",message:"Entitlement/plan limit checking detected"}:(await e.grepFiles(/subscription|stripe.*plan|pricing/i)).length===0?{result:"UNKNOWN",message:"No subscription/pricing code found"}:{result:"FAIL",message:"Subscription code exists but no entitlement/plan limit checks found",evidence:["Missing: checkPlanLimit, checkEntitlement, featureGate, or equivalent"]}}};var it={id:"BIL-07",name:"Customer \u2194 User sync",module:"billing",layer:"L2",priority:"P1",description:"Every user must map to exactly one Stripe customer. Missing sync = orphaned customers, broken subscription lookups.",fixCost:100,fixSize:"M",async run(e){let t=/stripe_customer_id|stripeCustomerId|customer_id.*stripe/i,s=/customers\.create|createCustomer|stripe.*customer/i,i=e.files.filter($),r=await e.grepFiles(t,i.length>0?i:void 0),n=await e.grepFiles(s);return r.length>0?{result:"PASS",message:"stripe_customer_id found in database schema \u2014 user-customer sync exists"}:n.length>0?{result:"PASS",message:"Stripe customer creation found in code"}:(await e.grepFiles(/stripe/i)).length>0?{result:"FAIL",message:"Stripe is used but no stripe_customer_id in schema and no customer creation logic",evidence:["Missing: stripe_customer_id column in users/profiles table"]}:{result:"UNKNOWN",message:"No Stripe references found"}}};var st={id:"BIL-08",name:"Webhook returns 200 for unknown events",module:"billing",layer:"L2",priority:"P1",description:"Returning 400/500 for unhandled events triggers Stripe retry loops and eventual endpoint deactivation.",fixCost:100,fixSize:"M",async run(e){let t=e.files.filter(o=>/webhook/i.test(o));if(t.length===0)return{result:"UNKNOWN",message:"No webhook handler files found"};let s=/else\s*\{[\s\S]*?(?:return.*(?:4\d\d|5\d\d)|throw|NextResponse.*(?:4\d\d|5\d\d))/i,i=/default\s*:[\s\S]*?(?:200|ok|return\s+new\s+Response)/i,r=await e.grepFiles(s,t),n=await e.grepFiles(i,t);return r.length>0?{result:"FAIL",message:"Webhook handler returns error for unknown events \u2014 will trigger Stripe retry loop",evidence:r.slice(0,3).map(o=>`${o.file}:${o.line} \u2192 ${o.content.substring(0,120)}`)}:n.length>0?{result:"PASS",message:"Webhook handler returns 200 for unhandled events"}:{result:"UNKNOWN",message:"Could not determine webhook default response behavior"}}};var nt={id:"BIL-09",name:"No client-side billing state as source of truth",module:"billing",layer:"L2",priority:"P1",description:"Subscription state in localStorage/React state can be manipulated. Server must be source of truth.",fixCost:100,fixSize:"M",async run(e){let t=[],s=/localStorage.*(?:subscription|plan|billing)|sessionStorage.*(?:subscription|plan)|useState.*(?:subscription|isPro|isPaid|plan)/i,r=(await e.grepFiles(s)).filter(p=>_(p.file));for(let p of r)t.push(`${p.file}:${p.line} \u2192 ${p.content.substring(0,120)}`);let n=/\.from\s*\(\s*['"](?:subscriptions|plans|credits|billing)['"]\s*\)\s*\.\s*select/i,a=(await e.grepFiles(n)).filter(p=>_(p.file));for(let p of a)t.push(`${p.file}:${p.line} \u2192 ${p.content.substring(0,120)}`);let c=/(?:useSubscription|useBilling|usePlan)\b/,l=e.files.filter(p=>c.test(p)||/hook/i.test(p));for(let p of l){let f;try{f=await e.readFile(p)}catch{continue}let h=/from\s*\(\s*['"](?:subscriptions|plans|credits)['"]\s*\)/.test(f),y=/isPro|isPaid|plan\s*===|plan\s*!==|plan\s*==|plan\s*!=/.test(f);h&&y&&t.push(`${p} \u2192 Client hook reads billing table and derives plan/isPro locally`)}return t.length>0?{result:"FAIL",message:`Client-side billing state as source of truth (${t.length} location${t.length>1?"s":""})`,evidence:t.slice(0,5)}:{result:"PASS",message:"No client-side billing state as source of truth detected"}}};var rt={id:"BIL-10",name:"Reconciliation mechanism",module:"billing",layer:"L2",priority:"P2",description:"Even good webhook handling drifts 1-2x/month. A reconciliation job comparing Stripe vs DB prevents silent revenue loss.",fixCost:100,fixSize:"M",async run(e){let t=/reconcil|sync.*stripe|stripe.*sync|cron.*billing|billing.*cron|verify.*subscription|subscription.*verify/i,s=e.files.filter(n=>/reconcil/i.test(n)),i=await e.grepFiles(t);return s.length>0||i.length>0?{result:"PASS",message:"Reconciliation mechanism detected"}:(await e.grepFiles(/stripe/i)).length===0?{result:"UNKNOWN",message:"No Stripe references found"}:{result:"FAIL",message:"No billing reconciliation mechanism found",evidence:["Missing: reconciliation script/job to compare Stripe state vs DB state"]}}};var ot={id:"BIL-11",name:"Cancellation handling",module:"billing",layer:"L2",priority:"P1",description:"Explicit cancel flow: server-side cancellation + webhook processing + DB update + access revocation.",fixCost:100,fixSize:"M",async run(e){let t=/cancel.*subscription|subscription.*cancel|customer\.subscription\.deleted|cancelAt|cancel_at_period_end/i,s=await e.grepFiles(t);return s.length>=2?{result:"PASS",message:"Cancellation handling detected in multiple locations"}:s.length===1?{result:"PASS",message:"Cancellation handling detected"}:(await e.grepFiles(/subscription|stripe/i)).length===0?{result:"UNKNOWN",message:"No subscription code found"}:{result:"FAIL",message:"No cancellation handling found \u2014 users may retain access after canceling or be charged after canceling",evidence:["Missing: cancel subscription flow, subscription.deleted webhook handler"]}}};var at={id:"BIL-12",name:"Stripe env vars configured",module:"billing",layer:"L2",priority:"P2",description:"Missing Stripe env vars = billing won't work in production or keys get hardcoded.",fixCost:100,fixSize:"M",async run(e){let t=e.files.filter(E);if(t.length===0)return{result:"UNKNOWN",message:"No .env files found"};let s=["STRIPE_SECRET_KEY","STRIPE_WEBHOOK_SECRET","STRIPE_PUBLISHABLE_KEY"],i=[],r=[];for(let o of t){let a;try{a=await e.readFile(o)}catch{continue}for(let c of s)a.includes(c)&&!i.includes(c)&&i.push(c)}for(let o of s)i.includes(o)||r.push(o);return(await e.grepFiles(/stripe/i)).length===0?{result:"UNKNOWN",message:"No Stripe references found"}:r.length>0?{result:"FAIL",message:`Missing Stripe env vars: ${r.join(", ")}`,evidence:r.map(o=>`${o} not found in any .env file`)}:{result:"PASS",message:"All required Stripe env vars configured"}}};var ct={id:"BIL-13",name:"Error handling in payment flows",module:"billing",layer:"L2",priority:"P2",description:"Missing error handling around Stripe API calls = white screen on payment failure, no diagnostics.",fixCost:100,fixSize:"M",async run(e){let t=/stripe\.\w+\.\w+\(|checkout\.sessions\.create|subscriptions\.create|customers\.create/i,s=/try\s*\{|\.catch\s*\(|catch\s*\(/,i=e.files.filter(o=>/api|route|action|server/i.test(o)),r=await e.grepFiles(t,i.length>0?i:void 0);return r.length===0?{result:"UNKNOWN",message:"No Stripe API calls found"}:(await e.grepFiles(s,i.length>0?i:void 0)).length>0?{result:"PASS",message:"Error handling found in payment flow files"}:{result:"FAIL",message:"Stripe API calls found without error handling",evidence:r.slice(0,3).map(o=>`${o.file}:${o.line} \u2192 ${o.content.substring(0,120)}`)}}};var lt={id:"BIL-14",name:"Checkout flow is server-initiated",module:"billing",layer:"L2",priority:"P0",description:"Checkout session must be created on the server. Client-side creation requires secret key in browser.",fixCost:100,fixSize:"M",async run(e){let t=/checkout\.sessions\.create|createCheckoutSession/,s=await e.grepFiles(t);if(s.length===0)return{result:"UNKNOWN",message:"No Checkout session creation found"};let i=s.filter(n=>_(n.file)&&!w(n.file));for(let n of i){let o;try{o=await e.readFile(n.file)}catch{continue}if(/["']use client["']/.test(o))return{result:"FAIL",message:"Checkout session created in client component \u2014 secret key required in browser",evidence:[`${n.file}:${n.line} \u2192 ${n.content.substring(0,120)}`]}}return s.filter(n=>w(n.file)).length>0?{result:"PASS",message:"Checkout session created server-side"}:{result:"PASS",message:"Checkout session creation found (not in client component)"}}};var ut={id:"BIL-15",name:"Stripe Price ID tampering prevention",module:"billing",layer:"L2",priority:"P0",description:"Price ID from client request must be validated server-side against an allowlist. Client can send any price_id.",fixCost:100,fixSize:"M",async run(e){let t=/checkout\.sessions\.create|createCheckoutSession/;if((await e.grepFiles(t)).length===0)return{result:"UNKNOWN",message:"No Stripe Checkout session creation found"};let i=/req\.body.*price|req\.json.*price|request\.json.*price|body\.price|priceId|price_id/i,r=/allowedPrices|ALLOWED_PRICES|validPrices|PRICE_IDS|priceWhitelist|priceLookup|PLANS\[|plans\[|PRICES\[|prices\./i,n=await e.grepFiles(i),o=await e.grepFiles(r);return n.length>0&&o.length===0?{result:"FAIL",message:"Price ID accepted from client without server-side validation",evidence:n.slice(0,3).map(a=>`${a.file}:${a.line} \u2192 ${a.content.substring(0,120)}`)}:o.length>0?{result:"PASS",message:"Price ID validation/allowlist detected"}:{result:"PASS",message:"Checkout session creation found with no client price input detected"}}};var dt={id:"BIL-16",name:"Never fulfill on success_url",module:"billing",layer:"L2",priority:"P0",description:"Fulfillment (DB writes, access grants) must happen via webhook, not on the success redirect page.",fixCost:100,fixSize:"M",async run(e){let t=[/success/i,/thank/i,/payment.*confirm/i],s=e.files.filter(n=>(n.includes("page.")||n.includes("index."))&&t.some(a=>a.test(n)));if(s.length===0)return{result:"UNKNOWN",message:"No success/thank-you pages found"};let i=/\.insert\(|\.update\(|\.upsert\(|createSubscription|grantAccess|activateUser|fulfillOrder|UPDATE.*SET|INSERT.*INTO/i,r=[];for(let n of s){let o;try{o=await e.readFile(n)}catch{continue}let a=o.split(`
|
|
6
|
+
`);for(let c=0;c<a.length;c++)i.test(a[c])&&r.push({file:n,line:c+1,content:a[c].trim()})}return r.length>0?{result:"FAIL",message:`Fulfillment logic found in success page (${r.length} location${r.length>1?"s":""}) \u2014 must use webhook instead`,evidence:r.map(n=>`${n.file}:${n.line} \u2192 ${n.content.substring(0,120)}`)}:{result:"PASS",message:"Success pages contain no fulfillment logic"}}};var pt={id:"BIL-17",name:"PCI raw card data safety",module:"billing",layer:"L2",priority:"P0",description:"Raw card numbers, CVV, expiration must never touch your server. Use Stripe Elements or Checkout.",fixCost:100,fixSize:"M",async run(e){let t=/card\s*\[\s*number\s*\]|cardNumber|card_number|cvv|cvc|expiry.*month|card.*expir/i,s=/<input[^>]*(?:card|cvv|cvc|expir)/i,i=/CardElement|PaymentElement|useStripe|useElements|@stripe\/react-stripe-js|stripe\.elements/i,r=await e.grepFiles(t),n=await e.grepFiles(s),o=[...r,...n].filter(c=>!(c.content.trimStart().startsWith("//")||c.content.trimStart().startsWith("*")||c.content.trimStart().startsWith("#")||c.file.includes("test")||c.file.includes("spec")||c.file.includes(".md"))),a=await e.grepFiles(i);return o.length>0&&a.length===0?{result:"FAIL",message:`Raw card data handling detected without Stripe Elements (${o.length} location${o.length>1?"s":""})`,evidence:o.slice(0,5).map(c=>`${c.file}:${c.line} \u2192 ${c.content.substring(0,120)}`)}:a.length>0?{result:"PASS",message:"Stripe Elements/Checkout detected \u2014 card data handled by Stripe"}:{result:"UNKNOWN",message:"No card data handling or Stripe Elements detected"}}};var ft={id:"BIL-18",name:"Refund/dispute handling",module:"billing",layer:"L2",priority:"P1",description:"Webhook must handle charge.refunded and charge.dispute.created to revoke access and update billing state.",fixCost:100,fixSize:"M",async run(e){let t=e.files.filter(a=>/webhook/i.test(a));if(t.length===0)return{result:"UNKNOWN",message:"No webhook handler files found"};let s=/charge\.refunded|refund/i,i=/charge\.dispute|dispute/i,r=await e.grepFiles(s,t),n=await e.grepFiles(i,t);return r.length>0&&n.length>0?{result:"PASS",message:"Both refund and dispute handling detected in webhook"}:r.length>0||n.length>0?{result:"PASS",message:"Partial refund/dispute handling detected"}:(await e.grepFiles(/stripe/i,t)).length===0?{result:"UNKNOWN",message:"No Stripe webhook handler found"}:{result:"FAIL",message:"No refund or dispute handling in webhook \u2014 access may persist after refund",evidence:["Missing: charge.refunded and charge.dispute.created event handlers"]}}};var mt={id:"BIL-19",name:"Stripe API version pinning",module:"billing",layer:"L2",priority:"P2",description:"Pin Stripe API version to avoid breaking changes from automatic upgrades.",fixCost:100,fixSize:"M",async run(e){let t=/apiVersion|api_version/i;if((await e.grepFiles(t)).length>0)return{result:"PASS",message:"Stripe API version pinning detected"};let i=await e.grepFiles(/new\s+Stripe\s*\(|Stripe\s*\(/i);return i.length===0?{result:"UNKNOWN",message:"No Stripe initialization found"}:{result:"FAIL",message:"Stripe initialized without explicit API version \u2014 may break on Stripe upgrades",evidence:i.slice(0,2).map(r=>`${r.file}:${r.line} \u2192 ${r.content.substring(0,120)}`)}}};var gt={id:"BIL-20",name:"Portal session auth",module:"billing",layer:"L2",priority:"P1",description:"Billing portal session must use customer ID from authenticated user, not from client request.",fixCost:100,fixSize:"M",async run(e){let t=/billingPortal|billing_portal|customer_portal/i,s=await e.grepFiles(t);if(s.length===0)return{result:"UNKNOWN",message:"No billing portal usage found"};let i=/getUser|getSession|auth\(\)|requireAuth/i,r=[...new Set(s.map(o=>o.file))];return(await e.grepFiles(i,r)).length>0?{result:"PASS",message:"Billing portal session created with authenticated customer ID"}:{result:"FAIL",message:"Billing portal session created without verifying authenticated user",evidence:s.slice(0,3).map(o=>`${o.file}:${o.line} \u2192 ${o.content.substring(0,120)}`)}}};var ht={id:"BIL-21",name:"Webhook event coverage",module:"billing",layer:"L2",priority:"P1",description:"Webhook handler must process at minimum 3 core subscription events. Missing events = state drift.",fixCost:100,fixSize:"M",async run(e){let t=e.files.filter(n=>/webhook/i.test(n));if(t.length===0)return{result:"UNKNOWN",message:"No webhook handler files found"};let s=[{pattern:/checkout\.session\.completed/i,name:"checkout.session.completed"},{pattern:/customer\.subscription\.updated/i,name:"customer.subscription.updated"},{pattern:/customer\.subscription\.deleted/i,name:"customer.subscription.deleted"},{pattern:/invoice\.payment_succeeded/i,name:"invoice.payment_succeeded"},{pattern:/invoice\.payment_failed/i,name:"invoice.payment_failed"}],i=[],r=[];for(let n of s)(await e.grepFiles(n.pattern,t)).length>0?i.push(n.name):r.push(n.name);return i.length===0?(await e.grepFiles(/stripe/i,t)).length===0?{result:"UNKNOWN",message:"Webhook files found but no Stripe event handling"}:{result:"FAIL",message:"Stripe webhook handler found but no subscription events handled",evidence:["Missing: checkout.session.completed, subscription.updated, subscription.deleted"]}:i.length<3?{result:"FAIL",message:`Only ${i.length}/5 core webhook events handled`,evidence:[`Handled: ${i.join(", ")}`,`Missing: ${r.join(", ")}`]}:{result:"PASS",message:`${i.length}/5 core webhook events handled`}}};var St={id:"BIL-22",name:"Trial period handling",module:"billing",layer:"L2",priority:"P2",description:"If trials are offered, handle trial_will_end webhook and trial-to-paid conversion properly.",fixCost:100,fixSize:"M",async run(e){let t=/trial_period_days|trial_end|trialing|trial_will_end|trialDays/i,s=await e.grepFiles(t);return s.length>=2?{result:"PASS",message:"Trial period handling detected"}:s.length===1?{result:"PASS",message:"Trial reference found (verify trial_will_end webhook is handled)"}:(await e.grepFiles(/subscription|stripe/i)).length===0?{result:"UNKNOWN",message:"No subscription code found"}:{result:"N/A",message:"No trial period usage detected"}}};var yt={id:"BIL-23",name:"Card testing protection",module:"billing",layer:"L2",priority:"P2",description:"Rate limiting on payment endpoints prevents card testing attacks (automated validation of stolen cards).",fixCost:100,fixSize:"M",async run(e){let t=e.files.filter(n=>/checkout|payment|billing|subscribe/i.test(n));if(t.length===0)return{result:"UNKNOWN",message:"No payment endpoint files found"};let s=/rateLimit|rate.limit|throttle|limiter|too.many.requests|429|upstash/i;return(await e.grepFiles(s,t)).length>0?{result:"PASS",message:"Rate limiting detected on payment endpoints"}:(await e.grepFiles(s)).length>0?{result:"PASS",message:"Rate limiting detected in codebase (verify it covers payment endpoints)"}:{result:"FAIL",message:"No rate limiting found on payment endpoints \u2014 vulnerable to card testing",evidence:["Missing: rate limiting middleware on checkout/payment routes"]}}};var At={id:"BIL-24",name:"Metered billing usage reporting",module:"billing",layer:"L2",priority:"P2",description:"If using metered pricing, usage must be reported to Stripe. Missing reporting = customers never billed for usage.",fixCost:100,fixSize:"M",async run(e){let t=/metered|usage_type|usageRecord|usage.*report|createUsageRecord|meter/i,s=await e.grepFiles(t);return s.length>=2?{result:"PASS",message:"Metered billing usage reporting detected"}:s.length===1?{result:"PASS",message:"Metered billing reference found"}:{result:"N/A",message:"No metered billing usage detected"}}};var bt={id:"BIL-25",name:"Subscription table user-writable",module:"billing",layer:"L2",priority:"P0",description:"RLS policies on billing tables (subscriptions, plans, credits) must not allow authenticated users to UPDATE or INSERT plan/status columns. User-writable billing state enables subscription fraud (Free \u2192 Pro without payment).",fixCost:250,fixSize:"L",async run(e){let t=["subscriptions","plans","credits","billing"],s=/create\s+table\s+(?:public\.)?(subscriptions|plans|credits|billing)/i,i=e.files.filter(l=>/\.sql$/i.test(l)||/supabase.*migration/i.test(l)),r=await e.grepFiles(s,i.length>0?i:void 0);if(r.length===0){let l=await e.grepFiles(s);if(l.length===0)return{result:"PASS",message:"No billing tables (subscriptions/plans/credits) found"};r.push(...l)}let n=new Set(r.map(l=>{let p=l.content.match(/(?:public\.)?(subscriptions|plans|credits|billing)/i);return p?p[1].toLowerCase():null}).filter(Boolean)),o=[];for(let l of n)for(let p of i.length>0?i:e.files.filter(f=>/\.sql$/i.test(f))){let f;try{f=await e.readFile(p)}catch{continue}let h=new RegExp(`create\\s+policy[^;]*?on\\s+(?:public\\.)?${l}\\s+for\\s+(update|insert|all)`,"gis"),y;for(;(y=h.exec(f))!==null;){let A=Math.max(0,y.index),F=f.substring(A,f.indexOf(";",y.index)+1||A+500),d=/auth\.uid\(\)/i.test(F),b=/with\s+check\s*\([^)]*(?:false|service_role|is_admin)/i.test(F);if(d&&!b){let K=f.substring(0,y.index).split(`
|
|
7
|
+
`).length,Bi=y[1].toUpperCase();o.push(`${p}:${K} \u2192 RLS ${Bi} policy on ${l} allows user writes (auth.uid() = user_id)`)}}}if(o.length>0)return{result:"FAIL",message:`Billing table${n.size>1?"s":""} (${[...n].join(", ")}) allow user writes via RLS \u2014 subscription fraud risk`,evidence:o.slice(0,5)};let a=/\.from\s*\(\s*['"](?:subscriptions|plans|credits|billing)['"]\s*\)\s*\.\s*(?:update|insert|upsert)/i,c=await e.grepFiles(a);return c.length>0?{result:"FAIL",message:`Client-side writes to billing table detected (${c.length} location${c.length>1?"s":""})`,evidence:c.slice(0,3).map(l=>`${l.file}:${l.line} \u2192 ${l.content.substring(0,120)}`)}:{result:"PASS",message:"No user-writable billing tables detected"}}};var $t={id:"ADM-01",name:"Admin endpoints have server-side auth",module:"admin",layer:"L2",priority:"P0",description:"Every /admin or /api/admin endpoint must verify admin role server-side. Server Actions are public POST endpoints.",fixCost:100,fixSize:"M",async run(e){let t=e.files.filter(r=>/api\/admin|app\/api\/admin/i.test(r)&&(r.endsWith(".ts")||r.endsWith(".js")));if(t.length===0)return{result:"UNKNOWN",message:"No admin API route handlers found"};let s=/requireAdmin|role.*admin|isAdmin|checkPermission|requireRole|app_metadata.*admin|admin.*guard/i,i=[];for(let r of t){let n;try{n=await e.readFile(r)}catch{continue}s.test(n)||i.push(r)}return i.length>0?{result:"FAIL",message:`${i.length} admin API route${i.length>1?"s":""} without admin role verification`,evidence:i.slice(0,5).map(r=>`${r} \u2014 no admin role check detected`)}:{result:"PASS",message:`All ${t.length} admin API routes have admin role verification`}}};var vt={id:"ADM-02",name:"Admin routes not accessible without auth",module:"admin",layer:"L2",priority:"P0",description:"Admin pages and API must return 401/403 without valid admin token. Middleware alone is insufficient (CVE-2025-29927).",fixCost:100,fixSize:"M",async run(e){let t=e.files.filter(o=>/app\/admin\/.*page\.|pages\/admin\//i.test(o)||/(?:pages|views|routes)\/Admin[A-Z][^/]*\.(tsx|ts|jsx|js)$/.test(o)||/(?:pages|views|routes)\/admin-[^/]*\.(tsx|ts|jsx|js)$/.test(o));if(t.length===0)return{result:"UNKNOWN",message:"No admin pages found"};let s=/getUser|getSession|requireAuth|requireAdmin|redirect.*login|redirect.*auth|middleware|auth\(\)|checkPermission/i,i=[];for(let o of t){let a;try{a=await e.readFile(o)}catch{continue}s.test(a)||i.push(o)}let r=e.files.some(o=>/middleware\.(ts|js)$/i.test(o)),n=!1;if(r){let o=e.files.find(a=>/middleware\.(ts|js)$/i.test(a));if(o)try{let a=await e.readFile(o);n=/admin/i.test(a)}catch{}}return i.length>0&&!n?{result:"FAIL",message:`${i.length} admin page${i.length>1?"s":""} without auth guard (no middleware coverage either)`,evidence:i.slice(0,5).map(o=>`${o} \u2014 no auth guard detected`)}:i.length>0&&n?{result:"PASS",message:`Admin pages protected via middleware (${t.length} pages). Note: add per-route auth for defense-in-depth.`}:{result:"PASS",message:`All ${t.length} admin pages have auth guards`}}};var Ct={id:"ADM-03",name:"No client-side-only role checks",module:"admin",layer:"L2",priority:"P1",description:"Role checks in JSX ({isAdmin && <Panel/>}) without server-side enforcement are bypassable via dev tools.",fixCost:100,fixSize:"M",async run(e){let t=/isAdmin|is_admin|role.*admin|admin.*role/i,s=/requireAdmin|requireRole|checkPermission|app_metadata.*admin/i,r=(await e.grepFiles(t)).filter(o=>_(o.file)&&!w(o.file));return r.length===0?{result:"PASS",message:"No client-side-only role checks detected"}:(await e.grepFiles(s)).length>0?{result:"PASS",message:"Client-side role checks backed by server-side enforcement"}:{result:"FAIL",message:`Client-side role checks found without server-side enforcement (${r.length} location${r.length>1?"s":""})`,evidence:r.slice(0,3).map(o=>`${o.file}:${o.line} \u2192 ${o.content.substring(0,120)}`)}}};var _t={id:"ADM-04",name:"Audit log for admin actions",module:"admin",layer:"L2",priority:"P1",description:"Admin operations (delete user, change role, modify data) must be logged. Required for SOC 2 compliance.",fixCost:100,fixSize:"M",async run(e){let t=/audit.*log|admin.*log|action.*log|createAuditEntry|logAdminAction|activity.*log/i,s=/audit_log|admin_log|activity_log/i,i=await e.grepFiles(t),r=e.files.filter($),n=await e.grepFiles(s,r.length>0?r:void 0);return i.length>0||n.length>0?{result:"PASS",message:"Audit logging detected for admin actions"}:(await e.grepFiles(/admin/i)).length===0?{result:"UNKNOWN",message:"No admin functionality detected"}:{result:"FAIL",message:"No audit logging found for admin actions",evidence:["Missing: audit_log table or logAdminAction function"]}}};var kt={id:"ADM-05",name:"RBAC beyond binary admin",module:"admin",layer:"L2",priority:"P2",description:"Binary admin/user model means every admin can do everything. Granular roles limit blast radius.",fixCost:100,fixSize:"M",async run(e){let t=e.files.filter($),s=/RBAC|permission|capability|role.*check|roles.*table|user_roles/i,i=/isAdmin|is_admin|boolean.*admin/i,r=await e.grepFiles(s),n=await e.grepFiles(/role.*enum|roles.*table|permissions.*table/i,t.length>0?t:void 0);if(r.length>0||n.length>0)return{result:"PASS",message:"RBAC or granular permissions model detected"};let o=await e.grepFiles(i);return o.length>0?{result:"FAIL",message:"Binary admin model (isAdmin boolean) \u2014 no granular permissions",evidence:o.slice(0,3).map(a=>`${a.file}:${a.line} \u2192 ${a.content.substring(0,120)}`)}:{result:"UNKNOWN",message:"No admin role model detected"}}};var It={id:"ADM-06",name:"Safe impersonation (if exists)",module:"admin",layer:"L2",priority:"P1",description:"If admin can 'login as user', the action must be logged with admin ID preserved. Replacing admin session = invisible abuse.",fixCost:100,fixSize:"M",async run(e){let t=/impersonat|loginAs|actAs|switchUser|act_as|login_as/i,s=await e.grepFiles(t);if(s.length===0)return{result:"N/A",message:"No impersonation functionality detected"};let i=/audit|log.*admin|admin.*log|logAction|track/i;return(await e.grepFiles(i)).length>0?{result:"PASS",message:"Impersonation exists with audit logging"}:{result:"FAIL",message:"Impersonation found without audit logging \u2014 admin actions will appear as user actions",evidence:s.slice(0,3).map(n=>`${n.file}:${n.line} \u2192 ${n.content.substring(0,120)}`)}}};var Pt={id:"ADM-07",name:"UUIDs not sequential IDs",module:"admin",layer:"L2",priority:"P1",description:"Sequential integer IDs enable enumeration attacks (IDOR). Use UUIDs for all user-facing primary keys.",fixCost:100,fixSize:"M",async run(e){let t=e.files.filter($);if(t.length===0)return{result:"UNKNOWN",message:"No SQL migration files found"};let s=/(?:SERIAL|BIGSERIAL|INTEGER\s+PRIMARY\s+KEY\s+(?:AUTO_INCREMENT|GENERATED))/i,i=/UUID\s+(?:PRIMARY\s+KEY\s+)?DEFAULT\s+(?:gen_random_uuid|uuid_generate)/i,r=await e.grepFiles(s,t),n=await e.grepFiles(i,t),o=r.filter(a=>!/migration|schema_migration|_prisma/i.test(a.content));return o.length>0&&n.length===0?{result:"FAIL",message:`Sequential IDs found without UUID usage (${o.length} table${o.length>1?"s":""})`,evidence:o.slice(0,5).map(a=>`${a.file}:${a.line} \u2192 ${a.content.substring(0,120)}`)}:n.length>0?{result:"PASS",message:"UUID primary keys detected in schema"}:{result:"UNKNOWN",message:"No primary key definitions found in migrations"}}};var wt={id:"ADM-08",name:"No unprotected debug/admin routes",module:"admin",layer:"L2",priority:"P0",description:"Debug, internal, and admin routes must have auth guards. Hidden URLs are not security.",fixCost:100,fixSize:"M",async run(e){let t=[/app\/admin\//,/pages\/admin\//,/app\/internal\//,/app\/debug\//,/api\/admin\//,/api\/debug\//,/api\/internal\//,/api\/graphql/,/api\/seed/,/api\/reset/,/api\/test/,/supabase\/functions\/.*(?:seed|admin|debug|reset|test)/],s=e.files.filter(n=>t.some(o=>o.test(n)));if(s.length===0)return{result:"PASS",message:"No admin/debug/internal routes detected"};let i=/requireAdmin|requireAuth|getUser|getSession|verifyToken|isAdmin|checkPermission|requireRole|auth\(\)|middleware/i,r=[];for(let n of s){if(!n.endsWith(".ts")&&!n.endsWith(".tsx")&&!n.endsWith(".js")&&!n.endsWith(".jsx"))continue;let o;try{o=await e.readFile(n)}catch{continue}i.test(o)||r.push(n)}return r.length>0?{result:"FAIL",message:`${r.length} admin/debug route${r.length>1?"s":""} without auth check`,evidence:r.map(n=>`${n} \u2014 no auth guard detected`)}:{result:"PASS",message:`All ${s.length} admin/debug routes have auth checks`}}};var Lt={id:"ADM-09",name:"Destructive ops require extra authorization",module:"admin",layer:"L2",priority:"P2",description:"Bulk delete, data wipe, billing override should require confirmation step or elevated permission.",fixCost:100,fixSize:"M",async run(e){let t=/deleteAll|bulkDelete|wipeData|truncate|DROP\s+TABLE|removeAll|destroyAll|purge/i,s=/confirm|double.*auth|re.?authenticate|verification.*step|two.*step/i,r=(await e.grepFiles(t)).filter(o=>/admin/i.test(o.file));return r.length===0?{result:"N/A",message:"No bulk destructive admin operations detected"}:(await e.grepFiles(s)).length>0?{result:"PASS",message:"Confirmation/extra auth detected for destructive operations"}:{result:"FAIL",message:"Destructive admin operations without extra authorization",evidence:r.slice(0,3).map(o=>`${o.file}:${o.line} \u2192 ${o.content.substring(0,120)}`)}}};var Nt={id:"ADM-10",name:"Admin code separated from user app",module:"admin",layer:"L2",priority:"P2",description:"Admin code in separate directories prevents user app bugs from affecting admin, and vice versa.",fixCost:100,fixSize:"M",async run(e){let t=e.files.some(i=>/^app\/admin\/|^domains\/admin\/|^src\/admin\//i.test(i)),s=e.files.filter(i=>/admin/i.test(i)&&!/(app|domains|src)\/admin\//i.test(i));return t?{result:"PASS",message:"Admin code in dedicated directory (app/admin/ or domains/admin/)"}:s.length>0?{result:"FAIL",message:"Admin code scattered across user-facing directories",evidence:s.slice(0,5).map(i=>i)}:{result:"N/A",message:"No admin code detected"}}};var Rt={id:"ADM-11",name:"No hardcoded admin credentials",module:"admin",layer:"L2",priority:"P0",description:"Hardcoded admin passwords, tokens, or emails in source code = anyone with repo access is admin",fixCost:100,fixSize:"M",async run(e){let t=[/admin.*password\s*[:=]\s*["']/i,/admin.*token\s*[:=]\s*["']/i,/admin.*secret\s*[:=]\s*["']/i,/password\s*[:=]\s*["'](?:admin|password|123456|secret)/i,/DEFAULT_ADMIN_PASSWORD/i,/ADMIN_PASSWORD\s*[:=]/i,/seed.*admin.*password/i],s=[];for(let r of t){let o=(await e.grepFiles(r)).filter(a=>!(E(a.file)||a.content.trimStart().startsWith("//")||a.content.trimStart().startsWith("#")||a.content.trimStart().startsWith("*")||a.file.includes("test")||a.file.includes("spec")));s.push(...o)}let i=[...new Map(s.map(r=>[`${r.file}:${r.line}`,r])).values()];return i.length>0?{result:"FAIL",message:`Hardcoded admin credentials found (${i.length} location${i.length>1?"s":""})`,evidence:i.map(r=>`${r.file}:${r.line} \u2192 ${r.content.substring(0,80).replace(/["'][^"']{8,}["']/g,'"[REDACTED]"')}`)}:{result:"PASS",message:"No hardcoded admin credentials found in source code"}}};var Et={id:"ADM-12",name:"Admin error handling",module:"admin",layer:"L2",priority:"P2",description:"Admin API errors must not leak stack traces, SQL queries, or internal schema details.",fixCost:100,fixSize:"M",async run(e){let t=e.files.filter(o=>/admin.*route|api.*admin/i.test(o));if(t.length===0)return{result:"UNKNOWN",message:"No admin API routes found"};let s=/try\s*\{|\.catch\s*\(|catch\s*\(/,i=/stack|trace|SQL|query.*error|\.message/i,r=!1,n=[];for(let o of t){let a;try{a=await e.readFile(o)}catch{continue}s.test(a)&&(r=!0);let c=a.split(`
|
|
8
|
+
`);for(let l=0;l<c.length;l++)/error\.stack|error\.message|err\.stack|JSON\.stringify.*error/i.test(c[l])&&n.push({file:o,line:l+1,content:c[l].trim()})}return n.length>0?{result:"FAIL",message:"Admin API may leak error details (stack traces, error messages)",evidence:n.slice(0,3).map(o=>`${o.file}:${o.line} \u2192 ${o.content.substring(0,120)}`)}:r?{result:"PASS",message:"Admin error handling present without obvious information leaks"}:{result:"FAIL",message:"No error handling in admin API routes",evidence:t.slice(0,3).map(o=>`${o} \u2014 no try/catch`)}}};var Ft={id:"ADM-13",name:"MFA requirement for admin roles",module:"admin",layer:"L2",priority:"P0",description:"Admin auth must require MFA/AAL2. Compromised admin password without MFA = total takeover.",fixCost:100,fixSize:"M",async run(e){let t=/mfa|aal2|getAuthenticatorAssuranceLevel|totp|authenticator|multi.?factor|two.?factor/i,s=e.files.filter(n=>/admin/i.test(n));if(s.length===0)return{result:"UNKNOWN",message:"No admin files found"};let i=await e.grepFiles(t,s),r=await e.grepFiles(t);return i.length>0?{result:"PASS",message:"MFA/AAL2 enforcement detected in admin code"}:r.length>0?{result:"PASS",message:"MFA implementation detected in codebase (verify it covers admin routes)"}:{result:"FAIL",message:"No MFA/AAL2 enforcement found for admin roles",evidence:["No references to mfa, aal2, totp, or authenticator in admin code"]}}};var Tt={id:"ADM-14",name:"Rate limiting on admin endpoints",module:"admin",layer:"L2",priority:"P1",description:"Admin API endpoints without rate limiting are vulnerable to brute force and DoS attacks.",fixCost:100,fixSize:"M",async run(e){let t=e.files.filter(n=>/api.*admin|admin.*route/i.test(n));if(t.length===0)return{result:"UNKNOWN",message:"No admin API endpoints found"};let s=/rateLimit|rate.limit|throttle|limiter|upstash|redis.*limit|too.many.requests|429/i;return(await e.grepFiles(s,t)).length>0?{result:"PASS",message:"Rate limiting detected on admin endpoints"}:(await e.grepFiles(s)).length>0?{result:"PASS",message:"Rate limiting detected in codebase (verify it covers admin endpoints)"}:{result:"FAIL",message:"No rate limiting found on admin endpoints",evidence:["Missing: rate limiting middleware on admin API routes"]}}};var xt={id:"ADM-16",name:"Separate admin session timeouts",module:"admin",layer:"L2",priority:"P1",description:"Admin sessions should have shorter timeouts than user sessions to limit session hijacking window.",fixCost:100,fixSize:"M",async run(e){let t=/admin.*timeout|admin.*expir|admin.*maxAge|admin.*session.*duration|session.*admin.*short/i;return(await e.grepFiles(t)).length>0?{result:"PASS",message:"Differentiated admin session timeout detected"}:(await e.grepFiles(/admin/i)).length===0?{result:"UNKNOWN",message:"No admin functionality detected"}:{result:"FAIL",message:"No separate admin session timeout \u2014 admin sessions use same duration as user sessions",evidence:["Missing: shorter session timeout for admin roles"]}}};var Dt={id:"ADM-18",name:"Admin action notification/alerting",module:"admin",layer:"L2",priority:"P2",description:"Critical admin actions should trigger notifications (Slack, email) so compromised admin accounts are detected quickly.",fixCost:100,fixSize:"M",async run(e){let t=/slack.*webhook|sendSlack|sendEmail.*admin|notify.*admin|alert.*admin|admin.*notify|admin.*alert|webhook.*notify/i,s=e.files.filter(n=>/admin/i.test(n));if(s.length===0)return{result:"UNKNOWN",message:"No admin functionality detected"};let i=await e.grepFiles(t,s),r=await e.grepFiles(t);return i.length>0||r.length>0?{result:"PASS",message:"Admin action notifications/alerting detected"}:{result:"FAIL",message:"No notification/alerting for admin actions \u2014 compromised admin goes undetected",evidence:["Missing: Slack webhook, email alert, or notification for critical admin actions"]}}};var Ut={id:"ADM-19",name:"CSRF protection for admin mutations",module:"admin",layer:"L2",priority:"P1",description:"Admin mutation endpoints need CSRF protection. If victim is admin, CSRF compromises entire app.",fixCost:100,fixSize:"M",async run(e){let t=e.files.filter(n=>/admin.*route\.(ts|js)$|api.*admin/i.test(n));if(t.length===0)return{result:"UNKNOWN",message:"No admin route handlers found"};let s=/csrf|csurf|csrfToken|SameSite|x-csrf|anti.?forgery/i;return(await e.grepFiles(s)).length>0?{result:"PASS",message:"CSRF protection detected"}:(await e.grepFiles(/["']use server["']/i)).length>0?{result:"PASS",message:"Server Actions used (built-in CSRF protection in Next.js 14+)"}:{result:"FAIL",message:"No CSRF protection on admin mutation routes",evidence:t.slice(0,3).map(n=>`${n} \u2014 no CSRF token or SameSite cookie`)}}};var Mt={id:"ADM-20",name:"Data export controls",module:"admin",layer:"L2",priority:"P1",description:"Admin bulk export/download must have authorization and logging. Compromised admin can exfiltrate all user data.",fixCost:100,fixSize:"M",async run(e){let t=/export.*csv|downloadCSV|bulk.*fetch|dump.*data|export.*users|export.*data/i,s=await e.grepFiles(t);if(s.length===0)return{result:"N/A",message:"No bulk data export functionality detected"};let i=/requireAdmin|requireAuth|getUser|checkPermission/i,r=/audit|log.*export|log.*download/i,n=[...new Set(s.map(c=>c.file))],o=await e.grepFiles(i,n),a=await e.grepFiles(r,n);return o.length>0&&a.length>0?{result:"PASS",message:"Data export has auth and logging"}:o.length>0?{result:"PASS",message:"Data export has auth (consider adding export logging)"}:{result:"FAIL",message:"Data export without auth/logging \u2014 PII exfiltration risk",evidence:s.slice(0,3).map(c=>`${c.file}:${c.line} \u2192 ${c.content.substring(0,120)}`)}}};var Ot={id:"ADM-21",name:"Admin provisioning control",module:"admin",layer:"L2",priority:"P1",description:"Admin role must not be self-assignable. AI tools often generate 'isFirstUser \u2192 admin' or open role selection on signup.",fixCost:100,fixSize:"M",async run(e){let t=/role\s*=\s*['"]admin['"]|isFirstUser|first.*user.*admin|role.*select|self.*assign.*admin/i,s=e.files.filter(a=>/signup|register|onboard/i.test(a)),i=await e.grepFiles(t),n=[...s.length>0?await e.grepFiles(t,s):[],...i.filter(a=>/signup|register|onboard/i.test(a.file))],o=[...new Map(n.map(a=>[`${a.file}:${a.line}`,a])).values()];return o.length>0?{result:"FAIL",message:"Self-assign admin pattern detected in signup/onboarding flow",evidence:o.slice(0,3).map(a=>`${a.file}:${a.line} \u2192 ${a.content.substring(0,120)}`)}:{result:"PASS",message:"No self-assign admin pattern detected in signup flow"}}};var Wt={id:"ADM-22",name:"Edge Function authentication",module:"admin",layer:"L2",priority:"P0",description:"Supabase Edge Functions must verify the Authorization header or user session. Unprotected Edge Functions can be called by anyone with the public anon key, enabling privilege escalation (e.g. seed-admin).",fixCost:100,fixSize:"M",async run(e){let t=e.files.filter(r=>/supabase\/functions\/[^/]+\/index\.(ts|js)$/i.test(r));if(t.length===0)return{result:"PASS",message:"No Supabase Edge Functions found"};let s=/auth\.getUser|authorization.*header|req\.headers\.get\s*\(\s*['"]authorization['"]\)|verifyJWT|supabaseClient\.auth|createClient.*serviceRole/i,i=[];for(let r of t){let n=await e.readFile(r);s.test(n)||i.push(r)}return i.length>0?{result:"FAIL",message:`${i.length} Edge Function${i.length>1?"s":""} without auth verification`,evidence:i.slice(0,5).map(r=>`${r} \u2192 No Authorization header check or auth.getUser() call`)}:{result:"PASS",message:`All ${t.length} Edge Functions have auth checks`}}};var Hi=["/auth/","middleware.ts","middleware.js","/domains/auth/"],zi=/\b(getUser|createServerClient|supabase\.auth|currentUser|clerkMiddleware|useUser|getServerSession|useSession|authOptions|getSession)\b/;function qi(e){return Hi.some(t=>e.includes(t))}function Ki(e){return e.includes("node_modules")||e.includes(".next/")||e.includes("test")||e.includes("spec")||e.includes(".d.ts")||e.includes("types.ts")||e.includes("types.js")}var jt={id:"DRIFT-AUTH-01",name:"Auth logic spreading outside auth directory",module:"auth",layer:"L2",priority:"P1",category:"drift",description:"Auth patterns (getUser, createServerClient, etc.) found outside auth directories indicate module boundary drift.",fixCost:100,fixSize:"M",async run(e){let s=(await e.grepFiles(zi)).filter(i=>!(Ki(i.file)||i.content.trimStart().startsWith("//")||i.content.trimStart().startsWith("*")||qi(i.file)));return s.length>3?{result:"FAIL",message:`Auth logic found in ${s.length} locations outside auth directories \u2014 module boundary is leaking`,evidence:s.slice(0,5).map(i=>`${i.file}:${i.line} \u2192 ${i.content.substring(0,100)}`)}:s.length>0?{result:"PASS",message:`Minor auth references outside auth dir (${s.length}) \u2014 within acceptable range`}:{result:"PASS",message:"Auth logic is contained within auth directories"}}};var Gi=/\b(getUser|createServerClient|supabase\.auth|currentUser|clerkMiddleware|auth\(\)|getServerSession|NextAuth)\b/,Bt={id:"DRIFT-AUTH-02",name:"Duplicate auth middleware files",module:"auth",layer:"L2",priority:"P1",category:"drift",description:"Multiple middleware files with auth logic indicate fragmented auth \u2014 a common AI generation pattern.",fixCost:100,fixSize:"M",async run(e){let t=e.files.filter(i=>(i.includes("middleware.ts")||i.includes("middleware.js"))&&!i.includes("node_modules")&&!i.includes(".next/")),s=[];for(let i of t)try{let r=await e.readFile(i);Gi.test(r)&&s.push(i)}catch{continue}return s.length>1?{result:"FAIL",message:`${s.length} middleware files contain auth logic \u2014 auth should have a single entry point`,evidence:s.map(i=>i)}:s.length===1?{result:"PASS",message:`Single auth middleware found: ${s[0]}`}:{result:"UNKNOWN",message:"No middleware with auth logic found"}}};var Vi=/\b(getUser|createServerClient|supabase\.auth|currentUser|auth\(\)|getServerSession|requireAuth|withAuth|isAuthenticated)\b/,Ht={id:"DRIFT-AUTH-03",name:"New API routes without auth checks",module:"auth",layer:"L2",priority:"P0",category:"drift",description:"API route handlers without auth verification \u2014 any visitor can call unprotected endpoints.",fixCost:100,fixSize:"M",async run(e){let t=e.files.filter(i=>i.includes("/api/")&&(i.endsWith("route.ts")||i.endsWith("route.js"))&&!i.includes("node_modules")&&!i.includes(".next/")&&!i.includes("/api/auth/")&&!i.includes("/api/webhook")&&!i.includes("/api/health")&&!i.includes("/api/public"));if(t.length===0)return{result:"UNKNOWN",message:"No API route files found"};let s=[];for(let i of t)try{let r=await e.readFile(i);Vi.test(r)||s.push(i)}catch{continue}return s.length>0?{result:"FAIL",message:`${s.length} of ${t.length} API routes have no auth verification`,evidence:s.slice(0,5).map(i=>i)}:{result:"PASS",message:`All ${t.length} API routes have auth checks`}}};var Yi=/\b(isAdmin|role\s*===?\s*['"`]admin['"`]|user\.role|userRole|hasRole)\b/,Xi=/\b(getUser|createServerClient|supabase\.auth|currentUser|getServerSession|requireAdmin|checkAdmin)\b/,zt={id:"DRIFT-AUTH-04",name:"Client-side auth bypass \u2014 role check without server verification",module:"auth",layer:"L2",priority:"P1",category:"drift",description:"Role checks in JSX/client code without server-side verification \u2014 UI-only gates can be bypassed.",fixCost:100,fixSize:"M",async run(e){let s=(await e.grepFiles(Yi)).filter(n=>n.content.trimStart().startsWith("//")||n.file.includes("node_modules")||n.file.includes(".next/")?!1:_(n.file));if(s.length===0)return{result:"PASS",message:"No client-side role checks found"};let r=(await e.grepFiles(Xi,["**/api/**","**/server/**","**/actions.*"])).some(n=>!n.file.includes("node_modules"));return s.length>0&&!r?{result:"FAIL",message:`${s.length} client-side role checks found but no server-side role verification \u2014 admin UI can be bypassed`,evidence:s.slice(0,5).map(n=>`${n.file}:${n.line} \u2192 ${n.content.substring(0,100)}`)}:{result:"PASS",message:`Client-side role checks (${s.length}) backed by server-side verification`}}};var Qi=/localStorage\.(getItem|setItem|removeItem)\s*\(\s*['"`](token|jwt|session|auth|access_token|refresh_token|user)/,qt={id:"DRIFT-AUTH-05",name:"Auth tokens stored in localStorage",module:"auth",layer:"L2",priority:"P1",category:"drift",description:"localStorage for auth tokens is vulnerable to XSS \u2014 use httpOnly cookies or secure session management.",fixCost:100,fixSize:"M",async run(e){let s=(await e.grepFiles(Qi)).filter(i=>!(i.content.trimStart().startsWith("//")||i.file.includes("node_modules")||i.file.includes(".next/")));return s.length>0?{result:"FAIL",message:`Auth tokens stored in localStorage (${s.length} location${s.length>1?"s":""}) \u2014 vulnerable to XSS`,evidence:s.slice(0,5).map(i=>`${i.file}:${i.line} \u2192 ${i.content.substring(0,100)}`)}:{result:"PASS",message:"No localStorage auth token usage found"}}};var Ji=["/billing/","/stripe/","/webhook/","/payment/","/subscription/","/domains/billing/"],Zi=/\b(stripe|Stripe|createCheckout|price_id|priceId|subscription_id|subscriptionId|checkout\.sessions|customer\.subscriptions)\b/;function es(e){return Ji.some(t=>e.includes(t))}function ts(e){return e.includes("node_modules")||e.includes(".next/")||e.includes("test")||e.includes("spec")||e.includes(".d.ts")||e.includes("types.ts")||e.includes("types.js")||e.includes("package.json")||e.includes("package-lock")||e.includes("pnpm-lock")||e.includes(".env")}var Kt={id:"DRIFT-BIL-01",name:"Billing logic spreading outside billing directory",module:"billing",layer:"L2",priority:"P1",category:"drift",description:"Stripe/payment imports outside billing directories indicate module boundary drift.",fixCost:100,fixSize:"M",async run(e){let s=(await e.grepFiles(Zi)).filter(i=>!(ts(i.file)||i.content.trimStart().startsWith("//")||i.content.trimStart().startsWith("*")||i.content.trimStart().startsWith("#")||es(i.file)));return s.length>3?{result:"FAIL",message:`Billing/Stripe logic found in ${s.length} locations outside billing directories \u2014 module boundary is leaking`,evidence:s.slice(0,5).map(i=>`${i.file}:${i.line} \u2192 ${i.content.substring(0,100)}`)}:s.length>0?{result:"PASS",message:`Minor billing references outside billing dir (${s.length}) \u2014 within acceptable range`}:{result:"PASS",message:"Billing logic is contained within billing directories"}}};var is=/\b(webhook|stripe.*event|event\.type|constructEvent)\b/i,Gt={id:"DRIFT-BIL-02",name:"Duplicate webhook handler endpoints",module:"billing",layer:"L2",priority:"P0",category:"drift",description:"Multiple webhook endpoints create race conditions and duplicate event processing.",fixCost:100,fixSize:"M",async run(e){let t=e.files.filter(i=>!i.includes("node_modules")&&!i.includes(".next/")&&!i.includes("test")&&(i.includes("webhook")||i.includes("stripe"))&&(i.endsWith(".ts")||i.endsWith(".js")||i.endsWith(".py"))),s=[];for(let i of t)try{let r=await e.readFile(i);is.test(r)&&s.push(i)}catch{continue}return s.length>1?{result:"FAIL",message:`${s.length} webhook handler files found \u2014 should be exactly 1`,evidence:s}:s.length===1?{result:"PASS",message:`Single webhook handler: ${s[0]}`}:{result:"UNKNOWN",message:"No webhook handler files found \u2014 Stripe webhooks may not be configured"}}};var ss=/\b(subscription|plan|isPro|isFreeTier|isPaid|currentPlan|userPlan|hasSubscription)\b/,Vt={id:"DRIFT-BIL-03",name:"Client-side subscription check without server verification",module:"billing",layer:"L2",priority:"P1",category:"drift",description:"Plan/subscription checks in client code without server-side verification \u2014 users can bypass paywalls.",fixCost:100,fixSize:"M",async run(e){let s=(await e.grepFiles(ss)).filter(n=>n.content.trimStart().startsWith("//")||n.file.includes("node_modules")||n.file.includes(".next/")||n.file.includes("test")||n.file.includes("types")?!1:_(n.file)&&!n.file.includes("/api/"));if(s.length===0)return{result:"PASS",message:"No client-side subscription checks found"};let r=(await e.grepFiles(/\b(subscription|check.*plan|check.*limit|entitlement|getSubscription)\b/i,["**/api/**","**/server/**","**/actions.*","**/billing/**"])).some(n=>!n.file.includes("node_modules"));return s.length>0&&!r?{result:"FAIL",message:`${s.length} client-side subscription checks but no server-side plan verification \u2014 paywall can be bypassed`,evidence:s.slice(0,5).map(n=>`${n.file}:${n.line} \u2192 ${n.content.substring(0,100)}`)}:{result:"PASS",message:`Client-side subscription checks (${s.length}) backed by server-side verification`}}};var ns=/(?:price|amount|cost|fee)\s*[:=]\s*(\d{2,}(?:\.\d{2})?|['"`]\$?\d+)/i,rs=/(?:amount|price|unit_amount)\s*[:=]\s*\d{3,}/,Yt={id:"DRIFT-BIL-04",name:"Hardcoded prices instead of plan configuration",module:"billing",layer:"L2",priority:"P2",category:"drift",description:"Hardcoded dollar amounts or price values should come from plan configuration or Stripe metadata.",fixCost:100,fixSize:"M",async run(e){let t=await e.grepFiles(ns),s=await e.grepFiles(rs),r=[...t,...s].filter(n=>!(n.content.trimStart().startsWith("//")||n.content.trimStart().startsWith("*")||n.file.includes("node_modules")||n.file.includes(".next/")||n.file.includes("test")||n.file.includes("package.json")||n.file.includes(".lock")||n.file.includes(".css")||n.file.includes("migration")));return r.length>3?{result:"FAIL",message:`${r.length} hardcoded price/amount values found \u2014 prices should come from plan config or Stripe`,evidence:r.slice(0,5).map(n=>`${n.file}:${n.line} \u2192 ${n.content.substring(0,100)}`)}:r.length>0?{result:"PASS",message:`Minor hardcoded amounts found (${r.length}) \u2014 review recommended`}:{result:"PASS",message:"No hardcoded price values detected"}}};var os=/\b(canAccess|hasFeature|featureEnabled|isEnabled|featureFlag|canUse)\b/,as=/\b(checkLimit|checkSubscription|checkPlan|entitlement|billingGuard|requirePlan|checkQuota)\b/,Xt={id:"DRIFT-BIL-05",name:"Feature access without billing verification",module:"billing",layer:"L2",priority:"P2",category:"drift",description:"New features with access gates but no billing/entitlement check \u2014 free users can access paid features.",fixCost:100,fixSize:"M",async run(e){let s=(await e.grepFiles(os)).filter(n=>!(n.content.trimStart().startsWith("//")||n.file.includes("node_modules")||n.file.includes(".next/")||n.file.includes("test")));if(s.length===0)return{result:"UNKNOWN",message:"No feature gate patterns found \u2014 may not use feature flags"};let r=(await e.grepFiles(as)).some(n=>!n.file.includes("node_modules")&&!n.file.includes(".next/"));return s.length>0&&!r?{result:"FAIL",message:`${s.length} feature gate(s) found but no billing/entitlement verification \u2014 paid features may be accessible for free`,evidence:s.slice(0,5).map(n=>`${n.file}:${n.line} \u2192 ${n.content.substring(0,100)}`)}:{result:"PASS",message:`Feature gates (${s.length}) backed by billing verification`}}};var cs=["/admin/","/domains/admin/"],ls=/\b(isAdmin|requireAdmin|checkAdmin|adminGuard|role\s*===?\s*['"`]admin['"`]|user_role|superAdmin|isSuperAdmin)\b/;function us(e){return cs.some(t=>e.includes(t))}function ds(e){return e.includes("node_modules")||e.includes(".next/")||e.includes("test")||e.includes("spec")||e.includes(".d.ts")||e.includes("types.ts")||e.includes("types.js")||e.includes("migration")||e.includes(".sql")}var Qt={id:"DRIFT-ADM-01",name:"Admin logic spreading outside admin directory",module:"admin",layer:"L2",priority:"P1",category:"drift",description:"Admin permission patterns found outside admin directories indicate module boundary drift.",fixCost:100,fixSize:"M",async run(e){let s=(await e.grepFiles(ls)).filter(i=>!(ds(i.file)||i.content.trimStart().startsWith("//")||i.content.trimStart().startsWith("*")||us(i.file)));return s.length>3?{result:"FAIL",message:`Admin logic found in ${s.length} locations outside admin directories \u2014 module boundary is leaking`,evidence:s.slice(0,5).map(i=>`${i.file}:${i.line} \u2192 ${i.content.substring(0,100)}`)}:s.length>0?{result:"PASS",message:`Minor admin references outside admin dir (${s.length}) \u2014 within acceptable range`}:{result:"PASS",message:"Admin logic is contained within admin directories"}}};var ps=/\b(requireAdmin|checkAdmin|adminGuard|isAdmin|role\s*===?\s*['"`]admin['"`]|isSuperAdmin)\b/,Jt={id:"DRIFT-ADM-02",name:"New admin routes without permission guards",module:"admin",layer:"L2",priority:"P0",category:"drift",description:"Admin route handlers without permission checks \u2014 any authenticated user can access admin features.",fixCost:100,fixSize:"M",async run(e){let t=e.files.filter(i=>i.includes("/admin/")&&!i.includes("node_modules")&&!i.includes(".next/")&&!i.includes("test")&&(i.endsWith("route.ts")||i.endsWith("route.js")||i.endsWith("page.tsx")||i.endsWith("page.jsx")||i.endsWith("page.ts")));if(t.length===0)return{result:"UNKNOWN",message:"No admin route files found"};let s=[];for(let i of t)try{let r=await e.readFile(i);ps.test(r)||s.push(i)}catch{continue}return s.length>0?{result:"FAIL",message:`${s.length} of ${t.length} admin routes have no permission guard`,evidence:s.slice(0,5).map(i=>i)}:{result:"PASS",message:`All ${t.length} admin routes have permission guards`}}};var Zt=/\b(logAuditEvent|auditLog|audit_log|createAuditEntry|logAdminAction|insertAuditLog)\b/,ei=/\b(DELETE|PUT|PATCH|POST)\b/,ti={id:"DRIFT-ADM-03",name:"Admin mutations without audit logging",module:"admin",layer:"L2",priority:"P1",category:"drift",description:"Admin mutation endpoints (POST/PUT/DELETE) without audit log calls \u2014 no trail of admin actions.",fixCost:100,fixSize:"M",async run(e){let t=e.files.filter(i=>i.includes("/admin/")&&i.includes("/api/")&&!i.includes("node_modules")&&!i.includes(".next/")&&!i.includes("test")&&(i.endsWith("route.ts")||i.endsWith("route.js")));if(t.length===0){let i=e.files.filter(n=>n.includes("/admin/")&&!n.includes("node_modules")&&!n.includes(".next/")&&(n.includes("action")||n.includes("service")||n.includes("handler"))&&(n.endsWith(".ts")||n.endsWith(".js")||n.endsWith(".py")));if(i.length===0)return{result:"UNKNOWN",message:"No admin API routes or action files found"};let r=[];for(let n of i)try{let o=await e.readFile(n);ei.test(o)&&!Zt.test(o)&&r.push(n)}catch{continue}return r.length>0?{result:"FAIL",message:`${r.length} admin action file(s) with mutations but no audit logging`,evidence:r.slice(0,5)}:{result:"PASS",message:"Admin action files have audit logging"}}let s=[];for(let i of t)try{let r=await e.readFile(i);ei.test(r)&&!Zt.test(r)&&s.push(i)}catch{continue}return s.length>0?{result:"FAIL",message:`${s.length} of ${t.length} admin API routes have mutations without audit logging`,evidence:s.slice(0,5)}:{result:"PASS",message:`All ${t.length} admin API routes have audit logging`}}};var ii={id:"ENV-01",name:".env.example exists",module:"foundation",layer:"L3",priority:"P1",description:"Project should have .env.example documenting required environment variables",fixCost:50,fixSize:"S",async run(e){return e.files.some(i=>i===".env.example"||i.endsWith("/.env.example"))?{result:"PASS",message:".env.example found"}:e.files.some(i=>i===".env"||i===".env.local"||i.endsWith("/.env")||i.endsWith("/.env.local"))?{result:"FAIL",message:"Project uses .env files but has no .env.example for documentation",evidence:["Create .env.example listing all required vars (without secret values)"]}:{result:"UNKNOWN",message:"No .env files found \u2014 project may not use environment variables"}}};var si={id:"ENV-02",name:"No secrets in committed .env",module:"foundation",layer:"L3",priority:"P0",description:"Committed .env files must not contain actual secret values \u2014 only .env.example with placeholders",fixCost:100,fixSize:"M",async run(e){let t=e.files.filter(n=>{let o=n.split("/").pop()||"";return(o===".env"||o===".env.local"||o===".env.production")&&!o.includes("example")});if(t.length===0)return{result:"PASS",message:"No committed .env files found"};let s=/^[A-Z_]+=(?:sk_live_|sk_test_|whsec_|sbp_|eyJ|ghp_|gho_|AKIA|supabase.*service_role)/m,i=/^[A-Z_]+=.{20,}/m,r=[];for(let n of t){let o;try{o=await e.readFile(n)}catch{continue}if(s.test(o))r.push(`${n} \u2014 contains secret key patterns`);else if(i.test(o)){let a=o.split(`
|
|
9
|
+
`).filter(c=>/^[A-Z_]+=.{20,}/.test(c)&&!c.startsWith("#")&&!c.includes("your_")&&!c.includes("xxx"));a.length>2&&r.push(`${n} \u2014 appears to contain real values (${a.length} long values)`)}}return r.length>0?{result:"FAIL",message:`Secrets found in committed .env file${r.length>1?"s":""}`,evidence:r}:{result:"FAIL",message:`.env file${t.length>1?"s":""} committed to git \u2014 should be in .gitignore`,evidence:t.map(n=>`${n} \u2014 add to .gitignore`)}}};var ni={id:"CFG-01",name:"TypeScript strict mode",module:"foundation",layer:"L3",priority:"P1",description:"tsconfig.json should have strict: true to catch type errors at build time",fixCost:250,fixSize:"L",async run(e){let t;try{t=await e.readFile("tsconfig.json")}catch{return{result:"UNKNOWN",message:"No tsconfig.json found \u2014 project may not use TypeScript"}}return/"strict"\s*:\s*true/.test(t)?{result:"PASS",message:"TypeScript strict mode is enabled"}:/"strict"\s*:\s*false/.test(t)?{result:"FAIL",message:"TypeScript strict mode is explicitly disabled",evidence:['tsconfig.json \u2014 set "strict": true']}:{result:"FAIL",message:"TypeScript strict mode not configured (defaults to false)",evidence:['tsconfig.json \u2014 add "strict": true to compilerOptions']}}};var ri={id:"ERR-01",name:"Global error boundary exists",module:"foundation",layer:"L3",priority:"P1",description:"React apps should have a top-level error boundary to prevent full white-screen crashes",fixCost:100,fixSize:"M",async run(e){if(e.files.filter(i=>i.endsWith(".tsx")||i.endsWith(".jsx")).length===0)return{result:"UNKNOWN",message:"No React files found"};let s=/ErrorBoundary|componentDidCatch|getDerivedStateFromError|error\.tsx|error\.jsx/;for(let i of e.files){if(!i.endsWith(".ts")&&!i.endsWith(".tsx")&&!i.endsWith(".js")&&!i.endsWith(".jsx"))continue;let r;try{r=await e.readFile(i)}catch{continue}if(s.test(r))return{result:"PASS",message:`Error boundary found in ${i}`}}return{result:"FAIL",message:"No error boundary component found \u2014 unhandled errors will crash the entire app",evidence:["Add an ErrorBoundary component wrapping your app root or use Next.js error.tsx"]}}};var oi={id:"ERR-03",name:"Unhandled promise rejection handling",module:"foundation",layer:"L3",priority:"P1",description:"Application should handle unhandled promise rejections to prevent silent failures",fixCost:50,fixSize:"S",async run(e){let t=e.files.filter(r=>(r.includes("/api/")||r.includes("route.ts")||r.includes("route.js")||r.includes("actions.ts")||r.includes("actions.js"))&&(r.endsWith(".ts")||r.endsWith(".js")));if(t.length===0)return{result:"UNKNOWN",message:"No API routes or server actions found"};let s=/try\s*\{/,i=[];for(let r of t){let n;try{n=await e.readFile(r)}catch{continue}/async\s+function|async\s*\(/.test(n)&&!s.test(n)&&i.push(r)}return i.length>0?{result:"FAIL",message:`${i.length} async API route${i.length>1?"s":""} without try/catch error handling`,evidence:i.slice(0,5).map(r=>`${r} \u2014 add try/catch around async operations`)}:{result:"PASS",message:`All ${t.length} API routes have error handling`}}};var ai={id:"SCH-01",name:"Database migrations exist",module:"schema",layer:"L4",priority:"P1",description:"Project should have migration files (Supabase, Prisma, or Drizzle) for reproducible schema changes",fixCost:100,fixSize:"M",async run(e){let t=[/supabase\/migrations\//,/prisma\/migrations\//,/drizzle\/migrations\//,/migrations\/.*\.sql$/,/schema\.prisma$/],s=e.files.filter(o=>t.some(a=>a.test(o)));if(s.length>0)return{result:"PASS",message:`${s.length} migration file${s.length>1?"s":""} found`};let i=e.files.some(o=>o.includes("supabase")||o.includes("@supabase")),r=e.files.some(o=>o.includes("prisma")),n=e.files.some(o=>o.includes("drizzle"));return i||r||n?{result:"FAIL",message:"Database ORM/client detected but no migration files found",evidence:["Create migration files to track schema changes reproducibly"]}:{result:"UNKNOWN",message:"No database usage detected"}}};var ci={id:"SCH-02",name:"No raw SQL in application code",module:"schema",layer:"L4",priority:"P2",description:"Inline SQL in route handlers/components instead of ORM/query builder is fragile and injection-prone",fixCost:250,fixSize:"L",async run(e){let t=e.files.filter(r=>!r.includes("migration")&&!r.endsWith(".sql")&&!r.includes("seed")&&!r.includes("supabase/")&&!r.includes("prisma/")&&!r.includes("drizzle/")&&(r.endsWith(".ts")||r.endsWith(".tsx")||r.endsWith(".js")||r.endsWith(".jsx"))),s=/\b(SELECT|INSERT INTO|UPDATE\s+\w+\s+SET|DELETE FROM|CREATE TABLE|ALTER TABLE|DROP TABLE)\b/,i=[];for(let r of t){let n;try{n=await e.readFile(r)}catch{continue}let o=n.split(`
|
|
10
|
+
`);for(let a=0;a<o.length;a++){let c=o[a].trim();c.startsWith("//")||c.startsWith("*")||c.startsWith("#")||s.test(c)&&i.push(`${r}:${a+1} \u2192 ${c.substring(0,100)}`)}}return i.length>0?{result:"FAIL",message:`Raw SQL found in ${i.length} location${i.length>1?"s":""} in application code`,evidence:i.slice(0,5)}:{result:"PASS",message:"No raw SQL detected in application code"}}};var li={id:"SCH-04",name:"Soft delete pattern consistency",module:"schema",layer:"L4",priority:"P2",description:"If soft delete is used, all delete operations should use the same pattern (deleted_at column)",fixCost:100,fixSize:"M",async run(e){let t=e.files.filter(n=>(n.endsWith(".ts")||n.endsWith(".tsx")||n.endsWith(".js")||n.endsWith(".jsx"))&&!n.includes("node_modules")),s=!1,i=!1,r=[];for(let n of t){let o;try{o=await e.readFile(n)}catch{continue}/deleted_at|deletedAt|is_deleted|isDeleted|soft.?delete/i.test(o)&&(s=!0),/\.delete\(\)|\.remove\(\)|DELETE FROM/i.test(o)&&!o.includes("deleted_at")&&(i=!0,r.push(n))}return!s&&!i?{result:"UNKNOWN",message:"No delete operations detected"}:s&&i?{result:"FAIL",message:"Mixed soft delete and hard delete patterns \u2014 inconsistent data deletion strategy",evidence:r.slice(0,5).map(n=>`${n} \u2014 uses hard delete while project has soft delete pattern`)}:s?{result:"PASS",message:"Consistent soft delete pattern used"}:{result:"PASS",message:"Consistent hard delete pattern (no soft delete used)"}}};var ui={id:"SCH-09",name:"N+1 query pattern detection",module:"schema",layer:"L4",priority:"P2",description:"Detects fetch-list-then-fetch-each-item patterns that cause performance collapse under load",fixCost:100,fixSize:"M",async run(e){let t=e.files.filter(o=>(o.endsWith(".ts")||o.endsWith(".tsx")||o.endsWith(".js")||o.endsWith(".jsx"))&&!o.includes("node_modules")&&!o.includes("migration")),s=/\.(from|select)\(.*\)(?:(?!\.single\(\)).)*$/,i=/for\s*\(.*\)\s*\{[^}]*(?:\.from\(|\.select\(|\.rpc\(|supabase\.|fetch\()/s,r=/\.map\(\s*(?:async\s*)?\([^)]*\)\s*=>\s*(?:{[^}]*)?(?:\.from\(|\.select\(|\.rpc\(|supabase\.|fetch\()/s,n=[];for(let o of t){let a;try{a=await e.readFile(o)}catch{continue}i.test(a)?n.push(`${o} \u2014 DB/API call inside for loop`):r.test(a)&&n.push(`${o} \u2014 DB/API call inside .map()`)}return n.length>0?{result:"FAIL",message:`N+1 query pattern detected in ${n.length} file${n.length>1?"s":""}`,evidence:n.slice(0,5)}:{result:"PASS",message:"No N+1 query patterns detected"}}};var di={id:"SCH-10",name:"Unbounded query guard",module:"schema",layer:"L4",priority:"P2",description:"Queries without LIMIT/pagination can exhaust memory under real data volumes",fixCost:100,fixSize:"M",async run(e){let t=e.files.filter(n=>(n.endsWith(".ts")||n.endsWith(".tsx")||n.endsWith(".js")||n.endsWith(".jsx"))&&!n.includes("node_modules")&&!n.includes("migration")&&!n.includes("seed")),s=/\.from\(['"`]\w+['"`]\)\s*\.select\(/,i=/\.limit\(|\.range\(|\.single\(|\.maybeSingle\(|\.eq\(.*id|pagination|paginate|page\s*[=:]/i,r=[];for(let n of t){let o;try{o=await e.readFile(n)}catch{continue}s.test(o)&&!i.test(o)&&r.push(`${n} \u2014 .from().select() without .limit()/.range()/.single()`)}return r.length>0?{result:"FAIL",message:`Unbounded queries found in ${r.length} file${r.length>1?"s":""}`,evidence:r.slice(0,5)}:{result:"PASS",message:"All queries appear to have bounds (limit/range/single)"}}};var fs=new Set(["ARCH-01","ARCH-02","ARCH-03","ARCH-04","ARCH-05","AUTH-01","AUTH-02","AUTH-03","AUTH-05","AUTH-06","AUTH-11","AUTH-13","AUTH-14","BIL-01","BIL-02","BIL-03","BIL-04","BIL-09","BIL-14","BIL-16","BIL-17","ADM-01","ADM-02","ADM-08","ADM-11","ENV-01","ENV-02","CFG-01","ERR-01"]),fi=[ge,he,Se,ye,Ae,ve,Ce,_e,ke,Ie,Pe,we,Le,Ne,Re,Ee,Fe,Te,xe,De,Ue,Me,Oe,We,je,Be,He,ze,qe,Ke,Ge,Ve,Ye,Xe,Qe,Je,Ze,et,tt,it,st,nt,rt,ot,at,ct,lt,ut,dt,pt,ft,mt,gt,ht,St,yt,At,bt,$t,vt,Ct,_t,kt,It,Pt,wt,Lt,Nt,Rt,Et,Ft,Tt,xt,Dt,Ut,Mt,Ot,Wt,jt,Bt,Ht,zt,qt,Kt,Gt,Vt,Yt,Xt,Qt,Jt,ti,ii,si,ni,ri,oi,ai,ci,li,ui,di],pi={"AUTH-01":"supabase","AUTH-02":"supabase","AUTH-03":"supabase","AUTH-10":"supabase","AUTH-11":"supabase","AUTH-13":"supabase","AUTH-17":"supabase","AUTH-18":"supabase","AUTH-27":"supabase","AUTH-28":"supabase","BIL-01":"stripe","BIL-02":"stripe","BIL-03":"stripe","BIL-05":"stripe","BIL-12":"stripe","BIL-15":"stripe","BIL-19":"stripe","BIL-20":"stripe","BIL-22":"stripe"};for(let e of fi)e.phase=fs.has(e.id)?1:2,pi[e.id]&&(e.vendor=pi[e.id]);var mi=fi;import hi from"fs/promises";import G from"path";var ms=new Set(["node_modules",".git",".next","dist","build",".vercel",".turbo","coverage",".nyc_output","__pycache__",".svelte-kit"]),gi=new Set([".ts",".tsx",".js",".jsx",".mjs",".cjs",".sql",".env",".env.local",".env.example"]);async function Si(e){let t=[];return await yi(e,e,t),t}async function yi(e,t,s){let i;try{i=await hi.readdir(e,{withFileTypes:!0})}catch{return}for(let r of i)if(!(r.name.startsWith(".")&&r.name!==".env"&&r.name!==".env.local"&&r.name!==".env.example"&&r.isDirectory()))if(r.isDirectory()){if(ms.has(r.name))continue;await yi(G.join(e,r.name),t,s)}else{let n=G.extname(r.name),o=r.name;(gi.has(n)||gi.has(o))&&s.push(G.relative(t,G.join(e,r.name)))}}async function X(e,t){let s=G.resolve(e,t);return hi.readFile(s,"utf-8")}import gs from"fs/promises";import hs from"path";async function Ai(e,t,s){let i={hasStripe:!1,hasSupabase:!1,detectedVendors:[]},r=hs.join(e,"package.json");try{let n=await gs.readFile(r,"utf-8"),o=JSON.parse(n),a={...o.dependencies,...o.devDependencies};(a.stripe||a["@stripe/stripe-js"]||a["@stripe/react-stripe-js"])&&(i.hasStripe=!0),(a["@supabase/supabase-js"]||a["@supabase/ssr"]||a["@supabase/auth-helpers-nextjs"])&&(i.hasSupabase=!0)}catch{}if((!i.hasStripe||!i.hasSupabase)&&t&&s){let n=/from\s+["']stripe["']|new\s+Stripe\s*\(|stripe[_-]webhook|STRIPE_SECRET|loadStripe\s*\(/,o=/from\s+["']@supabase|createClient\s*\(|SUPABASE_URL|supabase\.auth\.|supabase\.from\s*\(/,a=t.filter(f=>f.endsWith(".ts")||f.endsWith(".tsx")||f.endsWith(".js")||f.endsWith(".jsx")),c=a.filter(f=>/stripe|billing|payment|webhook|checkout|supabase|auth/i.test(f)),l=a.filter(f=>!/stripe|billing|payment|webhook|checkout|supabase|auth/i.test(f)),p=[...c,...l.slice(0,30)];for(let f of p){if(i.hasStripe&&i.hasSupabase)break;try{let h=await s(f);!i.hasStripe&&n.test(h)&&(i.hasStripe=!0),!i.hasSupabase&&o.test(h)&&(i.hasSupabase=!0)}catch{continue}}}return i.hasStripe&&i.detectedVendors.push("stripe"),i.hasSupabase&&i.detectedVendors.push("supabase"),i}var Ss={L1:"Architecture",L2:"Safety",L3:"Foundation",L4:"Schema"},Q=class{evaluate(t,s,i){let r=this.computeLayerScores(t);return{project:i,timestamp:new Date().toISOString(),mode:s,checks:t,layers:r}}computeLayerScores(t){let s={},i=new Set;for(let r of t)i.add(r.layer);for(let r of i){let o=t.filter(f=>f.layer===r).filter(f=>f.result!=="N/A"&&f.result!=="UNKNOWN"&&f.result!=="NOT_APPLICABLE"),a=o.filter(f=>f.result==="PASS").length,c=o.filter(f=>f.result==="FAIL").length,l=o.length,p=l>0?Math.round(a/l*10):0;s[r]={name:Ss[r]??r,passed:a,failed:c,total:l,score:p}}return s}};var ys={"trust-score":["L2","L3","L4"],architecture:["L1"],all:["L1","L2","L3","L4"]};async function $i(e,t={}){let{mode:s="trust-score",engine:i=new Q,allChecks:r=!1,disabledChecks:n}=t,o=bi.resolve(e),a=await Si(o),c=ys[s],p=await Ai(o,a,d=>X(o,d)),f={rootDir:o,files:a,fingerprint:p,readFile:d=>X(o,d),grepFiles:(d,b)=>be({rootDir:o,files:a,readFile:K=>X(o,K)},d,b)},h=n??As(),y=mi.filter(d=>c.includes(d.layer)&&(r||d.phase===1)),A=[];for(let d of y)if(!h.has(d.id)){if(bs(d,p)){A.push({id:d.id,name:d.name,module:d.module,layer:d.layer,priority:d.priority,category:d.category,result:"NOT_APPLICABLE",message:$s(d)});continue}try{let b=await d.run(f);A.push({id:d.id,name:d.name,module:d.module,layer:d.layer,priority:d.priority,category:d.category,result:b.result,message:b.message,evidence:b.evidence,shadow:d.shadow})}catch(b){A.push({id:d.id,name:d.name,module:d.module,layer:d.layer,priority:d.priority,category:d.category,result:"UNKNOWN",message:`Check failed with error: ${b instanceof Error?b.message:String(b)}`,shadow:d.shadow})}}let F=bi.basename(o);return i.evaluate(A,s,F)}function As(){let e=process.env.VIBECODIQ_DISABLED_CHECKS||"";return e.trim()?new Set(e.split(",").map(t=>t.trim().toUpperCase()).filter(Boolean)):new Set}function bs(e,t){return e.vendor==="stripe"&&!t.hasStripe||e.vendor==="supabase"&&!t.hasSupabase}function $s(e){return e.vendor==="stripe"?"No Stripe integration detected \u2014 check not applicable":e.vendor==="supabase"?"No Supabase integration detected \u2014 check not applicable":"Prerequisite not detected \u2014 check not applicable"}var u="\x1B[0m",g="\x1B[1m",m="\x1B[2m",P="\x1B[31m",k="\x1B[32m",R="\x1B[33m",oe="\x1B[36m",J="\x1B[37m",vs="\x1B[41m";var Cs="\x1B[43m";function T(e){switch(e){case"PASS":return`${k}\u2713${u}`;case"FAIL":return`${P}\u2717${u}`;case"REVIEW":return`${R}\u26A0${u}`;case"UNKNOWN":return`${R}?${u}`;case"NOT_APPLICABLE":return`${m}\u25CB${u}`;case"N/A":return`${m}\u2014${u}`;default:return" "}}function _i(e){switch(e){case"P0":return`${vs}${J}${g} P0 ${u}`;case"P1":return`${Cs}${J}${g} P1 ${u}`;case"P2":return`${m} P2 ${u}`;default:return` ${e} `}}function ki(e){let t=e.score,s=10-t;return`${t>=7?k:t>=4?R:P}${"\u2588".repeat(t)}${m}${"\u2591".repeat(s)}${u} ${t}/10`}var Ii={"trust-score":"SAFETY SCAN",architecture:"ARCHITECTURE SCAN",all:"FULL SCAN"};function _s(e,t,s){return e>0?{icon:"\u{1F534}",text:"NOT READY FOR PRODUCTION",color:P}:t>0?{icon:"\u{1F7E1}",text:"NEEDS ATTENTION",color:R}:s>0?{icon:"\u26AA",text:"MINOR ISSUES",color:m}:{icon:"\u{1F7E2}",text:"READY FOR PRODUCTION",color:k}}function Z(e){let t=d=>process.stdout.write(d+`
|
|
11
|
+
`),s=Ii[e.mode]??"SCAN",i=e.checks.filter(d=>!d.shadow),r=i.filter(d=>d.result==="FAIL"),n=i.filter(d=>d.result==="PASS"),o=i.filter(d=>d.result==="UNKNOWN"),a=i.filter(d=>d.result==="NOT_APPLICABLE"),c=i.filter(d=>d.result==="REVIEW"),l=r.filter(d=>d.priority==="P0"),p=r.filter(d=>d.priority==="P1"),f=r.filter(d=>d.priority!=="P0"&&d.priority!=="P1"),h=_s(l.length,p.length,f.length);if(t(""),t(`${g}\u2554\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2557${u}`),t(`${g}\u2551 VIBECODIQ \u2014 ${s.padEnd(43)}\u2551${u}`),t(`${g}\u2551 Project: ${e.project.padEnd(46)}\u2551${u}`),t(`${g}\u255A\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u255D${u}`),t(""),t(` ${h.icon} ${g}${h.color}${h.text}${u}`),r.length>0){let d=[];l.length>0&&d.push(`${P}${l.length} critical${u}`),p.length>0&&d.push(`${R}${p.length} important${u}`),f.length>0&&d.push(`${m}${f.length} quality${u}`),t(` ${r.length} issues found (${d.join(", ")})`)}t(""),t(`${m}${"\u2500".repeat(62)}${u}`),t("");let y=["L1","L2","L3","L4"];for(let d of y){let b=e.layers[d];if(!b)continue;let K=`${d} ${b.name}`;t(` ${g}${K.padEnd(22)}${u} ${ki(b)} ${b.passed}/${b.total} pass ${b.failed>0?`${P}${b.failed} fail${u}`:`${k}0 fail${u}`}`)}if(l.length>0){t(""),t(`${m}${"\u2500".repeat(62)}${u}`),t(""),t(` \u{1F534} ${P}${g}TIER 1 \u2014 Launch Blockers (${l.length})${u}`);for(let d of l)if(t(""),t(` ${T(d.result)} ${_i(d.priority)} ${g}${d.id}${u} ${d.name}`),t(` ${m}${d.message}${u}`),d.evidence){for(let b of d.evidence.slice(0,3))t(` ${m}\u2192 ${b}${u}`);d.evidence.length>3&&t(` ${m} ... and ${d.evidence.length-3} more${u}`)}}if(p.length>0){t(""),t(`${m}${"\u2500".repeat(62)}${u}`),t(""),t(` \u{1F7E1} ${R}${g}TIER 2 \u2014 Important Issues (${p.length})${u}`),t("");for(let d of p)t(` ${T(d.result)} ${m}${d.id.padEnd(16)}${u}${d.name}`);t(""),t(` ${oe}\u2192 npx @vibecodiq/cli scan --fix${u} ${m}for details + AI fix prompts${u}`)}if(f.length>0){t(""),t(`${m}${"\u2500".repeat(62)}${u}`),t(""),t(` \u26AA ${m}${g}TIER 3 \u2014 Quality & Drift (${f.length})${u}`),t("");for(let d of f)t(` ${T(d.result)} ${m}${d.id.padEnd(16)}${d.name}${u}`)}if(n.length>0){t(""),t(`${m}${"\u2500".repeat(62)}${u}`),t(""),t(`${k}${g} PASSED (${n.length})${u}`),t("");for(let d of n)t(` ${T(d.result)} ${m}${d.id.padEnd(16)}${u}${d.name}`)}if(c.length>0){t(""),t(`${m}${"\u2500".repeat(62)}${u}`),t(""),t(`${R}${g} MANUAL REVIEW (${c.length})${u} ${m}\u2014 pattern detected, needs verification${u}`),t("");for(let d of c)t(` ${T(d.result)} ${m}${d.id.padEnd(16)}${u}${d.name}`)}if(o.length>0){t(""),t(`${m}${"\u2500".repeat(62)}${u}`),t(""),t(`${R}${g} UNKNOWN (${o.length})${u} ${m}\u2014 could not determine${u}`),t("");for(let d of o)t(` ${T(d.result)} ${m}${d.id.padEnd(16)}${d.name}${u}`)}if(a.length>0){t(""),t(`${m}${"\u2500".repeat(62)}${u}`),t(""),t(`${m}${g} NOT APPLICABLE (${a.length})${u} ${m}\u2014 vendor/feature not detected in project${u}`),t("");for(let d of a)t(` ${T(d.result)} ${m}${d.id.padEnd(16)}${d.name}${u}`)}t(""),t(`${m}${"\u2500".repeat(62)}${u}`),t("");let A=e.checks.length,F=[`${k}${n.length} pass${u}`,r.length>0?`${P}${r.length} fail${u}`:`${k}0 fail${u}`];c.length>0&&F.push(`${R}${c.length} review${u}`),o.length>0&&F.push(`${o.length} unknown`),a.length>0&&F.push(`${m}${a.length} n/a${u}`),t(` ${g}Total:${u} ${A} checks \u2014 ${F.join(", ")}`),r.length>0?(t(""),t(` ${oe}${g}npx @vibecodiq/cli scan --fix${u} ${m}Trust Score + fix cost + AI fix prompts (Pro)${u}`)):t(` ${k}${g}All checks passed.${u}`),wi()}function ee(e){console.log(JSON.stringify(e,null,2))}var vi="\x1B[35m";function Ci(e){switch(e){case"A":return k;case"B":return k;case"C":return R;case"D":return P;case"F":return P;default:return J}}function Pi(e,t){let s=p=>process.stdout.write(p+`
|
|
12
|
+
`),i=Ii[e.mode]??"SCAN",r=e.checks.filter(p=>!p.shadow),n=r.filter(p=>p.result==="FAIL"),o=r.filter(p=>p.result==="PASS"),a=r.filter(p=>p.result==="UNKNOWN");s(""),s(`${g}\u2554\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2557${u}`),s(`${g}\u2551 VIBECODIQ \u2014 ${i.padEnd(43)}\u2551${u}`),s(`${g}\u2551 Project: ${e.project.padEnd(46)}\u2551${u}`),s(`${g}\u255A\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u255D${u}`),s(""),s(` ${g}Trust Score:${u} ${Ci(t.grade)}${g}${t.grade}${u} ${m}(${t.trust_score}/100)${u}`),s(` ${g}Fix Cost:${u} ${oe}~${t.fix_cost_hours}h${u} estimated`),s(` ${g}Verdict:${u} ${t.fix_vs_rebuild==="FIX"?`${k}FIX${u} \u2014 worth fixing`:`${P}REBUILD${u} \u2014 consider rebuilding`}`),s(""),s(`${m}${"\u2500".repeat(62)}${u}`),s("");let c=["L1","L2","L3","L4"];for(let p of c){let f=e.layers[p];if(!f)continue;let h=`${p} ${f.name}`;s(` ${g}${h.padEnd(22)}${u} ${ki(f)} ${f.passed}/${f.total} pass ${f.failed>0?`${P}${f.failed} fail${u}`:`${k}0 fail${u}`}`)}if(n.length>0){s(""),s(`${m}${"\u2500".repeat(62)}${u}`),s(""),s(` ${P}${g}FAILED (${n.length})${u}`);for(let p of n)if(s(""),s(` ${T(p.result)} ${_i(p.priority)} ${g}${p.id}${u} ${p.name}`),s(` ${m}${p.message}${u}`),p.evidence){for(let f of p.evidence.slice(0,3))s(` ${m}\u2192 ${f}${u}`);p.evidence.length>3&&s(` ${m} ... and ${p.evidence.length-3} more${u}`)}}if(t.fix_batches&&t.fix_batches.length>0){s(""),s(`${m}${"\u2500".repeat(62)}${u}`),s(""),s(` ${vi}${g}FIX BATCHES (${t.fix_batches.length})${u}`),s(` ${m}Copy-paste these prompts into your AI tool to fix issues.${u}`);for(let p of t.fix_batches)s(""),s(` ${vi}${g}Batch: ${p.title}${u} ${m}(${p.checks.join(", ")})${u}`),s(` ${m}${"\u2504".repeat(56)}${u}`),s(` ${J}${p.prompt}${u}`)}if(o.length>0){s(""),s(`${m}${"\u2500".repeat(62)}${u}`),s(""),s(`${k}${g} PASSED (${o.length})${u}`),s("");for(let p of o)s(` ${T(p.result)} ${m}${p.id.padEnd(16)}${u}${p.name}`)}s(""),s(`${m}${"\u2500".repeat(62)}${u}`),s("");let l=e.checks.length;s(` ${g}Total:${u} ${l} checks \u2014 ${k}${o.length} pass${u}, ${n.length>0?`${P}${n.length} fail${u}`:`${k}0 fail${u}`}, ${a.length} unknown`),s(` ${g}Trust Score:${u} ${Ci(t.grade)}${g}${t.grade}${u} (${t.trust_score}/100) ${g}Fix:${u} ~${t.fix_cost_hours}h ${g}Verdict:${u} ${t.fix_vs_rebuild}`),s(""),s(` ${m}${t.summary}${u}`),wi()}function wi(){let e=t=>process.stdout.write(t+`
|
|
13
|
+
`);e(""),e(`${m}${"\u2500".repeat(62)}${u}`),e(` ${m}Static source-code analysis focused on high-confidence patterns.${u}`),e(` ${m}Not a complete security audit. Some findings may need manual review.${u}`),e(` ${m}https://vibecodiq.com/scan | https://asastandard.org${u}`),e("")}function ae(){let e=t=>process.stdout.write(t+`
|
|
14
|
+
`);e(""),e(` ${R}\u26A0 API unavailable. Showing local results only.${u}`),e(` ${m}Run again later for Trust Score + fix prompts.${u}`),e(` ${m}Status: https://status.vibecodiq.com${u}`),e("")}import Y from"fs";import ce from"path";var V=["#!/bin/bash","# ASA Structure Check \u2014 validates Slice Architecture rules","# Run: npx @vibecodiq/cli guard check (or ./check-structure.sh)","","set -e","",'RULES_FILE=".asa/rules/architecture.md"',"ERRORS=0","WARNINGS=0","",'echo "\u{1F50D} ASA Structure Check \u2014 validating architecture rules..."','echo " Rules: $RULES_FILE"','echo ""',"","fail() {",' echo "\u274C FAIL: $1"',' echo " \u2192 $2"',' echo " \u2192 Fix: see $RULES_FILE"',' echo ""'," ERRORS=$((ERRORS + 1))","}","","warn() {",' echo "\u26A0\uFE0F WARN: $1"',' echo " \u2192 $2"',' echo ""'," WARNINGS=$((WARNINGS + 1))","}","","pass() {",' echo "\u2705 PASS: $1"',"}","","# CHECK 1: Business logic in src/domains/",'echo "--- Check 1: Business logic in src/domains/ ---"',"",'BIZ_IN_PAGES=$(grep -rl "supabase\\.\\(from\\|auth\\|rpc\\|storage\\)" src/pages/ 2>/dev/null || true)','if [ -n "$BIZ_IN_PAGES" ]; then'," for f in $BIZ_IN_PAGES; do",' fail "Business logic in page file: $f" \\',' "Supabase calls must be in src/domains/, not in pages."'," done","else",' pass "No business logic found in src/pages/"',"fi","",'if [ -d "src/components" ]; then',' BIZ_IN_COMPONENTS=$(grep -rl "supabase\\.\\(from\\|auth\\|rpc\\|storage\\)" src/components/ 2>/dev/null || true)',' if [ -n "$BIZ_IN_COMPONENTS" ]; then'," for f in $BIZ_IN_COMPONENTS; do",' fail "Business logic in component file: $f" \\',' "Supabase calls must be in src/domains/, not in components/."'," done"," else",' pass "No business logic found in src/components/"'," fi","fi","","# CHECK 2: src/domains/ directory exists",'echo ""','echo "--- Check 2: src/domains/ directory exists ---"',"",'TS_FILES=$(find src/ -name "*.ts" -o -name "*.tsx" | grep -v "node_modules" | grep -v "vite-env" | wc -l)',"",'if [ "$TS_FILES" -gt 5 ]; then',' if [ -d "src/domains" ]; then',' pass "src/domains/ directory exists"'," DOMAIN_COUNT=$(find src/domains -mindepth 1 -maxdepth 1 -type d | wc -l)",' if [ "$DOMAIN_COUNT" -gt 0 ]; then',' pass "Found $DOMAIN_COUNT domain(s) in src/domains/"'," else",' warn "src/domains/ exists but is empty" \\',' "Add domain folders like src/domains/auth/, src/domains/tasks/"'," fi"," else",' fail "src/domains/ directory missing" \\',' "Business logic must be in src/domains/<domain>/<slice>/."'," fi","else",' pass "Project is small ($TS_FILES files) \u2014 src/domains/ not yet required"',"fi","","# CHECK 3: No cross-domain imports",'echo ""','echo "--- Check 3: No cross-domain imports ---"',"",'if [ -d "src/domains" ]; then'," CROSS_DOMAIN_VIOLATIONS=0"," for domain_dir in src/domains/*/; do",' [ ! -d "$domain_dir" ] && continue',' domain_name=$(basename "$domain_dir")',' OTHER_DOMAINS=$(find src/domains -mindepth 1 -maxdepth 1 -type d ! -name "$domain_name" -exec basename {} \\;)'," for other in $OTHER_DOMAINS; do",' VIOLATIONS=$(grep -rn "from.*domains/$other\\|import.*domains/$other" "$domain_dir" 2>/dev/null || true)',' if [ -n "$VIOLATIONS" ]; then'," while IFS= read -r line; do",' fail "Cross-domain import in $domain_name \u2192 $other" "$line"',' done <<< "$VIOLATIONS"'," CROSS_DOMAIN_VIOLATIONS=$((CROSS_DOMAIN_VIOLATIONS + 1))"," fi"," done"," done",' [ "$CROSS_DOMAIN_VIOLATIONS" -eq 0 ] && pass "No cross-domain imports detected"',"else",' pass "No domains yet \u2014 cross-domain check skipped"',"fi","","# CHECK 4: Pages are thin wrappers",'echo ""','echo "--- Check 4: Pages are thin wrappers ---"',"",'if [ -d "src/pages" ]; then'," for page in src/pages/*.tsx; do",' [ ! -f "$page" ] && continue',' PAGE_LINES=$(wc -l < "$page")',' if [ "$PAGE_LINES" -gt 80 ]; then',' fail "Page too large: $page ($PAGE_LINES lines)" \\',' "Pages should be <80 lines. Move logic to src/domains/."'," fi"," done",' pass "Page thin wrapper check complete"',"fi","","# CHECK 5: shared/ has no business logic",'echo ""','echo "--- Check 5: shared/ has no business logic ---"',"",'if [ -d "src/shared" ]; then',' BIZ_IN_SHARED=$(grep -rln "TaskList\\|TaskForm\\|PricingCard\\|AdminUser\\|LoginForm\\|RegisterForm" src/shared/ 2>/dev/null || true)',' if [ -n "$BIZ_IN_SHARED" ]; then'," for f in $BIZ_IN_SHARED; do",' fail "Business component in shared/: $f" \\',' "Domain-specific components belong in src/domains/."'," done"," else",' pass "No business logic found in src/shared/"'," fi","else",' pass "src/shared/ not yet created \u2014 check skipped"',"fi","","# RESULTS",'echo ""','echo "=========================================="','if [ "$ERRORS" -gt 0 ]; then',' echo "\u274C ASA STRUCTURE CHECK FAILED"',' echo " $ERRORS error(s), $WARNINGS warning(s)"',' echo " Read the architecture rules: $RULES_FILE"',' echo "=========================================="'," exit 1","else",' echo "\u2705 ASA STRUCTURE CHECK PASSED"',' echo " 0 errors, $WARNINGS warning(s)"',' echo "=========================================="'," exit 0","fi",""].join(`
|
|
15
|
+
`);var te=["# ASA Architecture Rules","","This project follows the ASA Slice Architecture. All code in this repository","MUST follow these rules. CI checks will fail if rules are violated.","","---","","## Rule 1: Business logic goes in `src/domains/`","","All business logic MUST be organized in domain folders:","","```","src/domains/","\u251C\u2500\u2500 auth/ # Authentication and authorization","\u251C\u2500\u2500 billing/ # Payments and subscriptions","\u2514\u2500\u2500 admin/ # Admin panel and user management","```","","Each domain contains **slices** \u2014 self-contained features:","","```","src/domains/<domain>/<slice>/","\u251C\u2500\u2500 <Component>.tsx # React component (UI)","\u251C\u2500\u2500 use<Action>.ts # React hook (data fetching / mutations)","\u2514\u2500\u2500 types.ts # Types (optional)","```","","### What is a Slice?","","One slice = one user action. Examples:","","| Domain | Slice | What it does |","|--------|-------|-------------|","| `auth` | `login` | User logs in |","| `auth` | `register` | User creates account |","| `billing` | `subscribe` | User subscribes to a plan |","| `billing` | `check-limits` | Check if user hit plan limits |","| `admin` | `user-list` | Admin sees all users |","","---","","## Rule 2: Pages are thin wrappers","","Page files MUST be thin wrappers that import from `src/domains/`.","Pages contain NO business logic \u2014 only layout and composition.","Maximum 80 lines per page file.","","---","","## Rule 3: Shared infrastructure goes in `src/shared/`","","Database clients, auth helpers, and external service configs go in `src/shared/`:","","```","src/shared/","\u251C\u2500\u2500 db/","\u2502 \u2514\u2500\u2500 supabase-client.ts","\u251C\u2500\u2500 auth/","\u2502 \u251C\u2500\u2500 AuthGuard.tsx","\u2502 \u2514\u2500\u2500 useCurrentUser.ts","\u2514\u2500\u2500 billing/"," \u2514\u2500\u2500 stripe-client.ts","```","","---","","## Rule 4: No cross-domain imports","","A file in one domain MUST NOT import from another domain.","Use `src/shared/` for cross-domain communication.","","---","","## Rule 5: File naming conventions","","| Type | Pattern | Example |","|------|---------|---------|","| Component | `PascalCase.tsx` | `LoginForm.tsx` |","| Hook | `use<Action>.ts` | `useLogin.ts` |","| Types | `types.ts` | `types.ts` |","| Page | `page.tsx` | `page.tsx` |","| Shared utility | `camelCase.ts` | `supabase-client.ts` |","","---","","**If CI fails, restructure your code to follow these rules.**",""].join(`
|
|
16
|
+
`),ie=["name: Vibecodiq Guard","","on:"," push:"," branches: [main]"," paths-ignore:"," - '.asa/logs/**'"," pull_request:"," branches: [main]","","permissions:"," contents: read"," pull-requests: write"," actions: read","","jobs:"," vibecodiq-guard:"," name: Vibecodiq Safety Scan"," runs-on: ubuntu-latest",""," steps:"," - name: Checkout code"," uses: actions/checkout@v4"," with:"," fetch-depth: 0",""," - name: Setup Node.js"," uses: actions/setup-node@v4"," with:"," node-version: '18'",""," - name: Run Vibecodiq scan"," id: scan"," run: |"," npx @vibecodiq/cli@latest scan --all --json > /tmp/scan-result.json 2>/dev/null || true",` FAIL_COUNT=$(cat /tmp/scan-result.json | python3 -c "import sys,json; r=json.load(sys.stdin); print(sum(1 for c in r.get('checks',[]) if c.get('result')=='FAIL'))" 2>/dev/null || echo 0)`,` TOTAL=$(cat /tmp/scan-result.json | python3 -c "import sys,json; r=json.load(sys.stdin); print(len(r.get('checks',[])))" 2>/dev/null || echo 0)`,' echo "fail_count=$FAIL_COUNT" >> "$GITHUB_OUTPUT"',' echo "total=$TOTAL" >> "$GITHUB_OUTPUT"',' if [ "$FAIL_COUNT" -gt 0 ]; then',' echo "has_failures=true" >> "$GITHUB_OUTPUT"'," else",' echo "has_failures=false" >> "$GITHUB_OUTPUT"'," fi",""," - name: Call API for fix prompts"," id: api"," if: steps.scan.outputs.has_failures == 'true' && env.VIBECODIQ_API_TOKEN != ''"," env:"," VIBECODIQ_API_TOKEN: ${{ secrets.VIBECODIQ_API_TOKEN }}"," run: |"," npx @vibecodiq/cli@latest scan --all --fix --json > /tmp/fix-result.json 2>/dev/null || true"," if [ -s /tmp/fix-result.json ]; then",' echo "api_ok=true" >> "$GITHUB_OUTPUT"'," else",' echo "api_ok=false" >> "$GITHUB_OUTPUT"'," fi",""," - name: Upload scan artifact"," if: always()"," uses: actions/upload-artifact@v4"," with:"," name: vibecodiq-scan"," path: /tmp/scan-result.json"," retention-days: 30",""," - name: Write job summary"," if: always()"," run: |"," echo '## Vibecodiq Safety Scan' >> $GITHUB_STEP_SUMMARY"," FAIL_COUNT=${{ steps.scan.outputs.fail_count }}"," TOTAL=${{ steps.scan.outputs.total }}",' if [ "$FAIL_COUNT" = "0" ]; then'," echo '\u2705 **All checks passed** \u2014 your app is production-ready.' >> $GITHUB_STEP_SUMMARY"," else",' echo "\u274C **$FAIL_COUNT failures** out of $TOTAL checks." >> $GITHUB_STEP_SUMMARY'," echo '' >> $GITHUB_STEP_SUMMARY"," echo 'Download the scan artifact for full details.' >> $GITHUB_STEP_SUMMARY"," fi"," echo '' >> $GITHUB_STEP_SUMMARY"," echo '---' >> $GITHUB_STEP_SUMMARY"," echo '*Scanned by [Vibecodiq](https://vibecodiq.com) production-readiness checks*' >> $GITHUB_STEP_SUMMARY",""," - name: Post PR comment"," if: github.event_name == 'pull_request' && steps.scan.outputs.has_failures == 'true'"," uses: actions/github-script@v7"," with:"," script: |"," const fs = require('fs');"," const failCount = '${{ steps.scan.outputs.fail_count }}';"," const total = '${{ steps.scan.outputs.total }}';"," "," // Build comment body"," let body = `## \u274C Vibecodiq Safety Scan \u2014 ${failCount} failures\\n\\n`;"," body += `**${failCount}** of **${total}** checks failed.\\n\\n`;"," "," // Try to include fix prompts from API result"," try {"," const fixResult = JSON.parse(fs.readFileSync('/tmp/fix-result.json', 'utf-8'));"," if (fixResult.fix_batches && fixResult.fix_batches.length > 0) {"," body += `**Trust Score:** ${fixResult.grade} (${fixResult.trust_score}/100) \xB7 `;"," body += `**Fix:** ~${fixResult.fix_cost_hours}h \xB7 `;"," body += `**Verdict:** ${fixResult.fix_vs_rebuild}\\n\\n`;"," body += `### Top Fix Batches\\n\\n`;"," for (const batch of fixResult.fix_batches.slice(0, 3)) {"," body += `<details>\\n<summary>${batch.title} (${batch.checks.join(', ')})</summary>\\n\\n`;"," body += '```\\n' + batch.prompt + '\\n```\\n\\n';"," body += `</details>\\n\\n`;"," }"," }"," } catch (e) {}"," "," body += `---\\n*Scanned by [Vibecodiq](https://vibecodiq.com) production-readiness checks*`;"," "," // Find and update existing comment (sticky)"," const marker = '## \u274C Vibecodiq Safety Scan';"," const { data: comments } = await github.rest.issues.listComments({"," owner: context.repo.owner,"," repo: context.repo.repo,"," issue_number: context.issue.number,"," });"," const existing = comments.find(c => c.body && c.body.startsWith(marker));"," "," if (existing) {"," await github.rest.issues.updateComment({"," owner: context.repo.owner,"," repo: context.repo.repo,"," comment_id: existing.id,"," body: body,"," });"," } else {"," await github.rest.issues.createComment({"," owner: context.repo.owner,"," repo: context.repo.repo,"," issue_number: context.issue.number,"," body: body,"," });"," }",""," - name: Delete PR comment on pass"," if: github.event_name == 'pull_request' && steps.scan.outputs.has_failures == 'false'"," uses: actions/github-script@v7"," with:"," script: |"," const marker = '## \u274C Vibecodiq Safety Scan';"," const { data: comments } = await github.rest.issues.listComments({"," owner: context.repo.owner,"," repo: context.repo.repo,"," issue_number: context.issue.number,"," });"," const existing = comments.find(c => c.body && c.body.startsWith(marker));"," if (existing) {"," await github.rest.issues.deleteComment({"," owner: context.repo.owner,"," repo: context.repo.repo,"," comment_id: existing.id,"," });"," }",""," - name: Fail if checks failed"," if: steps.scan.outputs.has_failures == 'true'"," run: |",' echo "\u274C Vibecodiq scan found ${{ steps.scan.outputs.fail_count }} failures."',' echo "Download the scan artifact or check the PR comment for fix instructions."'," exit 1",""].join(`
|
|
17
|
+
`);var ks="\x1B[36m",Is="\x1B[32m",Ps="\x1B[33m";var Li="\x1B[1m",O="\x1B[2m",L="\x1B[0m";async function Ni(e,t=!1){let s=ce.resolve(e);console.log(""),console.log(` ${ks}\u{1F6E1}\uFE0F Initializing ASA Guard in ${s}${L}`),console.log("");let i=[{path:".asa/rules/architecture.md",content:te},{path:".github/workflows/asa-guard.yml",content:ie},{path:"check-structure.sh",content:V,executable:!0}],r=0,n=0;for(let o of i){let a=ce.join(s,o.path),c=ce.dirname(a);Y.existsSync(c)||Y.mkdirSync(c,{recursive:!0}),Y.existsSync(a)?(console.log(` ${Ps}\u23ED ${o.path}${L} ${O}(already exists)${L}`),n++):(Y.writeFileSync(a,o.content,"utf-8"),o.executable&&Y.chmodSync(a,493),console.log(` ${Is}\u2713 ${o.path}${L}`),r++)}console.log(""),console.log(` ${Li}Done:${L} ${r} file${r!==1?"s":""} created, ${n} skipped.`),console.log(""),console.log(` ${Li}Next steps:${L}`),console.log(` ${O}1.${L} Commit the new files: ${O}git add -A && git commit -m "chore: add ASA Guard"${L}`),console.log(` ${O}2.${L} Push to GitHub \u2014 CI will run architecture checks on every PR`),console.log(` ${O}3.${L} Run locally: ${O}npx @vibecodiq/cli guard check${L}`),console.log(""),console.log(` ${O}Learn more: https://asastandard.org/checks/methodology${L}`),console.log("")}import I from"fs";import C from"path";var ws="\x1B[36m",Ri="\x1B[32m",Ls="\x1B[33m",Ei="\x1B[31m",Fi="\x1B[1m",j="\x1B[2m",N="\x1B[0m";async function Ti(e){let t=C.resolve(e);console.log(""),console.log(` ${ws}\u{1F6E1}\uFE0F ASA Guard \u2014 Architecture Check${N}`),console.log(` ${j} Target: ${t}${N}`),console.log("");let s=[],i=Ns(t);s.push(await Rs(t,i)),s.push(await Es(t,i)),s.push(await Fs(t,i)),s.push(await Ts(t,i)),s.push(await xs(t,i));let r=0,n=0,o=0;for(let a of s){let c=a.status==="PASS"?`${Ri}\u2705${N}`:a.status==="FAIL"?`${Ei}\u274C${N}`:a.status==="WARN"?`${Ls}\u26A0\uFE0F${N}`:`${j}\u23ED${N}`;if(console.log(` ${c} ${a.name}`),a.status!=="PASS"&&a.status!=="SKIP"&&(console.log(` ${j}${a.message}${N}`),a.evidence))for(let l of a.evidence.slice(0,3))console.log(` ${j}\u2192 ${l}${N}`);a.status==="FAIL"?r++:a.status==="WARN"?n++:a.status==="PASS"&&o++}console.log(""),r>0?(console.log(` ${Ei}${Fi}\u274C ASA GUARD CHECK FAILED${N} \u2014 ${r} error${r>1?"s":""}, ${n} warning${n>1?"s":""}`),console.log(` ${j} Fix violations and run again.${N}`),console.log(` ${j} Rules: .asa/rules/architecture.md${N}`)):console.log(` ${Ri}${Fi}\u2705 ASA GUARD CHECK PASSED${N} \u2014 ${o} passed, ${n} warning${n>1?"s":""}`),console.log(""),process.exit(r>0?1:0)}function Ns(e){return I.existsSync(C.join(e,"src"))?"src":I.existsSync(C.join(e,"app"))?"app":null}function B(e,t){let s=[];if(!I.existsSync(e))return s;let i=I.readdirSync(e,{withFileTypes:!0});for(let r of i){let n=C.join(e,r.name);r.name==="node_modules"||r.name===".git"||(r.isDirectory()?s.push(...B(n,t)):t.some(o=>r.name.endsWith(o))&&s.push(n))}return s}async function Rs(e,t){let s="Business logic in domains/";if(!t)return{name:s,status:"SKIP",message:"No src/ directory found"};let i=C.join(e,t,"pages"),r=C.join(e,t,"components"),n=[],o=/supabase\.(from|auth|rpc|storage)\b/;for(let a of[i,r]){if(!I.existsSync(a))continue;let c=B(a,[".ts",".tsx",".js",".jsx"]);for(let l of c){let p=I.readFileSync(l,"utf-8");o.test(p)&&n.push(C.relative(e,l))}}return n.length>0?{name:s,status:"FAIL",message:`Supabase calls found in pages/components (${n.length} file${n.length>1?"s":""})`,evidence:n}:{name:s,status:"PASS",message:""}}async function Es(e,t){let s="domains/ directory exists";if(!t)return{name:s,status:"SKIP",message:"No src/ directory found"};let i=C.join(e,t,"domains"),r=B(C.join(e,t),[".ts",".tsx"]);if(r.length<=5)return{name:s,status:"PASS",message:"Project is small \u2014 domains/ not yet required"};if(!I.existsSync(i))return{name:s,status:"FAIL",message:`${r.length} files in ${t}/ but no domains/ directory`,evidence:["Create src/domains/<domain>/<slice>/ for business logic"]};let n=I.readdirSync(i,{withFileTypes:!0}).filter(o=>o.isDirectory());return n.length===0?{name:s,status:"WARN",message:"domains/ exists but is empty"}:{name:s,status:"PASS",message:`${n.length} domain${n.length>1?"s":""} found`}}async function Fs(e,t){let s="No cross-domain imports";if(!t)return{name:s,status:"SKIP",message:"No src/ directory found"};let i=C.join(e,t,"domains");if(!I.existsSync(i))return{name:s,status:"PASS",message:"No domains yet"};let r=I.readdirSync(i,{withFileTypes:!0}).filter(o=>o.isDirectory()).map(o=>o.name),n=[];for(let o of r){let a=C.join(i,o),c=B(a,[".ts",".tsx",".js",".jsx"]),l=r.filter(p=>p!==o);for(let p of c){let f=I.readFileSync(p,"utf-8");for(let h of l)new RegExp(`from.*domains/${h}|import.*domains/${h}`).test(f)&&n.push(`${C.relative(e,p)} \u2192 imports from ${h}/`)}}return n.length>0?{name:s,status:"FAIL",message:`${n.length} cross-domain import${n.length>1?"s":""} found`,evidence:n}:{name:s,status:"PASS",message:""}}async function Ts(e,t){let s="Pages are thin wrappers";if(!t)return{name:s,status:"SKIP",message:"No src/ directory found"};let i=C.join(e,t,"pages");if(!I.existsSync(i))return{name:s,status:"PASS",message:"No pages/ directory (Next.js App Router or no pages)"};let r=B(i,[".tsx",".jsx"]),n=[];for(let o of r){let c=I.readFileSync(o,"utf-8").split(`
|
|
18
|
+
`).length;c>80&&n.push(`${C.relative(e,o)} (${c} lines)`)}return n.length>0?{name:s,status:"FAIL",message:`${n.length} page${n.length>1?"s":""} over 80 lines`,evidence:n}:{name:s,status:"PASS",message:""}}async function xs(e,t){let s="shared/ has no business logic";if(!t)return{name:s,status:"SKIP",message:"No src/ directory found"};let i=C.join(e,t,"shared");if(!I.existsSync(i))return{name:s,status:"PASS",message:"No shared/ directory yet"};let r=/TaskList|TaskForm|PricingCard|AdminUser|LoginForm|RegisterForm/,n=B(i,[".ts",".tsx",".js",".jsx"]),o=[];for(let a of n){let c=I.readFileSync(a,"utf-8");r.test(c)&&o.push(C.relative(e,a))}return o.length>0?{name:s,status:"FAIL",message:`Business components found in shared/ (${o.length} file${o.length>1?"s":""})`,evidence:o}:{name:s,status:"PASS",message:""}}import se from"fs";import xi from"path";var Ds="\x1B[32m",Us="\x1B[33m",Ms="\x1B[36m",Di="\x1B[1m",le="\x1B[2m",x="\x1B[0m";async function Ui(e){let t=xi.resolve(e);console.log(""),console.log(` ${Ms}\u{1F504} Upgrading ASA Guard rules...${x}`),console.log("");let s=[{path:".asa/rules/architecture.md",content:te},{path:".github/workflows/asa-guard.yml",content:ie},{path:"check-structure.sh",content:V,executable:!0}],i=0;for(let r of s){let n=xi.join(t,r.path);if(!se.existsSync(n)){console.log(` ${Us}\u23ED ${r.path}${x} ${le}(not found \u2014 run guard init first)${x}`);continue}if(se.readFileSync(n,"utf-8")===r.content){console.log(` ${le}\u2713 ${r.path} (already up to date)${x}`);continue}se.writeFileSync(n,r.content,"utf-8"),r.executable&&se.chmodSync(n,493),console.log(` ${Ds}\u2713 ${r.path}${x} ${Di}updated${x}`),i++}console.log(""),i>0?console.log(` ${Di}${i} file${i>1?"s":""} updated.${x} Commit the changes.`):console.log(` ${le}All rules are already up to date.${x}`),console.log("")}function Os(e,t){if(e.startsWith(t))return e.slice(t.length).replace(/^[/\\]/,"");let s=/^\/(?:home|Users)\/[^/]+\//;if(s.test(e))return e.replace(s,"");let i=/^[A-Z]:\\Users\\[^\\]+\\/i;return i.test(e)?e.replace(i,""):e}function Ws(e,t){return e.map(s=>s.replace(/(?:\/(?:home|Users)\/[^\s:]+|[A-Z]:\\Users\\[^\s:]+)/g,i=>Os(i,t)))}function ue(e,t,s,i,r){let n=e.map(o=>({check_id:o.id,result:o.result,name:o.name,module:o.module,layer:o.layer,priority:o.priority,category:o.category,message:o.message,evidence:o.evidence?Ws(o.evidence,t):void 0}));return{cli_version:s,scan_mode:i,project_name:r,timestamp:new Date().toISOString(),findings:n}}import{readFileSync as js}from"fs";import{join as Bs}from"path";import{homedir as Hs}from"os";var Mi=process.env.VIBECODIQ_API_URL||"https://api.vibecodiq.com",Oi=1e4,H=1;async function Wi(e,t,s){let i=new AbortController,r=setTimeout(()=>i.abort(),s);try{return await fetch(e,{...t,signal:i.signal})}finally{clearTimeout(r)}}async function de(e,t){for(let s=0;s<=H;s++)try{let i=await Wi(`${Mi}/scan`,{method:"POST",headers:{"Content-Type":"application/json",Authorization:`Bearer ${t}`},body:JSON.stringify(e)},Oi);if(i.ok)return await i.json();if(i.status===401){let r=await i.json().catch(()=>({detail:"Unauthorized"}));throw new z(r.detail??"Invalid API token")}if(i.status>=500&&s<H)continue;return null}catch(i){if(i instanceof z)throw i;if(s<H)continue;return null}return null}async function ji(e){for(let t=0;t<=H;t++)try{let s=await Wi(`${Mi}/scan/free`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(e)},Oi);if(s.ok)return await s.json();if(s.status>=500&&t<H)continue;return null}catch{if(t<H)continue;return null}return null}var z=class extends Error{constructor(t){super(t),this.name="AuthError"}};function pe(){let e=process.env.VIBECODIQ_API_TOKEN;if(e)return e;try{let t=Bs(Hs(),".vibecodiq","token"),s=js(t,"utf-8").trim();if(s)return s}catch{}return null}var qs="533f2e11ca9bf0419a066f73ff9cc910e23a39053fcb3051eb03f9521794e8db",ne="0.5.0",D="\x1B[1m",v="\x1B[2m",U="\x1B[36m",fe="\x1B[33m",S="\x1B[0m",M=process.argv.slice(2),q=M[0],me=M[1];function re(e){for(let t of e.slice(1))if(!t.startsWith("-"))return t;return"."}function W(e){return M.includes(e)}if(q==="scan"){let e=W("--fix"),t=re(M),s=W("--json"),i=!1;if(W("--unreleased")){let h=process.env.VIBECODIQ_INTERNAL_KEY||"";zs("sha256").update(h).digest("hex")!==qs&&(console.log(""),console.log(` ${fe}\u26A0 --unreleased requires VIBECODIQ_INTERNAL_KEY environment variable.${S}`),console.log(` ${v}This flag is restricted to authorized internal use.${S}`),console.log(""),process.exit(1)),i=!0}let r="trust-score";W("--architecture")&&(r="architecture"),W("--all")&&(r="all");let n=i?"all checks":"Phase 1",o=r==="trust-score"?`Safety Scan (${n})`:r==="architecture"?`Architecture Scan (${n})`:`Full Scan (${n})`;s||(console.log(""),console.log(` ${U}\u23F3 Scanning project \u2014 ${o}...${S}`));let a=await $i(t,{mode:r,allChecks:i}),c=a.checks.some(h=>h.result==="FAIL");if(e){let h=pe();h||(console.log(""),console.log(` ${fe}\u26A0 No API token found.${S}`),console.log(""),console.log(" Set your token via:"),console.log(` ${U}export VIBECODIQ_API_TOKEN=<your-token>${S}`),console.log(" Or save to file:"),console.log(` ${U}mkdir -p ~/.vibecodiq && echo "<your-token>" > ~/.vibecodiq/token${S}`),console.log(""),console.log(` ${v}Get your token at: https://vibecodiq.com/pricing${S}`),console.log(""),process.exit(1)),s||console.log(` ${U}\u23F3 Sending sanitized findings to API...${S}`);let y=ue(a.checks,t.startsWith("/")?t:process.cwd(),ne,r,a.project);try{let A=await de(y,h);A?s?console.log(JSON.stringify({...a,...A},null,2)):(Pi(a,A),A.share_url&&(console.log(""),console.log(` ${v}\u{1F4CB} Full report: ${U}${A.share_url}${S}`))):s?ee(a):(Z(a),ae())}catch(A){A instanceof z&&(console.log(""),console.log(` ${fe}\u26A0 API authentication failed: ${A.message}${S}`),console.log(` ${v}Check your VIBECODIQ_API_TOKEN.${S}`),console.log(""),process.exit(1)),s?ee(a):(Z(a),ae())}process.exit(c?1:0)}s?ee(a):Z(a);let l=ue(a.checks,t.startsWith("/")?t:process.cwd(),ne,r,a.project),p=pe(),f;try{p?f=(await de(l,p))?.share_url??void 0:f=(await ji(l))?.share_url??void 0}catch{}f&&!s&&(console.log(""),console.log(` ${v}\u{1F4CB} Full report: ${U}${f}${S}`)),process.exit(c?1:0)}else if(q==="guard")if(me==="init"){let e=re(M.slice(1)),t=W("--architecture"),s=W("--all");await Ni(e,t||s)}else if(me==="check"){let e=re(M.slice(1));await Ti(e)}else if(me==="upgrade"){let e=re(M.slice(1));await Ui(e)}else console.log(""),console.log(` ${D}@vibecodiq/cli guard${S} \u2014 Architecture rules & CI enforcement`),console.log(""),console.log(` ${D}Commands:${S}`),console.log(" guard init Install safety rules + CI into your repo"),console.log(" guard init --architecture + ASA architecture rules"),console.log(" guard init --all Install everything"),console.log(" guard check Run architecture checks locally"),console.log(" guard upgrade Upgrade rules to latest version"),console.log(""),console.log(` ${v}Learn more: https://vibecodiq.com/guard${S}`),console.log("");else if(q==="login"){let e=M[1];e||(console.log(""),console.log(` ${D}@vibecodiq/cli login${S} \u2014 Save your API token`),console.log(""),console.log(` ${D}Usage:${S}`),console.log(" npx @vibecodiq/cli login <your-token>"),console.log(""),console.log(` ${v}Your token is saved to ~/.vibecodiq/token${S}`),console.log(` ${v}Get your token at: https://vibecodiq.com/pricing${S}`),console.log(""),process.exit(1));let{mkdirSync:t,writeFileSync:s}=await import("fs"),{join:i}=await import("path"),{homedir:r}=await import("os"),n=i(r(),".vibecodiq"),o=i(n,"token");t(n,{recursive:!0}),s(o,e.trim()+`
|
|
19
|
+
`,{mode:384}),console.log(""),console.log(` ${U}\u2713${S} Token saved to ${v}${o}${S}`),console.log(` ${v}Run: npx @vibecodiq/cli scan --fix${S}`),console.log("")}else if(q==="logout"){let{unlinkSync:e,existsSync:t}=await import("fs"),{join:s}=await import("path"),{homedir:i}=await import("os"),r=s(i(),".vibecodiq","token");t(r)?(e(r),console.log(""),console.log(` ${U}\u2713${S} Token removed.`),console.log("")):(console.log(""),console.log(` ${v}No token found. Already logged out.${S}`),console.log(""))}else q==="--version"||q==="-v"?console.log(`@vibecodiq/cli v${ne}`):(console.log(""),console.log(` ${D}@vibecodiq/cli${S} v${ne} \u2014 Production safety for AI-built apps`),console.log(""),console.log(` ${D}Scan${S} ${v}(check what you already have)${S}`),console.log(" scan [path] Safety scan \u2014 Phase 1 (L2+L3+L4)"),console.log(" scan --architecture Architecture scan \u2014 Phase 1 (L1)"),console.log(" scan --all Full scan \u2014 Phase 1 (all layers)"),console.log(` scan --unreleased Run all checks incl. unreleased ${v}(internal)${S}`),console.log(" scan --json JSON output for CI pipelines"),console.log(` scan --fix AI fix prompts for failures ${v}(Pro)${S}`),console.log(""),console.log(` ${D}Guard${S} ${v}(build safely from day one)${S}`),console.log(" guard init Install safety rules + CI"),console.log(" guard init --architecture + ASA architecture rules"),console.log(" guard init --all Install everything"),console.log(" guard check Run architecture checks locally"),console.log(" guard upgrade Upgrade rules to latest"),console.log(""),console.log(` ${D}Account${S}`),console.log(" login Authenticate for Pro features"),console.log(" logout Sign out"),console.log(" --version Show version"),console.log(""),console.log(` ${v}Docs: https://asastandard.org/checks${S}`),console.log(` ${v}Pricing: https://vibecodiq.com/pricing${S}`),console.log(""));
|