@lunora/studio 1.0.0-alpha.82 → 1.0.0-alpha.83
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/standalone/{chunk-73GRNONE.js → chunk-7L7FXGJU.js} +1 -1
- package/dist/standalone/chunk-JOFD43OI.js +1 -0
- package/dist/standalone/{chunk-CPSGJSHY.js → chunk-KXSSTTEY.js} +1 -1
- package/dist/standalone/{chunk-UBPYN4DU.js → chunk-PD4LANXJ.js} +1 -1
- package/dist/standalone/{chunk-YSRW5RNU.js → chunk-ZI5ZVNIN.js} +1 -1
- package/dist/standalone/studio.js +1 -1
- package/package.json +2 -2
- package/dist/standalone/chunk-U457LHCO.js +0 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
import{a as Y}from"./chunk-QXXIJAD5.js";import{a as G}from"./chunk-X665E2HU.js";import{b as Q}from"./chunk-GRL5MG3Z.js";import{a as z}from"./chunk-SFDOII3B.js";import{e as P}from"./chunk-SMIESS7P.js";import"./chunk-7T7UOTYK.js";import{a as V}from"./chunk-LFKCLLPT.js";import"./chunk-CFPZYHPY.js";import"./chunk-T6VMSOBB.js";import"./chunk-7QIFDWQV.js";import{b as Z,c as ee}from"./chunk-
|
|
1
|
+
import{a as Y}from"./chunk-QXXIJAD5.js";import{a as G}from"./chunk-X665E2HU.js";import{b as Q}from"./chunk-GRL5MG3Z.js";import{a as z}from"./chunk-SFDOII3B.js";import{e as P}from"./chunk-SMIESS7P.js";import"./chunk-7T7UOTYK.js";import{a as V}from"./chunk-LFKCLLPT.js";import"./chunk-CFPZYHPY.js";import"./chunk-T6VMSOBB.js";import"./chunk-7QIFDWQV.js";import{b as Z,c as ee}from"./chunk-JOFD43OI.js";import{a as F,b as W}from"./chunk-HJD2YUQS.js";import{c as U}from"./chunk-FXCBF4XT.js";import"./chunk-2OUPA2WJ.js";import"./chunk-6ST5ECQA.js";import"./chunk-53WEPGUD.js";import"./chunk-XXQUMVWH.js";import"./chunk-RNL6BSFM.js";import"./chunk-QLQADHFE.js";import{a as E}from"./chunk-ERMTJYQF.js";import{c as A}from"./chunk-XDAT7LHC.js";import{f as J}from"./chunk-EN2QAEO4.js";import"./chunk-XBDKZZ67.js";import"./chunk-MIQTKZAR.js";import"./chunk-UZERI5YC.js";import{d as k,f as L,g as _,m as N}from"./chunk-OG4N2XHD.js";import"./chunk-AADILRJW.js";import{a as l,e as K,f as ue,g as fe,h as me}from"./chunk-3XIESWL4.js";var q=K(fe(),1);var c=K(ue(),1);var I=K(me(),1);var he=l(t=>{let{suggestedIndex:e,table:r}=t;return!(typeof r!="string"||r.length===0||typeof e!="object"||e===null||!Array.isArray(e.fields)||e.fields.length===0)},"he"),pe=l((t,e,r)=>{let a=`"${t}"`,n=`"${e}"`,d=r.map(o=>`"${o}"`).join(", ");return`CREATE INDEX IF NOT EXISTS ${n} ON ${a} (${d});`},"pe"),ge=l(t=>{let e=(0,q.c)(19),{fields:r,indexName:a,table:n,testId:d}=t,o=k(),[w,f]=(0,c.useState)(!1),m;e[0]!==r||e[1]!==a||e[2]!==n?(m=l(()=>{let y="navigator"in globalThis?globalThis.navigator.clipboard:void 0;if(y===void 0)return;let x=pe(n,a,r);N(y.writeText(x)),f(!0)},"u"),e[0]=r,e[1]=a,e[2]=n,e[3]=m):m=e[3];let p=m;if(w){let y=`${d}-applied`,x;e[4]!==o?(x=o("CREATE INDEX SQL copied to clipboard."),e[4]=o,e[5]=x):x=e[5];let $;return e[6]!==y||e[7]!==x?($=(0,I.jsx)("span",{className:"text-xs text-muted-foreground","data-testid":y,children:x}),e[6]=y,e[7]=x,e[8]=$):$=e[8],$}let g;e[9]!==o?(g=o("Apply?"),e[9]=o,e[10]=g):g=e[10];let h;e[11]!==o||e[12]!==n?(h=o("Apply index on {table}",{table:n}),e[11]=o,e[12]=n,e[13]=h):h=e[13];let v;return e[14]!==p||e[15]!==g||e[16]!==h||e[17]!==d?(v=(0,I.jsx)(Y,{confirmLabel:g,onConfirm:p,testId:d,children:h}),e[14]=p,e[15]=g,e[16]=h,e[17]=d,e[18]=v):v=e[18],v},"ge"),be=l((t,e)=>{let r=new Map;for(let a of e){let n=`${a.table} ${a.index}`;r.set(n,(r.get(n)??0)+a.reads)}return t.map(a=>({index:a.index,reads:r.get(`${a.table} ${a.index}`)??0,table:a.table}))},"be"),ve=l(t=>{let e=new Map;for(let r of t)for(let a of r.scannedTables??[])e.set(a.table,(e.get(a.table)??0)+a.scans);return[...e].map(([r,a])=>({scans:a,table:r}))},"ve"),xe=l((t,e)=>e.map(r=>({index:r.name,table:t})),"xe"),ye=l(t=>{let e=be(t.declaredIndexes??[],t.indexHits??[]),r=ve(t.functions??[]),a=t.shardTraffic??[],n=t.analyticsMetrics,d=n&&n.indexHits.length>0?n.indexHits:e,o=n&&n.tableScans.length>0?n.tableScans:r,w=n&&n.shardTraffic.length>0?n.shardTraffic:a,f=ee({indexHits:d,schema:{tables:[]},shardTraffic:w,tableScans:o},{lints:Z,source:"runtime"}),m=t.suppressHotScanTables;return(m===void 0?f:f.filter(p=>!(p.metadata.kind==="hot_scan"&&typeof p.metadata.table=="string"&&m.has(p.metadata.table)))).map(p=>F(p))},"ye"),we=L(E.listTables),Se=L(E.listTableIndexes),j=l(t=>`${(t*100).toFixed(1)}%`,"M"),te=l(t=>`${(t/1e3).toFixed(2)}s`,"D"),Te=l(t=>{if(t.length<=1)return t[0]??"";let e=t[t.length-1]??"";return`${t.slice(0,-1).join(", ")} and ${e}`},"Se"),Ie=l((t,e)=>({"high-error-rate":t("High error rate"),"high-evictions":t("High cache eviction rate"),"high-write-contention":t("High write contention"),"low-cache-hit-rate":t("Low cache hit rate"),"missing-index":t("Missing index"),"slow-function":t("Slow function")})[e.kind],"Te"),ae=l((t,e)=>({"high-error-rate":t("{rate} of calls failed.",{rate:j(e.value)}),"high-evictions":t("{count} entries evicted recently.",{count:e.value}),"high-write-contention":t("{rate} of calls hit a write conflict \u2014 consider sharding to cut contention.",{rate:j(e.value)}),"low-cache-hit-rate":t("{rate} hit rate over recent traffic.",{rate:j(e.value)}),"missing-index":t("Slowest call took {duration} \u2014 it full-scanned {tables} with no index.",{duration:te(e.value),tables:Te(e.tables??[])}),"slow-function":t("Slowest call took {duration}.",{duration:te(e.value)})})[e.kind],"X"),$e=l(t=>{let e=(0,q.c)(10),{onJump:r,table:a}=t,n=k(),d;e[0]!==r||e[1]!==a?(d=l(()=>{r(a)},"l"),e[0]=r,e[1]=a,e[2]=d):d=e[2];let o=d,w=`in-add-index-${a}`,f;e[3]!==n||e[4]!==a?(f=n("Add index on {table}",{table:a}),e[3]=n,e[4]=a,e[5]=f):f=e[5];let m;return e[6]!==o||e[7]!==w||e[8]!==f?(m=(0,I.jsx)(U,{"data-testid":w,onClick:o,size:"sm",type:"button",variant:"outline",children:f}),e[6]=o,e[7]=w,e[8]=f,e[9]=m):m=e[9],m},"Ie"),De=l(({initialShardKey:t,loadShardTraffic:e})=>{let r=J(),a=P(),n=k(),[d,o]=(0,c.useState)(t??""),[w,f]=(0,c.useState)(null),[m,p]=(0,c.useState)(null),g=V(d.trim(),400),h=(0,c.useRef)(""),v=A(E.getMetrics,{},{live:!0,shardKey:g}),y=A(E.getFunctionStats,{},{live:!0,shardKey:g}),x=A(E.getAdvisories,{},{live:!0,shardKey:g}),$=v.data??null,R=y.data?.functions??null,D=x.data?.advisories??null,ne=v.data?.indexHits??null,O=v.error!==null&&y.error!==null?v.error:null,re=e??(i=>r.shardTraffic(i)),ie=l(async(i,T)=>{let s=i[0];if(s===void 0||s===""){h.current===T&&p(null);return}try{let S=await re(s);if(h.current!==T)return;let b=new Map;for(let u of S.shards)b.has(u.shardKey)||b.set(u.shardKey,{requests:u.requests,shardKey:u.shardKey});p([...b.values()])}catch{h.current===T&&p(null)}},"V"),H=(0,c.useCallback)(async i=>{h.current=i;let T=[];try{let s=await r.query(we,{},_(i));if(h.current!==i)return;Q(i),T=s.map(u=>u.name);let S=await Promise.allSettled(s.map(async u=>[u.name,await r.query(Se,{table:u.name},_(i))]));if(h.current!==i)return;let b=[];for(let u of S)if(u.status==="fulfilled"){let[C,de]=u.value;b.push(...xe(C,de.indexes))}f(b)}catch{h.current===i&&f(null)}await ie(T,i)},[r]);(0,c.useEffect)(()=>{N(H(g))},[g,H]);let se=(0,c.useEffectEvent)(()=>{v.refetch(),y.refetch(),x.refetch(),N(H(g))});(0,c.useEffect)(()=>{let i=l(()=>{document.visibilityState==="visible"&&se()},"i");return document.addEventListener("visibilitychange",i),()=>{document.removeEventListener("visibilitychange",i)}},[]);let X=(0,c.useCallback)(i=>{N(a({search:{table:i},to:"/schema"}))},[a]),M=z($,R),oe=new Set(M.filter(i=>i.kind==="missing-index").flatMap(i=>i.tables??[])),B=ye({declaredIndexes:w??[],functions:R,indexHits:ne,shardTraffic:m,suppressHotScanTables:oe}),le=(0,c.useMemo)(()=>{let i=M.map(s=>{let S=s.kind==="missing-index"?s.tables??[]:[];return{action:S.length>0?S.map(b=>(0,I.jsx)($e,{onJump:X,table:b},b)):void 0,description:s.message===void 0?ae(n,s):`${ae(n,s)} \u2014 ${s.message}`,entity:s.fn,issueType:Ie(n,s),key:`${s.kind}:${s.fn??""}`,level:s.severity}}),T=new Set(["unindexed_foreign_key","unindexed_relation_target"]);return[...(D??[]).map(s=>{let S=F(s);if(T.has(s.name)&&he(s.metadata)){let{table:b,suggestedIndex:u}=s.metadata,C=`in-apply-index-${b}-${u.name}`;return{...S,action:(0,I.jsx)(ge,{fields:u.fields,indexName:u.name,table:b,testId:C})}}return S}),...B,...i]},[D,M,X,B,n]),ce=(0,I.jsx)(G,{onChange:o,testId:"in-shard-input",value:d});return(0,I.jsx)(W,{error:O,errorSource:O===null?void 0:v.errorSource,rows:le,testId:"lunora-insights",toolbar:ce})},"Be");export{De as InsightsPanel};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{a as r}from"./chunk-3XIESWL4.js";var zt={ERROR:20,INFO:5,WARN:10},vt={ERROR:3,INFO:1,WARN:2},jt=r(t=>Number.isFinite(t)&&t>=0?t:0,"n"),xt=r(t=>zt[t],"c"),_t=r((t,e)=>vt[t]>=vt[e]?t:e,"u"),$t=r((t,e="none")=>{let a=t.visibility==="public"?2:.5,i=t.kind==="query"?.5:a;return e==="high"?i*2:i},"h"),kt=r(t=>Math.max(1,t*.2),"g"),L=r(t=>{let e=t.reduce((a,i)=>a+jt(i.weight),0);return Math.min(100,Math.max(0,100-e))},"d"),Rt=r(t=>{let e=t.reduce((i,n)=>i+n.weight,0);if(e===0)return 100;let a=t.reduce((i,n)=>i+n.score*n.weight,0);return Math.round(a/e)},"l"),At=r(t=>t>=90?"excellent":t>=70?"good":t>=50?"needs-work":"at-risk","R"),Nt=r(t=>t===100?"clean":t>=50?"warned":"failing","m");var Xt=[{reason:"writes an identity table",test:r(t=>t.writesUserTable,"test")},{reason:"sends mail",test:r(t=>t.callsMail,"test")},{reason:"fans out to a privileged dispatch surface",test:r(t=>t.fanOut,"test")},{reason:"bypasses validators with insertManyUnsafe",test:r(t=>t.usesInsertManyUnsafe,"test")},{reason:"runs an unbounded AI generation",test:r(t=>t.unboundedAiGeneration,"test")}],St=r(t=>{let e=[];for(let a of Xt)a.test(t)&&e.push(a.reason);return{level:e.length===0?"none":"high",reasons:e}},"a");var U=r((t,e)=>t===e?0:t<e?-1:1,"S"),Et=r((t,e)=>U(t.name,e.name),"w"),It=r((t,e)=>{let a=t[e];return typeof a=="string"?a:void 0},"x"),P=r((t,e)=>`${t}#${e}`,"p"),Ht=r((t,e,a,i)=>t===void 0?{level:e,name:a,occurrences:1,weight:i}:{level:_t(t.level,e),name:a,occurrences:t.occurrences+1,weight:Math.max(t.weight,i)},"R"),Yt=r((t,e,a)=>{let i=new Map;for(let s of t)i.set(P(s.file,s.exportName),new Map);let n=new Map;for(let s of e){let l=It(s.metadata,"file"),c=It(s.metadata,"exportName"),d=(l!==void 0&&c!==void 0?i.get(P(l,c)):void 0)??n;d.set(s.name,Ht(d.get(s.name),s.level,s.name,a(s)))}return{byProcedure:new Map([...i].map(([s,l])=>[s,[...l.values()].toSorted(Et)])),project:[...n.values()].toSorted(Et)}},"A"),Bt=r((t,e)=>{let a={clean:0,exempt:0,failing:0,warned:0},i=e.checks.length;for(let n of t)a[n.coverage]+=1,i+=n.checks.length;return{clean:a.clean,exempt:a.exempt,failing:a.failing,procedures:t.length,rulesFired:i,warned:a.warned}},"I"),Tt=1,Vt=r((t,e,a={})=>{let i=new Set(a.exempt),n=Yt(t,e,m=>xt(m.level)),s=t.map(m=>{let g=P(m.file,m.exportName),v=n.byProcedure.get(g)??[],u=L(v),h=i.has(g)||m.exempt===!0,p=St(m);return{checks:v,coverage:h?"exempt":Nt(u),...h?{exemptReason:m.exemptReason??""}:{},exportName:m.exportName,file:m.file,id:g,kind:m.kind,score:u,sensitivity:p,visibility:m.visibility,weight:h?0:$t(m,p.level)}}).toSorted((m,g)=>U(m.id,g.id)),l={checks:n.project,score:L(n.project)},c=s.filter(m=>m.coverage!=="exempt"),d=c.reduce((m,g)=>m+g.weight,0),f=Rt([...c,{score:l.score,weight:kt(d)}]);return{generatedAt:a.generatedAt??new Date().toISOString(),grade:At(f),procedures:s,project:l,score:f,summary:Bt(s,l),version:Tt}},"W");var $=r(t=>{let e=new Map;return t.map(a=>{let i=(e.get(a.cacheKey)??0)+1;return e.set(a.cacheKey,i),i===1?a:{...a,cacheKey:`${a.cacheKey}:${i.toString()}`}})},"n");var o=r((t,e)=>({cacheKey:e.cacheKey,categories:e.categories??t.categories,description:t.description,detail:e.detail,facing:e.facing??t.facing,level:e.level??t.level,metadata:e.metadata,name:t.name,remediation:t.remediation,title:t.title}),"t");var k=5,q=r(t=>typeof t._id=="string"?t._id:"?","f"),K=r((t,e,a)=>t?` (sample capped at ${e.toString()} rows \u2014 ${a})`:"","m"),M=r((t,e)=>{let a=t.slice(0,k),i=e-a.length,n=a.map(s=>`"${s}"`).join(", ");return i>0?`${n} (+${i.toString()} more)`:n},"p"),Gt=r((t,e)=>{if(t==="_id")return e.existingIds;let a=new Set;for(let i of e.rows){let n=i[t];n!=null&&a.add(String(n))}return a},"v"),Qt=r((t,e,a,i)=>{if(a.kind!=="one"||i.truncated)return;let n=a.references,s=Gt(n,i),l=a.field,c=[];for(let v of e.rows){let u=v[l];if(u==null)continue;let h=String(u);s.has(h)||c.push(q(v))}if(c.length===0)return;let d=e.truncated,{cap:f}=e,m=n==="_id"?`"${a.table}"`:`"${a.table}"."${n}"`,g=`Table "${e.table}": ${c.length.toString()} sampled row(s) have a dangling FK value in column "${l}" \u2014 the referenced ${m} row does not exist. Row ids: ${M(c,c.length)}${K(d,f,"more rows may exist beyond the window")}.`;return o(t,{cacheKey:`constraint_validator:fk:${e.table}:${l}`,detail:g,metadata:{cap:f,column:l,count:c.length,examples:c.slice(0,k),kind:"fk",references:n,referencesTable:a.table,table:e.table,truncated:d}})},"x"),Jt=r((t,e,a,i)=>{if(!i)return;let n=[];for(let l of e.rows)l[a]==null&&n.push(q(l));if(n.length===0)return;let s=`Table "${e.table}": ${n.length.toString()} sampled row(s) have a null/missing value in column "${a}", which is declared as non-optional. Row ids: ${M(n,n.length)}${K(e.truncated,e.cap,"more rows may exist beyond the window")}.`;return o(t,{cacheKey:`constraint_validator:null:${e.table}:${a}`,detail:s,metadata:{cap:e.cap,column:a,count:n.length,examples:n.slice(0,k),kind:"null",table:e.table,truncated:e.truncated}})},"S"),Zt=r((t,e)=>{let a=[];for(let i of e){let n=t[i];if(n==null)return;a.push(JSON.stringify(n))}return a.join("|")},"y"),ea=r((t,e,a)=>{a.includes(e)||a.push(e),a.push(t)},"k"),ta=r((t,e,a)=>{if(a.kind!=="index"||a.unique!==!0)return;let i=new Map,n=[];for(let l of e.rows){let c=q(l),d=Zt(l,a.fields);if(d===void 0)continue;let f=i.get(d);f===void 0?i.set(d,c):ea(c,f,n)}if(n.length===0)return;let s=`Table "${e.table}": ${n.length.toString()} sampled row(s) share duplicate values on unique index "${a.name}" (${a.fields.join(", ")}). Row ids: ${M(n,n.length)}${K(e.truncated,e.cap,"duplicates beyond the window may exist")}.`;return o(t,{cacheKey:`constraint_validator:unique:${e.table}:${a.name}`,detail:s,metadata:{cap:e.cap,count:n.length,examples:n.slice(0,k),fields:a.fields,index:a.name,kind:"unique",table:e.table,truncated:e.truncated}})},"_"),aa=r((t,e,a,i)=>{let n=i.get(e.table);if(!n)return[];let s=[];for(let l of n.relations){let c=a.get(l.table);if(!c)continue;let d=Qt(t,e,l,c);d&&s.push(d)}for(let l of n.fields){let c=!n.optionalFields?.has(l),d=Jt(t,e,l,c);d&&s.push(d)}for(let l of n.indexes){let c=ta(t,e,l);c&&s.push(c)}return s},"R"),O={categories:["SCHEMA"],description:"Sampled rows violate one or more declared constraints: a foreign-key column references a non-existent row, a non-optional column contains null, or a unique-indexed column has duplicate values. These violations indicate data inserted before a constraint was enforced (e.g. a schema migration, a raw import, or a bug in a past data-migration transform).",facing:"INTERNAL",level:"WARN",name:"constraint_validator",remediation:"Inspect the listed rows and correct or remove the violating values. Run a data migration to backfill nulls, dedup unique violations, or re-link dangling FK references. For FK violations, confirm the target table rows exist before fixing the referencing rows.",run:r(t=>{if(!t.tableSamples||t.tableSamples.length===0)return[];let e=new Map;for(let i of t.tableSamples)e.set(i.table,i);let a=new Map(t.schema.tables.map(i=>[i.name,i]));return t.tableSamples.flatMap(i=>aa(O,i,e,a))},"run"),source:"runtime",title:"Constraint violation"};var ia=.5,ra=50,D={categories:["PERFORMANCE"],description:"One shard is taking a disproportionate share of a sharded function's traffic, so the busy shard becomes a single-DO bottleneck while its siblings sit idle \u2014 the hot-key skew sharding is meant to avoid.",facing:"EXTERNAL",level:"WARN",name:"hot_shard",remediation:"Re-shard on a higher-cardinality / more evenly-distributed key, or split the hot entity's state, so request volume spreads across shards instead of concentrating on one.",run:r(t=>{let e=(t.shardTraffic??[]).filter(n=>n.requests>0),a=new Map;for(let n of e){let s=n.group??"",l=a.get(s);l===void 0?a.set(s,[n]):l.push(n)}let i=[];for(let[n,s]of a){let l=s.reduce((c,d)=>c+d.requests,0);if(!(s.length<2||l<ra))for(let c of s){let d=c.requests/l;if(d<ia)continue;let f=c.group!==void 0&&c.group!==""?`"${c.group}" `:"",m=c.shardKey===""?"the root shard":`shard "${c.shardKey}"`,g=Math.round(d*100);i.push(o(D,{cacheKey:`hot_shard:${n}:${c.shardKey}`,detail:`${f}${m} handled ${c.requests.toString()} of ${l.toString()} requests (${g.toString()}%) across ${s.length.toString()} shards \u2014 a hot-key skew. Re-shard on a more evenly-distributed key.`,metadata:{group:c.group,requests:c.requests,shardCount:s.length,shardKey:c.shardKey,share:d,totalRequests:l}}))}}return i},"run"),source:"runtime",title:"Hot shard"};var na=25,R={categories:["PERFORMANCE"],description:"Recorded reads show an index the workload doesn't pay for: either a declared index no read ever used (dead overhead on every write) or a table read hot with no index at all (a repeated full scan that degrades as it grows).",facing:"INTERNAL",level:"INFO",name:"index_utilization",remediation:"Drop a dead index so writes stop maintaining it; add an index covering the hot full-scanned table's read predicate so the read stops scanning every row.",run:r(t=>{let e=[];for(let a of t.indexHits??[])a.reads>0||e.push(o(R,{cacheKey:`index_utilization:dead_index:${a.table}:${a.index}`,detail:`Index "${a.index}" on table "${a.table}" has recorded no reads since its counter was created \u2014 it's dead overhead: every write maintains it and nothing reads through it.`,metadata:{index:a.index,kind:"dead_index",reads:a.reads,table:a.table}}));for(let a of t.tableScans??[])a.scans<na||e.push(o(R,{cacheKey:`index_utilization:hot_scan:${a.table}`,detail:`Table "${a.table}" has been full-scanned ${a.scans.toString()} times (cumulative) with no index \u2014 a repeated full scan that degrades linearly as "${a.table}" grows. Add an index covering the read predicate.`,facing:"EXTERNAL",level:"WARN",metadata:{kind:"hot_scan",scans:a.scans,table:a.table}}));return e},"run"),source:"runtime",title:"Index utilization"};var W={categories:["SECURITY"],description:"An action's `ctx.fetch(url)` fetches a URL derived from the handler's `args`. `ctx.fetch` applies no host allowlist, so request-controlled URLs let a caller reach cloud metadata endpoints or internal services \u2014 server-side request forgery.",facing:"EXTERNAL",level:"ERROR",name:"action_fetch_ssrf",remediation:"Validate the URL against an allowlist of expected hosts before fetching, and reject private, loopback, and link-local addresses (notably `169.254.169.254`). Never pass request input straight to `ctx.fetch`.",run:r(t=>t.argumentDerivedFetches===void 0?[]:t.argumentDerivedFetches.map(e=>o(W,{cacheKey:`action_fetch_ssrf:${e.file}:${e.line.toString()}`,detail:`\`ctx.fetch\` in \`${e.exportName}\` (${e.file}:${e.line.toString()}) fetches a URL derived from \`args\` \u2014 a server-side request forgery vector. Validate the URL against a host allowlist and reject private/link-local targets before fetching.`,metadata:{exportName:e.exportName,file:e.file,line:e.line}})),"run"),source:"static",title:"Possible SSRF from arg-derived ctx.fetch URL"};var F={categories:["SCHEMA"],description:"An `action` calls an outbound surface (fetch, mail, queues, storage, sql, ai) with no `try`/`catch`. Those fail routinely, and an uncaught rejection reaches the caller with no indication of which dependency broke.",facing:"EXTERNAL",level:"WARN",name:"action_without_error_handling",remediation:"Wrap the outbound call in `try`/`catch`, then either degrade or rethrow a coded `LunoraError` naming the dependency that failed.",run:r(t=>{if(t.procedureProtections===void 0)return[];let e=[];for(let a of t.procedureProtections)a.kind!=="action"||a.reachesOutbound!==!0||a.handlesErrors!==!1||e.push(o(F,{cacheKey:`action_without_error_handling:${a.file}:${a.exportName}`,detail:`Action \`${a.exportName}\` (${a.file}) calls an outbound surface with no \`try\`/\`catch\`. A dependency failure will surface to the caller unexplained.`,metadata:{exportName:a.exportName,file:a.file,kind:a.kind}}));return e},"run"),source:"static",title:"Action performs outbound I/O with no error handling"};var z={categories:["SECURITY"],description:"An `httpRoute` on an admin/privileged path has no visible auth/admin guard. REST routes aren't covered by RLS, so an unguarded `/admin/*` route is callable by anyone who reaches the URL.",facing:"EXTERNAL",level:"WARN",name:"admin_route_without_guard",remediation:"Assert an authenticated, authorized caller at the top of the handler \u2014 check `ctx.auth` / `getSession(...)`, or a `requireAdmin`-style guard \u2014 and reject (401/403) before doing privileged work. For machine callers, verify a secret with a timing-safe compare.",run:r(t=>{if(t.adminRoutes===void 0)return[];let e=[];for(let a of t.adminRoutes)a.usesGuard||e.push(o(z,{cacheKey:`admin_route_without_guard:${a.file}:${a.method}:${a.path}`,detail:`Route \`${a.method} ${a.path}\` (\`${a.exportName}\` in ${a.file}) is on an admin/privileged path but its handler shows no auth/admin guard. Assert an authorized caller before doing privileged work.`,metadata:{exportName:a.exportName,file:a.file,method:a.method,path:a.path}}));return e},"run"),source:"static",title:"Admin route without an auth guard"};var j={categories:["SECURITY"],description:"A `ctx.ai.run(model, \u2026)` call selects its model from the handler's `args` with no server-side scoping. `ctx.ai.run` is the raw Workers AI binding passthrough, so an arg-derived model id lets any caller pick an arbitrary model \u2014 bypassing the typed `ctx.ai.model(...)` + AI-SDK layer's cap/schema.",facing:"EXTERNAL",level:"WARN",name:"ai_raw_run_escape_hatch",remediation:"Select the model from a fixed server-side allowlist \u2014 never from `args`. Prefer the typed `ctx.ai.model(...)` + `generateText`/`streamText` path with an output cap over the raw `ctx.ai.run` escape hatch.",run:r(t=>t.aiRawRuns===void 0?[]:t.aiRawRuns.map(e=>o(j,{cacheKey:`ai_raw_run_escape_hatch:${e.file}:${e.line.toString()}`,detail:`\`ctx.ai.run\` in \`${e.exportName}\` (${e.file}:${e.line.toString()}) selects its model from \`args\` with no server-side scoping \u2014 any caller can pick an arbitrary model, bypassing the typed AI-SDK layer's cap/schema. Select the model from a fixed server-side allowlist instead.`,metadata:{exportName:e.exportName,file:e.file,line:e.line}})),"run"),source:"static",title:"Possible arbitrary model selection from arg-derived ctx.ai.run"};var X={categories:["PERFORMANCE"],description:"A procedure runs an AI generation but emits no structured event. Model calls bill per invocation and are not reproducible, so without one there is no way to attribute spend or trace a bad answer back to its prompt.",facing:"INTERNAL",level:"INFO",name:"ai_run_without_logging",remediation:'Emit an event around the generation \u2014 `ctx.span("<name>", \u2026)`, or a `ctx.log` line carrying the model and token usage.',run:r(t=>{if(t.procedureProtections===void 0)return[];let e=[];for(let a of t.procedureProtections)a.runsAiGeneration!==!0||a.emitsEvent!==!1||e.push(o(X,{cacheKey:`ai_run_without_logging:${a.file}:${a.exportName}`,detail:`${a.kind} \`${a.exportName}\` (${a.file}) runs an AI generation with no structured event, so its cost and output cannot be attributed later.`,metadata:{exportName:a.exportName,file:a.file,kind:a.kind}}));return e},"run"),source:"static",title:"AI generation with no structured event"};var H={categories:["SECURITY"],description:"A `generateText` / `streamText` call whose prompt/messages carry user-supplied text and whose model-callable `tools` perform a privileged side effect (DB write, `ctx.run` dispatch, or outbound send). An injected instruction in the user text can steer the model into firing that action \u2014 the confused-deputy prompt-injection hazard.",facing:"EXTERNAL",level:"WARN",name:"ai_tool_side_effect_prompt_injection",remediation:"Don't let a model with user-derived input directly trigger a privileged action. Have the tool return a proposal the server validates and authorizes before executing, gate the side effect behind an explicit human/step confirmation, or keep side-effecting tools out of any generation whose prompt includes untrusted input.",run:r(t=>t.aiToolSideEffects===void 0?[]:t.aiToolSideEffects.filter(e=>e.userInputDerived).map(e=>{let a=`\`${e.exportName}\` (${e.file}:${e.line.toString()})`;return o(H,{cacheKey:`ai_tool_side_effect_prompt_injection:${e.file}:${e.line.toString()}`,detail:`\`${e.method}\` in ${a} feeds user-derived input to a model whose tools reach \`${e.sideEffect}\`. An injected instruction can steer the model into firing that side effect.`,metadata:{exportName:e.exportName,file:e.file,line:e.line,method:e.method,sideEffect:e.sideEffect}})}),"run"),source:"static",title:"AI tool side effect reachable via prompt injection"};var Y={categories:["SECURITY"],description:"A public procedure runs an AI generation (`generateText`/`streamText`/`generateObject`/`streamObject`) with no `maxOutputTokens` bound. Generation bills per output token, so an anonymous caller can request arbitrarily long completions in a loop \u2014 a denial-of-wallet vector that also ties up worker time on long streams.",facing:"EXTERNAL",level:"WARN",name:"ai_unbounded_generation_public",remediation:"Cap the completion with `maxOutputTokens` in the generation config, and rate-limit the public entry point (`.use(rateLimit(...))`). For expensive models, prefer running generation from an internal, authenticated path rather than exposing it directly to anonymous callers.",run:r(t=>t.procedureProtections===void 0?[]:t.procedureProtections.filter(e=>e.unboundedAiGeneration&&e.visibility==="public").map(e=>o(Y,{cacheKey:`ai_unbounded_generation_public:${e.file}:${e.exportName}`,detail:`\`${e.exportName}\` (${e.file}) is public and runs an AI generation with no \`maxOutputTokens\` bound \u2014 an anonymous caller can drive unbounded, billable completions in a loop. Add a \`maxOutputTokens\` cap and rate-limit the entry point.`,metadata:{exportName:e.exportName,file:e.file,kind:e.kind}})),"run"),source:"static",title:"Public procedure runs unbounded AI generation (no maxOutputTokens)"};var B={categories:["SECURITY"],description:"`.extend(() => ({ allowUnauthenticatedShardAccess: true }))` disables the fail-closed default that denies an unauthenticated shard lookup. Combined with a schema that doesn't enforce `.rls(\"required\")` everywhere (or that leaves a table `.public()`), an unauthenticated caller can shard-hop into another tenant's rows with no row-security guard behind the door.",facing:"EXTERNAL",level:"WARN",name:"allow_unauthenticated_shard_access_enabled",remediation:"Prefer `authorizeShard` / `authorizeFanOut` (which take precedence over `allowUnauthenticatedShardAccess` and let you allow specific unauthenticated cases) over the blanket opt-out. If the app genuinely needs open shard access, enforce `.rls(\"required\")` on the schema with no `.public()` tables so a missing identity still can't read another tenant's rows.",run:r(t=>t.configCalls===void 0?[]:t.schema.rlsMode!=="required"||t.schema.tables.some(e=>e.isPublic)?t.configCalls.filter(e=>e.callee==="extend"&&e.trueKeys.includes("allowUnauthenticatedShardAccess")).map(e=>o(B,{cacheKey:`allow_unauthenticated_shard_access_enabled:${e.file}:${e.line.toString()}`,detail:`\`.extend(...)\` in ${e.file}:${e.line.toString()} sets \`allowUnauthenticatedShardAccess: true\`, and the schema has an RLS gap (no \`.rls("required")\`, or a \`.public()\` table) \u2014 an unauthenticated caller can shard-hop into another tenant's rows with no row-security guard behind the door.`,metadata:{callee:e.callee,file:e.file,line:e.line}})):[],"run"),source:"static",title:"Unauthenticated shard access enabled on an RLS-gapped schema"};var V={categories:["SECURITY"],description:"A `ctx.authApi.<method>(...)` call omits `headers` \u2014 better-auth skips session authorization and runs the call with full server-to-server privileges, regardless of the caller's identity.",facing:"EXTERNAL",level:"WARN",name:"auth_api_call_without_headers",remediation:"Pass the inbound `headers` from the incoming request into every `ctx.authApi.*` call: `ctx.authApi.<method>({ body, headers: request.headers })`. See the `withAuthPlugins` middleware doc for details.",run:r(t=>{if(t.authApiCalls===void 0)return[];let e=[],a=new Map;for(let i of t.authApiCalls){if(i.hasHeaders)continue;let n=`${i.file}:${i.line.toString()}:${i.method}`,s=(a.get(n)??0)+1;a.set(n,s);let l=s>1?`:${s.toString()}`:"";e.push(o(V,{cacheKey:`auth_api_call_without_headers:${n}${l}`,detail:`\`ctx.authApi.${i.method||"<method>"}(\u2026)\` in ${i.exportName} (${i.file}:${i.line.toString()}) is called without \`headers\` \u2014 better-auth skips session authorization, so this runs with full privileges. Pass the inbound \`headers\`.`,metadata:{exportName:i.exportName,file:i.file,line:i.line,method:i.method}}))}return e},"run"),source:"static",title:"Privileged ctx.authApi call missing headers"};var G={categories:["SECURITY"],description:"A `createAuth({...})` call sets `advanced.disableCSRFCheck: true`, turning off better-auth's origin validation for state-changing auth requests \u2014 a cross-site request riding the ambient auth cookie is processed like a same-origin one.",facing:"EXTERNAL",level:"ERROR",name:"auth_csrf_check_disabled",remediation:"Remove `advanced.disableCSRFCheck: true` from `createAuth({...})` and rely on `trustedOrigins` for origin validation instead of disabling the check.",run:r(t=>t.authConfigs===void 0?[]:t.authConfigs.filter(e=>e.analyzable&&e.disableCsrfCheck).map(e=>o(G,{cacheKey:`auth_csrf_check_disabled:${e.file}:${e.line.toString()}`,detail:`\`createAuth\` in \`${e.exportName}\` (${e.file}:${e.line.toString()}) sets \`advanced.disableCSRFCheck: true\`, turning off origin validation for state-changing auth requests. Remove the flag and rely on \`trustedOrigins\` instead.`,metadata:{exportName:e.exportName,file:e.file,line:e.line}})),"run"),source:"static",title:"createAuth CSRF check disabled"};var Q={categories:["SECURITY"],description:"A `createAuth({...})` call enables `emailAndPassword` with no `requireEmailVerification: true`. better-auth defaults email verification off, so an account is usable before the caller has proven ownership of the email address.",facing:"EXTERNAL",level:"WARN",name:"auth_email_verification_disabled",remediation:"Set `emailAndPassword.requireEmailVerification: true` on `createAuth({...})` so a new account can't act until its email address is verified.",run:r(t=>t.authConfigs===void 0?[]:t.authConfigs.filter(e=>e.analyzable&&e.emailPasswordEnabled&&!e.requireEmailVerification).map(e=>o(Q,{cacheKey:`auth_email_verification_disabled:${e.file}:${e.line.toString()}`,detail:`\`createAuth\` in \`${e.exportName}\` (${e.file}:${e.line.toString()}) enables \`emailAndPassword\` with no \`requireEmailVerification: true\`, so an account is usable before its email is proven. Set \`emailAndPassword.requireEmailVerification: true\`.`,metadata:{exportName:e.exportName,file:e.file,line:e.line}})),"run"),source:"static",title:"createAuth email verification disabled"};var J={categories:["SECURITY"],description:"A `createAuth({...})` call loads `scim()` with a `database` that has no native transactions (`lunoraD1Adapter` / `lunoraAuthAdapter`). `@better-auth/scim` refuses to serve on such an adapter, so every SCIM request fails.",facing:"EXTERNAL",level:"ERROR",name:"auth_scim_without_transactions",remediation:"Move the auth tables somewhere with real transactions: `.auth({ namespace })` with a `LunoraAuthDO` (Durable Object storage), or a Postgres/MySQL kysely dialect over `@lunora/hyperdrive` with `transaction: true`. D1 cannot host SCIM.",run:r(t=>t.authConfigs===void 0?[]:t.authConfigs.filter(e=>e.analyzable&&e.scimOnNonTransactionalAdapter).map(e=>o(J,{cacheKey:`auth_scim_without_transactions:${e.file}:${e.line.toString()}`,detail:`\`createAuth\` in \`${e.exportName}\` (${e.file}:${e.line.toString()}) loads \`scim()\` on an adapter without native transactions, so the SCIM plugin will refuse to serve and every provisioning request from your IdP fails. Host the auth tables in a Durable Object (\`.auth({ namespace })\`) or on Postgres/MySQL via \`@lunora/hyperdrive\`.`,metadata:{exportName:e.exportName,file:e.file,line:e.line}})),"run"),source:"static",title:"SCIM configured on an adapter without transactions"};var Z={categories:["SECURITY"],description:"A `createAuth({...})` call sets `advanced.useSecureCookies: false`, overriding Lunora's secure-by-default posture \u2014 the session cookie ships without `Secure` even on an HTTPS deployment, so it can be sent over a plaintext connection.",facing:"EXTERNAL",level:"ERROR",name:"auth_secure_cookies_disabled",remediation:"Remove `advanced.useSecureCookies: false` from `createAuth({...})` \u2014 let Lunora's secure-by-default posture set the `Secure` cookie attribute for an HTTPS deployment.",run:r(t=>t.authConfigs===void 0?[]:t.authConfigs.filter(e=>e.analyzable&&e.secureCookiesDisabled).map(e=>o(Z,{cacheKey:`auth_secure_cookies_disabled:${e.file}:${e.line.toString()}`,detail:`\`createAuth\` in \`${e.exportName}\` (${e.file}:${e.line.toString()}) sets \`advanced.useSecureCookies: false\`, so the session cookie ships without \`Secure\` even on an HTTPS deployment. Remove the override and let Lunora's secure-by-default posture apply.`,metadata:{exportName:e.exportName,file:e.file,line:e.line}})),"run"),source:"static",title:"createAuth secure cookies disabled"};var ee={categories:["SECURITY"],description:"A `createAuth({...})` call sets `session.freshAge: 0`, disabling better-auth's recent-reauth check for sensitive operations \u2014 a long-lived stolen session can perform them without proving the caller still controls the credentials.",facing:"EXTERNAL",level:"WARN",name:"auth_session_freshage_zero",remediation:"Remove `session.freshAge: 0` from `createAuth({...})` (or set it to a real window) so sensitive operations require a recently re-authenticated session.",run:r(t=>t.authConfigs===void 0?[]:t.authConfigs.filter(e=>e.analyzable&&e.sessionFreshAgeZero).map(e=>o(ee,{cacheKey:`auth_session_freshage_zero:${e.file}:${e.line.toString()}`,detail:`\`createAuth\` in \`${e.exportName}\` (${e.file}:${e.line.toString()}) sets \`session.freshAge: 0\`, disabling the recent-reauth check for sensitive operations. Remove the override or set a real window.`,metadata:{exportName:e.exportName,file:e.file,line:e.line}})),"run"),source:"static",title:"createAuth session freshAge zero"};var te={categories:["SECURITY"],description:"A `createAuth({...})` call's `trustedOrigins` array contains a `\"*\"` entry, disabling better-auth's CSRF/origin validation entirely \u2014 any origin is accepted for state-changing auth requests.",facing:"EXTERNAL",level:"ERROR",name:"auth_trusted_origins_wildcard",remediation:'List the specific origins your deployment serves in `trustedOrigins` instead of `"*"` \u2014 a wildcard disables origin validation for every caller.',run:r(t=>t.authConfigs===void 0?[]:t.authConfigs.filter(e=>e.analyzable&&e.trustedOriginsWildcard).map(e=>o(te,{cacheKey:`auth_trusted_origins_wildcard:${e.file}:${e.line.toString()}`,detail:`\`createAuth\` in \`${e.exportName}\` (${e.file}:${e.line.toString()}) sets \`trustedOrigins\` to include \`"*"\`, disabling CSRF/origin validation for every request. List the specific origins your deployment serves instead.`,metadata:{exportName:e.exportName,file:e.file,line:e.line}})),"run"),source:"static",title:"createAuth trustedOrigins wildcard"};var ae={categories:["SECURITY"],description:"`createBrowser({ allowPrivateTargets: true })` disables the private/internal-IP SSRF guard, letting a navigated URL reach cloud metadata endpoints, internal services, or loopback. Combined with a request-supplied URL this is a server-side request forgery path.",facing:"EXTERNAL",level:"ERROR",name:"browser_allow_private_targets",remediation:"Remove `allowPrivateTargets: true` from `createBrowser({...})` so the private/internal-address guard stays on. If a specific internal host is genuinely required, pin it with an `allowedHosts` allowlist instead of opening all private targets.",run:r(t=>t.configCalls===void 0?[]:t.configCalls.filter(e=>e.callee==="createBrowser"&&e.trueKeys.includes("allowPrivateTargets")).map(e=>o(ae,{cacheKey:`browser_allow_private_targets:${e.file}:${e.line.toString()}`,detail:`\`createBrowser({ allowPrivateTargets: true })\` in ${e.file}:${e.line.toString()} disables the private/internal-address SSRF guard \u2014 a navigated URL can then reach cloud metadata, internal services, or loopback. Remove the override, or pin the specific host with \`allowedHosts\` instead.`,metadata:{callee:e.callee,file:e.file,line:e.line}})),"run"),source:"static",title:"Browser with private-target SSRF guard disabled"};var b=r(t=>{let e={categories:t.categories,description:t.description,facing:t.facing,level:t.level,name:t.name,remediation:t.remediation,run:r(a=>{let i=t.getAccesses(a);return i===void 0||t.suppressWhen?.(a)===!0?[]:i.map(n=>o(e,{cacheKey:t.cacheKey(n),detail:t.detail(n),metadata:t.metadata(n)}))},"run"),source:"static",title:t.title};return e},"s");var Ct=b({cacheKey:r(t=>`browser_user_url_without_allowlist:${t.file}:${t.line.toString()}`,"cacheKey"),categories:["SECURITY"],description:"A `ctx.browser.<method>(url, \u2026)` call navigates to a URL derived from the handler's `args` with no server-side scoping, and no `createBrowser` allowlist contains it. The default guard blocks private targets but not arbitrary public URLs, so any caller can make the headless browser an open-proxy / SSRF tool (fetch arbitrary third-party URLs, DNS-rebind to internal hosts).",detail:r(t=>`\`ctx.browser.${t.method}\` in \`${t.exportName}\` (${t.file}:${t.line.toString()}) navigates to a URL derived from \`args\` with no server-side scoping, and no \`createBrowser\` allowlist contains it \u2014 the default guard blocks private targets but not arbitrary public URLs, so any caller can turn the headless browser into an open-proxy / SSRF tool. Pin \`allowedHosts\` (and/or \`resolveDns\`) on \`createBrowser({...})\`, and derive the URL from server-trusted state where possible.`,"detail"),facing:"EXTERNAL",getAccesses:r(t=>t.browserUrlAccesses,"getAccesses"),level:"WARN",metadata:r(t=>({exportName:t.exportName,file:t.file,line:t.line,method:t.method}),"metadata"),name:"browser_user_url_without_allowlist",remediation:"Pin the browser with an `allowedHosts` allowlist (and/or `resolveDns`) on `createBrowser({...})`, and derive the navigation URL from server-trusted state where possible rather than passing `args` straight to `ctx.browser`.",suppressWhen:r(t=>(t.configCalls??[]).some(e=>e.callee==="createBrowser"&&e.analyzable&&(e.presentKeys.includes("allowedHosts")||e.presentKeys.includes("resolveDns"))),"suppressWhen"),title:"Browser navigates to arg-derived URL with no allowlist"});var oa=100,ie={categories:["SCHEMA"],description:"A chain of foreign-key relations forms a cycle (e.g. A \u2192 B \u2192 C \u2192 A). Circular FK dependencies can cause unexpected behavior during DELETE operations \u2014 a CASCADE chain may loop indefinitely, and even a RESTRICT cycle prevents deletion of any row in the loop without temporarily disabling constraints.",facing:"INTERNAL",level:"WARN",name:"circular_fk",remediation:"Remove or break the cycle by dropping at least one FK relation from the loop. Consider replacing the circular dependency with a nullable FK and explicit application logic, or restructure the schema using a junction table.",run:r(t=>{let e=[],a=[...new Set(t.schema.tables.map(u=>u.name))].sort(),i=new Map(a.map((u,h)=>[u,h])),n=new Map;for(let u of t.schema.tables)for(let h of u.relations){if(h.kind!=="one"||!i.has(h.table))continue;let p=n.get(u.name);p===void 0&&(p=new Set,n.set(u.name,p)),p.add(h.table)}let s=new Set,l=new Map,c=[],d=new Set,f=!1,m=r(u=>{s.delete(u);let h=l.get(u);if(h!==void 0)for(let p of h)h.delete(p),s.has(p)&&m(p)},"m"),g=r(()=>{let u=[...c];if(u.length<2)return;let h=0;for(let w=1;w<u.length;w+=1)u[w]<u[h]&&(h=w);let p=[...u.slice(h),...u.slice(0,h)],x=p.join("\u2192");if(d.has(x))return;d.add(x);let y=[...p,p[0]].join(" \u2192 ");e.push(o(ie,{cacheKey:`circular_fk:${x}`,detail:`Circular foreign-key dependency detected: ${y}. This cycle can cause unexpected behavior during DELETE operations.`,metadata:{cycle:p,path:y,tables:p}})),d.size>=oa&&(f=!0)},"b"),v=r((u,h,p)=>{let x=!1;c.push(u),s.add(u);for(let y of n.get(u)??[]){if(f)break;i.get(y)<p||(y===h?(g(),x=!0):!s.has(y)&&v(y,h,p)&&(x=!0))}if(x)m(u);else for(let y of n.get(u)??[]){if(i.get(y)<p)continue;let w=l.get(y);w===void 0&&(w=new Set,l.set(y,w)),w.add(u)}return c.pop(),x},"y");for(let u=0;u<a.length&&!f;u+=1)s.clear(),l.clear(),v(a[u],a[u],u);return e},"run"),source:"static",title:"Circular foreign-key dependency"};var Lt=b({cacheKey:r(t=>`container_instance_key_from_user_input:${t.file}:${t.line.toString()}`,"cacheKey"),categories:["SECURITY"],description:"A `ctx.containers.<name>.get` call routes to a container instance using a key derived from the handler's `args` with no server-side scoping. Any caller can supply another tenant's key and reach that tenant's container instance \u2014 a cross-tenant insecure direct object reference (IDOR).",detail:r(t=>`\`ctx.containers.*.${t.method}\` in \`${t.exportName}\` (${t.file}:${t.line.toString()}) routes to a container instance using a key derived from \`args\` with no server-side scoping \u2014 any caller can reach another tenant's container (IDOR). Derive the key from a server-trusted identity (e.g. \`\${ctx.auth.userId}\`).`,"detail"),facing:"EXTERNAL",getAccesses:r(t=>t.containerKeyAccesses,"getAccesses"),level:"WARN",metadata:r(t=>({exportName:t.exportName,file:t.file,line:t.line,method:t.method}),"metadata"),name:"container_instance_key_from_user_input",remediation:"Derive the container instance key from a server-trusted identity (e.g. `${ctx.auth.userId}`) or a record the caller owns \u2014 never pass request input straight to `ctx.containers.<name>.get`.",title:"Possible cross-tenant IDOR from arg-derived unscoped container key"});var sa=new Set(["standard-3","standard-4"]),la=2,ca=4096,da=r(t=>typeof t=="string"?sa.has(t):(t.vcpu??0)>la||(t.memoryMib??0)>ca,"d"),re={categories:["PERFORMANCE"],description:"A container is declared on a large instance type, whose provisioned memory and disk are billed for the lifetime of every running instance.",facing:"INTERNAL",level:"INFO",name:"container_oversized_instance",remediation:"Confirm the workload needs this size; a smaller instanceType (lite/basic/standard-1) costs less while idle-but-running.",run:r(t=>{let e=[];for(let a of t.containers??[]){let{instanceType:i}=a;if(i===void 0||!da(i))continue;let n=typeof i=="string"?i:JSON.stringify(i);e.push(o(re,{cacheKey:`container_oversized_instance:${a.exportName}`,detail:`Container "${a.exportName}" uses a large instance type (${n}); its memory + disk are billed while any instance runs.`,metadata:{container:a.exportName,instanceType:i}}))}return e},"run"),source:"static",title:"Oversized container instance"};var ne={categories:["SECURITY"],description:"A container leaves outbound internet access at the default (enabled), which is billed per GB of egress and widens the attack surface.",facing:"INTERNAL",level:"INFO",name:"container_public_internet",remediation:"Set `enableInternet: false` on the container if it doesn't call external services, or `true` to opt in deliberately.",run:r(t=>{let e=[];for(let a of t.containers??[])a.enableInternet===void 0&&e.push(o(ne,{cacheKey:`container_public_internet:${a.exportName}`,detail:`Container "${a.exportName}" doesn't set enableInternet, so outbound internet is on by default (egress is billed).`,metadata:{container:a.exportName}}));return e},"run"),source:"static",title:"Container egress enabled by default"};var oe={categories:["SECURITY"],description:"A runtime `.egress.allow(...)` / `.egress.deny(...)` / `.egress.setAllowed(...)` call mutates the container's egress firewall at runtime, re-opening a network surface the static analysis assumes is locked to the `defineContainer` declaration.",facing:"EXTERNAL",level:"WARN",name:"container_runtime_egress_relaxation",remediation:"Prefer declaring the egress policy statically in `lunora/containers.ts` where it is auditable. When a runtime mutation is unavoidable, scope it behind a server-trusted check and keep the change narrow (a specific host, not a broad allow) rather than an unconditional relaxation.",run:r(t=>t.containerOverrides===void 0?[]:t.containerOverrides.filter(e=>e.kind==="egress_relaxation").map(e=>o(oe,{cacheKey:`container_runtime_egress_relaxation:${e.file}:${e.line.toString()}`,detail:`\`${e.exportName}\` calls \`.egress.${e.detail}(...)\` at runtime, mutating the container's egress firewall.`,metadata:{exportName:e.exportName,file:e.file,line:e.line,method:e.detail}})),"run"),source:"static",title:"Runtime egress mutation relaxes the container firewall"};var se={categories:["SECURITY"],description:"A runtime `.start({ enableInternet: true, \u2026 })` call overrides the static `defineContainer` declaration and re-opens public internet egress for that container instance, defeating the `container_public_internet` lint's assumption that the declaration is authoritative.",facing:"EXTERNAL",level:"WARN",name:"container_start_enable_internet_override",remediation:"Avoid overriding `enableInternet` at `.start()` time. Set it once on the `defineContainer` declaration in `lunora/containers.ts` so the static advisors can reason about it, and gate any runtime opt-in behind a server-trusted check rather than an unconditional literal.",run:r(t=>t.containerOverrides===void 0?[]:t.containerOverrides.filter(e=>e.kind==="enable_internet").map(e=>o(se,{cacheKey:`container_start_enable_internet_override:${e.file}:${e.line.toString()}`,detail:`\`${e.exportName}\` calls \`.start({ enableInternet: true })\`, overriding the container's static egress posture.`,metadata:{exportName:e.exportName,file:e.file,line:e.line}})),"run"),source:"static",title:"Runtime .start() override re-enables container internet access"};var ua=r((t,e)=>t.length>e.length?!1:t.every((a,i)=>a===e[i]),"r"),le={categories:["PERFORMANCE"],description:"A secondary index is redundant because another index already covers every lookup it serves (its columns are a leading prefix of the other's). The redundant index costs storage and is maintained on every write for no read benefit.",facing:"INTERNAL",level:"INFO",name:"duplicate_index",remediation:"Drop the redundant index; the covering index already serves its lookups.",run:r(t=>{let e=[];for(let a of t.schema.tables){let i=a.indexes.filter(n=>n.kind==="index");for(let n of i){if(n.unique===!0)continue;let s=i.find(l=>l===n||!ua(n.fields,l.fields)?!1:n.fields.length===l.fields.length?n.name>l.name:!0);s&&e.push(o(le,{cacheKey:`duplicate_index:${a.name}:${n.name}`,detail:`Index "${n.name}" on table "${a.name}" (${n.fields.join(", ")}) is redundant \u2014 index "${s.name}" (${s.fields.join(", ")}) already covers its lookups.`,metadata:{coveredBy:{fields:s.fields,name:s.name},fields:n.fields,index:n.name,table:a.name}}))}}return e},"run"),source:"static",title:"Duplicate / redundant index"};var ce={categories:["SCHEMA"],description:"An index declares no columns, so it indexes nothing and can never narrow a read.",facing:"INTERNAL",level:"WARN",name:"empty_index",remediation:"Give the index its columns, or remove it.",run:r(t=>{let e=[];for(let a of t.schema.tables)for(let i of a.indexes)i.kind!=="index"||i.fields.length>0||e.push(o(ce,{cacheKey:`empty_index:${a.name}:${i.name}`,detail:`Index "${i.name}" on table "${a.name}" declares no columns.`,metadata:{index:i.name,table:a.name}}));return e},"run"),source:"static",title:"Empty index"};var de={categories:["SCHEMA"],description:"A procedure throws a bare `new Error(...)`. It reaches the client as an opaque message the caller cannot branch on, and error grouping cannot fingerprint it into a stable issue.",facing:"EXTERNAL",level:"WARN",name:"error_without_catalog",remediation:'Throw a coded error instead: `throw new LunoraError("<CODE>", { \u2026 })` from `@lunora/errors`, adding the code to `ERROR_CATALOG` if it is new.',run:r(t=>{if(t.procedureProtections===void 0)return[];let e=[];for(let a of t.procedureProtections)a.throwsBareError===!0&&e.push(o(de,{cacheKey:`error_without_catalog:${a.file}:${a.exportName}`,detail:`${a.kind} \`${a.exportName}\` (${a.file}) throws a bare \`new Error(...)\`. Use \`LunoraError\` with a catalog code so the client can branch on it and Studio can group it.`,metadata:{exportName:a.exportName,file:a.file,kind:a.kind}}));return e},"run"),source:"static",title:"Bare Error thrown instead of a coded LunoraError"};var ma={defineExportSink:["name","deliver"],r2Sink:["bucket","name"],webhookExportSink:["name","url"]},ue={categories:["SCHEMA"],description:"A CDC export sink (`defineExportSink` / `webhookExportSink` / `r2Sink`) is missing a required config field (a webhook with no `url`, an R2 sink with no `bucket`, or a sink with no `name`/`deliver`). The sink can never deliver a change batch, so the export tap silently drains nothing.",facing:"INTERNAL",level:"ERROR",name:"export_sink_misconfigured",remediation:"Supply the missing field: a non-empty `url` for `webhookExportSink`, a `bucket` R2 binding for `r2Sink`, or `name` + `deliver` for `defineExportSink`.",run:r(t=>{if(t.exportSinks===void 0)return[];let e=[];for(let a of t.exportSinks){if(!a.analyzable)continue;let i=new Set(a.presentKeys),n=new Set(a.emptyKeys);for(let s of ma[a.factory]??[]){if(i.has(s)&&!n.has(s))continue;let l=n.has(s)?"is set to an empty string":"is missing";e.push(o(ue,{cacheKey:`export_sink_misconfigured:${a.file}:${a.line.toString()}:${s}`,detail:`\`${a.factory}({ \u2026 })\` in ${a.file}:${a.line.toString()} ${l} its required \`${s}\` field \u2014 the sink can never deliver a change batch, so the export tap drains nothing.`,metadata:{factory:a.factory,field:s,file:a.file,line:a.line}}))}}return e},"run"),source:"static",title:"Export sink is misconfigured"};var A={categories:["SCHEMA"],description:"A `.source({ mode: 'incremental' })` table declares no delete-visibility path (`reconcileEveryMs` or `softDeleteColumn`), so upstream deletes are never applied and the materialized table accumulates phantom rows.",facing:"EXTERNAL",level:"ERROR",name:"external_source_incremental_no_delete_path",remediation:"Add `reconcileEveryMs` (a periodic full-pull sweep that GCs vanished rows) or `softDeleteColumn` (an upstream tombstone column the incremental pull returns). Incremental ingest can't see a delete otherwise.",run:r(t=>{let e=[];for(let a of t.schema.tables){let i=a.externalSource;if(i!==void 0){if(i.unanalyzable){e.push(o(A,{cacheKey:`external_source_incremental_no_delete_path:${a.name}`,detail:`Table \`${a.name}\`'s \`.source(...)\` config is not a static object literal, so its \`mode\`/delete-visibility can't be verified. If it is \`mode: "incremental"\`, confirm it declares \`reconcileEveryMs\` or \`softDeleteColumn\` \u2014 an incremental source without one accumulates phantom rows on every upstream delete.`,level:"WARN",metadata:{table:a.name}}));continue}i.mode!=="incremental"||i.hasReconcile||i.hasSoftDelete||e.push(o(A,{cacheKey:`external_source_incremental_no_delete_path:${a.name}`,detail:`Table \`${a.name}\` is \`.source({ mode: "incremental" })\` but declares neither \`reconcileEveryMs\` nor \`softDeleteColumn\` \u2014 an incremental pull never sees upstream deletes, so the materialized table would accumulate phantom rows. Add one of the two delete-visibility paths.`,metadata:{table:a.name}}))}}return e},"run"),source:"static",title:"Incremental sourced table has no delete-visibility path"};var me={categories:["SCHEMA"],description:"A table is both `.source(...)` and `.global()`. A global table lives in the external/global tier; a sourced table is materialized into a shard DO's SQLite \u2014 the two are mutually exclusive.",facing:"INTERNAL",level:"ERROR",name:"external_source_on_global",remediation:'Drop either `.source(...)` or `.global()`. To ingest an external database into per-tenant DOs, use `.source(...)` with `.shardBy(...)`; to read it in place as a global table, use `.global({ backend: "hyperdrive" })`.',run:r(t=>{let e=[];for(let a of t.schema.tables)a.externalSource===void 0||a.shardKind!=="global"||e.push(o(me,{cacheKey:`external_source_on_global:${a.name}`,detail:`Table \`${a.name}\` is both \`.source(...)\` and \`.global()\` \u2014 contradictory. A sourced table materializes into a shard DO's SQLite; a global table lives in the external tier.`,metadata:{table:a.name}}));return e},"run"),source:"static",title:"Sourced table cannot also be global"};var N={categories:["SECURITY"],description:"A `.source(...)` table that is also `.shardBy(...)` has no `tenantBy` mapper, so every tenant's Durable Object runs the same unscoped query and replicates the whole multitenant table \u2014 a cross-tenant data leak.",facing:"EXTERNAL",level:"ERROR",name:"external_source_unscoped",remediation:"Add `tenantBy: (shardKey) => [shardKey]` (matching the query's bound parameters) so each shard DO pulls only its own tenant's rows. The shard key MUST bind into the source `WHERE`.",run:r(t=>{let e=[];for(let a of t.schema.tables)if(!(a.externalSource===void 0||a.shardKind!=="shardBy")){if(a.externalSource.unanalyzable){e.push(o(N,{cacheKey:`external_source_unscoped:${a.name}`,detail:`Table \`${a.name}\` is \`.source(...)\` + \`.shardBy(...)\` but its source config is not a static object literal, so \`tenantBy\` can't be verified. Confirm \`tenantBy\` binds the shard key into the query, or inline the config so the linter can check it \u2014 an unscoped query leaks every tenant's rows into each DO.`,level:"WARN",metadata:{table:a.name}}));continue}a.externalSource.hasTenantBy||e.push(o(N,{cacheKey:`external_source_unscoped:${a.name}`,detail:`Table \`${a.name}\` is \`.source(...)\` + \`.shardBy(...)\` but declares no \`tenantBy\` \u2014 every tenant DO would pull the whole table. Add a \`tenantBy\` that binds the shard key into the query.`,metadata:{table:a.name}}))}return e},"run"),source:"static",title:"Sourced + sharded table is not tenant-scoped"};var he={categories:["PERFORMANCE"],description:"A query filters on `_id`, the primary key. That walks the table comparing every row's id, when `ctx.db.get(id)` fetches the row directly. There is no case where the scan is preferable.",facing:"EXTERNAL",level:"WARN",name:"filter_on_primary_key",remediation:'Replace `ctx.db.query("table").filter((d) => d._id === id).first()` with `ctx.db.get(id)`. Passing a typed `Id<"table">` also narrows the result to that table\'s `Doc`, where the scan form returns the shared row type.',run:r(t=>{let e=[];for(let a of t.queries??[]){if(a.filtersPrimaryKey!==!0||a.table==="")continue;let i=a.line>0?`${a.file}:${a.line.toString()}`:a.file;e.push(o(he,{cacheKey:`filter_on_primary_key:${a.file}:${a.line.toString()}:${a.table}`,detail:`Query on "${a.table}" at ${i} filters on \`_id\` \u2014 it scans "${a.table}" to find a row \`ctx.db.get(id)\` addresses directly.`,metadata:{exportName:a.exportName,file:a.file,line:a.line,table:a.table}}))}return e},"run"),source:"static",title:"Filter on primary key instead of ctx.db.get"};var S={categories:["PERFORMANCE"],description:"A query calls `.filter()` without a `.withIndex()` / `.withSearchIndex()`, so it loads every row in the table and filters in memory \u2014 a full table scan that gets linearly slower as the table grows.",facing:"EXTERNAL",level:"WARN",name:"filter_without_index",remediation:'Narrow the read with `.withIndex("name", (q) => q.eq(...))` first, then `.filter()` only for what the index cannot express.',run:r(t=>{let e=[],a=new Map(t.schema.tables.map(i=>[i.name,i.shardKind]));for(let i of t.queries??[]){if(!i.hasFilter||i.hasIndex||i.table===""||i.filtersPrimaryKey===!0)continue;let n=i.line>0?`${i.file}:${i.line.toString()}`:i.file,s=a.get(i.table),l={exportName:i.exportName,file:i.file,line:i.line,shardKind:s??"unknown",table:i.table},c=`filter_without_index:${i.file}:${i.line.toString()}:${i.table}`;if(s==="shardBy"){e.push(o(S,{cacheKey:c,detail:`Query on "${i.table}" at ${n} calls .filter() without an index. "${i.table}" is \`.shardBy()\`, so the read is already scoped to one shard rather than the whole table \u2014 this is bounded by one tenant's row count, not the dataset. Add an index if that per-shard count grows large.`,level:"INFO",metadata:l}));continue}let d={global:`it scans the whole D1 table "${i.table}" \u2014 unbounded, and the cost is a cross-region round trip`,root:`it loads every row of "${i.table}" from the root Durable Object's SQLite and filters in memory`}[s]??`it loads every row of "${i.table}" and filters in memory`;e.push(o(S,{cacheKey:c,detail:`Query on "${i.table}" at ${n} calls .filter() without an index \u2014 ${d}.`,metadata:l}))}return e},"run"),source:"static",title:"Filter without index"};var ha=new Set(["enforce","gate","lockdown","rls"]),pa=new Set(["allow","bypass","permit"]),fa=/(?<=[A-Z])(?=[A-Z][a-z])/gu,ga=/(?<=[a-z0-9])(?=[A-Z])/gu,ba=/[^a-z0-9]+/u,ya=r(t=>t.replaceAll(fa," ").replaceAll(ga," ").toLowerCase().split(ba).filter(e=>e.length>0),"d"),wa=r(t=>{let e=ya(t.key),a=e.some(n=>ha.has(n)),i=e.some(n=>pa.has(n));return a===i?!1:a?!t.defaultValue:t.defaultValue},"c"),pe={categories:["SECURITY"],description:"A `ctx.flags.boolean(key, default)` read on a security-shaped key has a fail-open default that selects the permissive branch. OpenFeature returns the default when the provider errors, so an outage silently disables a protection (a `false` default on an `enforce`/`rls`/`gate`/`lockdown` key) or grants a permission (a `true` default on an `allow`/`permit`/`bypass` key).",facing:"EXTERNAL",level:"WARN",name:"flag_gates_security_with_unsafe_default",remediation:"Flip the default so a provider outage fails closed: default a protection flag (`enforce*`/`rls*`/`gate*`/`lockdown*`) to `true`, and a permission/bypass flag (`allow*`/`permit*`/`bypass*`) to `false`. The safe default is always the restrictive branch \u2014 never let an unreachable flag backend open access.",run:r(t=>t.flagSecurityDefaults===void 0?[]:t.flagSecurityDefaults.filter(e=>wa(e)).map(e=>o(pe,{cacheKey:`flag_gates_security_with_unsafe_default:${e.file}:${e.line.toString()}`,detail:`\`ctx.flags.boolean("${e.key}", ${String(e.defaultValue)})\` in \`${e.exportName}\` (${e.file}:${e.line.toString()}) fails open to the permissive branch \u2014 a provider outage returns \`${String(e.defaultValue)}\`, ${e.defaultValue?"granting the guarded permission":"disabling the guarded protection"}.`,metadata:{defaultValue:e.defaultValue,exportName:e.exportName,file:e.file,key:e.key,line:e.line}})),"run"),source:"static",title:"Security flag fails open to the permissive branch"};var fe={categories:["SCHEMA"],description:"A `.geoIndex(name, { field })` points at a column that is not a `v.geoPoint()`, so its geohash companion can't be built and `withGeoIndex(...)` can never match.",facing:"INTERNAL",level:"ERROR",name:"geo_index_field_not_geopoint",remediation:"Point the geo index at a `v.geoPoint()` column, or change the column's type to `v.geoPoint()`.",run:r(t=>{let e=[];for(let a of t.schema.tables)for(let i of a.indexes){if(i.kind!=="geo")continue;let n=i.fields[0];if(n===void 0)continue;let s=a.columnKinds?.[n];s===void 0||s==="geoPoint"||e.push(o(fe,{cacheKey:`geo_index_field_not_geopoint:${a.name}:${i.name}:${n}`,detail:`Geo index "${i.name}" on table "${a.name}" indexes column "${n}", which is a ${s}, not a v.geoPoint().`,metadata:{field:n,index:i.name,kind:s,table:a.name}}))}return e},"run"),source:"static",title:"Geo index field is not a geoPoint"};var ge={categories:["SCHEMA"],description:"A `.geoIndex(name, { field })` is declared but no handler reads it via `withGeoIndex(name, \u2026)`. The geohash companion column is maintained on every write and read by nothing \u2014 dead overhead, the geo analogue of a dead index.",facing:"INTERNAL",level:"INFO",name:"geo_index_unused",remediation:'Query the index from a handler with `ctx.db.query("<table>").withGeoIndex("<name>", q => q.near(point, radius))`, or drop the `.geoIndex(...)` declaration so writes stop maintaining its geohash companion.',run:r(t=>{if(t.geoIndexUsages===void 0)return[];let e=t.geoIndexUsages;if(e.some(n=>n.indexName===""))return[];let a=new Set(e.map(n=>n.indexName)),i=[];for(let n of t.schema.tables)for(let s of n.indexes)s.kind!=="geo"||a.has(s.name)||i.push(o(ge,{cacheKey:`geo_index_unused:${n.name}:${s.name}`,detail:`No handler reads geo index "${s.name}" on table "${n.name}" via \`withGeoIndex("${s.name}", \u2026)\` \u2014 it's declared but never queried, so its geohash companion is maintained on every write for nothing.`,metadata:{index:s.name,table:n.name}}));return i},"run"),source:"static",title:"Geo index is declared but never queried"};var be={categories:["SECURITY"],description:"A secret-shaped string literal (live API key, access key, private key, or high-entropy token) is hard-coded in the source. Committed secrets leak via clone/fork/history and force a redeploy to rotate.",facing:"INTERNAL",level:"ERROR",name:"hardcoded_secret",remediation:'Move the secret out of source: set it locally in `.dev.vars` (`lunora env set <NAME> "<value>"`) and in production with `wrangler secret put <NAME>`, then read it at runtime from `env.<NAME>`. Rotate the exposed value \u2014 assume it is already compromised.',run:r(t=>{if(t.secretLiterals===void 0)return[];let e=[];for(let a of t.secretLiterals)e.push(o(be,{cacheKey:`hardcoded_secret:${a.file}:${a.line.toString()}:${a.kind}`,detail:`A ${a.kind.replaceAll("_"," ")} (${a.preview}) is hard-coded at ${a.file}:${a.line.toString()}. Move it to \`.dev.vars\` / \`wrangler secret put\` and read it from \`env\`. Rotate the exposed value.`,metadata:{file:a.file,kind:a.kind,line:a.line,preview:a.preview}}));return $(e)},"run"),source:"static",title:"Hard-coded secret in source"};var va=r((t,e)=>t==="httpRoute"&&e?`\`httpRoute\` ${e} handler`:"`httpAction` handler","n"),ye={categories:["SECURITY"],description:"An `httpAction`/`httpRoute` handler performs a side effect (`ctx.runMutation`/`ctx.runAction`/a `ctx.db` write) but never reads `ctx.auth` \u2014 an unauthenticated HTTP endpoint driving a state change, bypassing the identity/RLS checks that guard the rest of the app.",facing:"EXTERNAL",level:"WARN",name:"http_action_missing_auth_guard",remediation:"Read `ctx.auth` in the handler before the side effect \u2014 call `await ctx.auth.getIdentity()` (or check `ctx.auth.userId`) and reject unauthenticated/unauthorized requests, or forward through a `mutation`/`action` whose RLS policies enforce access. If the endpoint is intentionally public (e.g. a signed webhook), verify the provider signature before the write.",run:r(t=>t.httpActionGuards===void 0?[]:t.httpActionGuards.filter(e=>!e.readsAuth).map(e=>{let a=va(e.kind,e.method);return o(ye,{cacheKey:`http_action_missing_auth_guard:${e.file}:${e.line.toString()}`,detail:`${a} \`${e.exportName}\` (${e.file}:${e.line.toString()}) calls \`ctx.${e.sideEffect}\` but never reads \`ctx.auth\` \u2014 an anonymous caller can drive this write. Authenticate the request before the side effect.`,metadata:{exportName:e.exportName,file:e.file,kind:e.kind,line:e.line,sideEffect:e.sideEffect,...e.method?{method:e.method}:{}}})}),"run"),source:"static",title:"Unauthenticated HTTP handler performs a side effect"};var xa={"headers-append":"a `headers.append(...)` call","headers-ctor":"a `new Headers({...})` initializer","headers-set":"a `headers.set(...)` call","response-init":"a `Response` init `headers` object"},we={categories:["SECURITY"],description:"An `httpAction` handler writes a response-header value derived from raw request input (`request.headers`/URL/query/body) with no CR/LF sanitizer. A `Request`-derived string placed verbatim into a response header lets a caller smuggle `\\r\\n` and inject extra headers or split the response (HTTP response splitting / header injection).",facing:"EXTERNAL",level:"WARN",name:"http_action_response_header_injection",remediation:"Never place a raw request value into a response header. Route it through a CR/LF guard (`isSafeHeaderValue` from `@lunora/server`, rejecting values containing `\\r`/`\\n`/`\\0`), URL-encode it (`encodeURIComponent`), or coerce it to a number \u2014 before writing the header. `String(...)` / `.toString()` do NOT strip CR/LF and are not sufficient.",run:r(t=>t.httpHeaderWrites===void 0?[]:t.httpHeaderWrites.map(e=>{let a=e.headerName===""?"a response header":`the \`${e.headerName}\` response header`;return o(we,{cacheKey:`http_action_response_header_injection:${e.file}:${e.line.toString()}`,detail:`\`${e.exportName}\` (${e.file}:${e.line.toString()}) writes ${a} from raw request input via ${xa[e.via]} with no CR/LF guard \u2014 a caller can smuggle \`\\r\\n\` to inject headers or split the response. Route the value through \`isSafeHeaderValue\` or \`encodeURIComponent\` first.`,metadata:{exportName:e.exportName,file:e.file,headerName:e.headerName,line:e.line,via:e.via}})}),"run"),source:"static",title:"Response header written from unsanitized request input"};var ve={categories:["SCHEMA"],description:"A `query`/`mutation` handler accesses Hyperdrive via `ctx.sql`. Hyperdrive hits an external database Lunora does not own: queries are non-deterministic (like `fetch`) and external writes are invisible to live queries. `ctx.sql` is available on `ActionCtx` only and must be confined to `action` handlers.",facing:"EXTERNAL",level:"WARN",name:"hyperdrive_outside_action",remediation:"Move the `ctx.sql` access into an `action(...)` (the only context where it is typed), where external I/O is allowed. If a query/mutation needs the data, have the action read it via `ctx.sql` and write a projection into a `defineSchema` DO/D1 table \u2014 that write is tracked by live queries, whereas the external DB is not.",run:r(t=>{if(t.hyperdriveCalls===void 0)return[];let e=[];for(let a of t.hyperdriveCalls)e.push(o(ve,{cacheKey:`hyperdrive_outside_action:${a.file}:${a.line.toString()}:${a.callee}`,detail:`\`${a.callee}(\u2026)\` in ${a.exportName} (${a.file}:${a.line.toString()}) runs inside a ${a.kind} handler \u2014 Hyperdrive's \`ctx.sql\` is non-deterministic and non-reactive, so it is available only in actions. Move the external SQL into an \`action\` and project the result into a Lunora table if a query/mutation needs it.`,metadata:{callee:a.callee,exportName:a.exportName,file:a.file,kind:a.kind,line:a.line}}));return e},"run"),source:"static",title:"Hyperdrive `ctx.sql` used outside an action"};var xe={categories:["SECURITY"],description:"An authorization read of `auth.identity.<key>` for a claim outside the declared `defineIdentity({ ... })` contract. `defineIdentity` validates only declared claims and forwards undeclared ones verbatim, so the value the policy trusts was never validated \u2014 an attacker's token can forge it.",facing:"EXTERNAL",level:"WARN",name:"identity_undeclared_claim_trusted",remediation:"Declare the claim in `defineIdentity({ ... })` (e.g. `role: v.optional(v.string())`) so the worker validates it at the trust boundary before a policy reads it, or stop trusting it for authorization. Declared claims (and `userId`) are validated; undeclared claims are forwarded unchecked.",run:r(t=>t.identityClaimReads===void 0?[]:t.identityClaimReads.filter(e=>!e.declared).map(e=>{let a=`\`${e.exportName}\` (${e.file}:${e.line.toString()})`;return o(xe,{cacheKey:`identity_undeclared_claim_trusted:${e.file}:${e.line.toString()}:${e.key}`,detail:`\`${e.key}\` in ${a} is read off \`identity\` but is not declared in \`defineIdentity({ ... })\`. Undeclared claims are forwarded unvalidated, so this authorization decision trusts a forgeable value.`,metadata:{exportName:e.exportName,file:e.file,key:e.key,line:e.line}})}),"run"),source:"static",title:"Authorization trusts an undeclared identity claim"};var Pt=b({cacheKey:r(t=>`images_url_source_from_user_input:${t.file}:${t.line.toString()}`,"cacheKey"),categories:["SECURITY"],description:"A `buildImageDeliveryUrl({ key, \u2026 })` call's `key` \u2014 the CDN transform's source image, an absolute URL or an origin-relative key \u2014 is derived from the handler's `args` with no server-side scoping, so any caller can point the CDN's on-the-fly transform at an attacker-chosen origin (SSRF / open proxy) or an arbitrary key under the account's own store.",detail:r(t=>`\`buildImageDeliveryUrl\` in \`${t.exportName}\` (${t.file}:${t.line.toString()}) builds its delivery URL from a \`key\` derived from \`args\` with no server-side scoping \u2014 any caller can point the CDN's on-the-fly transform at an attacker-chosen origin or store key. Validate \`key\` against an allowlist, or derive it from server-trusted state.`,"detail"),facing:"EXTERNAL",getAccesses:r(t=>t.imageDeliveryUrlAccesses,"getAccesses"),level:"WARN",metadata:r(t=>({exportName:t.exportName,file:t.file,line:t.line}),"metadata"),name:"images_url_source_from_user_input",remediation:"Validate `key` against an allowlist of known origins/prefixes before calling `buildImageDeliveryUrl`, or derive it from server-trusted state (a stored record's own image key) rather than passing `args` straight through.",title:"Image delivery URL built from unscoped user input"});var _a=new Set(["accountId","authorId","createdBy","createdById","organizationId","orgId","ownerId","tenantId","updatedBy","userId","workspaceId"]),_e=new Set(["address","dateOfBirth","dob","email","firstName","lastName","phone","phoneNumber","socialSecurityNumber","ssn"]),$a=new Set(["_creationTime","_id"]),Ut=r(t=>t.fields.filter(e=>_a.has(e)||_e.has(e)),"n"),E=r(t=>new Set([...$a,...t.fields]),"s"),_=r(t=>t.visibility==="public"&&(t.kind==="mutation"||t.kind==="action"),"o");var $e={categories:["SCHEMA"],description:"An index references a column that is not declared on its table. The index can never match, and the typo would otherwise surface only at runtime.",facing:"INTERNAL",level:"ERROR",name:"index_references_unknown_field",remediation:"Fix the column name in the index declaration, or add the column to the table.",run:r(t=>{let e=[];for(let a of t.schema.tables){let i=E(a);for(let n of a.indexes)for(let s of n.fields)i.has(s)||e.push(o($e,{cacheKey:`index_references_unknown_field:${a.name}:${n.name}:${s}`,detail:`Index "${n.name}" on table "${a.name}" references column "${s}", which is not declared on the table.`,metadata:{field:s,index:n.name,indexKind:n.kind,table:a.name}}))}return e},"run"),source:"static",title:"Index references unknown field"};var ke={categories:["SECURITY"],description:"A public procedure calls `ctx.db.insertManyUnsafe(...)`, which writes rows straight to storage and bypasses both the per-row validators and the schema's insert triggers (server-trusted columns, ownership stamping, RLS write checks). On a public entry point the rows are request-shaped, so a caller can write columns they shouldn't control and skip every trigger-enforced invariant.",facing:"EXTERNAL",level:"WARN",name:"insert_many_unsafe_user_data",remediation:"Use the validated `ctx.db.insert(...)` path in public procedures so validators and insert triggers run. If you need the unsafe bulk write, move it into an internal function (`internalMutation`/`internalAction`) that is fed only server-constructed rows, never request input.",run:r(t=>t.procedureProtections===void 0?[]:t.procedureProtections.filter(e=>e.usesInsertManyUnsafe&&e.visibility==="public").map(e=>o(ke,{cacheKey:`insert_many_unsafe_user_data:${e.file}:${e.exportName}`,detail:`\`${e.exportName}\` (${e.file}) is public and calls \`ctx.db.insertManyUnsafe(...)\`, bypassing validators and insert triggers on request-shaped rows. Switch to \`ctx.db.insert(...)\`, or move the bulk write into an internal function fed server-built rows.`,metadata:{exportName:e.exportName,file:e.file,kind:e.kind}})),"run"),source:"static",title:"Public procedure uses insertManyUnsafe (bypasses validators and triggers)"};var qt=b({cacheKey:r(t=>`kv_unscoped_user_key_idor:${t.file}:${t.line.toString()}`,"cacheKey"),categories:["SECURITY"],description:"A `ctx.kv` read/write uses a namespace key derived from the handler's `args` with no server-side scoping. Workers KV is a flat namespace, so an unscoped key lets any caller read, overwrite, or delete another user's entry \u2014 an insecure direct object reference (IDOR).",detail:r(t=>`\`ctx.kv.${t.method}\` in \`${t.exportName}\` (${t.file}:${t.line.toString()}) uses a KV key derived from \`args\` with no server-side scoping \u2014 any caller can read/overwrite/delete another user's entry (IDOR). Prefix the key with a server-trusted identity (e.g. \`\${ctx.auth.userId}:\u2026\`).`,"detail"),facing:"EXTERNAL",getAccesses:r(t=>t.kvKeyAccesses,"getAccesses"),level:"ERROR",metadata:r(t=>({exportName:t.exportName,file:t.file,line:t.line,method:t.method}),"metadata"),name:"kv_unscoped_user_key_idor",remediation:"Prefix every `ctx.kv` key with a server-trusted identity (e.g. `${ctx.auth.userId}:${args.id}`) so a caller can only address their own entries. Never pass request input straight to `ctx.kv.get`/`put`/`delete`.",title:"Possible IDOR from arg-derived unscoped KV key"});var Re={categories:["SECURITY"],description:"`createInboundEmailHandler({...})` has no `verify` hook, so it trusts every inbound message \u2014 the `from` address is spoofable and DKIM/SPF/DMARC verdicts go unchecked. If the handler dispatches into a Lunora function (admin bearer, RLS disabled), a forged sender can drive privileged writes (a confused-deputy escalation).",facing:"EXTERNAL",level:"ERROR",name:"mail_inbound_dispatch_without_verify",remediation:"Pass a `verify` hook to `createInboundEmailHandler({...})` that rejects the message unless its DKIM/SPF/DMARC authentication verdicts pass and the sender is expected \u2014 before any dispatch into a Lunora function. Never trust the `from` header alone.",run:r(t=>t.configCalls===void 0?[]:t.configCalls.filter(e=>e.callee==="createInboundEmailHandler"&&e.analyzable&&!e.presentKeys.includes("verify")).map(e=>o(Re,{cacheKey:`mail_inbound_dispatch_without_verify:${e.file}:${e.line.toString()}`,detail:`\`createInboundEmailHandler({...})\` in ${e.file}:${e.line.toString()} has no \`verify\` hook \u2014 it trusts the spoofable \`from\` address and unchecked DKIM/SPF/DMARC verdicts. A forged sender can then reach whatever the handler dispatches to (a Lunora function runs with the admin bearer and RLS disabled). Add a \`verify\` gate that enforces the authentication verdicts before dispatch.`,metadata:{callee:e.callee,file:e.file,line:e.line}})),"run"),source:"static",title:"Inbound email handler without a verify hook"};var Ae={categories:["SECURITY"],description:"A `ctx.mail`/`ctx.email` `send`/`queue` call sets a recipient field (to/cc/bcc) from the handler's `args` with no server-side scoping. Sending to an address taken straight from request input turns the deployment into an open relay / spam amplifier.",facing:"EXTERNAL",level:"WARN",name:"mail_recipient_from_request_input",remediation:"Derive recipients from server-trusted state (e.g. the authenticated user's own record via `ctx.auth`), never straight from `args`. If user-chosen recipients are intended, gate the path behind authentication and rate limiting.",run:r(t=>t.mailRecipientAccesses===void 0?[]:t.mailRecipientAccesses.map(e=>o(Ae,{cacheKey:`mail_recipient_from_request_input:${e.file}:${e.line.toString()}`,detail:`\`ctx.mail.${e.method}\` in \`${e.exportName}\` (${e.file}:${e.line.toString()}) sets a recipient field (to/cc/bcc) derived from \`args\` with no server-side scoping \u2014 any caller can direct mail to an arbitrary address (open relay / spam amplifier). Derive the recipient from server-trusted state instead (e.g. \`ctx.auth.user.email\`).`,metadata:{exportName:e.exportName,file:e.file,line:e.line,method:e.method}})),"run"),source:"static",title:"Mail recipient derived from request input"};var Ne={categories:["SECURITY"],description:"A public procedure reads a table whose columns are masked by `.use(mask(...))` elsewhere in the app, but this procedure's builder chain does not include `.use(mask(...))`. It returns those columns in the clear while a sibling procedure redacts them.",facing:"EXTERNAL",level:"WARN",name:"mask_uncovered_pii_column",remediation:"Add `.use(mask(policies))` to the procedure's builder chain \u2014 e.g. `c.use(mask(myMasks)).query(...)` \u2014 so its returned rows redact the same columns as the rest of the app. If this procedure is intentionally privileged (e.g. an admin export), declare it with `internalQuery` / `internalMutation` / `internalAction` instead of the public builder so the intent is explicit.",run:r(t=>{if(t.maskProcedures===void 0)return[];let e=new Map;for(let i of t.maskProcedures)for(let{column:n,table:s}of i.maskColumns){if(s===""||n==="")continue;let l=e.get(s)??new Set;l.add(n),e.set(s,l)}if(e.size===0)return[];let a=[];for(let i of t.maskProcedures){if(i.usesMask||i.visibility==="internal")continue;let n=new Set;for(let s of i.tablesRead)s!==""&&e.has(s)&&n.add(s);for(let s of n){let l=[...e.get(s)??new Set].toSorted((d,f)=>d.localeCompare(f)),c=l.map(d=>`\`${d}\``).join(", ");a.push(o(Ne,{cacheKey:`mask_uncovered_pii_column:${i.file}:${i.exportName}:${s}`,detail:`\`${i.exportName}\` in ${i.file} reads table \`${s}\` without \`.use(mask(...))\` \u2014 its column${l.length===1?"":"s"} ${c} ${l.length===1?"is":"are"} masked elsewhere in the app but returned in the clear here.`,metadata:{columns:l,exportName:i.exportName,file:i.file,table:s}}))}}return a},"run"),source:"static",title:"Maskable column returned without mask() middleware"};var ka=/address|birthdate|creditcard|dateofbirth|dob|driverslicense|email|firstname|fullname|lastname|nationalid|passport|phone|socialsecurity|ssn|taxid/u,Ra=r(t=>ka.test(t.replaceAll(/[^a-z0-9]/giu,"").toLowerCase()),"o"),Se={categories:["SECURITY"],description:'A `mask(policies)` column named like PII (email, SSN, phone, \u2026) uses the `"hash"` strategy. `"hash"` is an unsalted 32-bit FNV-1a digest \u2014 brute-force-recoverable and cross-row-correlatable \u2014 not a confidentiality control, so it does not actually hide the value.',facing:"EXTERNAL",level:"WARN",name:"mask_weak_hash_strategy_on_pii",remediation:'Use `"redact"` instead of `"hash"` for this column \u2014 e.g. `mask({ <table>: { <column>: "redact" } })`. Reserve `"hash"` for columns where a stable, joinable pseudonym is the goal and the value itself is not sensitive.',run:r(t=>{if(t.maskStrategies===void 0)return[];let e=[];for(let a of t.maskStrategies)a.strategy!=="hash"||!Ra(a.column)||e.push(o(Se,{cacheKey:`mask_weak_hash_strategy_on_pii:${a.file}:${a.line.toString()}`,detail:`\`${a.exportName}\` in ${a.file} masks \`${a.table===""?a.column:`${a.table}.${a.column}`}\` with \`"hash"\` \u2014 its name suggests PII, and \`"hash"\` is brute-force-recoverable and leaks cross-row equality. Use \`"redact"\` instead.`,metadata:{column:a.column,exportName:a.exportName,file:a.file,table:a.table}}));return e},"run"),source:"static",title:'Weak "hash" mask strategy on a PII column'};var Aa=r(t=>{let e=new Set;for(let a of t.maskProcedures??[])for(let{table:i}of a.maskColumns)i!==""&&e.add(i);for(let a of t.maskStrategies??[])a.table!==""&&e.add(a.table);return e},"l"),Na=r(t=>{let e=new Map;for(let a of t.schema.tables)for(let i of a.relations)e.set(`${a.name} ${i.name}`,i.table);return e},"d"),Ee={categories:["SECURITY"],description:"A public read hydrates a masked table through a `with` relation. Column masking does not descend into `with`-hydrated relations, so the related table's masked columns are returned in the clear.",facing:"EXTERNAL",level:"INFO",name:"masked_relation_leak_via_with",remediation:"Don't hydrate a masked table through `with` on an unprotected read: drop the relation, project only non-sensitive columns, or route the parent read through a procedure that re-applies masking to the joined table. Masking is per-procedure and top-level only \u2014 it never reaches `with`-loaded relations.",run:r(t=>{if(t.relationLoads===void 0)return[];let e=Aa(t);if(e.size===0)return[];let a=Na(t),i=[];for(let n of t.relationLoads)if(!(n.visibility!=="public"||n.parentTable===""))for(let s of n.relations){let l=a.get(`${n.parentTable} ${s}`);l===void 0||!e.has(l)||i.push(o(Ee,{cacheKey:`masked_relation_leak_via_with:${n.file}:${n.line.toString()}:${n.parentTable}:${s}`,detail:`The public read in \`${n.exportName}\` (${n.file}:${n.line.toString()}) hydrates relation \`${s}\` (masked table \`${l}\`) via \`with\` on \`${n.parentTable}\`. Masking is top-level only and does not descend into \`with\`, so \`${l}\`'s masked columns are returned in the clear.`,metadata:{exportName:n.exportName,file:n.file,line:n.line,parentTable:n.parentTable,relation:s,relationTable:l}}))}return i},"run"),source:"static",title:"Masked table surfaced unmasked through a with-relation on a public read"};var Ie={categories:["SCHEMA"],description:"A custom mutator's `server` impl writes with `ctx.db.replace(id, document)` \u2014 a whole-row overwrite. On a synced table this clobbers a concurrent edit to a different column; `ctx.db.patch(id, { field })` merges at the column level so independent field edits coexist.",facing:"EXTERNAL",level:"WARN",name:"mutator_full_row_replace",remediation:"Prefer `ctx.db.patch(id, { onlyTheChangedField })` so a concurrent edit to another column isn't lost. Keep `replace` only when the mutator genuinely owns the entire row (e.g. a full-form save).",run:r(t=>t.mutatorWrites===void 0?[]:t.mutatorWrites.map(e=>o(Ie,{cacheKey:`mutator_full_row_replace:${e.exportName}:${String(e.line)}`,detail:`Mutator \`${e.exportName}\` (${e.file}:${String(e.line)}) writes with \`ctx.db.replace(...)\`, overwriting the whole row. A concurrent edit to a different column is clobbered \u2014 use \`ctx.db.patch(id, { field })\` to merge at the column level.`,metadata:{exportName:e.exportName,file:e.file,line:e.line}})),"run"),source:"static",title:"Mutator overwrites the whole row (clobbers concurrent column edits)"};var Te={categories:["SCHEMA"],description:"A `query`/`mutation` handler calls a non-deterministic API (`Date.now`, `Math.random`, `crypto.randomUUID`, `crypto.getRandomValues`, or `fetch`). These handlers may be re-run on OCC retry / subscription re-evaluation, so they must be deterministic \u2014 time, randomness, and network belong in an `action`.",facing:"EXTERNAL",level:"WARN",name:"nondeterministic_query_mutation",remediation:"Move the non-deterministic call into an `action(...)` (which runs once and may use ambient APIs), then pass the computed value into the mutation as an argument \u2014 e.g. compute `Date.now()` in the action and call `ctx.runMutation(api.\u2026, { now })`. Take generated ids/timestamps as args instead of producing them in the handler.",run:r(t=>{if(t.nondeterministicCalls===void 0)return[];let e=[],a=new Map;for(let i of t.nondeterministicCalls){let n=`${i.file}:${i.line.toString()}:${i.callee}`,s=(a.get(n)??0)+1;a.set(n,s);let l=s>1?`:${s.toString()}`:"";e.push(o(Te,{cacheKey:`nondeterministic_query_mutation:${n}${l}`,detail:`\`${i.callee}(\u2026)\` in ${i.exportName} (${i.file}:${i.line.toString()}) runs inside a ${i.kind} handler \u2014 query/mutation handlers must be deterministic. Compute it in an \`action\` and pass the value into the mutation as an argument.`,metadata:{callee:i.callee,exportName:i.exportName,file:i.file,kind:i.kind,line:i.line}}))}return e},"run"),source:"static",title:"Non-deterministic call in query/mutation handler"};var Sa={delete:"deletes the row",get:"reads the row",patch:"patches the row"},Ce={categories:["SECURITY"],description:"A `.public()` `query`/`mutation` gates a `ctx.db.get`/`patch`/`delete` on a null-checked `ctx.db.normalizeId(table, id)` result with no ownership predicate and no RLS. `normalizeId` validates id *shape* only \u2014 it never reads the database \u2014 so a non-null result proves the id is well-formed, never that the caller owns the row (an IDOR).",facing:"EXTERNAL",level:"INFO",name:"normalize_id_used_as_authorization",remediation:'Don\'t treat a non-null `normalizeId` result as authorization \u2014 it only validates id shape. After loading the row, compare its ownership column (`userId`/`ownerId`/\u2026) against the server-trusted identity (`ctx.auth.userId`), or protect the procedure with `.use(rls(...))` / put the schema in `.rls("required")` mode so the row policy runs on every access.',run:r(t=>{if(t.normalizeIdAuthorizations===void 0)return[];let e=t.schema.rlsMode==="required",a=new Set(t.schema.tables.filter(n=>n.isPublic).map(n=>n.name)),i=[];for(let n of t.normalizeIdAuthorizations){if(n.visibility!=="public"||n.usesRls||n.mentionsOwnership||e&&(n.table===""||!a.has(n.table)))continue;let s=n.table===""?"the row":`the \`${n.table}\` row`;i.push(o(Ce,{cacheKey:`normalize_id_used_as_authorization:${n.file}:${n.line.toString()}`,detail:`Public procedure \`${n.exportName}\` (${n.file}:${n.line.toString()}) gates access on \`ctx.db.normalizeId(...)\` and then ${Sa[n.sinkMethod]} \u2014 but \`normalizeId\` only validates id shape, it never reads the database or checks ownership. Any caller supplying a well-formed id of ${s} reaches it (IDOR). Compare the loaded row's ownership column against \`ctx.auth\`, or cover the procedure with \`.use(rls(...))\`.`,metadata:{exportName:n.exportName,file:n.file,line:n.line,sinkMethod:n.sinkMethod,table:n.table}}))}return i},"run"),source:"static",title:"normalizeId result used as authorization (validates id shape, not ownership)"};var Le={categories:["SCHEMA"],description:"The app sends push notifications via `ctx.push` but `defineNotify(...)` configures neither a Web Push nor an FCM channel, so every send fails at runtime. Wire at least one push channel (`webPush` and/or `fcm`) and provide its secrets (`VAPID_*` / `FCM_*`).",facing:"INTERNAL",level:"WARN",name:"notify_missing_push_config",remediation:"In `lunora/notify.ts`, wire a push channel: `defineNotify({ webPush: (env) => webPushFromEnv(env) })` and set `VAPID_PUBLIC_KEY` / `VAPID_PRIVATE_KEY` / `VAPID_SUBJECT` in `.dev.vars` (or configure `fcm` with `FCM_PROJECT_ID`). Generate a VAPID keypair with `npx web-push generate-vapid-keys`.",run:r(t=>{let e=t.notifyConfig;return e===void 0||!e.usesPush||e.hasWebPush||e.hasFcm?[]:[o(Le,{cacheKey:"notify_missing_push_config",detail:"`ctx.push` is used to send notifications, but `defineNotify(...)` wires neither `webPush` nor `fcm` \u2014 every push send will fail at runtime. Configure at least one push channel and its secrets.",metadata:{hasFcm:e.hasFcm,hasWebPush:e.hasWebPush,usesPush:e.usesPush}})]},"run"),source:"static",title:"Push used with no Web Push / FCM channel configured"};var Pe={categories:["SCHEMA"],description:"A `query`/`mutation` handler sends a notification via `ctx.notify`/`ctx.push`. A send is external I/O (a `fetch` to a push service / FCM): it is non-deterministic like `fetch`, and a mutation re-run on OCC retry would re-send it (duplicate pushes). These facades are available on `ActionCtx` only and must be confined to `action` handlers.",facing:"EXTERNAL",level:"WARN",name:"notify_send_outside_action",remediation:"Move the `ctx.notify`/`ctx.push` send into an `action(...)`, where external I/O is allowed. If a query/mutation must trigger a notification, have it enqueue the send (`enqueuePushBroadcast` via `@lunora/queue`) or schedule an action \u2014 the queue/scheduler runs the send exactly once, off the transactional path.",run:r(t=>{if(t.notifyCalls===void 0)return[];let e=[];for(let a of t.notifyCalls)e.push(o(Pe,{cacheKey:`notify_send_outside_action:${a.file}:${a.line.toString()}:${a.callee}`,detail:`\`${a.callee}(\u2026)\` in ${a.exportName} (${a.file}:${a.line.toString()}) runs inside a ${a.kind} handler \u2014 a notification send is non-deterministic external I/O and a retried ${a.kind} would re-send it. Move it into an \`action\`, or enqueue/schedule the send.`,metadata:{callee:a.callee,exportName:a.exportName,file:a.file,kind:a.kind,line:a.line}}));return e},"run"),source:"static",title:"Notification send used outside an action"};var Ue={categories:["SECURITY"],description:"A `.public()` `query` returns raw table rows (no `.output(...)` projection, no `.use(mask(...))`) from a table carrying PII-named columns (`email`, `phone`, `ssn`, \u2026). Every column ships to the caller, and a column added to the table later leaks by default.",facing:"EXTERNAL",level:"INFO",name:"output_projection_missing_on_public_read",remediation:"Add an explicit return projection to the public query \u2014 `.output(v.object({ \u2026 }))` listing only the fields a client needs \u2014 or apply a `.use(mask(...))` policy to the PII columns. This makes the exposed shape intentional and stops a newly-added column from leaking through this query by default.",run:r(t=>{if(t.rawRowReturns===void 0)return[];let e=new Map;for(let i of t.schema.tables){let n=i.fields.filter(s=>_e.has(s));n.length>0&&e.set(i.name,n)}let a=[];for(let i of t.rawRowReturns){if(i.visibility!=="public"||i.usesOutput||i.usesMask||i.table==="")continue;let n=e.get(i.table);n!==void 0&&a.push(o(Ue,{cacheKey:`output_projection_missing_on_public_read:${i.file}:${i.line.toString()}`,detail:`Public query \`${i.exportName}\` (${i.file}:${i.line.toString()}) returns raw \`${i.table}\` rows with no \`.output(...)\` projection \u2014 shipping PII column(s) ${n.join(", ")} to every caller, and any column added to \`${i.table}\` later leaks by default. Project the return with \`.output(v.object({ \u2026 }))\` or mask the PII columns.`,metadata:{columns:n,exportName:i.exportName,file:i.file,line:i.line,table:i.table}}))}return a},"run"),source:"static",title:"Public query returns raw rows with PII and no output projection"};var I={categories:["SECURITY"],description:"A `ctx.db` write sets an ownership/identity column (`userId`, `ownerId`, `tenantId`, \u2026) from the handler's `args`. The caller controls who the row belongs to, so any caller can write rows owned by another user or tenant \u2014 an act-as-any-user / cross-tenant IDOR.",facing:"EXTERNAL",level:"ERROR",name:"owner_field_from_args_not_auth",remediation:"Stamp the ownership column from the server-trusted identity (`ctx.auth.userId` / `ctx.identity`), never from request input. Drop the field from the accepted `args` so a caller cannot supply it.",run:r(t=>t.ownerFieldWrites===void 0?[]:t.ownerFieldWrites.map(e=>{let a=`\`${e.method}\` in \`${e.exportName}\` (${e.file}:${e.line.toString()})`,i={exportName:e.exportName,field:e.field,file:e.file,line:e.line,method:e.method,visibility:e.visibility??"unknown"};return e.visibility==="internal"?o(I,{cacheKey:`owner_field_from_args_not_auth:${e.file}:${e.line.toString()}:${e.field}`,detail:`${a} sets the ownership field \`${e.field}\` from \`args\`. This is expected for an \`internal\` procedure \u2014 no caller can reach it directly, and the trusted caller passes the subject along. Audit the PUBLIC procedures that dispatch to it: if one forwards \`args.${e.field}\` straight through, the IDOR is there.`,facing:"INTERNAL",level:"INFO",metadata:i}):o(I,{cacheKey:`owner_field_from_args_not_auth:${e.file}:${e.line.toString()}:${e.field}`,detail:`${a} sets the ownership field \`${e.field}\` from \`args\` instead of the server-trusted identity \u2014 any caller can write rows owned by another user/tenant (IDOR). Stamp \`${e.field}\` from \`ctx.auth\`/\`ctx.identity\`, never from request input.`,metadata:i})}),"run"),source:"static",title:"Ownership field written from args, not server identity"};var qe={categories:["SECURITY"],description:"`createPayment({...})` was constructed without an `authorize` callback, so its access-control gate never runs \u2014 any caller that reaches a payment reference id can drive a charge, refund, or subscription change against it (a broken-access-control / IDOR risk on money movement).",facing:"EXTERNAL",level:"ERROR",name:"payment_create_without_authorize",remediation:"Pass an `authorize(referenceId)` callback to `createPayment({...})` that verifies the current caller (via `ctx.auth`) owns the payment/subscription reference before the operation runs. Prefer the `ctx.payments` accessor, which is authorization-scoped by default.",run:r(t=>t.configCalls===void 0?[]:t.configCalls.filter(e=>e.callee==="createPayment"&&e.analyzable&&!e.presentKeys.includes("authorize")).map(e=>o(qe,{cacheKey:`payment_create_without_authorize:${e.file}:${e.line.toString()}`,detail:`\`createPayment({...})\` in ${e.file}:${e.line.toString()} has no \`authorize\` callback \u2014 its access-control gate never runs, so any caller reaching the payment reference can move money against it. Add an \`authorize(referenceId)\` gate that checks \`ctx.auth\` ownership before the operation.`,metadata:{callee:e.callee,file:e.file,line:e.line}})),"run"),source:"static",title:"createPayment without an authorize gate"};var Ea=3600,Ke={categories:["SECURITY"],description:"A payment webhook adapter configured with an implausibly wide `webhookToleranceSeconds` (over an hour). The timestamp tolerance is the webhook replay window; widening it far past the 300s default keeps captured signed events replayable long after capture, defeating the replay check.",facing:"EXTERNAL",level:"WARN",name:"payment_webhook_wide_tolerance",remediation:"Keep `webhookToleranceSeconds` at or near the 300s default \u2014 just enough for clock skew. If a wide window was set to work around retries or backfills, handle those with idempotency on the event id instead, not by loosening the replay-window guard.",run:r(t=>{if(t.paymentWebhooks===void 0)return[];let e=r(a=>a.toleranceSeconds!==void 0&&a.toleranceSeconds>=Ea,"t");return t.paymentWebhooks.filter(a=>e(a)).map(a=>{let i=`\`${a.exportName}\` (${a.file}:${a.line.toString()})`;return o(Ke,{cacheKey:`payment_webhook_wide_tolerance:${a.file}:${a.line.toString()}`,detail:`\`${a.callee}\` in ${i} sets \`webhookToleranceSeconds\` to ${(a.toleranceSeconds??0).toString()}s (default 300s). That replay window keeps captured signed events valid long after capture.`,metadata:{callee:a.callee,exportName:a.exportName,file:a.file,line:a.line,toleranceSeconds:a.toleranceSeconds}})})},"run"),source:"static",title:"Payment webhook replay-tolerance window is implausibly wide"};var Me={categories:["SECURITY"],description:"A plaintext secret is committed to `wrangler.jsonc`'s `vars` block. `vars` are cleartext \u2014 baked into the deployed Worker and checked into source control \u2014 so the secret leaks via the repo and the bundle, and rotating it needs a config edit plus a redeploy.",facing:"INTERNAL",level:"ERROR",name:"plaintext_secret_in_wrangler_vars",remediation:"Move the value out of `vars`: bind it through a Secrets Store (`ctx.secrets.get(<NAME>)`) or set it with `wrangler secret put <NAME>` and read it from `env.<NAME>`. Delete the `vars` entry and rotate the exposed value \u2014 assume it is already compromised. Public/publishable keys (name them `*_PUBLIC` / `*_PUBLISHABLE`) are exempt.",run:r(t=>{if(t.wranglerVariables===void 0)return[];let e=[],a=new Map;for(let i of t.wranglerVariables){let n=`${i.file}:${i.key}`,s=(a.get(n)??0)+1;a.set(n,s);let l=s>1?`:${s.toString()}`:"";e.push(o(Me,{cacheKey:`plaintext_secret_in_wrangler_vars:${n}${l}`,detail:`The \`vars\` entry \`${i.key}\` in ${i.file} holds a plaintext ${i.kind.replaceAll("_"," ")} (${i.preview}). \`vars\` ship in cleartext to the deployed Worker and are committed to source control \u2014 move it to a Secrets Store binding or \`wrangler secret put\` and rotate the exposed value.`,metadata:{file:i.file,key:i.key,kind:i.kind,preview:i.preview}}))}return e},"run"),source:"static",title:"Plaintext secret in wrangler vars"};var Oe={categories:["SECURITY"],description:"An RLS policy is bound to a `table` name that does not exist in the schema. The `rls()` middleware never matches it, so the table the policy was meant to protect is left ungated \u2014 reads return unrestricted rows and writes are allowed.",facing:"EXTERNAL",level:"WARN",name:"policy_references_unknown_table",remediation:"Fix the policy's `table` to a real table name (check for a typo or a table that was renamed/removed). If the table is gone, delete the dead policy.",run:r(t=>{if(t.rlsProcedures===void 0)return[];let e=new Set(t.schema.tables.map(n=>n.name)),a=new Set,i=[];for(let n of t.rlsProcedures)for(let s of n.rlsTables)s===""||e.has(s)||a.has(s)||(a.add(s),i.push(o(Oe,{cacheKey:`policy_references_unknown_table:${s}`,detail:`An RLS policy in \`${n.exportName}\` (${n.file}) is bound to table \`${s}\`, which is not declared in the schema. The policy never matches, so \`${s}\` \u2014 if it exists under another name \u2014 is left ungated.`,metadata:{exportName:n.exportName,file:n.file,table:s}})));return i},"run"),source:"static",title:"RLS policy bound to an unknown table"};var De={categories:["SECURITY"],description:"A `defineQueue`/`defineWorkflow` handler forwards its untrusted payload (queue body / workflow params) straight into a `ctx.run` dispatch whose target is RLS-gated. The handler runs under the system identity with RLS disabled, so the target's row policy \u2014 the only thing guarding it \u2014 is bypassed, letting a caller act as any user / write across tenants.",facing:"EXTERNAL",level:"ERROR",name:"privileged_dispatch_unvalidated_payload",remediation:"Don't forward an unchecked queue body / workflow param into an RLS-gated function. Re-derive the ownership/identity keys from server-trusted state (or an argument you validate against it) before dispatching, or route the work through a function that re-checks the caller's identity rather than relying on the row policy the privileged handler skips.",run:r(t=>{if(t.privilegedDispatches===void 0)return[];let e=t.rlsProcedures??[],a=r((i,n)=>e.some(s=>s.usesRls&&s.file===i&&s.exportName===n),"s");return t.privilegedDispatches.filter(i=>a(i.targetFile,i.targetExport)).map(i=>{let n=i.dispatchKind==="queue"?"queue":"workflow",s=i.dispatchKind==="queue"?"queue message body":"workflow params";return o(De,{cacheKey:`privileged_dispatch_unvalidated_payload:${i.file}:${i.line.toString()}`,detail:`The ${n} handler \`${i.handlerExport}\` (${i.file}:${i.line.toString()}) forwards its untrusted ${s} straight into \`${i.targetFile}.${i.targetExport}\`, which is guarded by \`.use(rls(...))\`. The handler runs under the system identity with RLS disabled, so the target's row policy is bypassed \u2014 any ownership/identity field the payload controls lets a caller act as another user or write across tenants. Re-derive the identity keys from server-trusted state before dispatching, or route through a function that re-checks the caller.`,metadata:{dispatchKind:i.dispatchKind,file:i.file,handlerExport:i.handlerExport,line:i.line,targetExport:i.targetExport,targetFile:i.targetFile}})})},"run"),source:"static",title:"Privileged handler forwards untrusted payload into an RLS-gated function"};var We={categories:["SECURITY"],description:"A public procedure fans work out to a privileged dispatch surface (`ctx.scheduler.runAfter`/`runAt`, a `ctx.queues.<name>` send, or `ctx.workflows.<name>.create`) with no rate-limit guard. Each dispatch runs later under the system identity and bills the account, so an anonymous caller can amplify cost and flood the async surface in a loop.",facing:"EXTERNAL",level:"WARN",name:"privileged_fanout_from_public_procedure",remediation:"Gate the public entry point with a rate limit (`.use(rateLimit(...))` or a `protectPublic({ rateLimit })` bundle), or make the fan-out procedure internal and trigger it only from an authenticated, guarded path. Never expose an unbounded scheduler/queue/workflow dispatch to anonymous callers.",run:r(t=>t.procedureProtections===void 0?[]:t.procedureProtections.filter(e=>e.fanOut&&e.visibility==="public"&&!e.usesRateLimit).map(e=>o(We,{cacheKey:`privileged_fanout_from_public_procedure:${e.file}:${e.exportName}`,detail:`\`${e.exportName}\` (${e.file}) is public and fans work out to a privileged dispatch surface (scheduler/queue/workflow) with no rate limit \u2014 an anonymous caller can drive that billable, system-identity dispatch in a loop. Add a rate-limit guard, or make it internal and trigger it from an authenticated path.`,metadata:{exportName:e.exportName,file:e.file,kind:e.kind}})),"run"),source:"static",title:"Public procedure fans out to a privileged dispatch surface"};var Fe={categories:["SCHEMA"],description:"A public `mutation`/`action` emits no structured event. When it fails you get a stack trace with no request context \u2014 no ids, no tenant, no outcome \u2014 so the failure is visible but not searchable.",facing:"INTERNAL",level:"INFO",name:"procedure_without_structured_event",remediation:'Emit one event on the primary path: `ctx.log.info("<verb>", { \u2026 })`, or wrap the handler in `ctx.span("<name>", \u2026)` to attach timing too.',run:r(t=>{if(t.procedureProtections===void 0)return[];let e=[];for(let a of t.procedureProtections)a.visibility!=="public"||a.kind==="query"||a.emitsEvent!==!1||e.push(o(Fe,{cacheKey:`procedure_without_structured_event:${a.file}:${a.exportName}`,detail:`Public ${a.kind} \`${a.exportName}\` (${a.file}) emits no structured event. Add a \`ctx.log\` line or a \`ctx.span\` so a failure carries its request context.`,metadata:{exportName:a.exportName,file:a.file,kind:a.kind}}));return e},"run"),source:"static",title:"Public write emits no structured event"};var ze={categories:["SECURITY"],description:"A public procedure declares a `v.any()` argument. `v.any()` accepts arbitrary untyped input from an untrusted client \u2014 defeating validation and opening injection / oversized-payload abuse.",facing:"EXTERNAL",level:"WARN",name:"public_arg_uses_any",remediation:"Replace `v.any()` with a precise validator \u2014 `v.object({...})`, `v.string()`, `v.union(...)`, etc. If the shape is genuinely dynamic, model the known variants with `v.union` rather than accepting anything.",run:r(t=>{if(t.argValidators===void 0)return[];let e=[];for(let a of t.argValidators)for(let i of a.anyArgs)e.push(o(ze,{cacheKey:`public_arg_uses_any:${a.file}:${a.exportName}:${i}`,detail:`Arg \`${i}\` of public procedure \`${a.exportName}\` (${a.file}:${a.line.toString()}) is \`v.any()\` \u2014 untrusted input with no validation. Give it a precise validator.`,metadata:{argument:i,exportName:a.exportName,file:a.file,line:a.line}}));return e},"run"),source:"static",title:"Public argument uses v.any()"};var Ia=/contact|forgot|login|magic|otp|register|reset|signin|signup|subscribe|verify/iu,je={categories:["SECURITY"],description:"A public `mutation`/`action` has no `rateLimit` middleware. Publicly-callable writes are flood and brute-force targets \u2014 an attacker can exhaust writes, mail quota, or credits, or guess credentials on auth-shaped endpoints.",facing:"EXTERNAL",level:"WARN",name:"public_mutation_without_ratelimit",remediation:'Attach a rate limit: `.use(rateLimit(limiter, "<bucket>"))` from `@lunora/ratelimit`, or wrap the recommended public-procedure guards with `.use(protectPublic({ rateLimit, captcha }))` from `@lunora/server`. Genuinely-open writes can be acknowledged by adding a permissive limiter.',run:r(t=>{if(t.procedureProtections===void 0)return[];let e=[];for(let a of t.procedureProtections){if(!_(a)||a.usesRateLimit)continue;let i=Ia.test(a.exportName);e.push(o(je,{cacheKey:`public_mutation_without_ratelimit:${a.file}:${a.exportName}`,detail:`Public ${a.kind} \`${a.exportName}\` (${a.file}) has no rate limit${i?" \u2014 its name suggests an auth/abuse-sensitive endpoint, so this is high-risk":""}. Add \`.use(rateLimit(...))\` or \`.use(protectPublic({ rateLimit }))\`.`,metadata:{exportName:a.exportName,file:a.file,kind:a.kind,sensitive:i}}))}return e},"run"),source:"static",title:"Public write without a rate limit"};var Xe={categories:["SECURITY"],description:'`.public()` opts a table OUT of the schema\'s `.rls("required")` enforcement \u2014 it does not mean the table holds public data. A `.public()` table whose columns look ownership- or PII-shaped (`userId`, `email`, `ssn`, \u2026) is exempt from the row-security guard, not "safe to expose".',facing:"EXTERNAL",level:"WARN",name:"public_table_rls_optout_confusion",remediation:'Double-check this table actually needs the RLS opt-out. If it does not, remove `.public()` so `.rls("required")` covers it like every other table. If it genuinely must stay open (a shared lookup table, for instance), rename or comment it so the exemption is intentional rather than a name-implies-safety mistake, and make sure no procedure trusts `.public()` as an authorization boundary.',run:r(t=>{if(t.schema.rlsMode!=="required")return[];let e=[];for(let a of t.schema.tables){if(!a.isPublic)continue;let i=Ut(a);i.length!==0&&e.push(o(Xe,{cacheKey:`public_table_rls_optout_confusion:${a.name}`,detail:`Table "${a.name}" is \`.public()\` (opted OUT of the schema's \`.rls("required")\` enforcement) but carries ownership/PII-shaped column(s): ${i.join(", ")}. \`.public()\` means "exempt from RLS", not "safe to expose" \u2014 confirm this table really needs the opt-out.`,metadata:{columns:i,table:a.name}}))}return e},"run"),source:"static",title:"Public table opts out of RLS but carries sensitive columns"};var He={categories:["SCHEMA"],description:"A queue consumer drops a message once it exhausts `maxRetries` (default 3) delivery attempts. Without a `deadLetterQueue`, that message is deleted permanently with no record \u2014 route exhausted messages to a DLQ so you can inspect, alert on, and replay them.",facing:"INTERNAL",level:"WARN",name:"queue_without_dlq",remediation:'Add `deadLetterQueue: "<name>-dlq"` to the `defineQueue({...})` options and declare a consumer for that queue so exhausted messages are inspectable (an unconsumed DLQ still expires at the retention window). `vis generate lunora-queue` with the best-practice setup scaffolds both. If the queue is fire-and-forget and message loss is acceptable, this advisory can be ignored.',run:r(t=>{if(t.queues===void 0)return[];let e=new Set(t.queues.map(i=>i.tuning.deadLetterQueue).filter(i=>i!==void 0&&i!=="")),a=[];for(let i of t.queues){let n=i.tuning.deadLetterQueue;if(n!==void 0&&n!==""||e.has(i.name))continue;let s=typeof i.tuning.maxRetries=="number"?i.tuning.maxRetries:3;a.push(o(He,{cacheKey:`queue_without_dlq:${i.exportName}`,detail:`Queue "${i.exportName}" declares no \`deadLetterQueue\`, so a message that exhausts its retries (\`maxRetries\` = ${String(s)}, ~${String(s+1)} delivery attempts) is dropped and permanently lost.`,metadata:{maxRetries:s,mode:i.mode,queue:i.exportName}}))}return a},"run"),source:"static",title:"Queue has no dead-letter queue"};var Ye={categories:["SCHEMA"],description:"A `query`/`mutation` handler accesses R2 SQL via `ctx.r2sql`. R2 SQL queries external Apache Iceberg tables over REST (no Workers binding): queries are non-deterministic (like `fetch`) and the reads are invisible to live queries. `ctx.r2sql` is available on `ActionCtx` only and must be confined to `action` handlers.",facing:"EXTERNAL",level:"WARN",name:"r2sql_outside_action",remediation:"Move the `ctx.r2sql` access into an `action(...)` (the only context where it is typed), where external I/O is allowed. If a query/mutation needs the data, have the action read it via `ctx.r2sql` and write a projection into a `defineSchema` DO/D1 table \u2014 that write is tracked by live queries, whereas R2 SQL is not.",run:r(t=>{if(t.r2sqlCalls===void 0)return[];let e=[];for(let a of t.r2sqlCalls)e.push(o(Ye,{cacheKey:`r2sql_outside_action:${a.file}:${a.line.toString()}:${a.callee}`,detail:`\`${a.callee}(\u2026)\` in ${a.exportName} (${a.file}:${a.line.toString()}) runs inside a ${a.kind} handler \u2014 R2 SQL's \`ctx.r2sql\` is non-deterministic and non-reactive, so it is available only in actions. Move the external query into an \`action\` and project the result into a Lunora table if a query/mutation needs it.`,metadata:{callee:a.callee,exportName:a.exportName,file:a.file,kind:a.kind,line:a.line}}));return e},"run"),source:"static",title:"R2 SQL `ctx.r2sql` used outside an action"};var Be={categories:["SECURITY"],description:"`new RateLimiter({...})` has no `store`, so it falls back to a per-instance in-memory store. On Workers each request may hit a different isolate, so the counter never sums across them \u2014 the limit silently under-counts and is effectively a no-op under load, giving false protection against brute-force and cost abuse.",facing:"EXTERNAL",level:"WARN",name:"ratelimit_default_memory_store",remediation:"Pass a shared, durable `store` to `new RateLimiter({...})` (a Durable-Object- or KV-backed store) so every Worker isolate reads and writes the same bucket. The default in-memory store is per-isolate and does not enforce a global limit.",run:r(t=>t.configCalls===void 0?[]:t.configCalls.filter(e=>e.callee==="RateLimiter"&&e.analyzable&&!e.presentKeys.includes("store")).map(e=>o(Be,{cacheKey:`ratelimit_default_memory_store:${e.file}:${e.line.toString()}`,detail:`\`new RateLimiter({...})\` in ${e.file}:${e.line.toString()} has no \`store\` \u2014 it uses the per-instance in-memory store, which never sums across Worker isolates, so the limit silently under-counts under load. Pass a Durable-Object- or KV-backed \`store\` so the limit is enforced globally.`,metadata:{callee:e.callee,file:e.file,line:e.line}})),"run"),source:"static",title:"RateLimiter using the default in-memory store"};var Ve={categories:["SECURITY"],description:"A `rateLimit`/`dbRateLimit` middleware call's `key` selector is derived from the handler's `args` with no server-side scoping, so an attacker can rotate the key on every request (e.g. by resubmitting a different email/id) and never share a bucket with themselves \u2014 the limit never actually limits them.",facing:"EXTERNAL",level:"WARN",name:"ratelimit_key_spoofable_or_global",remediation:'Derive the rate-limit `key` from a server-trusted value \u2014 `ctx.auth.userId` for authenticated callers, `ctx.ip` for anonymous ones (e.g. `key: (ctx) => ctx.auth.userId ?? ctx.ip ?? "anon"`) \u2014 instead of an `args` field the caller controls.',run:r(t=>t.ratelimitKeySelectors===void 0?[]:t.ratelimitKeySelectors.map(e=>o(Ve,{cacheKey:`ratelimit_key_spoofable_or_global:${e.file}:${e.line.toString()}`,detail:`\`${e.callee}(\u2026, "${e.limitName}", { key })\` in \`${e.exportName}\` (${e.file}:${e.line.toString()}) derives its \`key\` from \`args\` with no server-side scoping \u2014 an attacker can rotate the key per request and bypass the limit. Derive \`key\` from \`ctx.auth.userId\` / \`ctx.ip\` instead.`,metadata:{callee:e.callee,exportName:e.exportName,file:e.file,limitName:e.limitName,line:e.line}})),"run"),source:"static",title:"Rate-limit key derived from spoofable user input"};var Ta=["signin","signup","login","register","reset","password","otp","verify","payment","checkout"],Ca=r(t=>t.toLowerCase().replaceAll(/[^a-z0-9]+/gu,""),"n"),Kt=r(t=>{let e=Ca(t);return Ta.some(a=>e.includes(a))},"t"),La=r(t=>Kt(t.exportName)||Kt(t.limitName),"o"),Ge={categories:["SECURITY"],description:"A rate-limit or Turnstile middleware guarding a sensitive auth/payment procedure is configured `failOpen: true`, so a limiter store outage or a failed Turnstile siteverify silently admits every request \u2014 turning a transient failure into an unthrottled brute-force / abuse window.",facing:"EXTERNAL",level:"WARN",name:"ratelimit_middleware_fail_open",remediation:"Drop `failOpen: true` (the default is fail-closed) on the rate-limit / Turnstile guard for auth and payment flows, or move the escape hatch to a non-sensitive procedure. Prefer rejecting traffic with a 503 over admitting unlimited unauthenticated attempts when the limiter or siteverify is unavailable.",run:r(t=>t.failOpenGuards===void 0?[]:t.failOpenGuards.filter(e=>e.failOpen&&La(e)).map(e=>o(Ge,{cacheKey:`ratelimit_middleware_fail_open:${e.file}:${e.line.toString()}`,detail:`\`${e.callee}(...)\` in \`${e.exportName}\` (${e.file}:${e.line.toString()}) sets \`failOpen: true\` while guarding a sensitive flow \u2014 a limiter/siteverify outage then admits every request instead of rejecting it.`,metadata:{callee:e.callee,exportName:e.exportName,file:e.file,limitName:e.limitName,line:e.line}})),"run"),source:"static",title:"Fail-open rate-limit / CAPTCHA guard on a sensitive procedure"};var Pa=r((t,e,a,i)=>{let n=t.get(i.table);if(!n)return[];let s=i.kind==="one"?a:n,l=i.kind==="one"?n:a,c=[];return e(s).has(i.field)||c.push({column:i.field,owner:s.name,side:"field"}),e(l).has(i.references)||c.push({column:i.references,owner:l.name,side:"references"}),c},"m"),Qe={categories:["SCHEMA"],description:"A relation references a foreign-key or referenced column that is not declared on its table, so the join can never resolve.",facing:"INTERNAL",level:"ERROR",name:"relation_references_unknown_field",remediation:"Fix the `field` / `references` column name in the relation, or add the missing column.",run:r(t=>{let e=new Map(t.schema.tables.map(n=>[n.name,n])),a=new Map,i=r(n=>{let s=a.get(n);return s||(s=E(n),a.set(n,s)),s},"t");return t.schema.tables.flatMap(n=>n.relations.flatMap(s=>Pa(e,i,n,s).map(l=>o(Qe,{cacheKey:`relation_references_unknown_field:${n.name}:${s.name}:${l.side}`,detail:`Relation "${s.name}" on table "${n.name}" uses ${l.side} "${l.column}", which is not declared on table "${l.owner}".`,metadata:{column:l.column,owner:l.owner,relation:s.name,side:l.side,table:n.name}}))))},"run"),source:"static",title:"Relation references unknown field"};var Je={categories:["SCHEMA"],description:"A relation targets a table that does not exist in the schema. The relation can never load \u2014 the target is a typo or a removed/renamed table.",facing:"INTERNAL",level:"ERROR",name:"relation_references_unknown_table",remediation:"Fix the target table name in the relation, or add the missing table to the schema.",run:r(t=>{let e=[],a=new Set(t.schema.tables.map(i=>i.name));for(let i of t.schema.tables)for(let n of i.relations)a.has(n.table)||e.push(o(Je,{cacheKey:`relation_references_unknown_table:${i.name}:${n.name}`,detail:`Relation "${n.name}" on table "${i.name}" targets table "${n.table}", which does not exist in the schema.`,metadata:{relation:n.name,table:i.name,target:n.table}}));return e},"run"),source:"static",title:"Relation references unknown table"};var Ze={categories:["SECURITY"],description:"A public procedure reads or writes a table that is covered by an RLS policy elsewhere in the app, but this procedure's builder chain does not include `.use(rls(...))`. The raw, unwrapped `ctx.db` silently bypasses every policy in the list.",facing:"EXTERNAL",level:"WARN",name:"rls_uncovered_table",remediation:"Add `.use(rls(policies))` to the procedure's builder chain \u2014 e.g. `c.use(rls(myPolicies)).query(...)` \u2014 so its `ctx.db` is wrapped by the same policy evaluator as the rest of the app. If this procedure is intentionally privileged (e.g. a background job), declare it with `internalQuery` / `internalMutation` / `internalAction` instead of the public builder so the intent is explicit.",run:r(t=>{if(t.rlsProcedures===void 0)return[];let e=new Set;for(let i of t.rlsProcedures)for(let n of i.rlsTables)n!==""&&e.add(n);if(e.size===0)return[];let a=[];for(let i of t.rlsProcedures){if(i.usesRls||i.visibility==="internal")continue;let n=new Set;for(let s of[...i.tablesRead,...i.tablesWritten])s!==""&&e.has(s)&&n.add(s);for(let s of n)a.push(o(Ze,{cacheKey:`rls_uncovered_table:${i.file}:${i.exportName}:${s}`,detail:`\`${i.exportName}\` in ${i.file} accesses table \`${s}\` without \`.use(rls(...))\` \u2014 the table is policy-gated elsewhere in the app but this procedure bypasses those policies entirely.`,metadata:{exportName:i.exportName,file:i.file,table:s}}))}return a},"run"),source:"static",title:"RLS-gated table accessed without rls() middleware"};var et={categories:["PERFORMANCE"],description:"A `defineShape` replicates from a `.global()` table. Global tables live outside the shard DO's SQLite op-log, so the shape is served through the cross-shard tier \u2014 coordinator/poll-refreshed and latency-tiered, not poke-live like a sharded shape.",facing:"EXTERNAL",level:"WARN",name:"shape_targets_global_table",remediation:"Expected if you want the cross-shard tier \u2014 just don't assume poke-live freshness; global shapes refresh on a poll. For live updates, replicate from a sharded table instead (denormalize the columns you need into the shard).",run:r(t=>{if(t.shapes===void 0)return[];let e=new Set(t.schema.tables.filter(i=>i.shardKind==="global").map(i=>i.name)),a=[];for(let i of t.shapes)i.table===void 0||!e.has(i.table)||a.push(o(et,{cacheKey:`shape_targets_global_table:${i.exportName}`,detail:`Shape \`${i.exportName}\` (${i.file}) replicates from the \`.global()\` table \`${i.table}\`. It is served through the cross-shard global tier \u2014 poll-refreshed and latency-tiered, not poke-live.`,metadata:{exportName:i.exportName,file:i.file,table:i.table}}));return a},"run"),source:"static",title:"Shape replicates from a global (cross-shard) table"};var tt={categories:["SCHEMA"],description:"A `defineShape` is bound to a `table` name that does not exist in the schema. The shape can never resolve a rowset \u2014 its subscription seeds empty and errors at the first flush.",facing:"INTERNAL",level:"ERROR",name:"shape_unknown_table",remediation:"Fix the shape's `table` to a real table name (check for a typo or a table that was renamed/removed).",run:r(t=>{if(t.shapes===void 0)return[];let e=new Set(t.schema.tables.map(i=>i.name)),a=[];for(let i of t.shapes)i.table===void 0||e.has(i.table)||a.push(o(tt,{cacheKey:`shape_unknown_table:${i.exportName}`,detail:`Shape \`${i.exportName}\` (${i.file}) replicates from table \`${i.table}\`, which is not declared in the schema. The shape can never resolve a rowset.`,metadata:{exportName:i.exportName,file:i.file,table:i.table}}));return a},"run"),source:"static",title:"Shape bound to an unknown table"};var at={categories:["SECURITY"],description:"A public `mutation`/`action` that creates a user/session/account row has no disposable-email gate. Throwaway mailboxes farm free trials, evade bans, and pollute the user table.",facing:"EXTERNAL",level:"WARN",name:"signup_mutation_without_disposable_gating",remediation:"Add `.use(emailGateMiddleware({ email: (ctx) => ctx.args.email }))` from `@lunora/auth/email-guard` to reject disposable domains at signup, or gate better-auth's native signup with `withEmailGate(...)` / `emailGateDatabaseHooks(...)`.",run:r(t=>{if(t.procedureProtections===void 0)return[];let e=[];for(let a of t.procedureProtections)!_(a)||!a.writesUserTable||a.usesEmailGate||a.hasEmailArg!==!1&&e.push(o(at,{cacheKey:`signup_mutation_without_disposable_gating:${a.file}:${a.exportName}`,detail:`Public ${a.kind} \`${a.exportName}\` (${a.file}) writes a user/session/account table but has no disposable-email gate. Add \`.use(emailGateMiddleware(...))\` from \`@lunora/auth/email-guard\`.`,metadata:{exportName:a.exportName,file:a.file,kind:a.kind,writesUserTable:a.writesUserTable}}));return e},"run"),source:"static",title:"Account-creating write without a disposable-email gate"};var it={categories:["SECURITY"],description:"A `.public()` read passes `includeDeleted` (hardcoded `true` or derived from `args`) on a `.softDelete()` table, resurfacing soft-deleted rows to callers that should never see them.",facing:"EXTERNAL",level:"INFO",name:"soft_delete_include_deleted_from_args",remediation:"Drop `includeDeleted` from the public read so soft-deleted rows stay hidden, or move the trash/restore view behind an internal (or RLS/role-gated) procedure. Never wire `includeDeleted` from `args` on a public read \u2014 an attacker can set it to see rows the app treats as deleted.",run:r(t=>{if(t.softDeleteReads===void 0)return[];let e=new Set(t.schema.tables.filter(a=>a.softDelete!==void 0).map(a=>a.name));return e.size===0?[]:t.softDeleteReads.filter(a=>a.visibility==="public"&&a.table!==""&&e.has(a.table)&&(a.hardcodedTrue||a.fromArgs)).map(a=>{let i=`\`${a.exportName}\` (${a.file}:${a.line.toString()})`,n=a.fromArgs?`The public read in ${i} wires \`includeDeleted\` from request \`args\` on soft-delete table \`${a.table}\`, letting any caller resurface soft-deleted rows the app treats as gone.`:`The public read in ${i} hardcodes \`includeDeleted: true\` on soft-delete table \`${a.table}\`, returning soft-deleted rows to every caller.`;return o(it,{cacheKey:`soft_delete_include_deleted_from_args:${a.file}:${a.line.toString()}:${a.table}`,detail:n,metadata:{exportName:a.exportName,file:a.file,line:a.line,source:a.fromArgs?"args":"literal",table:a.table}})})},"run"),source:"static",title:"Soft-deleted rows resurfaced via includeDeleted on a public read"};var rt={categories:["SECURITY"],description:"A `ctx.sql` tagged-template interpolates an unparameterized string-building expression (concatenation or nested template) instead of a bound value \u2014 splicing raw text into the query and reopening SQL injection.",facing:"EXTERNAL",level:"ERROR",name:"sql_injection_risk",remediation:"Pass the value through a bound placeholder so the driver parameterizes it \u2014 keep request input inside a `ctx.sql` placeholder instead of concatenating it into the query string. Never build SQL text from request input by hand.",run:r(t=>t.sqlInterpolations===void 0?[]:t.sqlInterpolations.map(e=>o(rt,{cacheKey:`sql_injection_risk:${e.file}:${e.line.toString()}`,detail:`\`ctx.sql\` in \`${e.exportName}\` (${e.file}:${e.line.toString()}) interpolates a string-building expression instead of a bound value \u2014 a SQL-injection vector. Pass the value through a bound placeholder so the driver parameterizes it.`,metadata:{exportName:e.exportName,file:e.file,line:e.line}})),"run"),source:"static",title:"Possible SQL injection in ctx.sql interpolation"};var nt={categories:["SECURITY"],description:"A `ctx.storage.generateUploadUrl(key, \u2026)` call has no `contentType` pin, so the signed PUT URL it mints authorizes any content-type/size the client chooses \u2014 `upload()`'s `allowedContentTypes`/`maxSize` guards never run for a client-side PUT against this URL.",facing:"EXTERNAL",level:"WARN",name:"storage_generate_upload_url_no_content_type_pin",remediation:"Pass `contentType` to `ctx.storage.generateUploadUrl(key, { contentType })` so the signature only authorizes a PUT with that exact content-type. If the client's upload size/type also needs bounding, prefer `upload()`/`store()` (with `allowedContentTypes`/`maxSize`) over a client-side signed PUT.",run:r(t=>t.storageUploads===void 0?[]:t.storageUploads.filter(e=>e.method==="generateUploadUrl"&&e.analyzable&&!e.presentKeys.includes("contentType")).map(e=>o(nt,{cacheKey:`storage_generate_upload_url_no_content_type_pin:${e.file}:${e.line.toString()}`,detail:`\`ctx.storage.generateUploadUrl\` in \`${e.exportName}\` (${e.file}:${e.line.toString()}) has no \`contentType\` pin \u2014 the signed PUT it mints accepts any type/size, minted client-side and bypassing \`upload()\`'s guards.`,metadata:{exportName:e.exportName,file:e.file,line:e.line}})),"run"),source:"static",title:"generateUploadUrl signed PUT with no content-type pin"};var Mt=b({cacheKey:r(t=>`storage_key_from_user_args:${t.file}:${t.line.toString()}`,"cacheKey"),categories:["SECURITY"],description:"A `ctx.storage.*` call uses an R2 object key taken directly from the handler's `args` with no server-side scoping. The bucket methods key by the caller-supplied string, so any caller can read, overwrite, or delete another user's object \u2014 object-level IDOR.",detail:r(t=>`\`ctx.storage.*.${t.method}\` in \`${t.exportName}\` (${t.file}:${t.line.toString()}) uses an object key derived from \`args\` with no server-side scoping \u2014 any caller can read/overwrite/delete another user's object (IDOR). Prefix the key with a server-trusted identity (e.g. \`\${ctx.auth.userId}/\u2026\`) or resolve the object through an owned record.`,"detail"),facing:"EXTERNAL",getAccesses:r(t=>t.storageKeyAccesses,"getAccesses"),level:"ERROR",metadata:r(t=>({exportName:t.exportName,file:t.file,line:t.line,method:t.method}),"metadata"),name:"storage_key_from_user_args",remediation:"Prefix the object key with a server-trusted identity (e.g. `${ctx.auth.userId}/\u2026`) or resolve the object through a record the caller is known to own. Never pass request input straight through as an R2 object key.",title:"R2 object key taken directly from user args (IDOR)"});var Ua=new Set(["getPresignedUrl","getSignedUrl"]),qa=8640*60,Ot=r(t=>t.method==="getPresignedUrl","s"),Dt=r(t=>Ua.has(t.method)&&t.expiresInSeconds!==void 0&&t.expiresInSeconds>=qa,"a"),ot={categories:["SECURITY"],description:"`ctx.storage.getPresignedUrl(...)` mints a native S3 SigV4 URL that resolves directly against R2, bypassing the Worker's auth/RLS/rate-limit entirely \u2014 the wrong choice for private or policy-gated content. Either signer minting an `expiresInSeconds` near the shared 7-day ceiling also hands out a long-lived bearer credential.",facing:"EXTERNAL",level:"WARN",name:"storage_presigned_url_for_private_content",remediation:"For private/per-user/policy-gated content, use `getSignedUrl` (resolves back through the Worker, so your auth/RLS/rate-limit gates still run) instead of `getPresignedUrl`. Reserve `getPresignedUrl` for genuinely public or bulk content. On either signer, keep `expiresInSeconds` as short as the use case allows \u2014 well under the 7-day ceiling \u2014 rather than minting a near-maximum-lifetime URL.",run:r(t=>t.storageUploads===void 0?[]:t.storageUploads.filter(e=>Ot(e)||Dt(e)).map(e=>{let a=Ot(e),i=Dt(e),n=`\`${e.exportName}\` (${e.file}:${e.line.toString()})`,s;return a&&i?s=`\`ctx.storage.getPresignedUrl\` in ${n} mints a native S3 SigV4 URL \u2014 bypasses the Worker's auth/RLS/rate-limit entirely \u2014 with an \`expiresInSeconds\` of ${(e.expiresInSeconds??0).toString()}s, near the 7-day signing ceiling.`:a?s=`\`ctx.storage.getPresignedUrl\` in ${n} mints a native S3 SigV4 URL that resolves directly against R2, bypassing the Worker's auth/RLS/rate-limit entirely. Use \`getSignedUrl\` for app-gated access to private content.`:s=`\`ctx.storage.${e.method}\` in ${n} requests an \`expiresInSeconds\` of ${(e.expiresInSeconds??0).toString()}s, near the 7-day signing ceiling \u2014 a leaked link stays valid for almost a week.`,o(ot,{cacheKey:`storage_presigned_url_for_private_content:${e.file}:${e.line.toString()}`,detail:s,metadata:{expiresInSeconds:e.expiresInSeconds,exportName:e.exportName,file:e.file,line:e.line,method:e.method}})}),"run"),source:"static",title:"Native presigned URL or near-max-TTL signed URL for private content"};var Ka=new Set(["store","upload"]),st={categories:["SECURITY"],description:"A `ctx.storage.upload`/`store` call has no `allowedContentTypes` allowlist, so an uploader can store any content-type \u2014 including `text/html` or `image/svg+xml` \u2014 which, served back from `publicBaseUrl`, is a stored-XSS vector against your own origin.",facing:"EXTERNAL",level:"WARN",name:"storage_upload_without_content_type_allowlist",remediation:'Pass `allowedContentTypes: ["image/png", "image/jpeg", \u2026]` to `ctx.storage.upload`/`store` so an uploaded object\'s `contentType` must match an approved list. Reject anything HTML/SVG/script-executable unless the app genuinely needs to serve it.',run:r(t=>t.storageUploads===void 0?[]:t.storageUploads.filter(e=>Ka.has(e.method)&&e.analyzable&&!e.presentKeys.includes("allowedContentTypes")).map(e=>o(st,{cacheKey:`storage_upload_without_content_type_allowlist:${e.file}:${e.line.toString()}`,detail:`\`ctx.storage.${e.method}\` in \`${e.exportName}\` (${e.file}:${e.line.toString()}) has no \`allowedContentTypes\` allowlist \u2014 any content-type is accepted, including \`text/html\`/\`image/svg+xml\`, a stored-XSS risk if the object is served from \`publicBaseUrl\`.`,metadata:{exportName:e.exportName,file:e.file,line:e.line,method:e.method}})),"run"),source:"static",title:"Storage upload with no content-type allowlist"};var Ma=new Set(["store","upload"]),lt={categories:["SECURITY"],description:"A `ctx.storage.upload`/`store` call has no `maxSize` cap, so a caller can push an unbounded body straight into R2 through the Worker \u2014 unbounded storage/billing exhaustion, and for a streamed body, unbounded worker time.",facing:"EXTERNAL",level:"WARN",name:"storage_upload_without_max_size",remediation:"Pass a `maxSize` (bytes) to `ctx.storage.upload`/`store` sized to the largest legitimate object the app accepts. `ArrayBuffer`/`Blob` bodies are rejected up front when oversized; a `ReadableStream` body is aborted mid-transfer once the limit is exceeded.",run:r(t=>t.storageUploads===void 0?[]:t.storageUploads.filter(e=>Ma.has(e.method)&&e.analyzable&&!e.presentKeys.includes("maxSize")).map(e=>o(lt,{cacheKey:`storage_upload_without_max_size:${e.file}:${e.line.toString()}`,detail:`\`ctx.storage.${e.method}\` in \`${e.exportName}\` (${e.file}:${e.line.toString()}) has no \`maxSize\` cap \u2014 a caller can push an unbounded body into R2, exhausting storage/billing.`,metadata:{exportName:e.exportName,file:e.file,line:e.line,method:e.method}})),"run"),source:"static",title:"Storage upload with no size cap"};var ct={categories:["SCHEMA"],description:'No function inserts into this table via `ctx.db.insert("<table>", \u2026)`. It may be read-only by design (seeded by a migration, replicated, or written through a path the advisor can\'t see) \u2014 or it may be dead schema.',facing:"INTERNAL",level:"INFO",name:"table_without_insert",remediation:'If the table should be writable, add a mutation that calls `ctx.db.insert("<table>", \u2026)`. If it is read-only or seeded elsewhere, this advisory can be ignored.',run:r(t=>{if(t.inserts===void 0)return[];let e=new Set(t.inserts.filter(i=>i.table!=="").map(i=>i.table)),a=[];for(let i of t.schema.tables)e.has(i.name)||i.externallyManaged===!0||a.push(o(ct,{cacheKey:`table_without_insert:${i.name}`,detail:`No function calls \`ctx.db.insert("${i.name}", \u2026)\` \u2014 table "${i.name}" has no discovered insert path.`,metadata:{table:i.name}}));return a},"run"),source:"static",title:"Table has no insert path"};var Oa=new Set(["date","number","timestamp"]),dt={categories:["SCHEMA"],description:"A `.ttl(field)` policy points at a column that is not an epoch-millisecond timestamp, so the alarm sweep's `field < now` comparison can't decide expiry correctly.",facing:"INTERNAL",level:"ERROR",name:"ttl_field_not_timestamp",remediation:"Point `.ttl(field)` at a `v.timestamp()` / `v.date()` / `v.number()` (epoch-ms) column, or add such a column.",run:r(t=>{let e=[];for(let a of t.schema.tables){let{ttl:i}=a;if(!i)continue;let n=a.columnKinds?.[i.field];n===void 0||Oa.has(n)||e.push(o(dt,{cacheKey:`ttl_field_not_timestamp:${a.name}:${i.field}`,detail:`Table "${a.name}" declares \`.ttl("${i.field}")\`, but "${i.field}" is a ${n} column, not an epoch-millisecond timestamp.`,metadata:{field:i.field,kind:n,table:a.name}}))}return e},"run"),source:"static",title:"TTL field is not a timestamp"};var ut={categories:["SECURITY"],description:"A public `v.string()` argument has no maximum-length bound. An unbounded string lets a client submit arbitrarily large input \u2014 abusing storage and CPU on every request that processes it.",facing:"EXTERNAL",level:"INFO",name:"unbounded_string_arg",remediation:"Add a max-length bound via `.check(...)` / `.meta({ maxLength })` on the string validator (e.g. cap a name at 256, a body at a few KB). Size the cap to the field's real-world maximum.",run:r(t=>{if(t.argValidators===void 0)return[];let e=[];for(let a of t.argValidators)for(let i of a.unboundedStringArgs)e.push(o(ut,{cacheKey:`unbounded_string_arg:${a.file}:${a.exportName}:${i}`,detail:`Arg \`${i}\` of public procedure \`${a.exportName}\` (${a.file}:${a.line.toString()}) is an unbounded \`v.string()\`. Add a max-length bound to cap payload size.`,metadata:{argument:i,exportName:a.exportName,file:a.file,line:a.line}}));return e},"run"),source:"static",title:"Public string argument has no length bound"};var T=r(t=>`by${t.charAt(0).toUpperCase()}${t.slice(1)}`,"i"),C=r(t=>new Set(t.indexes.filter(e=>e.kind==="index").map(e=>e.fields[0]).filter(e=>e!==void 0)),"n");var ht={categories:["PERFORMANCE"],description:"A foreign-key column declared by a `one` relation has no index leading with it. Reads that filter or join on the column full-scan the table, which gets linearly slower as rows accumulate.",facing:"EXTERNAL",level:"INFO",name:"unindexed_foreign_key",remediation:'Add a secondary index leading with the FK column, e.g. `.index("byAuthorId", ["authorId"])`.',run:r(t=>{let e=[];for(let a of t.schema.tables){let i=C(a);for(let n of a.relations){if(n.kind!=="one")continue;let s=n.field;if(s==="_id"||i.has(s))continue;let l=T(s);e.push(o(ht,{cacheKey:`unindexed_foreign_key:${a.name}:${s}`,detail:`Relation "${n.name}" on table "${a.name}" references "${n.table}" via column "${s}", which is not the leading column of any index. Reads filtering or joining on "${s}" full-scan "${a.name}".`,metadata:{fkColumn:s,references:{column:n.references,table:n.table},relation:n.name,suggestedIndex:{fields:[s],name:l},table:a.name}}))}}return e},"run"),source:"static",title:"Unindexed foreign key"};var pt={categories:["PERFORMANCE"],description:"A `many` relation's foreign-key column on the target table has no index leading with it. Relation predicates (`some`/`none`/`every`) and `with:` child reads filter the target on that column, full-scanning it as rows accumulate.",facing:"EXTERNAL",level:"INFO",name:"unindexed_relation_target",remediation:'Add a secondary index on the target table leading with the FK column, e.g. `.index("byAuthorId", ["authorId"])`.',run:r(t=>{let e=[],a=new Map(t.schema.tables.map(i=>[i.name,i]));for(let i of t.schema.tables)for(let n of i.relations){if(n.kind!=="many")continue;let s=a.get(n.table);if(s===void 0)continue;let l=n.field;if(l==="_id"||s.relations.some(d=>d.kind==="one"&&d.field===l)||C(s).has(l))continue;let c=T(l);e.push(o(pt,{cacheKey:`unindexed_relation_target:${n.table}:${l}`,detail:`Relation "${n.name}" on table "${i.name}" is a to-many over "${n.table}" via its column "${l}", which is not the leading column of any index on "${n.table}". Relation predicates (\`some\`/\`none\`/\`every\`) and \`with:\` reads of "${n.name}" full-scan "${n.table}".`,metadata:{fkColumn:l,references:{column:n.references,table:i.name},relation:n.name,suggestedIndex:{fields:[l],name:c},table:n.table}}))}return e},"run"),source:"static",title:"Unindexed relation target"};var Wt={"empty-object":"`{}`",undefined:"`undefined`"},ft={categories:["SECURITY"],description:"A branching `defineShape({ where })` / `definePolicy({ when })` predicate returns `{}` or `undefined` from one arm. A row predicate is a filter, not a boolean: `{}` matches EVERY row, so an arm meant to deny instead replicates or exposes the whole table \u2014 silently.",facing:"EXTERNAL",level:"ERROR",name:"unrestricted_where_branch",remediation:"Return `deny()` (from `@lunora/server`) \u2014 or `false`, which compiles to the same vacuously-false `{ OR: [] }` predicate \u2014 from the denial arm. Reach for `allowAll()` only when that arm genuinely means 'no further restriction'. For an owner check, prefer `.ownedBy(field)` on the table plus `defineShape({ owner: true })`, which derives the predicate from the verified identity and cannot be written backwards.",run:r(t=>t.unrestrictedWhereBranches===void 0?[]:t.unrestrictedWhereBranches.map(e=>o(ft,{cacheKey:`unrestricted_where_branch:${e.file}:${e.line.toString()}:${e.key}`,detail:`\`${e.owner}\` \`${e.exportName}\` (${e.file}:${e.line.toString()}) returns ${Wt[e.form]} from one arm of its \`${e.key}\` predicate. A predicate is a filter, not a boolean \u2014 ${Wt[e.form]} imposes no restriction, so this arm matches every row instead of none. Return \`deny()\` (or \`false\`) if it was meant to deny; use \`allowAll()\` to state an intentionally unrestricted arm.`,metadata:{exportName:e.exportName,file:e.file,form:e.form,key:e.key,line:e.line,owner:e.owner}})),"run"),source:"static",title:"Predicate arm returns an unrestricted filter"};var gt={categories:["SECURITY"],description:"A public `mutation`/`action` that creates a user/session or sends mail has no CAPTCHA / bot check. Account-creating and mail-sending endpoints are prime automated-abuse targets (credential stuffing, mailbox flooding, disposable-account farming).",facing:"EXTERNAL",level:"WARN",name:"user_creating_mutation_without_captcha",remediation:"Add a server-verified human check: `.use(verifyTurnstile({ secret, token }))` from `@lunora/auth`, or wrap it with `.use(protectPublic({ rateLimit, captcha }))` from `@lunora/server`. Pair with a rate limit for defense in depth.",run:r(t=>{if(t.procedureProtections===void 0)return[];let e=[];for(let a of t.procedureProtections){let i=a.writesUserTable||a.callsMail;if(!_(a)||!i||a.usesCaptcha)continue;let n=a.writesUserTable?"writes a user/session table":"sends mail";e.push(o(gt,{cacheKey:`user_creating_mutation_without_captcha:${a.file}:${a.exportName}`,detail:`Public ${a.kind} \`${a.exportName}\` (${a.file}) ${n} but has no CAPTCHA check. Add \`.use(verifyTurnstile(...))\` or \`.use(protectPublic({ captcha }))\`.`,metadata:{callsMail:a.callsMail,exportName:a.exportName,file:a.file,kind:a.kind,writesUserTable:a.writesUserTable}}))}return e},"run"),source:"static",title:"Account-creating / mail-sending write without a CAPTCHA"};var Ft=b({cacheKey:r(t=>`vectors_namespace_from_user_input:${t.file}:${t.line.toString()}`,"cacheKey"),categories:["SECURITY"],description:"A `ctx.vectors.query`/`upsert`/`upsertMany` call uses a `namespace` derived from the handler's `args` with no server-side scoping. A Vectorize namespace partitions one index into isolated sub-collections, so an unscoped namespace lets any caller read or poison another tenant's vectors.",detail:r(t=>`\`ctx.vectors.${t.method}\` in \`${t.exportName}\` (${t.file}:${t.line.toString()}) uses a Vectorize namespace derived from \`args\` with no server-side scoping \u2014 any caller can read or poison another tenant's vectors. Derive the namespace from a server-trusted identity (e.g. \`\${ctx.auth.orgId}\`), never from \`args\`.`,"detail"),facing:"EXTERNAL",getAccesses:r(t=>t.vectorNamespaceAccesses,"getAccesses"),level:"WARN",metadata:r(t=>({exportName:t.exportName,file:t.file,line:t.line,method:t.method}),"metadata"),name:"vectors_namespace_from_user_input",remediation:"Derive the `namespace` from a server-trusted identity (e.g. `${ctx.auth.orgId}`), never from `args`.",title:"Vectorize namespace derived from unscoped user input"});var bt={categories:["SCHEMA"],description:"Two durable steps in this workflow share a name. Cloudflare memoizes a step by its name, so on replay the second call returns the first step's cached result instead of running its body \u2014 silently skipping the work without an error.",facing:"INTERNAL",level:"ERROR",name:"workflow_duplicate_step_name",remediation:"Give every `step.do` / `step.sleep` / `step.sleepUntil` / `step.waitForEvent` call in the workflow a unique name. If a step legitimately repeats (e.g. a loop), make the name distinct per iteration by interpolating the item id into the step name.",run:r(t=>{if(t.workflows===void 0)return[];let e=[];for(let a of t.workflows){let{steps:i}=a;if(i===void 0||i.length===0)continue;let n=new Map,s=new Set;for(let l of i){let c=n.get(l.name);if(c===void 0){n.set(l.name,l.line);continue}s.has(l.name)||(s.add(l.name),e.push(o(bt,{cacheKey:`workflow_duplicate_step_name:${a.exportName}:${l.name}`,detail:`Workflow "${a.exportName}" reuses the durable step name "${l.name}" (first at line ${String(c)}, again at line ${String(l.line)}). The second call returns the first's cached result instead of running.`,metadata:{firstLine:c,line:l.line,stepName:l.name,workflow:a.exportName}})))}}return e},"run"),source:"static",title:"Duplicate durable step name in workflow"};var yt={categories:["SCHEMA"],description:'A `ctx.workflows.get("<name>")` call references a workflow that is not declared by any `defineWorkflow` export in `lunora/workflows.ts`. The name is a typo or points at a removed/renamed workflow \u2014 the call throws at runtime.',facing:"INTERNAL",level:"ERROR",name:"workflow_unknown_target",remediation:'Fix the workflow name in the `ctx.workflows.get("\u2026")` call, or add the missing `defineWorkflow` export to `lunora/workflows.ts`.',run:r(t=>{if(t.workflows===void 0||t.workflowCalls===void 0)return[];let e=new Set(t.workflows.map(i=>i.exportName)),a=[];for(let i of t.workflowCalls)i.workflow===""||e.has(i.workflow)||a.push(o(yt,{cacheKey:`workflow_unknown_target:${i.file}:${i.exportName}:${i.workflow}`,detail:`\`ctx.workflows.get("${i.workflow}")\` in "${i.exportName}" (${i.file}) references workflow "${i.workflow}", which is not declared in lunora/workflows.ts.`,metadata:{exportName:i.exportName,file:i.file,line:i.line,workflow:i.workflow}}));return a},"run"),source:"static",title:"Workflow call references unknown workflow"};var wt={categories:["SCHEMA"],description:'No function starts this workflow via `ctx.workflows.get("<name>")`. It may be triggered through a path the advisor can\'t see (the Cloudflare API, a wrangler invocation, a cross-service binding) \u2014 or it may be dead code that still deploys as a billable WorkflowEntrypoint.',facing:"INTERNAL",level:"INFO",name:"workflow_unused",remediation:'If the workflow should be triggered in-app, start it from a mutation/action with `ctx.workflows.get("<name>").create({ params })`. If it is started externally or is no longer needed, this advisory can be ignored (or remove the `defineWorkflow` export).',run:r(t=>{if(t.workflows===void 0)return[];let e=t.workflowCalls??[];if(e.some(n=>n.workflow===""))return[];let a=new Set(e.map(n=>n.workflow)),i=[];for(let n of t.workflows)a.has(n.exportName)||i.push(o(wt,{cacheKey:`workflow_unused:${n.exportName}`,detail:`No function calls \`ctx.workflows.get("${n.exportName}")\` \u2014 workflow "${n.exportName}" is declared but never started in app code.`,metadata:{workflow:n.exportName}}));return i},"run"),source:"static",title:"Workflow is never started"};var Da=[$e,Je,Qe,yt,bt,tt,A,me,N,ce,de,fe,ge,ue,dt,ie,ht,pt,le,ct,wt,He,he,S,et,Ie,Te,ve,Ye,V,Oe,Ze,Ne,Se,re,ne,je,ft,gt,at,ze,ut,be,rt,z,qe,Re,Be,ae,We,Fe,ke,Y,W,F,I,Mt,qt,Lt,j,X,Ft,Ae,Ct,De,se,oe,J,te,G,Z,Q,ee,Pt,Ve,Xe,B,st,lt,nt,ot,ye,we,Ge,pe,H,xe,Ke,it,Ee,Ue,Ce,Pe,Le,Me],Wa=[D,R,O],Fa=[...Da,...Wa],qd=r((t,e={})=>{let a=e.lints??Fa,i=[];for(let n of a)e.source!==void 0&&n.source!==e.source||i.push(...n.run(t));return $(i)},"jt");export{Vt as a,Wa as b,qd as c};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{a as $}from"./chunk-
|
|
1
|
+
import{a as $}from"./chunk-JOFD43OI.js";import{a as b}from"./chunk-53WEPGUD.js";import"./chunk-XXQUMVWH.js";import"./chunk-RNL6BSFM.js";import"./chunk-QLQADHFE.js";import{a as u}from"./chunk-ERMTJYQF.js";import{c as p}from"./chunk-XDAT7LHC.js";import"./chunk-EN2QAEO4.js";import"./chunk-XBDKZZ67.js";import{a as y,b as w,c as j,d as h,e as i,f as m}from"./chunk-MIQTKZAR.js";import{a as l,e as c}from"./chunk-UZERI5YC.js";import{d as k}from"./chunk-OG4N2XHD.js";import"./chunk-AADILRJW.js";import{a as o,e as S,h as E,j as x}from"./chunk-3XIESWL4.js";var e=S(E(),1);var n={clean:"text-emerald-600 dark:text-emerald-400",exempt:"text-muted-foreground",failing:"text-red-600 dark:text-red-400",warned:"text-amber-600 dark:text-amber-400"},A={"at-risk":"text-red-600 dark:text-red-400",excellent:"text-emerald-600 dark:text-emerald-400",good:"text-emerald-600 dark:text-emerald-400","needs-work":"text-amber-600 dark:text-amber-400"},P=o((r,a)=>`exempt: ${r.exemptReason===void 0||r.exemptReason===""?a:r.exemptReason}`,"S"),T=o(r=>r.toSorted((a,d)=>a.coverage==="clean"&&d.coverage!=="clean"?1:d.coverage==="clean"&&a.coverage!=="clean"?-1:a.score-d.score),"E"),_=o(({testId:r="advisor-health"})=>{let a=k(),d=p(u.getAdvisories,{},{live:!0}),g=p(u.getAdvisorProcedures,{},{live:!0}),f=d.data?.advisories??null,N=g.data?.procedures??null,t=f===null||N===null?null:$(N,f),v=g.error??d.error;if(v!==null)return(0,e.jsx)(b,{error:v,testId:`${r}-error`});if(t===null)return(0,e.jsx)(l,{children:(0,e.jsx)(c,{className:"py-8 text-center text-muted-foreground","data-testid":`${r}-loading`,children:a("Loading\u2026")})});let I=T(t.procedures);return(0,e.jsxs)("div",{className:"space-y-4","data-testid":r,children:[(0,e.jsx)(l,{children:(0,e.jsxs)(c,{className:"flex flex-wrap items-baseline gap-x-6 gap-y-2 py-6",children:[(0,e.jsxs)("div",{children:[(0,e.jsx)("span",{className:x("text-4xl font-semibold tabular-nums",A[t.grade]),"data-testid":`${r}-score`,children:t.score}),(0,e.jsx)("span",{className:"text-muted-foreground text-lg",children:"/100"})]}),(0,e.jsx)("span",{className:x("text-sm font-medium uppercase tracking-wide",A[t.grade]),"data-testid":`${r}-grade`,children:t.grade}),(0,e.jsxs)("div",{className:"text-muted-foreground flex gap-4 text-sm tabular-nums","data-testid":`${r}-summary`,children:[(0,e.jsxs)("span",{className:n.clean,children:[t.summary.clean," ",a("clean")]}),(0,e.jsxs)("span",{className:n.warned,children:[t.summary.warned," ",a("warned")]}),(0,e.jsxs)("span",{className:n.failing,children:[t.summary.failing," ",a("failing")]}),(0,e.jsxs)("span",{className:n.exempt,children:[t.summary.exempt," ",a("exempt")]})]})]})}),t.project.checks.length>0&&(0,e.jsx)(l,{children:(0,e.jsxs)(c,{className:"py-4 text-sm","data-testid":`${r}-project`,children:[(0,e.jsxs)("span",{className:"font-medium",children:[a("Schema & config")," ",t.project.score,"/100"]}),(0,e.jsxs)("span",{className:"text-muted-foreground",children:[" \u2014 ",t.project.checks.map(s=>s.name).join(", ")]})]})}),(0,e.jsx)(l,{children:(0,e.jsx)(c,{className:"p-0",children:(0,e.jsxs)(y,{children:[(0,e.jsx)(w,{children:(0,e.jsxs)(h,{children:[(0,e.jsx)(i,{className:"w-20",children:a("Score")}),(0,e.jsx)(i,{children:a("Procedure")}),(0,e.jsx)(i,{className:"w-32",children:a("Kind")}),(0,e.jsx)(i,{children:a("Rules fired")})]})}),(0,e.jsx)(j,{children:I.map(s=>(0,e.jsxs)(h,{"data-testid":`${r}-row`,children:[(0,e.jsx)(m,{className:x("tabular-nums font-medium",n[s.coverage]),children:s.coverage==="exempt"?"\u2014":s.score}),(0,e.jsx)(m,{className:"font-mono text-xs",children:s.id}),(0,e.jsxs)(m,{className:"text-muted-foreground text-xs",children:[s.visibility," ",s.kind,s.sensitivity.level==="high"&&(0,e.jsx)("span",{className:"ml-1 text-amber-600 dark:text-amber-400",children:a("sensitive")})]}),(0,e.jsx)(m,{className:"text-muted-foreground text-xs",children:s.coverage==="exempt"?P(s,a("no reason given")):s.checks.map(R=>R.name).join(", ")})]},s.id))})]})})})]})},"U");export{_ as default};
|