@lunora/advisor 1.0.0-alpha.2 → 1.0.0-alpha.21

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (68) hide show
  1. package/LICENSE.md +6 -0
  2. package/README.md +1 -1
  3. package/__assets__/package-og.svg +1 -1
  4. package/dist/index.d.mts +2160 -4
  5. package/dist/index.d.ts +2160 -4
  6. package/dist/index.mjs +110 -8
  7. package/dist/packem_shared/{AE_METRIC_EVENTS-DexctYv6.mjs → AE_METRIC_EVENTS-BM14d0lm.mjs} +3 -1
  8. package/dist/packem_shared/actionFetchSsrf-wbmYgzmz.mjs +26 -0
  9. package/dist/packem_shared/aiRawRunEscapeHatch-C29jd32J.mjs +26 -0
  10. package/dist/packem_shared/aiToolSideEffectPromptInjection-BtD3alqB.mjs +33 -0
  11. package/dist/packem_shared/aiUnboundedGenerationPublic-CbAcSxLB.mjs +26 -0
  12. package/dist/packem_shared/allowUnauthenticatedShardAccessEnabled-BcASq-jP.mjs +30 -0
  13. package/dist/packem_shared/argument-derived-sink-C1xTTqAt.mjs +30 -0
  14. package/dist/packem_shared/authCsrfCheckDisabled-DCf9FSoD.mjs +26 -0
  15. package/dist/packem_shared/authEmailVerificationDisabled-Dk-Tc5oU.mjs +26 -0
  16. package/dist/packem_shared/authSecureCookiesDisabled-osJrHW9Y.mjs +26 -0
  17. package/dist/packem_shared/authSessionFreshageZero-CjufCc_Q.mjs +26 -0
  18. package/dist/packem_shared/authTrustedOriginsWildcard-ylv4PKzC.mjs +26 -0
  19. package/dist/packem_shared/browserAllowPrivateTargets-CBvQxLAM.mjs +26 -0
  20. package/dist/packem_shared/browserUserUrlWithoutAllowlist-CvhA6w59.mjs +26 -0
  21. package/dist/packem_shared/containerInstanceKeyFromUserInput-BUXj2J4w.mjs +19 -0
  22. package/dist/packem_shared/containerRuntimeEgressRelaxation-B0LlfmDA.mjs +26 -0
  23. package/dist/packem_shared/containerStartEnableInternetOverride-DDaHZQ1L.mjs +26 -0
  24. package/dist/packem_shared/externalSourceOnGlobal-Bg-NfCX9.mjs +30 -0
  25. package/dist/packem_shared/externalSourceUnscoped-5vT-Bup3.mjs +44 -0
  26. package/dist/packem_shared/flagGatesSecurityWithUnsafeDefault-BhIs0shr.mjs +41 -0
  27. package/dist/packem_shared/{fromServerSchema-DinF1nph.mjs → fromServerSchema-BiB6JSiW.mjs} +9 -0
  28. package/dist/packem_shared/helpers-BySnKhVB.mjs +31 -0
  29. package/dist/packem_shared/httpActionMissingAuthGuard-CxipddNx.mjs +35 -0
  30. package/dist/packem_shared/httpActionResponseHeaderInjection-DOFS7pFT.mjs +39 -0
  31. package/dist/packem_shared/identityUndeclaredClaimTrusted-D8nXV2dd.mjs +32 -0
  32. package/dist/packem_shared/imagesUrlSourceFromUserInput-YcDQ0b_Y.mjs +19 -0
  33. package/dist/packem_shared/{indexReferencesUnknownField-DH0_dbUY.mjs → indexReferencesUnknownField-BSWNngxX.mjs} +1 -1
  34. package/dist/packem_shared/insertManyUnsafeUserData-Dn77XpmX.mjs +26 -0
  35. package/dist/packem_shared/kvUnscopedUserKeyIdor-YWwmfE8X.mjs +19 -0
  36. package/dist/packem_shared/mailInboundDispatchWithoutVerify-CHSRA8zz.mjs +28 -0
  37. package/dist/packem_shared/mailRecipientFromRequestInput-Cka2qu5J.mjs +26 -0
  38. package/dist/packem_shared/maskWeakHashStrategyOnPii-1c4q8Opf.mjs +35 -0
  39. package/dist/packem_shared/maskedRelationLeakViaWith-CPI4s0sl.mjs +76 -0
  40. package/dist/packem_shared/mutatorFullRowReplace-BJnNDaIV.mjs +26 -0
  41. package/dist/packem_shared/normalizeIdUsedAsAuthorization-BVPtCpzT.mjs +50 -0
  42. package/dist/packem_shared/outputProjectionMissingOnPublicRead-Bl5IMx0k.mjs +51 -0
  43. package/dist/packem_shared/ownerFieldFromArgsNotAuth-mOw3hE5z.mjs +26 -0
  44. package/dist/packem_shared/paymentCreateWithoutAuthorize-BYm4JLxo.mjs +26 -0
  45. package/dist/packem_shared/paymentWebhookWideTolerance-D9QQJMJq.mjs +35 -0
  46. package/dist/packem_shared/plaintextSecretInWranglerVariables-NKO4YkKf.mjs +35 -0
  47. package/dist/packem_shared/privilegedDispatchUnvalidatedPayload-5Forjckt.mjs +37 -0
  48. package/dist/packem_shared/privilegedFanoutFromPublicProcedure-D3dL01B8.mjs +26 -0
  49. package/dist/packem_shared/{publicMutationWithoutRatelimit-xBpJ6GWK.mjs → publicMutationWithoutRatelimit-DbIhgi7j.mjs} +2 -2
  50. package/dist/packem_shared/publicTableRlsOptoutConfusion-Bn71yoD4.mjs +38 -0
  51. package/dist/packem_shared/queueWithoutDlq-CSkGNb_0.mjs +41 -0
  52. package/dist/packem_shared/ratelimitDefaultMemoryStore-BISChG5C.mjs +26 -0
  53. package/dist/packem_shared/ratelimitKeySpoofableOrGlobal-DqlHYQQ3.mjs +26 -0
  54. package/dist/packem_shared/ratelimitMiddlewareFailOpen-CJgDCaUw.mjs +33 -0
  55. package/dist/packem_shared/{relationReferencesUnknownField-YznyXt_7.mjs → relationReferencesUnknownField-CjbLScJ1.mjs} +1 -1
  56. package/dist/packem_shared/shapeTargetsGlobalTable-DHrf4Koi.mjs +34 -0
  57. package/dist/packem_shared/shapeUnknownTable-C8aDWFoe.mjs +34 -0
  58. package/dist/packem_shared/softDeleteIncludeDeletedFromArgs-BLqDKrkM.mjs +38 -0
  59. package/dist/packem_shared/storageGenerateUploadUrlNoContentTypePin-Da4L9Ge8.mjs +26 -0
  60. package/dist/packem_shared/storageKeyFromUserArgs-B86elJgS.mjs +19 -0
  61. package/dist/packem_shared/storagePresignedUrlForPrivateContent-yGmb8uqz.mjs +47 -0
  62. package/dist/packem_shared/storageUploadWithoutContentTypeAllowlist-BV-gF1lT.mjs +27 -0
  63. package/dist/packem_shared/storageUploadWithoutMaxSize-DpxO59wU.mjs +27 -0
  64. package/dist/packem_shared/{userCreatingMutationWithoutCaptcha-CH31YsUZ.mjs → userCreatingMutationWithoutCaptcha-2DZtWIPb.mjs} +2 -2
  65. package/dist/packem_shared/vectorsNamespaceFromUserInput-CQhr5bVn.mjs +19 -0
  66. package/dist/packem_shared/workflowDuplicateStepName-ioBxPBCy.mjs +48 -0
  67. package/package.json +4 -3
  68. package/dist/packem_shared/helpers-DNCkMWZQ.mjs +0 -4
package/dist/index.d.mts CHANGED
@@ -18,6 +18,69 @@ interface AdvisorAdminRoute {
18
18
  usesGuard: boolean;
19
19
  }
20
20
  /**
21
+ * One `ctx.ai.run(model, …)` call whose model-id argument is derived from the
22
+ * handler's `args` with no server-side scoping — the input the
23
+ * `ai_raw_run_escape_hatch` lint consumes. `ctx.ai.run` is the raw Workers AI
24
+ * binding passthrough, bypassing the typed `ctx.ai.model(...)` + AI-SDK layer
25
+ * (`generateText`/`streamText`/…) that caps output and enforces a schema. When
26
+ * the model id comes straight from request input, any caller can select an
27
+ * arbitrary model. A fixed literal model, or one scoped by a server-trusted
28
+ * `ctx.*` value, is *not* recorded; only an arg-derived, unscoped model id
29
+ * reaches here — an arg-derived `inputs` argument is normal usage and is never
30
+ * inspected. Produced by the codegen feeder; runtime callers don't supply it,
31
+ * so the lint finds nothing there.
32
+ */
33
+ interface AdvisorAiRawRun {
34
+ /** The exported binding name of the procedure performing the `ctx.ai.run` call. */
35
+ exportName: string;
36
+ /** Source file relative to the lunora dir, no extension. */
37
+ file: string;
38
+ /** 1-based line of the `ctx.ai.run` call, or `0` when unknown. */
39
+ line: number;
40
+ }
41
+ /**
42
+ * One `generateText` / `streamText` call whose `tools` reach a privileged side
43
+ * effect — the shared input for the `ai_tool_side_effect_prompt_injection` lint.
44
+ * A model-callable `tool({ execute })` that writes to the database, dispatches
45
+ * another function, or sends outbound (fetch / mail / queue) hands the LLM the
46
+ * trigger for a real-world action; `userInputDerived` records whether the model
47
+ * input (`prompt`/`messages`/`system`) flows from the handler's `args`, the
48
+ * channel a prompt injection rides in on. Produced by the codegen feeder; runtime
49
+ * callers don't supply it, so the lint finds nothing there.
50
+ */
51
+ interface AdvisorAiToolSideEffect {
52
+ /** The exported binding name of the procedure performing the call. */
53
+ exportName: string;
54
+ /** Source file relative to the lunora dir, no extension. */
55
+ file: string;
56
+ /** 1-based line of the generation call, or `0` when unknown. */
57
+ line: number;
58
+ /** The generation entrypoint invoked. */
59
+ method: "generateText" | "streamText";
60
+ /** The privileged side-effect sink a model-callable tool reaches (`ctx.db.insert`, `ctx.run`, `ctx.fetch`, …). */
61
+ sideEffect: string;
62
+ /** `true` when a model-input option is derived from the handler's `args` (a bare `args.x`, or a name destructured from `args`). */
63
+ userInputDerived: boolean;
64
+ }
65
+ /**
66
+ * One `ctx.fetch(url, …)` call inside an action whose URL argument is derived
67
+ * from the handler's `args` — the input the `action_fetch_ssrf` lint consumes.
68
+ * `ctx.fetch` is the action-only outbound-request escape hatch with no host
69
+ * allowlist, so a URL assembled from request input is a server-side request
70
+ * forgery vector (cloud metadata endpoints, internal services). A fixed literal
71
+ * URL, or one built from config/`ctx.*`, is *not* recorded; only an arg-derived
72
+ * URL reaches here. Produced by the codegen feeder; runtime callers don't supply
73
+ * it, so the lint finds nothing there.
74
+ */
75
+ interface AdvisorArgumentDerivedFetch {
76
+ /** The exported binding name of the action performing the `ctx.fetch` call. */
77
+ exportName: string;
78
+ /** Source file relative to the lunora dir, no extension. */
79
+ file: string;
80
+ /** 1-based line of the `ctx.fetch` call, or `0` when unknown. */
81
+ line: number;
82
+ }
83
+ /**
21
84
  * One public procedure's argument validators reduced to the input-safety facts
22
85
  * the `public_arg_uses_any` and `unbounded_string_arg` lints consume: which args
23
86
  * are declared `v.any()` (unvalidated input) and which `v.string()` args carry no
@@ -38,6 +101,46 @@ interface AdvisorArgumentValidator {
38
101
  unboundedStringArgs: ReadonlyArray<string>;
39
102
  }
40
103
  /**
104
+ * One `createAuth({...})` call's configuration snapshot — the shared input for
105
+ * the five `auth_*` security lints (`auth_trusted_origins_wildcard`,
106
+ * `auth_csrf_check_disabled`, `auth_secure_cookies_disabled`,
107
+ * `auth_email_verification_disabled`, `auth_session_freshage_zero`).
108
+ *
109
+ * The feeder matches the `createAuth` call by callee name and, when its config
110
+ * argument is a static object literal, reads the handful of nested facts each
111
+ * lint cares about. An opaque config (a top-level spread, or a non-object-literal
112
+ * argument) is recorded with `analyzable: false` and every boolean fact left at
113
+ * its SAFE (not-flagged) value, so a config assembled elsewhere can't be flagged
114
+ * on a key it may or may not set. Produced by the codegen feeder; runtime
115
+ * callers don't supply it, so the auth-config lints find nothing there.
116
+ */
117
+ interface AdvisorAuthConfig {
118
+ /**
119
+ * `true` when the call's config argument was a static object literal the
120
+ * feeder could read. `false` when the config was opaque (a variable, spread,
121
+ * call result, or missing argument) — every lint below skips such a config.
122
+ */
123
+ analyzable: boolean;
124
+ /** `advanced.disableCSRFCheck === true`. */
125
+ disableCsrfCheck: boolean;
126
+ /** `emailAndPassword.enabled === true`. */
127
+ emailPasswordEnabled: boolean;
128
+ /** The exported binding name enclosing the `createAuth(...)` call. */
129
+ exportName: string;
130
+ /** Source file relative to the lunora dir, no extension. */
131
+ file: string;
132
+ /** 1-based line of the `createAuth(...)` call, or `0` when unknown. */
133
+ line: number;
134
+ /** `emailAndPassword.requireEmailVerification === true` present. */
135
+ requireEmailVerification: boolean;
136
+ /** `advanced.useSecureCookies === false`. */
137
+ secureCookiesDisabled: boolean;
138
+ /** `session.freshAge === 0` (explicit literal). */
139
+ sessionFreshAgeZero: boolean;
140
+ /** `trustedOrigins` array literal contains a `"*"` element. */
141
+ trustedOriginsWildcard: boolean;
142
+ }
143
+ /**
41
144
  * One `ctx.authApi.&lt;method>(...)` call discovered in a function body — the input
42
145
  * the `auth_api_call_without_headers` lint consumes. Produced by the codegen
43
146
  * feeder; runtime callers don't supply it, so the lint finds nothing there.
@@ -55,6 +158,100 @@ interface AdvisorAuthApiCall {
55
158
  method: string;
56
159
  }
57
160
  /**
161
+ * One `ctx.browser.&lt;method>(url, …)` call whose navigation URL (`arguments[0]`)
162
+ * is derived from the handler's `args` with no server-side scoping — the input
163
+ * the `browser_user_url_without_allowlist` lint consumes. `@lunora/browser`
164
+ * blocks private/internal targets by default, but a request-supplied *public*
165
+ * URL can still turn the headless browser into an open-proxy / SSRF tool
166
+ * (fetching arbitrary third-party URLs, DNS-rebinding to internal hosts). A
167
+ * fixed literal URL, or one scoped by a server-trusted `ctx.*` value, is not
168
+ * recorded; only an arg-derived, unscoped URL reaches here. Produced by the
169
+ * codegen feeder; runtime callers don't supply it, so the lint finds nothing
170
+ * there.
171
+ */
172
+ interface AdvisorBrowserUrlAccess {
173
+ /** The exported binding name of the procedure performing the `ctx.browser` call. */
174
+ exportName: string;
175
+ /** Source file relative to the lunora dir, no extension. */
176
+ file: string;
177
+ /** 1-based line of the `ctx.browser` call, or `0` when unknown. */
178
+ line: number;
179
+ /** The browser method invoked: `content` / `pdf` / `scrape` / `screenshot`. */
180
+ method: string;
181
+ }
182
+ /**
183
+ * One factory/constructor call in `lunora/` whose configuration object literal a
184
+ * security lint inspects for a present-or-absent key — the shared input for the
185
+ * config-call security lints (payment authorize, inbound-mail verify, rate-limit
186
+ * store, browser private-targets).
187
+ *
188
+ * The feeder records the callee name and, when the config argument is a static
189
+ * object literal it could read, the set of keys present and the subset assigned
190
+ * the literal `true`. Produced by the codegen feeder; runtime callers don't
191
+ * supply it, so the config-call lints find nothing there.
192
+ */
193
+ interface AdvisorConfigCall {
194
+ /**
195
+ * `true` when the call's config argument was a static object literal the
196
+ * feeder could read. `false` when the config was opaque (a variable, spread,
197
+ * call result, or missing argument) — key-presence lints skip such calls so
198
+ * a config assembled elsewhere can't be flagged on a key it may well set.
199
+ */
200
+ analyzable: boolean;
201
+ /** The factory function or constructor name at the call site, e.g. `createPayment` / `RateLimiter`. */
202
+ callee: string;
203
+ /** Source file relative to the lunora dir, no extension. */
204
+ file: string;
205
+ /** 1-based line of the call site, or `0` when unknown. */
206
+ line: number;
207
+ /** Keys present in the config object literal (empty when not `analyzable`). */
208
+ presentKeys: ReadonlyArray<string>;
209
+ /** Keys in the config object literal explicitly assigned the literal `true`. */
210
+ trueKeys: ReadonlyArray<string>;
211
+ }
212
+ /**
213
+ * One `ctx.containers.&lt;exportName>.get(name, …)` call whose instance key is
214
+ * derived from the handler's `args` with no server-side scoping — the input
215
+ * the `container_instance_key_from_user_input` lint consumes. A container
216
+ * definition's `.get(name)` accessor routes to one instance per `name`, so a
217
+ * key taken straight from request input lets any caller reach any other
218
+ * tenant's container (a cross-tenant IDOR). A fixed literal key, or one
219
+ * derived from a server-trusted identity (`` `${ctx.auth.userId}` `` —
220
+ * references `ctx`, so treated as scoped), is not recorded; only an
221
+ * arg-derived, unscoped key reaches here. Produced by the codegen feeder;
222
+ * runtime callers don't supply it, so the lint finds nothing there.
223
+ */
224
+ interface AdvisorContainerKeyAccess {
225
+ /** The exported binding name of the procedure performing the `ctx.containers` access. */
226
+ exportName: string;
227
+ /** Source file relative to the lunora dir, no extension. */
228
+ file: string;
229
+ /** 1-based line of the `ctx.containers.*.get` call, or `0` when unknown. */
230
+ line: number;
231
+ /** The container accessor method invoked — always `get` (the only per-instance-key sink). */
232
+ method: string;
233
+ }
234
+ /**
235
+ * One runtime container-override call: a `&lt;handle>.start({ enableInternet: true, … })`
236
+ * launch override, or a `&lt;handle>.egress.&lt;method>(...)` runtime firewall mutation
237
+ * (`allow` / `deny` / `setAllowed`) — the `container_start_enable_internet_override`
238
+ * and `container_runtime_egress_relaxation` lint input. Both shapes re-open network
239
+ * access the static `defineContainer` declaration (and its `container_public_internet`
240
+ * lint) assumes is locked down. Structurally identical to `ContainerOverrideIR`.
241
+ */
242
+ interface AdvisorContainerOverride {
243
+ /** e.g. the egress method name, or `"enableInternet: true"`. */
244
+ detail: string;
245
+ /** Export binding name of the procedure performing the call. */
246
+ exportName: string;
247
+ /** Source file relative to the lunora dir, no extension. */
248
+ file: string;
249
+ /** Which override shape matched. */
250
+ kind: "egress_relaxation" | "enable_internet";
251
+ /** 1-based line of the call, or `0` when unknown. */
252
+ line: number;
253
+ }
254
+ /**
58
255
  * One container declaration discovered in `lunora/containers.ts` — the input
59
256
  * the `container_*` lints consume. Produced by the codegen feeder (which lifts
60
257
  * the static fields of each `defineContainer({...})` export); runtime callers
@@ -84,6 +281,108 @@ interface AdvisorContainer {
84
281
  sleepAfter?: number | string;
85
282
  }
86
283
  /**
284
+ * One rate-limit / Turnstile middleware call — the `ratelimit_middleware_fail_open`
285
+ * lint input. `rateLimit`/`dbRateLimit` (`@lunora/ratelimit`) and
286
+ * `verifyTurnstileMiddleware` (`@lunora/auth`) each accept a `failOpen` escape
287
+ * hatch that admits every request when the limiter/siteverify is unavailable;
288
+ * `failOpen` is `true` only when the options literal set it to the boolean
289
+ * literal `true` (anything else is fail-closed). The lint escalates a fail-open
290
+ * guard to a finding when the guarded procedure (`exportName`/`limitName`) looks
291
+ * auth/payment-sensitive. Produced by the codegen feeder; runtime callers don't
292
+ * supply it, so the lint finds nothing there.
293
+ */
294
+ interface AdvisorFailOpenGuard {
295
+ /** The middleware factory at the call site: `rateLimit` / `dbRateLimit` / `verifyTurnstileMiddleware`. */
296
+ callee: string;
297
+ /** The exported binding name of the procedure the guard is attached to, or `"&lt;module>"` at file scope. */
298
+ exportName: string;
299
+ /** `true` only when the options literal set `failOpen: true` as a boolean literal; a non-literal or absent option is treated as fail-closed. */
300
+ failOpen: boolean;
301
+ /** Source file relative to the lunora dir, no extension. */
302
+ file: string;
303
+ /** The rate-limit `name` (second string argument) for `rateLimit`/`dbRateLimit`; `""` for `verifyTurnstileMiddleware`. */
304
+ limitName: string;
305
+ /** 1-based line of the middleware call, or `0` when unknown. */
306
+ line: number;
307
+ }
308
+ /**
309
+ * One `ctx.flags.boolean("key", &lt;boolean-literal>)` read — the
310
+ * `flag_gates_security_with_unsafe_default` lint input. OpenFeature returns the
311
+ * `defaultValue` when the provider errors, so a fail-open default on a
312
+ * security-shaped key (an `enforce`/`rls`/`gate`/`lockdown` protection
313
+ * defaulting `false`, or an `allow`/`permit`/`bypass` permission defaulting
314
+ * `true`) silently opens access during an outage. Only reads with a
315
+ * statically-known string key and boolean-literal default are recorded; the lint
316
+ * owns the security-shape + polarity judgment. Produced by the codegen feeder;
317
+ * runtime callers don't supply it, so the lint finds nothing there.
318
+ */
319
+ interface AdvisorFlagSecurityDefault {
320
+ /** The boolean-literal default returned on a provider outage (fail-open value). */
321
+ defaultValue: boolean;
322
+ /** The exported binding name of the procedure performing the flag read, or `"&lt;module>"` at file scope. */
323
+ exportName: string;
324
+ /** Source file relative to the lunora dir, no extension. */
325
+ file: string;
326
+ /** The flag key — the first string-literal argument of `ctx.flags.boolean`. */
327
+ key: string;
328
+ /** 1-based line of the `ctx.flags.boolean` call, or `0` when unknown. */
329
+ line: number;
330
+ }
331
+ /**
332
+ * One discovered `httpAction`/`httpRoute` handler that performs a side effect
333
+ * (`ctx.runMutation` / `ctx.runAction` / a `ctx.db.{insert,patch,replace,delete,
334
+ * insertManyUnsafe}` write) from the HTTP edge, with whether it reads `ctx.auth`
335
+ * — the `http_action_missing_auth_guard` lint input. A handler that mutates state
336
+ * or dispatches an action without ever consulting the request identity is an
337
+ * unauthenticated write bypassing identity/RLS (distinct from
338
+ * `admin_route_without_guard`, which covers Studio/admin paths). Only handlers
339
+ * with a statically-resolvable inline body and `ctx` binding are recorded
340
+ * (fail-safe under-report); read-only handlers are never recorded. Produced by
341
+ * the codegen feeder; runtime callers don't supply it, so the lint finds nothing
342
+ * there.
343
+ */
344
+ interface AdvisorHttpActionGuard {
345
+ /** The exported binding name of the handler (or `"&lt;module>"` when mounted inline / not a named binding). */
346
+ exportName: string;
347
+ /** Source file relative to the lunora dir, no extension. */
348
+ file: string;
349
+ /** Which HTTP surface the handler is: a raw `httpAction` or a typed `httpRoute` route. */
350
+ kind: "httpAction" | "httpRoute";
351
+ /** 1-based line of the handler call, or `0` when unknown. */
352
+ line: number;
353
+ /** For an `httpRoute`, the uppercased verb (`"POST"`); absent for a raw `httpAction`. */
354
+ method?: string;
355
+ /** `true` when the handler reads `ctx.auth` (a direct member access or a `const { auth } = ctx` destructure). */
356
+ readsAuth: boolean;
357
+ /** The first side effect found, as a stable label: `runMutation`, `runAction`, or `db.&lt;method>`. */
358
+ sideEffect: string;
359
+ }
360
+ /**
361
+ * One response-header write, inside an `httpAction` handler, whose value is derived
362
+ * from raw request input (`request.headers`, `request.url`/query, `await
363
+ * request.json()`) with no CR/LF sanitizer — the
364
+ * `http_action_response_header_injection` lint input. A `Request`-derived string
365
+ * placed verbatim into a response header lets a caller smuggle `\r\n` and inject
366
+ * extra headers or split the response. Only request-tainted, unguarded sites are
367
+ * recorded — a value routed through `isSafeHeaderValue`, `encodeURIComponent`/
368
+ * `encodeURI`, a numeric coercion, or `btoa` is treated as safe (`String(...)` /
369
+ * `.toString()` are NOT sanitizers). Produced by the codegen feeder; runtime
370
+ * callers don't supply it, so the lint finds nothing there. Structurally identical
371
+ * to `HttpHeaderWriteIR`.
372
+ */
373
+ interface AdvisorHttpHeaderWrite {
374
+ /** The exported binding name of the enclosing handler, or `"&lt;module>"` when mounted inline. */
375
+ exportName: string;
376
+ /** Source file relative to the lunora dir, no extension. */
377
+ file: string;
378
+ /** The header name being written (`"location"`), or `""` when the key is not a string literal. */
379
+ headerName: string;
380
+ /** 1-based line of the request-tainted header value. */
381
+ line: number;
382
+ /** How the header was written. */
383
+ via: "headers-append" | "headers-ctor" | "headers-set" | "response-init";
384
+ }
385
+ /**
87
386
  * One `ctx.sql` access discovered lexically inside a `query(...)` or
88
387
  * `mutation(...)` handler body — the input the `hyperdrive_outside_action` lint
89
388
  * consumes. Produced by the codegen feeder, which walks each exported function's
@@ -110,6 +409,41 @@ interface AdvisorHyperdriveCall {
110
409
  line: number;
111
410
  }
112
411
  /**
412
+ * One `&lt;receiver>.identity.&lt;key>` claim read (where `&lt;receiver>` is an RLS/mask
413
+ * policy `auth`, or `ctx.auth`/`context.auth`) — the shared input for the
414
+ * `identity_undeclared_claim_trusted` lint. `defineIdentity` validates only its
415
+ * declared claims at the trust boundary and forwards undeclared claims
416
+ * verbatim, so a read of a claim outside the declared contract trusts an
417
+ * unvalidated, forgeable value. `declared` records whether `key` is in the
418
+ * contract (or the always-present `userId`); the lint flags the undeclared reads.
419
+ * Produced by the codegen feeder — and only when a resolvable `defineIdentity`
420
+ * contract exists — so runtime callers supply nothing and the lint finds nothing
421
+ * there.
422
+ */
423
+ interface AdvisorIdentityClaimRead {
424
+ /** `true` when `key` is a declared claim (in the `defineIdentity` contract, or the always-present `userId`). */
425
+ declared: boolean;
426
+ /** The exported binding name of the enclosing declaration (`&lt;module>` at file scope). */
427
+ exportName: string;
428
+ /** Source file relative to the lunora dir, no extension. */
429
+ file: string;
430
+ /** The claim key read off the identity bag. */
431
+ key: string;
432
+ /** 1-based line of the read, or `0` when unknown. */
433
+ line: number;
434
+ }
435
+ /**
436
+ * One `buildImageDeliveryUrl({ key, … })` call (`@lunora/bindings/images`) whose
437
+ * `key` — the CDN transform's source image, an absolute URL or an
438
+ * origin-relative key — is derived from the handler's `args` with no
439
+ * server-side scoping — the `images_url_source_from_user_input` lint input.
440
+ */
441
+ interface AdvisorImageDeliveryUrlAccess {
442
+ exportName: string;
443
+ file: string;
444
+ line: number;
445
+ }
446
+ /**
113
447
  * Observed read signal over a table — the input the `index_utilization` runtime
114
448
  * lint consumes. Produced by the studio backend from each shard's recorded
115
449
  * metrics.
@@ -177,6 +511,49 @@ interface AdvisorInsertWrite {
177
511
  table: string;
178
512
  }
179
513
  /**
514
+ * One `ctx.kv.&lt;method>(key, …)` call whose namespace key is derived from the
515
+ * handler's `args` with no server-side scoping — the input the
516
+ * `kv_unscoped_user_key_idor` lint consumes. Workers KV is a single flat
517
+ * namespace, so a key taken straight from request input lets any caller read,
518
+ * overwrite, or delete another user's entry (an insecure direct object
519
+ * reference). A fixed literal key, or one prefixed with a server-trusted identity
520
+ * (`` `${ctx.auth.userId}:…` `` — references `ctx`, so treated as scoped), is not
521
+ * recorded; only an arg-derived, unscoped key reaches here. Produced by the
522
+ * codegen feeder; runtime callers don't supply it, so the lint finds nothing
523
+ * there.
524
+ */
525
+ interface AdvisorKvKeyAccess {
526
+ /** The exported binding name of the procedure performing the `ctx.kv` access. */
527
+ exportName: string;
528
+ /** Source file relative to the lunora dir, no extension. */
529
+ file: string;
530
+ /** 1-based line of the `ctx.kv` call, or `0` when unknown. */
531
+ line: number;
532
+ /** The `ctx.kv` method invoked: `get` / `getRaw` / `getWithMetadata` / `put` / `delete`. */
533
+ method: string;
534
+ }
535
+ /**
536
+ * One `ctx.mail`/`ctx.email` `send`/`queue` call whose recipient field
537
+ * (`to`/`cc`/`bcc`) is derived from the handler's `args` with no server-side
538
+ * scoping — the input the `mail_recipient_from_request_input` lint consumes. A
539
+ * recipient taken straight from request input turns the deployment into an
540
+ * open relay / spam amplifier: any caller can direct mail to an arbitrary
541
+ * address. A fixed literal recipient, or one scoped by a server-trusted
542
+ * `ctx.*` value (e.g. `ctx.auth.user.email`), is not recorded; only an
543
+ * arg-derived, unscoped recipient reaches here. Produced by the codegen
544
+ * feeder; runtime callers don't supply it, so the lint finds nothing there.
545
+ */
546
+ interface AdvisorMailRecipientAccess {
547
+ /** The exported binding name of the procedure performing the `ctx.mail`/`ctx.email` call. */
548
+ exportName: string;
549
+ /** Source file relative to the lunora dir, no extension. */
550
+ file: string;
551
+ /** 1-based line of the `ctx.mail`/`ctx.email` call, or `0` when unknown. */
552
+ line: number;
553
+ /** The mailer method invoked: `send` / `queue`. */
554
+ method: string;
555
+ }
556
+ /**
180
557
  * One procedure (query / mutation / action) discovered in the lunora source,
181
558
  * reduced to the facts the `mask_uncovered_pii_column` lint needs: whether the
182
559
  * procedure's builder chain includes `.use(mask(...))`, which `(table, column)`
@@ -214,6 +591,52 @@ interface AdvisorMaskProcedure {
214
591
  visibility: "internal" | "public";
215
592
  }
216
593
  /**
594
+ * One masked column whose `mask(policies)` strategy is a statically-known
595
+ * literal (`"hash"` or `"redact"`) — the `mask_weak_hash_strategy_on_pii` lint
596
+ * input. Unlike `AdvisorMaskProcedure` (one row per procedure, `maskColumns`
597
+ * without a strategy), this is one row per masked column with its strategy
598
+ * literal attached, so the lint can flag `"hash"` applied to a PII-named
599
+ * column. A `MaskFn` (custom, non-literal) strategy carries no lint-relevant
600
+ * signal and is never recorded here. Produced by the codegen feeder; runtime
601
+ * callers don't supply it, so the lint finds nothing there.
602
+ */
603
+ interface AdvisorMaskStrategy {
604
+ /** Masked column name. */
605
+ column: string;
606
+ /** The exported binding name of the procedure whose `.use(mask(...))` chain declared this column, or `"&lt;module>"` when declared at file scope. */
607
+ exportName: string;
608
+ /** Source file relative to the lunora dir, no extension. */
609
+ file: string;
610
+ /** 1-based line of the masked column's strategy property. */
611
+ line: number;
612
+ /** The statically-known strategy literal: `"hash"` or `"redact"`. */
613
+ strategy: string;
614
+ /** Logical table the masked column belongs to. */
615
+ table: string;
616
+ }
617
+ /**
618
+ * One whole-row `ctx.db.replace(id, document)` write discovered inside a custom
619
+ * mutator's authoritative `server` impl (`lunora/mutators.ts`) — the input the
620
+ * `mutator_full_row_replace` lint consumes.
621
+ *
622
+ * In the local-first sync engine a `replace` overwrites the entire row, so a
623
+ * concurrent edit to a *different* column (committed between this mutator's read
624
+ * and its write) is silently clobbered. `patch(id, { onlyTheField })` merges at
625
+ * the column level instead, letting independent field edits coexist — the
626
+ * blessed pattern for mutators on a synced (poke-live) table. Produced by the
627
+ * codegen feeder, which attributes each `replace` to the mutator export
628
+ * performing it; runtime callers don't supply it, so the lint finds nothing
629
+ * there.
630
+ */
631
+ interface AdvisorMutatorWrite {
632
+ /** The mutator export whose `server` impl performs the replace (e.g. `renameChannel`). */
633
+ exportName: string;
634
+ /** Openable source path the replace appears in — always `lunora/mutators.ts`. */
635
+ file: string;
636
+ /** 1-based line of the `replace(...)` call, or `0` when unknown. */
637
+ line: number;
638
+ }
639
+ /**
217
640
  * One non-deterministic API call discovered lexically inside a `query(...)` or
218
641
  * `mutation(...)` handler body — the input the `nondeterministic_query_mutation`
219
642
  * lint consumes. Produced by the codegen feeder, which walks each exported
@@ -236,6 +659,110 @@ interface AdvisorNondeterministicCall {
236
659
  line: number;
237
660
  }
238
661
  /**
662
+ * One `query`/`mutation` handler that gates a `ctx.db.get`/`patch`/`delete` on a
663
+ * null-checked `ctx.db.normalizeId(table, id)` result — the shared input for the
664
+ * `normalize_id_used_as_authorization` lint. `normalizeId` validates an id's
665
+ * structural shape only (it never reads the database), so a non-null result proves
666
+ * the id is well-formed, never that the caller owns the row; gating access on it is
667
+ * an IDOR. The lint keeps only public procedures with no `.use(rls(...))` and no
668
+ * ownership/identity mention (`mentionsOwnership`), then joins `table` against the
669
+ * schema's RLS mode before flagging. Produced by the codegen feeder; runtime callers
670
+ * don't supply it, so the lint finds nothing there. Structurally identical to
671
+ * `@lunora/codegen`'s `NormalizeIdAuthorizationIR`.
672
+ */
673
+ interface AdvisorNormalizeIdAuthorization {
674
+ /** The exported binding name of the procedure performing the normalize-then-access. */
675
+ exportName: string;
676
+ /** Source file relative to the lunora dir, no extension. */
677
+ file: string;
678
+ /** 1-based line of the `ctx.db.normalizeId(...)` call the access is gated on. */
679
+ line: number;
680
+ /** `true` when the handler anywhere reads an ownership-named identifier or `ctx.auth`/`ctx.identity`/… — an intervening ownership signal. */
681
+ mentionsOwnership: boolean;
682
+ /** The id-first `ctx.db` sink the normalized id reaches. */
683
+ sinkMethod: "delete" | "get" | "patch";
684
+ /** Table named in the `normalizeId` call, or `""` when its table argument wasn't a string literal. */
685
+ table: string;
686
+ /** `true` when the procedure's builder chain carries a `.use(rls(...))` step. */
687
+ usesRls: boolean;
688
+ /** `"internal"` for `internalQuery`/`internalMutation`; `"public"` for `query`/`mutation`. */
689
+ visibility: "internal" | "public";
690
+ }
691
+ /**
692
+ * One `ctx.db` write (`insert` / `replace` / `patch` / `insertManyUnsafe`) that
693
+ * sets an ownership / identity column — `userId`, `ownerId`, `tenantId`, and the
694
+ * like — from the handler's `args` instead of the server-trusted identity. This
695
+ * is the `owner_field_from_args_not_auth` lint input: the ownership column decides
696
+ * who a row belongs to, so a value taken from request input lets any caller write
697
+ * rows owned by another user or tenant (the act-as-any-user / cross-tenant IDOR
698
+ * vector). A column stamped from `ctx.auth` / `ctx.identity`, or set to a fixed
699
+ * literal, is *not* recorded; only an arg-derived identity write reaches here.
700
+ * Produced by the codegen feeder; runtime callers don't supply it, so the lint
701
+ * finds nothing there. Structurally identical to `OwnerFieldWriteIR`.
702
+ */
703
+ interface AdvisorOwnerFieldWrite {
704
+ /** The exported binding name of the procedure performing the write. */
705
+ exportName: string;
706
+ /** The identity column being written from `args` (e.g. `userId`). */
707
+ field: string;
708
+ /** Source file relative to the lunora dir, no extension. */
709
+ file: string;
710
+ /** 1-based line of the `ctx.db` write call, or `0` when unknown. */
711
+ line: number;
712
+ /** The `ctx.db` write method (`insert` / `replace` / `patch` / `insertManyUnsafe`). */
713
+ method: string;
714
+ }
715
+ /**
716
+ * One payment webhook-adapter construction (`createStripeAdapter` /
717
+ * `createPolarAdapter` / `createAutumnAdapter` / `createDodoPaymentsAdapter`) —
718
+ * the shared input for the `payment_webhook_wide_tolerance` lint. The adapters
719
+ * verify a webhook's signed timestamp against a `webhookToleranceSeconds` replay
720
+ * window (default 300s); an implausibly wide window leaves the endpoint accepting
721
+ * stale, replayable signed payloads long after capture. `toleranceSeconds`
722
+ * carries the statically-known literal (when present and a plain numeric
723
+ * literal); the lint fires only above a conservative ceiling. Produced by the
724
+ * codegen feeder; runtime callers don't supply it, so the lint finds nothing
725
+ * there.
726
+ */
727
+ interface AdvisorPaymentWebhook {
728
+ /** The adapter factory invoked. */
729
+ callee: "createAutumnAdapter" | "createDodoPaymentsAdapter" | "createPolarAdapter" | "createStripeAdapter";
730
+ /** The exported binding name of the enclosing declaration (`&lt;module>` at file scope). */
731
+ exportName: string;
732
+ /** Source file relative to the lunora dir, no extension. */
733
+ file: string;
734
+ /** 1-based line of the construction, or `0` when unknown. */
735
+ line: number;
736
+ /** Statically-known `webhookToleranceSeconds` literal, when present and a plain numeric literal. */
737
+ toleranceSeconds?: number;
738
+ }
739
+ /**
740
+ * One payload-derived privileged dispatch — a `ctx.run`/`context.run` back into a
741
+ * Lunora function from inside a `defineQueue` push handler or a `defineWorkflow`
742
+ * handler, whose args reference the handler's untrusted payload (`context.params`
743
+ * for a workflow, a `for (… of batch.messages)` body for a queue) — the input the
744
+ * `privileged_dispatch_unvalidated_payload` lint consumes. Both handler kinds run
745
+ * under the **system identity** (RLS disabled), so forwarding attacker-influenced
746
+ * payload into the dispatch skips the target's row policy. The lint joins the
747
+ * resolved `targetFile`/`targetExport` against the RLS-procedure evidence and
748
+ * fires only when the target enforces RLS. Produced by the codegen feeder;
749
+ * runtime callers don't supply it, so the lint finds nothing there.
750
+ */
751
+ interface AdvisorPrivilegedDispatch {
752
+ /** `"queue"` for a `defineQueue` handler, `"workflow"` for a `defineWorkflow` handler. */
753
+ dispatchKind: "queue" | "workflow";
754
+ /** Source file relative to the lunora dir, no extension. */
755
+ file: string;
756
+ /** The exported handler binding performing the dispatch. */
757
+ handlerExport: string;
758
+ /** 1-based line of the dispatch call, or `0` when unknown. */
759
+ line: number;
760
+ /** Export name of the dispatched target (`send` in `api.messages.send`). */
761
+ targetExport: string;
762
+ /** File path of the dispatched target relative to the lunora dir (`messages` in `api.messages.send`). */
763
+ targetFile: string;
764
+ }
765
+ /**
239
766
  * One procedure (query / mutation / action) reduced to the protective middlewares
240
767
  * its builder chain installs plus the behavioural facts that decide whether a
241
768
  * guard is expected — the input the `public_mutation_without_ratelimit` and
@@ -249,12 +776,18 @@ interface AdvisorProcedureProtection {
249
776
  callsMail: boolean;
250
777
  /** The exported binding name of the procedure (e.g. `signUp`). */
251
778
  exportName: string;
779
+ /** `true` when the handler fans work out to a privileged, cost-bearing dispatch surface (scheduler `runAfter`/`runAt`, a queue producer send, or a workflow create). Read by the privileged-fanout lint, paired with public visibility and no rate limit. */
780
+ fanOut: boolean;
252
781
  /** Source file relative to the lunora dir, no extension. */
253
782
  file: string;
254
783
  /** Registration kind — `query` is read-only; `mutation`/`action` are write-shaped. */
255
784
  kind: "action" | "mutation" | "query";
785
+ /** `true` when the handler runs an AI generation (`generateText`/`streamText`/`generateObject`/`streamObject`) with no `maxOutputTokens` bound. Read by the `ai_unbounded_generation_public` lint (paired with public visibility). */
786
+ unboundedAiGeneration: boolean;
256
787
  /** `true` when the chain carries `.use(verifyTurnstile(...))` or a `protectPublic({ captcha })` bundle. */
257
788
  usesCaptcha: boolean;
789
+ /** `true` when the handler calls `ctx.db.insertManyUnsafe(...)`, which bypasses validators and triggers. Read by the `insert_many_unsafe_user_data` lint (paired with public visibility). */
790
+ usesInsertManyUnsafe: boolean;
258
791
  /** `true` when the chain carries `.use(mask(...))`. */
259
792
  usesMask: boolean;
260
793
  /** `true` when the chain carries `.use(rateLimit(...))` or a `protectPublic({ rateLimit })` bundle. */
@@ -285,6 +818,45 @@ interface AdvisorQueryRead {
285
818
  table: string;
286
819
  }
287
820
  /**
821
+ * The queue-shaped input the `queue_*` lints consume, produced by the codegen
822
+ * feeder — one per `defineQueue` export in `lunora/queues.ts`. Runtime callers
823
+ * don't supply it, so the queue lints simply find nothing there.
824
+ *
825
+ * A structural subset of codegen's `QueueIR`, so the feeder passes the IR array
826
+ * straight through without conversion (mirrors how `AdvisorWorkflow` tracks
827
+ * `WorkflowIR` and `AdvisorContainer` tracks `ContainerIR`).
828
+ */
829
+ /**
830
+ * The push-consumer batch/retry tuning a queue lint inspects — the subset of
831
+ * `QueueIR["tuning"]` mirrored onto the wrangler `queues.consumers[]` entry.
832
+ */
833
+ interface AdvisorQueueTuning {
834
+ /**
835
+ * The wrangler name of the dead-letter queue exhausted messages are routed
836
+ * to. `undefined` when none is declared — a message that exhausts its
837
+ * retries is then dropped and permanently lost.
838
+ */
839
+ deadLetterQueue?: string;
840
+ /** Max delivery retries before a message is dead-lettered/dropped (Cloudflare default 3). */
841
+ maxRetries?: number;
842
+ }
843
+ /** One queue declared via a `defineQueue()` export in `lunora/queues.ts`. */
844
+ interface AdvisorQueue {
845
+ /** The `lunora/queues.ts` export name, e.g. `notifications`. */
846
+ exportName: string;
847
+ /** How the queue is consumed: `"push"` (a worker `queue()` handler) or `"pull"` (external HTTP). */
848
+ mode: "pull" | "push";
849
+ /**
850
+ * The stable wrangler queue name (kebab-cased export name unless a `name:`
851
+ * literal overrides it). Used to recognise a queue that is itself another
852
+ * queue's `deadLetterQueue` target — a terminal sink that must not be
853
+ * flagged for lacking its own DLQ.
854
+ */
855
+ name: string;
856
+ /** Push-consumer batch/retry tuning; the `deadLetterQueue`/`maxRetries` the queue lints read. */
857
+ tuning: AdvisorQueueTuning;
858
+ }
859
+ /**
288
860
  * One `ctx.r2sql` access discovered lexically inside a `query(...)` or
289
861
  * `mutation(...)` handler body — the input the `r2sql_outside_action` lint
290
862
  * consumes. Produced by the codegen feeder, which walks each exported function's
@@ -312,6 +884,70 @@ interface AdvisorR2sqlCall {
312
884
  line: number;
313
885
  }
314
886
  /**
887
+ * One `rateLimit`/`dbRateLimit` middleware call (`@lunora/ratelimit`) whose
888
+ * `key` selector is derived from the handler's `args` with no server-side
889
+ * scoping — the `ratelimit_key_spoofable_or_global` lint input.
890
+ */
891
+ interface AdvisorRatelimitKeySelector {
892
+ callee: string;
893
+ exportName: string;
894
+ file: string;
895
+ limitName: string;
896
+ line: number;
897
+ }
898
+ /**
899
+ * One `query` handler whose `return` hands back the raw rows of a table — the
900
+ * result of a `ctx.db.&lt;table>.findMany()` / `.findFirst()` / `.get()` read, or a
901
+ * `ctx.db.query("&lt;table>")…collect()` fluent chain — returned directly (or through
902
+ * one local `const` hop) with no hand-built projection. The shared input for the
903
+ * `output_projection_missing_on_public_read` lint, which keeps only `visibility
904
+ * === "public"` rows with no `.output(...)` / `.use(mask(...))` on the chain and
905
+ * joins `table` against the schema's PII-named columns before flagging. Produced by
906
+ * the codegen feeder; runtime callers don't supply it, so the lint finds nothing
907
+ * there. Structurally identical to `@lunora/codegen`'s `RawRowReturnIR`.
908
+ */
909
+ interface AdvisorRawRowReturn {
910
+ /** The exported binding name of the query returning the raw rows. */
911
+ exportName: string;
912
+ /** Source file relative to the lunora dir, no extension. */
913
+ file: string;
914
+ /** 1-based line of the `return` (or concise-body) expression. */
915
+ line: number;
916
+ /** Table whose raw rows are returned, or `""` when the read's table couldn't be statically resolved. */
917
+ table: string;
918
+ /** `true` when the procedure's builder chain carries a `.use(mask(...))` step. */
919
+ usesMask: boolean;
920
+ /** `true` when the procedure's builder chain carries an `.output(...)` return-shape projection. */
921
+ usesOutput: boolean;
922
+ /** `"internal"` for `internalQuery`; `"public"` for `query`. */
923
+ visibility: "internal" | "public";
924
+ }
925
+ /**
926
+ * One `ctx.db.&lt;table>.findMany({ with: { &lt;rel> } })` relation-hydrating list read
927
+ * — the shared input for the `masked_relation_leak_via_with` lint. Column
928
+ * masking is applied per-procedure to the top-level rows of the table named in
929
+ * the read; it does **not** descend into `with`-hydrated relations, so a masked
930
+ * table surfaced only through a `with` on an unprotected parent read is returned
931
+ * in the clear. The lint resolves each relation accessor to its target table and
932
+ * joins it against the discovered mask evidence before flagging. Produced by the
933
+ * codegen feeder; runtime callers don't supply it, so the lint finds nothing
934
+ * there. Structurally identical to `@lunora/codegen`'s `RelationLoadIR`.
935
+ */
936
+ interface AdvisorRelationLoad {
937
+ /** The exported binding name of the procedure performing the read. */
938
+ exportName: string;
939
+ /** Source file relative to the lunora dir, no extension. */
940
+ file: string;
941
+ /** 1-based line of the read call. */
942
+ line: number;
943
+ /** Parent table the read targets, or `""` when it couldn't be statically resolved. */
944
+ parentTable: string;
945
+ /** Relation accessor names named in the read's `with: { … }` map — matched against the parent table's declared relations. */
946
+ relations: ReadonlyArray<string>;
947
+ /** `"internal"` for `internalQuery` / `internalMutation` / `internalAction`. */
948
+ visibility: "internal" | "public";
949
+ }
950
+ /**
315
951
  * One procedure (query / mutation / action) discovered in the lunora source,
316
952
  * reduced to the facts the `rls_uncovered_table` lint needs: whether the
317
953
  * procedure's builder chain includes `.use(rls(...))`, and which tables the
@@ -351,6 +987,15 @@ interface AdvisorRlsProcedure {
351
987
  * read — tables, their columns, indexes, and relations.
352
988
  */
353
989
  interface AdvisorSchema {
990
+ /**
991
+ * Set when the schema opted into `.rls("required")` — every table's `ctx.db`
992
+ * write path is denied without an RLS-covering procedure UNLESS the table
993
+ * itself is `.public()` (see {@link AdvisorTable.isPublic}). `undefined` when
994
+ * the schema never called `.rls("required")`. Read by the
995
+ * `public_table_rls_optout_confusion` and `allow_unauthenticated_shard_access_enabled`
996
+ * lints.
997
+ */
998
+ rlsMode?: "required";
354
999
  tables: ReadonlyArray<AdvisorTable>;
355
1000
  }
356
1001
  /** A table plus the column/index/relation metadata lints inspect. */
@@ -363,6 +1008,14 @@ interface AdvisorTable {
363
1008
  */
364
1009
  externallyManaged?: boolean;
365
1010
  /**
1011
+ * Set when the table was declared with `.source(...)` (plan 077) —
1012
+ * materialized from an external Hyperdrive-backed database. Read by the
1013
+ * `external_source_*` lints to enforce the tenant-scope boundary (mandatory
1014
+ * `tenantBy` under `.shardBy()`) and reject sourcing a `.global()` table.
1015
+ * Optional — feeders that don't know about sourced tables omit it.
1016
+ */
1017
+ externalSource?: AdvisorExternalSource;
1018
+ /**
366
1019
  * Declared column names (the `defineTable({...})` keys). Excludes the
367
1020
  * framework-managed system fields `_id` / `_creationTime`, which every table
368
1021
  * has implicitly — lints that resolve a column treat those as always valid.
@@ -370,6 +1023,15 @@ interface AdvisorTable {
370
1023
  fields: ReadonlyArray<string>;
371
1024
  /** Every declared index, across all kinds (secondary / search / rank / vector). */
372
1025
  indexes: ReadonlyArray<AdvisorIndex>;
1026
+ /**
1027
+ * `true` when the table was declared with `.public()` — an explicit opt-OUT
1028
+ * of the schema's `.rls("required")` enforcement for this one table (the
1029
+ * name is misleading: it means "unprotected by RLS", not "safe to read
1030
+ * publicly"). Has no effect when the schema itself never required RLS.
1031
+ * Defaults to `false`. Read by `public_table_rls_optout_confusion` and
1032
+ * `allow_unauthenticated_shard_access_enabled`.
1033
+ */
1034
+ isPublic?: boolean;
373
1035
  /** Table name. */
374
1036
  name: string;
375
1037
  /**
@@ -384,6 +1046,27 @@ interface AdvisorTable {
384
1046
  optionalFields?: ReadonlySet<string>;
385
1047
  /** Declared relations (`.relations((r) => …)`). */
386
1048
  relations: ReadonlyArray<AdvisorRelation>;
1049
+ /**
1050
+ * Storage tier the table is declared in: `"global"` (a `.global()` table,
1051
+ * lives in D1 — the cross-shard tier), `"shardBy"` (partitioned across
1052
+ * shard DOs by a key), or `"root"` (the default single-DO table). Read by
1053
+ * the `shape_*` lints to flag replication shapes targeting a `.global()`
1054
+ * table (poll-refreshed/latency-tiered, not poke-live). Optional — the
1055
+ * codegen feeder always supplies it, the runtime feeder derives it; a feeder
1056
+ * that omits it leaves tier-sensitive lints to treat the table as local.
1057
+ */
1058
+ shardKind?: "global" | "root" | "shardBy";
1059
+ /**
1060
+ * Set when the table opted into `.softDelete()` — the marker column
1061
+ * (`field`, default `deletedAt`) whose presence excludes a row from list
1062
+ * reads unless `includeDeleted: true` is passed. Read by
1063
+ * `soft_delete_include_deleted_from_args` to confirm a read's target actually
1064
+ * soft-deletes before flagging an `includeDeleted` toggle on a public read.
1065
+ * Optional — a feeder that doesn't track soft-delete omits it.
1066
+ */
1067
+ softDelete?: {
1068
+ field: string;
1069
+ };
387
1070
  }
388
1071
  /**
389
1072
  * One declared index, flattened across Lunora's index kinds so a single lint can
@@ -400,6 +1083,21 @@ interface AdvisorIndex {
400
1083
  name: string;
401
1084
  unique?: boolean;
402
1085
  }
1086
+ /** The statically-knowable `.source(...)` bits the `external_source_*` lints read. */
1087
+ interface AdvisorExternalSource {
1088
+ /** `true` when a `reconcileEveryMs` was given (the incremental-mode delete-visibility companion). */
1089
+ hasReconcile?: boolean;
1090
+ /** `true` when a `tenantBy` mapper was given — the tenant-isolation boundary. */
1091
+ hasTenantBy: boolean;
1092
+ /** Delete-detection mode literal, when given (`"full-pull"` | `"incremental"`). */
1093
+ mode?: string;
1094
+ /**
1095
+ * `true` when `.source(...)` was declared but its config wasn't a static object
1096
+ * literal, so `hasTenantBy` (and the rest) couldn't be read. Only the codegen
1097
+ * feeder can hit this; the runtime feeder always holds the real config.
1098
+ */
1099
+ unanalyzable?: boolean;
1100
+ }
403
1101
  /**
404
1102
  * One declared relation. For a `one` relation the FK column `field` lives on
405
1103
  * the holding table; for `many` it lives on the target. `name` is the accessor
@@ -440,6 +1138,28 @@ interface AdvisorSecretLiteral {
440
1138
  preview: string;
441
1139
  }
442
1140
  /**
1141
+ * A replication shape declared via `defineShape({ table, where, columns? })` in
1142
+ * `lunora/shapes.ts` (the local-first sync engine's partial-replication unit).
1143
+ * The `shape_*` lints cross-reference each shape's {@link AdvisorShape.table}
1144
+ * against the declared schema to flag a shape targeting an unknown table or a
1145
+ * `.global()` table (which replicates through the latency-tiered D1 poll path,
1146
+ * not the poke-live op-log). Supplied by the codegen feeder, which lifts only
1147
+ * the export name + the static `table` literal; absent for runtime callers,
1148
+ * where the shape lints find nothing.
1149
+ */
1150
+ interface AdvisorShape {
1151
+ /** Export binding name — the shape's registry key (e.g. `channelMessages`). */
1152
+ exportName: string;
1153
+ /** File the shape is declared in (relative, for the operator to open). */
1154
+ file: string;
1155
+ /**
1156
+ * The `table` string literal the shape replicates from, or `undefined` when
1157
+ * the feeder could not read it as a plain string literal — tier-sensitive
1158
+ * lints skip a shape with no resolvable table rather than guessing.
1159
+ */
1160
+ table?: string;
1161
+ }
1162
+ /**
443
1163
  * One shard's observed traffic share — the input the `hot_shard` runtime lint
444
1164
  * consumes. Produced by the studio backend, which fans out over a sharded
445
1165
  * function's shards and reads each shard's recorded request volume from the
@@ -469,6 +1189,32 @@ interface AdvisorShardTraffic {
469
1189
  shardKey: string;
470
1190
  }
471
1191
  /**
1192
+ * One `ctx.db.&lt;table>.findMany({ includeDeleted })` list read whose
1193
+ * `includeDeleted` is either a hardcoded `true` or derived from the handler's
1194
+ * `args` — the shared input for the `soft_delete_include_deleted_from_args`
1195
+ * lint. `includeDeleted` resurfaces rows a `.softDelete()` table would otherwise
1196
+ * hide from list reads; on a public read that means any caller (arg-derived) or
1197
+ * every caller (hardcoded `true`) can see soft-deleted rows. Produced by the
1198
+ * codegen feeder; runtime callers don't supply it, so the lint finds nothing
1199
+ * there. Structurally identical to `@lunora/codegen`'s `SoftDeleteReadIR`.
1200
+ */
1201
+ interface AdvisorSoftDeleteRead {
1202
+ /** The exported binding name of the procedure performing the read. */
1203
+ exportName: string;
1204
+ /** Source file relative to the lunora dir, no extension. */
1205
+ file: string;
1206
+ /** `true` when `includeDeleted` was derived from the handler's `args` (any caller can flip it). */
1207
+ fromArgs: boolean;
1208
+ /** `true` when `includeDeleted` was a hardcoded `true` literal (always resurfaces soft-deleted rows). */
1209
+ hardcodedTrue: boolean;
1210
+ /** 1-based line of the read call. */
1211
+ line: number;
1212
+ /** Table read, or `""` when the table couldn't be statically resolved. */
1213
+ table: string;
1214
+ /** `"internal"` for `internalQuery` / `internalMutation` / `internalAction`. */
1215
+ visibility: "internal" | "public";
1216
+ }
1217
+ /**
472
1218
  * One `ctx.sql` tagged-template interpolation that splices an unparameterized
473
1219
  * string-building expression into the query — the input the `sql_injection_risk`
474
1220
  * lint consumes. A `${…}` placeholder that simply names a value is bound as a
@@ -486,6 +1232,55 @@ interface AdvisorSqlInterpolation {
486
1232
  line: number;
487
1233
  }
488
1234
  /**
1235
+ * One `ctx.storage.&lt;bucket>.&lt;method>(key, …)` call whose R2 object key is derived
1236
+ * from the handler's `args` with no server-side scoping — the input the
1237
+ * `storage_key_from_user_args` lint consumes. An object key taken straight from
1238
+ * request input lets any caller read, overwrite, or delete another user's object
1239
+ * (object-level IDOR). A key prefixed with a server-trusted identity (a `ctx.*`
1240
+ * value such as `` `${ctx.auth.userId}/…` ``) is treated as scoped and is *not*
1241
+ * recorded; only an arg-derived, `ctx`-free key reaches here. Produced by the
1242
+ * codegen feeder; runtime callers don't supply it, so the lint finds nothing there.
1243
+ */
1244
+ interface AdvisorStorageKeyAccess {
1245
+ /** The exported binding name of the procedure performing the storage call. */
1246
+ exportName: string;
1247
+ /** Source file relative to the lunora dir, no extension. */
1248
+ file: string;
1249
+ /** 1-based line of the storage call, or `0` when unknown. */
1250
+ line: number;
1251
+ /** The bucket method invoked with the arg-derived key, e.g. `get` / `put` / `delete` / `download`. */
1252
+ method: string;
1253
+ }
1254
+ /**
1255
+ * One tracked `ctx.storage.&lt;bucket>.&lt;method>(...)` upload/signing call — the
1256
+ * shared input for the storage config-hygiene security lints
1257
+ * (`storage_upload_without_content_type_allowlist`, `storage_upload_without_max_size`,
1258
+ * `storage_generate_upload_url_no_content_type_pin`, `storage_presigned_url_for_private_content`).
1259
+ * `upload`/`store` carry the `UploadOptions` guards (`allowedContentTypes` /
1260
+ * `maxSize`); `generateUploadUrl` carries the signed-PUT `contentType` pin;
1261
+ * `getPresignedUrl`/`getSignedUrl` carry a statically-known `expiresInSeconds`
1262
+ * literal. `presentKeys` is empty (and `expiresInSeconds` unset) when the
1263
+ * options argument was absent, a non-literal, or a spread — see `analyzable`.
1264
+ * Produced by the codegen feeder; runtime callers don't supply it, so the
1265
+ * lints find nothing there.
1266
+ */
1267
+ interface AdvisorStorageUpload {
1268
+ /** `true` when the call's options-object argument (or its deliberate absence) was statically resolvable. */
1269
+ analyzable: boolean;
1270
+ /** Numeric literal value of an `expiresInSeconds` option, when statically known (`getSignedUrl` / `getPresignedUrl` only). */
1271
+ expiresInSeconds?: number;
1272
+ /** The exported binding name of the procedure performing the call. */
1273
+ exportName: string;
1274
+ /** Source file relative to the lunora dir, no extension. */
1275
+ file: string;
1276
+ /** 1-based line of the call, or `0` when unknown. */
1277
+ line: number;
1278
+ /** The `ctx.storage` method invoked. */
1279
+ method: "generateUploadUrl" | "getPresignedUrl" | "getSignedUrl" | "store" | "upload";
1280
+ /** Options-object keys present at the call site (empty when not `analyzable`, or when no options argument was passed). */
1281
+ presentKeys: string[];
1282
+ }
1283
+ /**
489
1284
  * A bounded sample of rows from one table, fed into the constraint-validator
490
1285
  * lint by the studio backend (via `readTablePage`). The cap prevents unbounded
491
1286
  * scans while still catching obvious violations on small-to-medium tables.
@@ -511,6 +1306,28 @@ interface AdvisorTableSample {
511
1306
  readonly truncated: boolean;
512
1307
  }
513
1308
  /**
1309
+ * One `ctx.vectors.&lt;method>(indexName, input)` call whose `input.namespace` is
1310
+ * derived from the handler's `args` with no server-side scoping — the input the
1311
+ * `vectors_namespace_from_user_input` lint consumes. A Vectorize namespace
1312
+ * partitions one index into isolated sub-collections, so a namespace taken
1313
+ * straight from request input lets any caller read or poison another tenant's
1314
+ * vectors. A fixed literal namespace, or one prefixed with a server-trusted
1315
+ * identity (`` `${ctx.auth.orgId}` `` — references `ctx`, so treated as
1316
+ * scoped), is not recorded; only an arg-derived, unscoped namespace reaches
1317
+ * here. Produced by the codegen feeder; runtime callers don't supply it, so
1318
+ * the lint finds nothing there.
1319
+ */
1320
+ interface AdvisorVectorNamespaceAccess {
1321
+ /** The exported binding name of the procedure performing the `ctx.vectors` access. */
1322
+ exportName: string;
1323
+ /** Source file relative to the lunora dir, no extension. */
1324
+ file: string;
1325
+ /** 1-based line of the `ctx.vectors` call, or `0` when unknown. */
1326
+ line: number;
1327
+ /** The `ctx.vectors` method invoked: `query` / `upsert` / `upsertMany`. */
1328
+ method: string;
1329
+ }
1330
+ /**
514
1331
  * The two workflow-shaped inputs the `workflow_*` lints consume, produced by the
515
1332
  * codegen feeder. {@link AdvisorWorkflow} is the declaration side (one per
516
1333
  * `defineWorkflow` export in `lunora/workflows.ts`); {@link AdvisorWorkflowCall}
@@ -523,10 +1340,27 @@ interface AdvisorTableSample {
523
1340
  * how `AdvisorContainer` tracks `ContainerIR` and `AdvisorInsertWrite` tracks
524
1341
  * `InsertWriteIR`).
525
1342
  */
1343
+ /** One durable step call lifted from a workflow handler body — the input the duplicate-step-name lint compares. Structural subset of codegen's `WorkflowStepIR`. */
1344
+ interface AdvisorWorkflowStep {
1345
+ /** 1-based line of the durable step call, or `0` when unknown. */
1346
+ line: number;
1347
+ /** The native step method invoked: `do` / `sleep` / `sleepUntil` / `waitForEvent`. */
1348
+ method: string;
1349
+ /** The step's static label (the first string-literal argument). */
1350
+ name: string;
1351
+ }
526
1352
  /** One workflow declared via a `defineWorkflow()` export in `lunora/workflows.ts`. */
527
1353
  interface AdvisorWorkflow {
528
1354
  /** The `lunora/workflows.ts` export name, e.g. `orderPipeline`. */
529
1355
  exportName: string;
1356
+ /**
1357
+ * The durable step labels discovered in the handler body, in source order —
1358
+ * the duplicate-step-name input. Cloudflare memoizes a step by its name, so a
1359
+ * name used twice makes the second call silently return the first's cached
1360
+ * result. Supplied by the codegen feeder; `undefined` for runtime callers,
1361
+ * where the lint finds nothing.
1362
+ */
1363
+ steps?: ReadonlyArray<AdvisorWorkflowStep>;
530
1364
  }
531
1365
  /** One `ctx.workflows.get("name")` call discovered in a function body. */
532
1366
  interface AdvisorWorkflowCall {
@@ -540,6 +1374,23 @@ interface AdvisorWorkflowCall {
540
1374
  workflow: string;
541
1375
  }
542
1376
  /**
1377
+ * One committed `wrangler.jsonc` `vars` entry whose value is a plaintext secret —
1378
+ * the input the `plaintext_secret_in_wrangler_vars` lint consumes. The full value
1379
+ * is never carried; only a redacted {@link AdvisorWranglerVariable.preview}.
1380
+ * Produced by `@lunora/config` (which reads `wrangler.jsonc`) and threaded through
1381
+ * codegen; runtime callers don't supply it, so the lint finds nothing there.
1382
+ */
1383
+ interface AdvisorWranglerVariable {
1384
+ /** The `wrangler.jsonc` file the var was read from, relative to the project root. */
1385
+ file: string;
1386
+ /** The offending `vars` key (e.g. `STRIPE_SECRET_KEY`). */
1387
+ key: string;
1388
+ /** Heuristic that matched, e.g. `stripe_live_key` / `private_key` / `secret_named_var`. */
1389
+ kind: string;
1390
+ /** Redacted preview (first few chars + length) — never the full secret. */
1391
+ preview: string;
1392
+ }
1393
+ /**
543
1394
  * Severity of a finding, mirroring splinter's `level`. `ERROR` is a definite
544
1395
  * problem, `WARN` a likely one, `INFO` an advisory nudge.
545
1396
  */
@@ -610,6 +1461,33 @@ interface LintContext {
610
1461
  */
611
1462
  adminRoutes?: ReadonlyArray<AdvisorAdminRoute>;
612
1463
  /**
1464
+ * `ctx.ai.run(model, …)` calls whose model-id argument is derived from the
1465
+ * handler's `args` with no server-side scoping — the `ai_raw_run_escape_hatch`
1466
+ * input. `ctx.ai.run` is the raw Workers AI passthrough, so an arg-derived model
1467
+ * id lets any caller select an arbitrary model, bypassing the typed
1468
+ * `ctx.ai.model(...)` + AI-SDK layer's cap/schema (an arg-derived `inputs`
1469
+ * argument is normal usage and is not recorded). Supplied by the codegen feeder;
1470
+ * absent for runtime callers, where the lint finds nothing.
1471
+ */
1472
+ aiRawRuns?: ReadonlyArray<AdvisorAiRawRun>;
1473
+ /**
1474
+ * `generateText` / `streamText` calls whose model-callable `tools` reach a
1475
+ * privileged side effect (DB write / function dispatch / outbound
1476
+ * fetch/mail/queue) — the `ai_tool_side_effect_prompt_injection` input. Each
1477
+ * row's `userInputDerived` says whether the model input flows from `args`; the
1478
+ * lint fires only when it does. Supplied by the codegen feeder; absent for
1479
+ * runtime callers, where the lint finds nothing.
1480
+ */
1481
+ aiToolSideEffects?: ReadonlyArray<AdvisorAiToolSideEffect>;
1482
+ /**
1483
+ * `ctx.fetch(url, …)` calls inside actions whose URL argument is derived from
1484
+ * the handler's `args` — the `action_fetch_ssrf` input. `ctx.fetch` has no
1485
+ * host allowlist, so a URL built from request input is a server-side request
1486
+ * forgery vector. Supplied by the codegen feeder; absent for runtime callers,
1487
+ * where the lint finds nothing.
1488
+ */
1489
+ argumentDerivedFetches?: ReadonlyArray<AdvisorArgumentDerivedFetch>;
1490
+ /**
613
1491
  * Per-public-procedure argument validators that weaken input safety — the
614
1492
  * `public_arg_uses_any` (`v.any()` args) and `unbounded_string_arg` (length-less
615
1493
  * `v.string()` args) input. Supplied by the codegen feeder for public procedures
@@ -623,12 +1501,98 @@ interface LintContext {
623
1501
  */
624
1502
  authApiCalls?: ReadonlyArray<AdvisorAuthApiCall>;
625
1503
  /**
1504
+ * Per-`createAuth({...})`-call configuration snapshots — the shared input for
1505
+ * the five `auth_*` security lints (`auth_trusted_origins_wildcard`,
1506
+ * `auth_csrf_check_disabled`, `auth_secure_cookies_disabled`,
1507
+ * `auth_email_verification_disabled`, `auth_session_freshage_zero`). Each
1508
+ * carries whether the call's config object literal was statically analyzable
1509
+ * and, when it was, the handful of nested facts the lints check (a
1510
+ * `trustedOrigins` wildcard, `advanced.disableCSRFCheck`/`useSecureCookies`,
1511
+ * `emailAndPassword.enabled`/`requireEmailVerification`,
1512
+ * `session.freshAge === 0`). Supplied by the codegen feeder; absent for
1513
+ * runtime callers, where the auth-config lints find nothing.
1514
+ */
1515
+ authConfigs?: ReadonlyArray<AdvisorAuthConfig>;
1516
+ /**
1517
+ * `ctx.browser.&lt;method>(url, …)` calls whose navigation URL is derived from the
1518
+ * handler's `args` with no server-side scoping — the
1519
+ * `browser_user_url_without_allowlist` input. `@lunora/browser` blocks
1520
+ * private/internal targets by default, but a request-supplied public URL can
1521
+ * still be an open-proxy / SSRF vector; the lint suppresses findings when a
1522
+ * `createBrowser` config-call is hardened with `allowedHosts` or `resolveDns`.
1523
+ * Supplied by the codegen feeder; absent for runtime callers, where the lint
1524
+ * finds nothing.
1525
+ */
1526
+ browserUrlAccesses?: ReadonlyArray<AdvisorBrowserUrlAccess>;
1527
+ /**
1528
+ * Factory/constructor calls in `lunora/` whose config object literal a
1529
+ * security lint inspects for a present-or-absent key — the shared input for
1530
+ * the config-call security lints (payment authorize, inbound-mail verify,
1531
+ * rate-limit store, browser private-targets). Supplied by the codegen feeder;
1532
+ * absent for runtime callers, where the config-call lints find nothing.
1533
+ */
1534
+ configCalls?: ReadonlyArray<AdvisorConfigCall>;
1535
+ /**
1536
+ * `ctx.containers.&lt;name>.get(key, …)` calls whose instance key is derived from
1537
+ * the handler's `args` with no server-side scoping — the
1538
+ * `container_instance_key_from_user_input` input. Each container definition's
1539
+ * `.get(name)` accessor routes to one instance per key, so an arg-derived key lets
1540
+ * any caller reach another tenant's container (cross-tenant IDOR). A key scoped by
1541
+ * a server-trusted `ctx.*` value, or a fixed literal, is not recorded. Supplied by
1542
+ * the codegen feeder; absent for runtime callers, where the lint finds nothing.
1543
+ */
1544
+ containerKeyAccesses?: ReadonlyArray<AdvisorContainerKeyAccess>;
1545
+ /**
1546
+ * Runtime container-override calls — a `.start({ enableInternet: true, … })`
1547
+ * launch override, or a `.egress.&lt;method>(...)` runtime firewall mutation — the
1548
+ * `container_start_enable_internet_override` and `container_runtime_egress_relaxation`
1549
+ * lint input. Supplied by the codegen feeder; absent for runtime callers, where
1550
+ * those lints find nothing.
1551
+ */
1552
+ containerOverrides?: ReadonlyArray<AdvisorContainerOverride>;
1553
+ /**
626
1554
  * Containers declared in `lunora/containers.ts` — the `container_*` lint
627
1555
  * input. Supplied by the codegen feeder; absent for runtime callers, where
628
1556
  * the container lints find nothing.
629
1557
  */
630
1558
  containers?: ReadonlyArray<AdvisorContainer>;
631
1559
  /**
1560
+ * `rateLimit`/`dbRateLimit` (`@lunora/ratelimit`) and `verifyTurnstileMiddleware`
1561
+ * (`@lunora/auth`) middleware calls, each with whether its options literal set
1562
+ * `failOpen: true` and the rate-limit `name` — the
1563
+ * `ratelimit_middleware_fail_open` input. These guards fail closed by default; a
1564
+ * `failOpen: true` admits every request during a limiter/siteverify outage, so
1565
+ * the lint fires when a fail-open guard protects an auth/payment-sensitive
1566
+ * procedure. Supplied by the codegen feeder; absent for runtime callers, where
1567
+ * the lint finds nothing.
1568
+ */
1569
+ failOpenGuards?: ReadonlyArray<AdvisorFailOpenGuard>;
1570
+ /**
1571
+ * `ctx.flags.boolean(key, default)` reads with a statically-known string key and
1572
+ * boolean-literal default — the `flag_gates_security_with_unsafe_default` input.
1573
+ * OpenFeature returns the default when the provider errors, so a fail-open
1574
+ * default on a security-shaped key (an `enforce`/`rls`/`gate`/`lockdown`
1575
+ * protection defaulting `false`, or an `allow`/`permit`/`bypass` permission
1576
+ * defaulting `true`) silently opens access during an outage. Supplied by the
1577
+ * codegen feeder; absent for runtime callers, where the lint finds nothing.
1578
+ */
1579
+ flagSecurityDefaults?: ReadonlyArray<AdvisorFlagSecurityDefault>;
1580
+ /**
1581
+ * `httpAction`/`httpRoute` handlers that perform a side effect
1582
+ * (`ctx.runMutation` / `ctx.runAction` / a `ctx.db` write) from the HTTP edge,
1583
+ * with whether each reads `ctx.auth` — the `http_action_missing_auth_guard`
1584
+ * input. Supplied by the codegen feeder; absent for runtime callers, where the
1585
+ * lint finds nothing.
1586
+ */
1587
+ httpActionGuards?: ReadonlyArray<AdvisorHttpActionGuard>;
1588
+ /**
1589
+ * Response-header writes, inside `httpAction` handlers, whose value is derived
1590
+ * from raw request input (`request.headers`/URL/query/body) with no CR/LF
1591
+ * sanitizer — the `http_action_response_header_injection` input. Supplied by the
1592
+ * codegen feeder; absent for runtime callers, where the lint finds nothing.
1593
+ */
1594
+ httpHeaderWrites?: ReadonlyArray<AdvisorHttpHeaderWrite>;
1595
+ /**
632
1596
  * Hyperdrive `ctx.sql` accesses discovered lexically inside `query`/`mutation`
633
1597
  * handler bodies — the `hyperdrive_outside_action` input. Supplied by the
634
1598
  * codegen feeder, which omits `action` handlers (where `ctx.sql` is the typed,
@@ -636,6 +1600,30 @@ interface LintContext {
636
1600
  */
637
1601
  hyperdriveCalls?: ReadonlyArray<AdvisorHyperdriveCall>;
638
1602
  /**
1603
+ * `&lt;receiver>.identity.&lt;key>` claim reads (RLS/mask policy `auth`, or
1604
+ * `ctx.auth`/`context.auth`) — the `identity_undeclared_claim_trusted` input.
1605
+ * `defineIdentity` validates only declared claims and forwards undeclared ones
1606
+ * verbatim, so each row's `declared` flag says whether `key` is in the contract
1607
+ * (or the always-present `userId`); the lint fires on the undeclared reads.
1608
+ * Supplied by the codegen feeder — and only when a resolvable `defineIdentity`
1609
+ * contract exists; absent for runtime callers, where the lint finds nothing.
1610
+ */
1611
+ identityClaimReads?: ReadonlyArray<AdvisorIdentityClaimRead>;
1612
+ /**
1613
+ * `buildImageDeliveryUrl({ key, … })` calls (`@lunora/bindings/images`) whose
1614
+ * `key` — the CDN transform's source image, an absolute URL or an
1615
+ * origin-relative key — is derived from the handler's `args` with no
1616
+ * server-side scoping — the `images_url_source_from_user_input` input.
1617
+ * `ctx.images.transform`/`info` take image bytes, never a URL, so they are
1618
+ * not sinks; only the `key` of `buildImageDeliveryUrl` accepts a URL-or-key
1619
+ * source and is inspected. An arg-derived key lets any caller point the
1620
+ * CDN's `/cdn-cgi/image/` transform at an attacker-chosen origin (SSRF /
1621
+ * open proxy). A fixed literal, or a key scoped by a server-trusted `ctx.*`
1622
+ * value, is not recorded. Supplied by the codegen feeder; absent for
1623
+ * runtime callers, where the lint finds nothing.
1624
+ */
1625
+ imageDeliveryUrlAccesses?: ReadonlyArray<AdvisorImageDeliveryUrlAccess>;
1626
+ /**
639
1627
  * Per-declared-index hit counts observed at runtime (the dead-index half of
640
1628
  * the `index_utilization` lint input). Supplied by the studio backend, which
641
1629
  * sums the per-`(table, index)` reads each shard records in the durable
@@ -651,6 +1639,26 @@ interface LintContext {
651
1639
  */
652
1640
  inserts?: ReadonlyArray<AdvisorInsertWrite>;
653
1641
  /**
1642
+ * `ctx.kv.&lt;method>(key, …)` calls whose namespace key is derived from the
1643
+ * handler's `args` with no server-side scoping — the `kv_unscoped_user_key_idor`
1644
+ * input. Workers KV is one flat namespace, so a key taken straight from request
1645
+ * input lets any caller read/overwrite/delete another user's entry (IDOR). Only
1646
+ * arg-derived, unscoped keys are recorded (a fixed literal or a
1647
+ * `${ctx.auth.userId}:…` prefix is not). Supplied by the codegen feeder; absent
1648
+ * for runtime callers, where the lint finds nothing.
1649
+ */
1650
+ kvKeyAccesses?: ReadonlyArray<AdvisorKvKeyAccess>;
1651
+ /**
1652
+ * `ctx.mail`/`ctx.email` `send`/`queue` calls whose `to`/`cc`/`bcc` recipient is
1653
+ * derived from the handler's `args` with no server-side scoping — the
1654
+ * `mail_recipient_from_request_input` input. A recipient taken straight from
1655
+ * request input turns the deployment into an open relay / spam amplifier (any
1656
+ * caller can direct mail to an arbitrary address). A recipient scoped by a
1657
+ * server-trusted `ctx.*` value, or a fixed literal, is not recorded. Supplied by
1658
+ * the codegen feeder; absent for runtime callers, where the lint finds nothing.
1659
+ */
1660
+ mailRecipientAccesses?: ReadonlyArray<AdvisorMailRecipientAccess>;
1661
+ /**
654
1662
  * Per-procedure column-masking usage discovered in function bodies (the
655
1663
  * `mask_uncovered_pii_column` input). Carries whether each procedure's builder
656
1664
  * chain includes `.use(mask(...))`, which `(table, column)` pairs its mask
@@ -660,6 +1668,23 @@ interface LintContext {
660
1668
  */
661
1669
  maskProcedures?: ReadonlyArray<AdvisorMaskProcedure>;
662
1670
  /**
1671
+ * Masked columns whose `mask(policies)` strategy is a statically-known
1672
+ * literal (the `mask_weak_hash_strategy_on_pii` input). One row per masked
1673
+ * column, with the `"hash"` / `"redact"` strategy literal attached; a
1674
+ * `MaskFn` (custom, non-literal) strategy is never recorded. Supplied by
1675
+ * the codegen feeder; absent for runtime callers, where the lint finds
1676
+ * nothing.
1677
+ */
1678
+ maskStrategies?: ReadonlyArray<AdvisorMaskStrategy>;
1679
+ /**
1680
+ * Whole-row `ctx.db.replace(id, document)` writes lifted from custom
1681
+ * mutators' authoritative `server` impls (the `mutator_full_row_replace`
1682
+ * input). Each `replace` overwrites the entire row, clobbering a concurrent
1683
+ * edit to a different column on a synced table. Supplied by the codegen
1684
+ * feeder; absent for runtime callers, where the lint finds nothing.
1685
+ */
1686
+ mutatorWrites?: ReadonlyArray<AdvisorMutatorWrite>;
1687
+ /**
663
1688
  * Non-deterministic API calls (`Date.now`, `Math.random`,
664
1689
  * `crypto.randomUUID`, `crypto.getRandomValues`, `fetch`) discovered lexically
665
1690
  * inside `query`/`mutation` handler bodies — the `nondeterministic_query_mutation`
@@ -669,6 +1694,48 @@ interface LintContext {
669
1694
  */
670
1695
  nondeterministicCalls?: ReadonlyArray<AdvisorNondeterministicCall>;
671
1696
  /**
1697
+ * `query`/`mutation` handlers that gate a `ctx.db.get`/`patch`/`delete` on a
1698
+ * null-checked `ctx.db.normalizeId(table, id)` result — the
1699
+ * `normalize_id_used_as_authorization` input. `normalizeId` validates an id's
1700
+ * structural shape only (it never reads the database), so gating access on a
1701
+ * non-null result is an IDOR. The lint keeps only public procedures with no
1702
+ * `.use(rls(...))` and no ownership/identity mention, then joins `table` against
1703
+ * the schema's RLS mode before flagging. Supplied by the codegen feeder; absent
1704
+ * for runtime callers, where the lint finds nothing.
1705
+ */
1706
+ normalizeIdAuthorizations?: ReadonlyArray<AdvisorNormalizeIdAuthorization>;
1707
+ /**
1708
+ * `ctx.db` writes (`insert` / `replace` / `patch` / `insertManyUnsafe`) that set
1709
+ * an ownership / identity column (`userId`, `ownerId`, `tenantId`, …) from the
1710
+ * handler's `args` instead of the server-trusted identity — the
1711
+ * `owner_field_from_args_not_auth` input. The ownership column decides who a row
1712
+ * belongs to, so an arg-derived value lets any caller write rows owned by another
1713
+ * user or tenant (act-as-any-user / cross-tenant IDOR). A column stamped from
1714
+ * `ctx.*`, or a fixed literal, is not recorded. Supplied by the codegen feeder;
1715
+ * absent for runtime callers, where the lint finds nothing.
1716
+ */
1717
+ ownerFieldWrites?: ReadonlyArray<AdvisorOwnerFieldWrite>;
1718
+ /**
1719
+ * Payment webhook-adapter constructions (`createStripeAdapter` /
1720
+ * `createPolarAdapter` / `createAutumnAdapter` / `createDodoPaymentsAdapter`) — the payment-webhook wide-tolerance lint's input. Each row's
1721
+ * `toleranceSeconds` is the statically-known `webhookToleranceSeconds` replay
1722
+ * window (default 300s); the lint fires only above a conservative ceiling, where
1723
+ * the endpoint would accept stale, replayable signed payloads. Supplied by the
1724
+ * codegen feeder; absent for runtime callers, where the lint finds nothing.
1725
+ */
1726
+ paymentWebhooks?: ReadonlyArray<AdvisorPaymentWebhook>;
1727
+ /**
1728
+ * Payload-derived privileged dispatches — the `privileged_dispatch_unvalidated_payload`
1729
+ * input. Each is a `ctx.run`/`context.run` back into a Lunora function from inside a
1730
+ * `defineQueue` push handler or a `defineWorkflow` handler, whose args reference the
1731
+ * handler's untrusted payload (`context.params` for a workflow, a `for (… of
1732
+ * batch.messages)` body for a queue). Both handler kinds run under the system identity
1733
+ * (RLS disabled), so the lint joins the resolved target against `rlsProcedures` and
1734
+ * fires only when the target enforces a row policy. Supplied by the codegen feeder;
1735
+ * absent for runtime callers, where the lint finds nothing.
1736
+ */
1737
+ privilegedDispatches?: ReadonlyArray<AdvisorPrivilegedDispatch>;
1738
+ /**
672
1739
  * Per-procedure protective-middleware snapshots — the
673
1740
  * `public_mutation_without_ratelimit` and `user_creating_mutation_without_captcha`
674
1741
  * input. Records which `.use(...)` guards (`rateLimit`, captcha, `rls`, `mask`,
@@ -684,6 +1751,13 @@ interface LintContext {
684
1751
  */
685
1752
  queries?: ReadonlyArray<AdvisorQueryRead>;
686
1753
  /**
1754
+ * Queues declared via `defineQueue` exports in `lunora/queues.ts` — the
1755
+ * declaration-side input for the `queue_*` lints (`queue_without_dlq`).
1756
+ * Supplied by the codegen feeder; absent for runtime callers, where the
1757
+ * queue lints find nothing.
1758
+ */
1759
+ queues?: ReadonlyArray<AdvisorQueue>;
1760
+ /**
687
1761
  * R2 SQL `ctx.r2sql` accesses discovered lexically inside `query`/`mutation`
688
1762
  * handler bodies — the `r2sql_outside_action` input. Supplied by the codegen
689
1763
  * feeder, which omits `action` handlers (where `ctx.r2sql` is the typed,
@@ -691,6 +1765,36 @@ interface LintContext {
691
1765
  */
692
1766
  r2sqlCalls?: ReadonlyArray<AdvisorR2sqlCall>;
693
1767
  /**
1768
+ * `rateLimit`/`dbRateLimit` middleware calls (`@lunora/ratelimit`) whose
1769
+ * `key` selector is derived from the handler's `args` with no server-side
1770
+ * scoping — the `ratelimit_key_spoofable_or_global` input. A key an
1771
+ * attacker controls lets them rotate it per request and bypass the limit
1772
+ * entirely, defeating its purpose. A selector scoped by `ctx` (e.g.
1773
+ * `ctx.auth.userId`, `ctx.ip`), or one with no `args` reference at all (a
1774
+ * fixed/global bucket), is not recorded. Supplied by the codegen feeder;
1775
+ * absent for runtime callers, where the lint finds nothing.
1776
+ */
1777
+ ratelimitKeySelectors?: ReadonlyArray<AdvisorRatelimitKeySelector>;
1778
+ /**
1779
+ * `query` handlers that `return` the raw rows of a table (a `ctx.db` row read
1780
+ * or `ctx.db.query(...)` fluent chain, returned directly or through one local
1781
+ * `const` hop, with no hand-built projection) — the
1782
+ * `output_projection_missing_on_public_read` input. The lint keeps only public
1783
+ * queries with no `.output(...)`/mask on the chain, then joins `table` against
1784
+ * the schema's PII-named columns before nudging. Supplied by the codegen
1785
+ * feeder; absent for runtime callers, where the lint finds nothing.
1786
+ */
1787
+ rawRowReturns?: ReadonlyArray<AdvisorRawRowReturn>;
1788
+ /**
1789
+ * `ctx.db.&lt;table>.findMany({ with: { &lt;rel> } })` relation-hydrating list reads
1790
+ * — the `masked_relation_leak_via_with` input. Column masking is applied to a
1791
+ * read's top-level rows but does not descend into `with`-hydrated relations,
1792
+ * so a masked table surfaced only through a `with` on an unprotected public
1793
+ * read is returned in the clear. Supplied by the codegen feeder; absent for
1794
+ * runtime callers, where the lint finds nothing.
1795
+ */
1796
+ relationLoads?: ReadonlyArray<AdvisorRelationLoad>;
1797
+ /**
694
1798
  * Per-procedure RLS usage discovered in function bodies (the
695
1799
  * `rls_uncovered_table` input). Carries whether each procedure's builder chain
696
1800
  * includes `.use(rls(...))`, which tables the procedure reads/writes, and which
@@ -708,6 +1812,14 @@ interface LintContext {
708
1812
  */
709
1813
  secretLiterals?: ReadonlyArray<AdvisorSecretLiteral>;
710
1814
  /**
1815
+ * Replication shapes declared via `defineShape` in `lunora/shapes.ts` — the
1816
+ * `shape_unknown_table` and `shape_targets_global_table` lint input. Each
1817
+ * carries the export name and its static `table` literal, cross-referenced
1818
+ * against {@link LintContext.schema}. Supplied by the codegen feeder; absent
1819
+ * for runtime callers, where the shape lints find nothing.
1820
+ */
1821
+ shapes?: ReadonlyArray<AdvisorShape>;
1822
+ /**
711
1823
  * Per-shard observed traffic — the `hot_shard` lint input. Supplied by the
712
1824
  * studio backend, which fans out over a sharded function's shards and reads
713
1825
  * each shard's recorded request volume from the durable `__lunora_metrics`
@@ -715,6 +1827,15 @@ interface LintContext {
715
1827
  */
716
1828
  shardTraffic?: ReadonlyArray<AdvisorShardTraffic>;
717
1829
  /**
1830
+ * `ctx.db.&lt;table>.findMany({ includeDeleted })` list reads whose
1831
+ * `includeDeleted` is a hardcoded `true` or derived from the handler's
1832
+ * `args` — the `soft_delete_include_deleted_from_args` input. On a public
1833
+ * read of a `.softDelete()` table this resurfaces soft-deleted rows to any
1834
+ * caller (arg-derived) or every caller (hardcoded). Supplied by the codegen
1835
+ * feeder; absent for runtime callers, where the lint finds nothing.
1836
+ */
1837
+ softDeleteReads?: ReadonlyArray<AdvisorSoftDeleteRead>;
1838
+ /**
718
1839
  * `ctx.sql` tagged-template interpolations that splice an unparameterized
719
1840
  * string-building expression into the query — the `sql_injection_risk` input.
720
1841
  * Supplied by the codegen feeder; absent for runtime callers, where the lint
@@ -722,6 +1843,29 @@ interface LintContext {
722
1843
  */
723
1844
  sqlInterpolations?: ReadonlyArray<AdvisorSqlInterpolation>;
724
1845
  /**
1846
+ * `ctx.storage.&lt;bucket>.&lt;method>(key, …)` calls whose R2 object key is derived
1847
+ * from the handler's `args` with no server-side scoping — the
1848
+ * `storage_key_from_user_args` input. The bucket read/write/URL/delete methods
1849
+ * key by their first argument, so an arg-derived key is object-level IDOR
1850
+ * (read/overwrite/delete anyone's object). A key referencing a server-trusted
1851
+ * `ctx.*` value (e.g. `${ctx.auth.userId}/…`) is treated as scoped and not
1852
+ * recorded. Supplied by the codegen feeder; absent for runtime callers, where
1853
+ * the lint finds nothing.
1854
+ */
1855
+ storageKeyAccesses?: ReadonlyArray<AdvisorStorageKeyAccess>;
1856
+ /**
1857
+ * Tracked `ctx.storage.&lt;bucket>.&lt;method>(...)` upload/signing calls — the
1858
+ * shared input for the storage config-hygiene lints
1859
+ * (`storage_upload_without_content_type_allowlist`, `storage_upload_without_max_size`,
1860
+ * `storage_generate_upload_url_no_content_type_pin`,
1861
+ * `storage_presigned_url_for_private_content`). Each row carries the method
1862
+ * invoked, which options-object keys were present, and (for the two URL
1863
+ * signers) a statically-known `expiresInSeconds` literal. Supplied by the
1864
+ * codegen feeder; absent for runtime callers, where these lints find
1865
+ * nothing.
1866
+ */
1867
+ storageUploads?: ReadonlyArray<AdvisorStorageUpload>;
1868
+ /**
725
1869
  * Bounded row samples per table — the `constraint_validator` lint input.
726
1870
  * Supplied by the studio backend, which reads up to the configured row cap
727
1871
  * from each table via `readTablePage` and assembles the existing-id set for
@@ -744,6 +1888,16 @@ interface LintContext {
744
1888
  */
745
1889
  tableScans?: ReadonlyArray<AdvisorTableScan>;
746
1890
  /**
1891
+ * `ctx.vectors.&lt;method>(index, { namespace, … })` calls whose `namespace` is
1892
+ * derived from the handler's `args` with no server-side scoping — the
1893
+ * `vectors_namespace_from_user_input` input. A Vectorize namespace partitions one
1894
+ * index into isolated sub-collections, so an arg-derived namespace lets any caller
1895
+ * read or poison another tenant's vectors. A namespace scoped by a server-trusted
1896
+ * `ctx.*` value, or a fixed literal, is not recorded. Supplied by the codegen
1897
+ * feeder; absent for runtime callers, where the lint finds nothing.
1898
+ */
1899
+ vectorNamespaceAccesses?: ReadonlyArray<AdvisorVectorNamespaceAccess>;
1900
+ /**
747
1901
  * `ctx.workflows.get("name")` call sites discovered in function bodies — the
748
1902
  * use-side input the `workflow_unused` and `workflow_unknown_target` lints
749
1903
  * cross-reference against {@link LintContext.workflows}. Supplied by the
@@ -758,6 +1912,13 @@ interface LintContext {
758
1912
  * nothing.
759
1913
  */
760
1914
  workflows?: ReadonlyArray<AdvisorWorkflow>;
1915
+ /**
1916
+ * Committed `wrangler.jsonc` `vars` entries holding plaintext secrets — the
1917
+ * input for the `plaintext_secret_in_wrangler_vars` lint. Supplied by
1918
+ * `@lunora/config` (which reads `wrangler.jsonc`) via the codegen pass-through;
1919
+ * absent for runtime callers, where the lint finds nothing.
1920
+ */
1921
+ wranglerVariables?: ReadonlyArray<AdvisorWranglerVariable>;
761
1922
  }
762
1923
  /**
763
1924
  * A single advisory rule. `run` is pure over its {@link LintContext} so lints are
@@ -786,9 +1947,9 @@ interface Lint {
786
1947
  title: string;
787
1948
  }
788
1949
  /**
789
- * Minimal structural view of the `@lunora/analytics` SQL client — just its
1950
+ * Minimal structural view of the `@lunora/bindings/analytics` SQL client — just its
790
1951
  * `query(sql)` method. Kept structural (not an `import type` from
791
- * `@lunora/analytics`) so the advisor needn't depend on the analytics package;
1952
+ * `@lunora/bindings/analytics`) so the advisor needn't depend on the analytics package;
792
1953
  * the real `AnalyticsSqlClient` satisfies it, as does a plain test double.
793
1954
  */
794
1955
  interface AnalyticsMetricsSource {
@@ -926,6 +2087,23 @@ declare const hotShard: Lint;
926
2087
  */
927
2088
  declare const indexUtilization: Lint;
928
2089
  /**
2090
+ * Flags an action's `ctx.fetch(url, …)` whose URL is derived from the handler's
2091
+ * `args` — a server-side request forgery (SSRF) vector.
2092
+ *
2093
+ * `ctx.fetch` is the action-only outbound-request escape hatch, and it applies no
2094
+ * host allowlist: whatever URL it is handed, it fetches. When that URL comes from
2095
+ * request input (`ctx.fetch(args.url)`, a template embedding `args.*`, or a URL
2096
+ * built one hop earlier from `args`), a caller can point the worker at the cloud
2097
+ * metadata endpoint (`169.254.169.254`) or an internal service and read the
2098
+ * response — classic SSRF. The fix is to validate the URL against an allowlist of
2099
+ * expected hosts before fetching, and to reject private / link-local targets.
2100
+ *
2101
+ * Runs only when the codegen feeder supplies fetch-taint evidence
2102
+ * (`context.argumentDerivedFetches`); a runtime caller flags nothing. One finding per
2103
+ * arg-derived `ctx.fetch` call.
2104
+ */
2105
+ declare const actionFetchSsrf: Lint;
2106
+ /**
929
2107
  * Flags an `httpRoute` on an admin/privileged-looking path whose handler shows no
930
2108
  * auth/admin guard.
931
2109
  *
@@ -942,6 +2120,91 @@ declare const indexUtilization: Lint;
942
2120
  */
943
2121
  declare const adminRouteWithoutGuard: Lint;
944
2122
  /**
2123
+ * Flags a `ctx.ai.run(model, …)` call whose model-id argument is derived from
2124
+ * the handler's `args` with no server-side scoping — arbitrary model
2125
+ * selection that bypasses the typed AI-SDK layer.
2126
+ *
2127
+ * `ctx.ai.run` is the raw Workers AI binding passthrough (void-style
2128
+ * `ai.run`), bypassing `ctx.ai.model(...)` plus the AI SDK functions
2129
+ * (`generateText`, `streamText`, …) entirely — no output cap, no schema. When
2130
+ * the model id comes straight from request input (`ctx.ai.run(args.model,
2131
+ * …)`, or one built one hop earlier from `args`), any caller can pick which
2132
+ * model runs, sidestepping whatever the typed path would have enforced. Only
2133
+ * the model argument (`arguments[0]`) is inspected; an arg-derived `inputs`
2134
+ * (`arguments[1]`) is normal, expected usage and is never flagged. A model
2135
+ * scoped by a server-trusted `ctx.*` value is treated as scoped and not
2136
+ * flagged.
2137
+ *
2138
+ * Runs only when the codegen feeder supplies raw-run evidence
2139
+ * (`context.aiRawRuns`); a runtime caller flags nothing. One finding per
2140
+ * arg-derived, unscoped `ctx.ai.run` call.
2141
+ */
2142
+ declare const aiRawRunEscapeHatch: Lint;
2143
+ /**
2144
+ * Flags a `generateText` / `streamText` call whose model input is user-derived
2145
+ * **and** whose model-callable `tools` reach a privileged side effect.
2146
+ *
2147
+ * The AI SDK lets the model call a `tool({ execute })` to take an action. When
2148
+ * that tool's `execute` performs a real side effect — a DB write, a function
2149
+ * dispatch (`ctx.run`), or an outbound send (fetch / mail / queue) — and the
2150
+ * model's prompt / messages carry user-supplied text, an injected instruction in
2151
+ * that text can steer the model into firing the side effect. The model becomes a
2152
+ * confused deputy: attacker-authored words drive privileged actions. This is the
2153
+ * canonical LLM prompt-injection-to-tool-call hazard.
2154
+ *
2155
+ * Runs only when the codegen feeder supplies generation evidence
2156
+ * (`context.aiToolSideEffects`); a runtime caller flags nothing. Fires only when
2157
+ * the model input is derived from the handler's `args` — a fully server-authored
2158
+ * prompt driving a side-effecting tool is not flagged. Deliberately narrow (an
2159
+ * inherently heuristic rule): one finding per matching call.
2160
+ */
2161
+ declare const aiToolSideEffectPromptInjection: Lint;
2162
+ /**
2163
+ * Flags a public procedure that runs an AI text/object generation
2164
+ * (`generateText` / `streamText` / `generateObject` / `streamObject`) with no
2165
+ * `maxOutputTokens` bound in its config.
2166
+ *
2167
+ * Each generation call bills against the account's Workers AI / provider budget
2168
+ * in proportion to the tokens produced. Left unbounded and reachable from a
2169
+ * `.public()` procedure, an anonymous caller can request arbitrarily long
2170
+ * completions in a loop — a denial-of-wallet vector — and can also tie up worker
2171
+ * CPU/time on long streams. The fix is to cap output with `maxOutputTokens` (and
2172
+ * ideally rate-limit the entry point).
2173
+ *
2174
+ * Only a call whose config is a visible object literal is judged; a hoisted or
2175
+ * spread config is statically opaque and left un-flagged (fail-open) to avoid a
2176
+ * false positive. Runs only when the codegen feeder supplies procedure-protection
2177
+ * evidence (`context.procedureProtections`); a runtime caller flags nothing.
2178
+ */
2179
+ declare const aiUnboundedGenerationPublic: Lint;
2180
+ /**
2181
+ * Flags an app that opts into `allowUnauthenticatedShardAccess: true` while its
2182
+ * schema has an RLS gap (no `.rls("required")`, or a `.public()` table).
2183
+ *
2184
+ * `allowUnauthenticatedShardAccess` (a `WorkerOptions` field consumed by
2185
+ * `createWorker(...)`) turns off the fail-closed default that denies a shard
2186
+ * lookup for a request with no verified identity. That is a deliberate, opt-in
2187
+ * posture switch — appropriate for a public/anonymous-first shard resolver — but
2188
+ * combined with a schema that never enforces `.rls("required")` (or that leaves
2189
+ * a table `.public()`, i.e. exempt from it), an unauthenticated caller can shard-hop
2190
+ * and read another tenant's rows with no row-security guard behind the door.
2191
+ *
2192
+ * **Evidence and coverage gap**: this reads `context.configCalls`, fed by the
2193
+ * codegen `discover-config-calls.ts` feeder's `.extend(fn)` callback-shape
2194
+ * support — it only sees the setting when a `lunora/`-local file calls the
2195
+ * generated `defineApp()...extend(() => ({ allowUnauthenticatedShardAccess:
2196
+ * true }))` escape hatch (the pattern the `nuxt` / `analog` templates use in
2197
+ * `lunora/server.ts`). An app that sets the same field via `@lunora/vite`'s
2198
+ * `LunoraPluginOptions` (`vite.config.ts`) or a hand-authored worker entry
2199
+ * outside `lunora/` (the `sveltekit` / `astro` / `react-router` /
2200
+ * `tanstack-start` template style) is invisible to this lint — a coverage gap,
2201
+ * not a false negative this lint claims to catch.
2202
+ *
2203
+ * Runs only when the codegen feeder supplies config-call evidence; a runtime
2204
+ * caller flags nothing. One finding per opted-in `.extend(...)` call site.
2205
+ */
2206
+ declare const allowUnauthenticatedShardAccessEnabled: Lint;
2207
+ /**
945
2208
  * Flags a `ctx.authApi.&lt;method>(...)` call whose argument object omits `headers`.
946
2209
  *
947
2210
  * `@lunora/auth`'s `withAuthPlugins` middleware attaches the full privileged
@@ -959,6 +2222,135 @@ declare const adminRouteWithoutGuard: Lint;
959
2222
  */
960
2223
  declare const authApiCallWithoutHeaders: Lint;
961
2224
  /**
2225
+ * Flags a `createAuth({...})` call whose `advanced.disableCSRFCheck` is
2226
+ * explicitly `true`.
2227
+ *
2228
+ * better-auth's CSRF check validates the request's origin against
2229
+ * `trustedOrigins` before applying a state-changing auth mutation (sign-in,
2230
+ * sign-up, session revocation, …). `disableCSRFCheck: true` turns that check
2231
+ * off outright, so a cross-site request riding the browser's ambient auth
2232
+ * cookie is processed the same as a same-origin one — the standard CSRF
2233
+ * exposure.
2234
+ *
2235
+ * Runs only when the codegen feeder supplies auth-config evidence
2236
+ * (`context.authConfigs`), and only for an analyzable config (a static,
2237
+ * spread-free object literal); an opaque config could set the key elsewhere
2238
+ * and is skipped rather than guessed at. One finding per matching `createAuth`
2239
+ * call.
2240
+ */
2241
+ declare const authCsrfCheckDisabled: Lint;
2242
+ /**
2243
+ * Flags a `createAuth({...})` call with `emailAndPassword.enabled: true` and no
2244
+ * `emailAndPassword.requireEmailVerification: true`.
2245
+ *
2246
+ * better-auth defaults `requireEmailVerification` off, so an email/password
2247
+ * account is usable — signed in, able to act — the moment it's created, before
2248
+ * the caller has proven ownership of the email address. That lets an attacker
2249
+ * sign up with a victim's email (or an address they don't control) and operate
2250
+ * the account immediately, and it weakens any downstream flow (password reset,
2251
+ * account recovery) that assumes a verified address.
2252
+ *
2253
+ * Runs only when the codegen feeder supplies auth-config evidence
2254
+ * (`context.authConfigs`), and only for an analyzable config (a static,
2255
+ * spread-free object literal); an opaque config could set the key elsewhere
2256
+ * and is skipped rather than guessed at. One finding per matching `createAuth`
2257
+ * call.
2258
+ */
2259
+ declare const authEmailVerificationDisabled: Lint;
2260
+ /**
2261
+ * Flags a `createAuth({...})` call whose `advanced.useSecureCookies` is
2262
+ * explicitly `false`.
2263
+ *
2264
+ * `@lunora/auth` defaults `useSecureCookies` ON unless the deployment's
2265
+ * `baseURL` is provably plain `http://` (a local dev origin) — see
2266
+ * `hardenAuthOptions` in `packages/auth/src/create-auth.ts`. An explicit
2267
+ * `useSecureCookies: false` overrides that secure-by-default posture, so the
2268
+ * session cookie ships without the `Secure` attribute even on an HTTPS
2269
+ * deployment — it is then sent over any plaintext connection an attacker can
2270
+ * coerce (mixed-content requests, a downgraded subdomain), exposing the
2271
+ * session.
2272
+ *
2273
+ * Runs only when the codegen feeder supplies auth-config evidence
2274
+ * (`context.authConfigs`), and only for an analyzable config (a static,
2275
+ * spread-free object literal); an opaque config could set the key elsewhere
2276
+ * and is skipped rather than guessed at. One finding per matching `createAuth`
2277
+ * call.
2278
+ */
2279
+ declare const authSecureCookiesDisabled: Lint;
2280
+ /**
2281
+ * Flags a `createAuth({...})` call whose `session.freshAge` is explicitly the
2282
+ * literal `0`.
2283
+ *
2284
+ * `freshAge` is the window better-auth treats a session as "recently
2285
+ * re-authenticated" for sensitive operations (changing the password, adding a
2286
+ * passkey, revoking other sessions, …) that gate on a fresh session rather than
2287
+ * merely a valid one. Setting it to `0` disables that recent-reauth check
2288
+ * entirely — every sensitive operation is treated as fresh regardless of how
2289
+ * old the session is, so a long-lived stolen session (or token) can perform
2290
+ * them without ever proving the caller still controls the credentials.
2291
+ *
2292
+ * Runs only when the codegen feeder supplies auth-config evidence
2293
+ * (`context.authConfigs`), and only for an analyzable config (a static,
2294
+ * spread-free object literal); an opaque config could set the key elsewhere
2295
+ * and is skipped rather than guessed at. One finding per matching `createAuth`
2296
+ * call.
2297
+ */
2298
+ declare const authSessionFreshageZero: Lint;
2299
+ /**
2300
+ * Flags a `createAuth({...})` call whose `trustedOrigins` array literal
2301
+ * contains a `"*"` entry.
2302
+ *
2303
+ * better-auth's `trustedOrigins` is the allowlist its CSRF/origin validation
2304
+ * checks every state-changing request against. A `"*"` entry disables that
2305
+ * check entirely — any origin is accepted, which is exactly the protection
2306
+ * Lunora leans on to keep cross-site requests from riding an authenticated
2307
+ * cookie.
2308
+ *
2309
+ * Runs only when the codegen feeder supplies auth-config evidence
2310
+ * (`context.authConfigs`), and only for an analyzable config (a static,
2311
+ * spread-free object literal); an opaque config could set `trustedOrigins`
2312
+ * elsewhere and is skipped rather than guessed at. One finding per matching
2313
+ * `createAuth` call.
2314
+ */
2315
+ declare const authTrustedOriginsWildcard: Lint;
2316
+ /**
2317
+ * Flags a `createBrowser({ allowPrivateTargets: true })`.
2318
+ *
2319
+ * `@lunora/browser` blocks navigation to private / internal / loopback addresses
2320
+ * by default — that guard is what stops a browser action from being turned into a
2321
+ * server-side request forgery (SSRF) tool that reaches cloud metadata endpoints
2322
+ * (`169.254.169.254`), internal services, or `localhost`. Setting
2323
+ * `allowPrivateTargets: true` disables it wholesale. Combined with a
2324
+ * request-supplied URL that is the classic SSRF-to-metadata exfiltration path.
2325
+ *
2326
+ * Runs only when the codegen feeder supplies config-call evidence
2327
+ * (`context.configCalls`); a runtime caller flags nothing. One finding per
2328
+ * opted-out browser.
2329
+ */
2330
+ declare const browserAllowPrivateTargets: Lint;
2331
+ /**
2332
+ * Flags a `ctx.browser.&lt;method>(url, …)` call whose navigation URL is derived
2333
+ * from the handler's `args` with no server-side scoping — and no hardened
2334
+ * `createBrowser` allowlist to contain it.
2335
+ *
2336
+ * `@lunora/browser` blocks navigation to private/internal/loopback addresses by
2337
+ * default, but that guard only stops SSRF to *internal* targets. A
2338
+ * request-supplied *public* URL still turns the headless browser into an
2339
+ * open-proxy / request-forgery tool: any caller can make the deployment fetch
2340
+ * an arbitrary third-party URL (SSRF to public cloud APIs that trust the egress
2341
+ * IP, data exfiltration through the fetched URL), and — without pinned DNS — a
2342
+ * public hostname can rebind to an internal address after the guard's check.
2343
+ * The containment is an `allowedHosts` allowlist on `createBrowser`, or a
2344
+ * pinned `resolveDns`. This lint therefore suppresses all findings when the
2345
+ * config-call evidence shows a `createBrowser` hardened with either key; only
2346
+ * an unhardened browser reaching an arg-derived URL is flagged.
2347
+ *
2348
+ * Runs only when the codegen feeder supplies browser URL-access evidence
2349
+ * (`context.browserUrlAccesses`); a runtime caller flags nothing. One finding
2350
+ * per arg-derived, unscoped `ctx.browser` navigation.
2351
+ */
2352
+ declare const browserUserUrlWithoutAllowlist: Lint;
2353
+ /**
962
2354
  * Detect FK cycles in the declared relation graph via a DFS.
963
2355
  *
964
2356
  * A "circular FK" exists when a chain of `one` relations forms a loop — for
@@ -986,6 +2378,26 @@ declare const authApiCallWithoutHeaders: Lint;
986
2378
  */
987
2379
  declare const circularFk: Lint;
988
2380
  /**
2381
+ * Flags a `ctx.containers.&lt;exportName>.get(name, …)` call whose instance key
2382
+ * is derived from the handler's `args` with no server-side scoping — a
2383
+ * cross-tenant container IDOR.
2384
+ *
2385
+ * A container definition's `.get(name)` accessor routes to one Durable
2386
+ * Object-backed container instance per `name` — one container per entity
2387
+ * (user, room, job…). When the key comes straight from request input
2388
+ * (`ctx.containers.app.get(args.id)`, a template embedding `args.*`, or a key
2389
+ * built one hop earlier from `args`), any caller can hand in another tenant's
2390
+ * key and reach that tenant's container instance. The fix is to derive the
2391
+ * key from a server-trusted identity (`` `${ctx.auth.userId}` ``) or a record
2392
+ * the caller owns — a key that references `ctx` is treated as scoped and is
2393
+ * not flagged. `.any()`/`.pool()` are not sinks (they take no key).
2394
+ *
2395
+ * Runs only when the codegen feeder supplies container key-access evidence
2396
+ * (`context.containerKeyAccesses`); a runtime caller flags nothing. One
2397
+ * finding per arg-derived, unscoped `ctx.containers.*.get` call.
2398
+ */
2399
+ declare const containerInstanceKeyFromUserInput: Lint;
2400
+ /**
989
2401
  * Flags a container declared on a large instance type. The big `standard-3` /
990
2402
  * `standard-4` sizes (and large custom shapes) are billed on their provisioned
991
2403
  * memory + disk for the whole time an instance runs, so an over-provisioned
@@ -1006,6 +2418,8 @@ declare const containerOversizedInstance: Lint;
1006
2418
  * alone.
1007
2419
  */
1008
2420
  declare const containerPublicInternet: Lint;
2421
+ declare const containerRuntimeEgressRelaxation: Lint;
2422
+ declare const containerStartEnableInternetOverride: Lint;
1009
2423
  /**
1010
2424
  * Lunora port of splinter's `0009_duplicate_index`.
1011
2425
  *
@@ -1031,6 +2445,40 @@ declare const duplicateIndex: Lint;
1031
2445
  */
1032
2446
  declare const emptyIndex: Lint;
1033
2447
  /**
2448
+ * Flags a table that is both `.source(...)` and `.global()`.
2449
+ *
2450
+ * The two are contradictory. `.global()` already places a table in an external
2451
+ * store (D1, or a Hyperdrive-fronted Postgres/MySQL) that Lunora owns the schema
2452
+ * for and reads through the global backend. `.source(...)` declares the table as
2453
+ * **materialized from** an external database into a shard DO's SQLite by the
2454
+ * ingest poll loop. A table cannot simultaneously live in the global tier and be
2455
+ * polled into per-shard SQLite — the ingest loop has no DO-local table to write,
2456
+ * and the global backend has no poll loop. This is a definite misconfiguration,
2457
+ * so it is an `ERROR`.
2458
+ *
2459
+ * **Evidence supply**: reads `table.externalSource` + `table.shardKind`. Skipped
2460
+ * unless both a sourced declaration and the `global` tier are present.
2461
+ */
2462
+ declare const externalSourceOnGlobal: Lint;
2463
+ /**
2464
+ * Flags a `.source(...)` + `.shardBy(...)` table that has no `tenantBy` mapper.
2465
+ *
2466
+ * Per-shard SQLite isolation only controls *where* materialized rows land — not
2467
+ * what* the ingest query pulls. A sourced + sharded table whose `tenantBy` is
2468
+ * absent runs the same unscoped membership query on every tenant's Durable
2469
+ * Object, so each agent replicates the **entire** multitenant table into its own
2470
+ * SQLite (and then to its clients via `defineShape`). That is a cross-tenant data
2471
+ * leak, not a performance nit — so it is an `ERROR` that fails the build.
2472
+ *
2473
+ * `tenantBy(shardKey)` is the boundary: it binds this DO's shard key into the
2474
+ * query's parameters so the tenant can only ever pull its own rows.
2475
+ *
2476
+ * **Evidence supply**: reads `table.externalSource` (the codegen feeder captures
2477
+ * it from `.source({...})`; the runtime feeder derives it). A table without a
2478
+ * sourced declaration, or one not sharded, is skipped.
2479
+ */
2480
+ declare const externalSourceUnscoped: Lint;
2481
+ /**
1034
2482
  * Flags a query read that calls `.filter()` without first narrowing with
1035
2483
  * `.withIndex()` / `.withSearchIndex()`. Such a read loads *every* row of the
1036
2484
  * table and applies the predicate in memory — a full table scan that degrades
@@ -1044,6 +2492,25 @@ declare const emptyIndex: Lint;
1044
2492
  */
1045
2493
  declare const filterWithoutIndex: Lint;
1046
2494
  /**
2495
+ * Flags a `ctx.flags.boolean(key, default)` read on a security-shaped key whose
2496
+ * fail-open default selects the *permissive* branch.
2497
+ *
2498
+ * OpenFeature returns the caller-supplied default when the provider errors, so a
2499
+ * flag read is a security decision that fails to its default. When the key names
2500
+ * a protection (`enforce*`/`rls*`/`gate*`/`lockdown*`) and defaults `false`, or
2501
+ * names a permission/bypass (`allow*`/`permit*`/`bypass*`) and defaults `true`,
2502
+ * a flag-backend outage silently disables the protection or grants the
2503
+ * permission for every request.
2504
+ *
2505
+ * Runs only when the codegen feeder supplies flag-default evidence
2506
+ * (`context.flagSecurityDefaults`); a runtime caller flags nothing. Deliberately
2507
+ * narrow — matched on security-shaped key tokens with an unambiguous polarity
2508
+ * plus a boolean-literal default; keys whose polarity is indeterminate (a bare
2509
+ * `auth`/`admin`) or contradictory are skipped to keep the false-positive rate
2510
+ * low. One finding per read.
2511
+ */
2512
+ declare const flagGatesSecurityWithUnsafeDefault: Lint;
2513
+ /**
1047
2514
  * Flags a secret-shaped string literal checked into the lunora source.
1048
2515
  *
1049
2516
  * A live API key, access key, private key, or high-entropy token committed to the
@@ -1059,6 +2526,50 @@ declare const filterWithoutIndex: Lint;
1059
2526
  */
1060
2527
  declare const hardcodedSecret: Lint;
1061
2528
  /**
2529
+ * Flags an `httpAction`/`httpRoute` handler that performs a side effect
2530
+ * (`ctx.runMutation` / `ctx.runAction` / a `ctx.db` write) but never reads
2531
+ * `ctx.auth`.
2532
+ *
2533
+ * Unlike `query`/`mutation`/`action` procedures — which run under a resolved
2534
+ * identity and RLS — a raw HTTP handler is reached directly from the public
2535
+ * internet with no framework-supplied auth step. `HttpActionCtx` still exposes
2536
+ * `ctx.auth` (`getIdentity()` / `userId`), but nothing forces the handler to
2537
+ * consult it. A handler that mutates state or dispatches an action without ever
2538
+ * touching `ctx.auth` is an unauthenticated write endpoint: any anonymous caller
2539
+ * can drive the side effect, and the downstream `runMutation`/`runAction`/`db`
2540
+ * write runs with whatever ambient authority the handler carries — bypassing the
2541
+ * identity/RLS checks the rest of the app relies on. Distinct from
2542
+ * `admin_route_without_guard`, which covers Studio/admin-path routes.
2543
+ *
2544
+ * Runs only when the codegen feeder supplies HTTP-handler evidence
2545
+ * (`context.httpActionGuards`); a runtime caller flags nothing. The feeder only
2546
+ * records handlers that already perform a side effect and whose `ctx` binding was
2547
+ * statically resolvable (a named-function or wrapped handler is skipped,
2548
+ * fail-safe), so this lint just filters to those that never read `ctx.auth`. One
2549
+ * finding per handler.
2550
+ */
2551
+ declare const httpActionMissingAuthGuard: Lint;
2552
+ /**
2553
+ * Flags an `httpAction` handler that writes a response-header value derived from
2554
+ * raw request input (`request.headers`, `request.url`/query, `await
2555
+ * request.json()`) with no CR/LF sanitizer.
2556
+ *
2557
+ * A `Request`-derived string placed verbatim into a response header lets a caller
2558
+ * smuggle carriage-return/line-feed sequences (`\r\n`) into the response — injecting
2559
+ * additional headers (`Set-Cookie`, `Location`, CORS) or splitting the response body
2560
+ * (HTTP response splitting / header injection). Unlike `query`/`mutation` handlers,
2561
+ * a raw `httpAction` builds its own `Response`, so nothing forces the value through
2562
+ * the framework's `isSafeHeaderValue` CR/LF guard.
2563
+ *
2564
+ * Runs only when the codegen feeder supplies header-write evidence
2565
+ * (`context.httpHeaderWrites`); a runtime caller flags nothing. The feeder records
2566
+ * a site only when its value is request-tainted AND unguarded — a value routed
2567
+ * through `isSafeHeaderValue`, `encodeURIComponent`/`encodeURI`, a numeric coercion
2568
+ * (`Number`/`parseInt`/`parseFloat`), or `btoa` is treated as safe and never
2569
+ * recorded. One finding per unsafe header write.
2570
+ */
2571
+ declare const httpActionResponseHeaderInjection: Lint;
2572
+ /**
1062
2573
  * Flags a Hyperdrive `ctx.sql` access inside a `query(...)` or `mutation(...)`
1063
2574
  * handler body.
1064
2575
  *
@@ -1083,6 +2594,42 @@ declare const hardcodedSecret: Lint;
1083
2594
  */
1084
2595
  declare const hyperdriveOutsideAction: Lint;
1085
2596
  /**
2597
+ * Flags an authorization read of an identity claim that is **not** in the app's
2598
+ * declared `defineIdentity({ ... })` contract.
2599
+ *
2600
+ * `defineIdentity` is the trust boundary for `ctx.auth.identity`: the worker
2601
+ * validates a resolver's returned claims against the *declared* validators, but
2602
+ * — by design — forwards any **undeclared** claims through verbatim, unchecked.
2603
+ * So a policy predicate or authorize hook that reads `auth.identity.&lt;key>` for a
2604
+ * `&lt;key>` the contract never declares is trusting a value the runtime never
2605
+ * validated — a claim an attacker's token can carry with an arbitrary value.
2606
+ * Reading `userId` (always declared) or any declared claim is fine.
2607
+ *
2608
+ * Runs only when the codegen feeder supplies claim-read evidence
2609
+ * (`context.identityClaimReads`) — which it does only when a resolvable
2610
+ * `defineIdentity` contract exists — so an app with no typed identity contract,
2611
+ * or a runtime caller, flags nothing. One finding per undeclared read.
2612
+ */
2613
+ declare const identityUndeclaredClaimTrusted: Lint;
2614
+ /**
2615
+ * Flags a `buildImageDeliveryUrl({ key, … })` call (`@lunora/bindings/images`)
2616
+ * whose `key` is derived from the handler's `args` with no server-side scoping.
2617
+ *
2618
+ * `key` is the CDN transform's source image — an absolute URL, or an
2619
+ * origin-relative key under the account's own store — that `buildImageDeliveryUrl`
2620
+ * splices into the `/cdn-cgi/image/…` delivery URL. `ctx.images.transform`/`info`
2621
+ * take image *bytes*, never a URL, so they carry no equivalent risk and are not
2622
+ * flagged. An arg-derived `key` lets any caller point the CDN's on-the-fly
2623
+ * transform at an attacker-chosen origin (SSRF / open proxy against whatever
2624
+ * that origin trusts the CDN's egress IP for) or at an arbitrary key under the
2625
+ * account's own store.
2626
+ *
2627
+ * Runs only when the codegen feeder supplies image-delivery-URL evidence
2628
+ * (`context.imageDeliveryUrlAccesses`); a runtime caller flags nothing. One
2629
+ * finding per arg-derived, unscoped `key`.
2630
+ */
2631
+ declare const imagesUrlSourceFromUserInput: Lint;
2632
+ /**
1086
2633
  * A correctness lint with no splinter analogue — it exploits Lunora's static
1087
2634
  * edge: the schema is fully declared, so a typo'd index column is catchable at
1088
2635
  * codegen time rather than surfacing as a runtime error or a silently
@@ -1095,6 +2642,77 @@ declare const hyperdriveOutsideAction: Lint;
1095
2642
  */
1096
2643
  declare const indexReferencesUnknownField: Lint;
1097
2644
  /**
2645
+ * Flags a public procedure whose handler calls `ctx.db.insertManyUnsafe(...)`.
2646
+ *
2647
+ * `insertManyUnsafe` is the bulk-insert escape hatch: it writes rows straight to
2648
+ * storage, bypassing the per-row argument validators AND the schema's insert
2649
+ * triggers (which is where server-trusted columns, ownership stamping, and RLS
2650
+ * write checks live). That is acceptable for a seed script or an internal import
2651
+ * fed server-trusted rows — but on a `.public()` procedure the rows are shaped
2652
+ * from request input, so the bypass lets a caller write columns they should never
2653
+ * control and skip every trigger-enforced invariant. The fix is to use the
2654
+ * validated `ctx.db.insert(...)` path, or keep the unsafe bulk write in an
2655
+ * internal function fed only server-built rows.
2656
+ *
2657
+ * Runs only when the codegen feeder supplies procedure-protection evidence
2658
+ * (`context.procedureProtections`); a runtime caller flags nothing. One finding
2659
+ * per public procedure using the unsafe bulk insert.
2660
+ */
2661
+ declare const insertManyUnsafeUserData: Lint;
2662
+ /**
2663
+ * Flags a `ctx.kv` read/write whose namespace key is derived from the handler's
2664
+ * `args` with no server-side scoping — a namespace-level insecure direct object
2665
+ * reference (IDOR).
2666
+ *
2667
+ * Workers KV is a single flat namespace with no per-caller isolation. When a key
2668
+ * comes straight from request input (`ctx.kv.get(args.key)`, a template embedding
2669
+ * `args.*`, or a key built one hop earlier from `args`), any caller can hand in
2670
+ * another user's key and read, overwrite, or delete that user's entry. The fix is
2671
+ * to prefix every key with a server-trusted identity (`` `${ctx.auth.userId}:…` ``)
2672
+ * so a caller can only ever address their own entries — a key that references
2673
+ * `ctx` is treated as scoped and is not flagged. `list` is not a sink (it takes a
2674
+ * prefix, not a per-entry key).
2675
+ *
2676
+ * Runs only when the codegen feeder supplies KV key-access evidence
2677
+ * (`context.kvKeyAccesses`); a runtime caller flags nothing. One finding per
2678
+ * arg-derived, unscoped `ctx.kv` call.
2679
+ */
2680
+ declare const kvUnscopedUserKeyIdor: Lint;
2681
+ /**
2682
+ * Flags a `createInboundEmailHandler({...})` built without a `verify` hook.
2683
+ *
2684
+ * An inbound email handler receives mail from the public internet, where the
2685
+ * `from` address is trivially spoofable and the DKIM/SPF/DMARC verdicts are only
2686
+ * meaningful if the handler actually checks them. The `verify` hook is where that
2687
+ * check belongs. Omit it and the handler trusts every message it receives — and
2688
+ * when the handler dispatches into a Lunora function (which runs under the admin
2689
+ * bearer, with RLS disabled), a forged sender can drive privileged writes: a
2690
+ * confused-deputy escalation reachable by anyone who can send an email.
2691
+ *
2692
+ * Runs only when the codegen feeder supplies config-call evidence
2693
+ * (`context.configCalls`); a runtime caller flags nothing. Skips calls whose
2694
+ * config wasn't a static object literal. One finding per unverified handler.
2695
+ */
2696
+ declare const mailInboundDispatchWithoutVerify: Lint;
2697
+ /**
2698
+ * Flags a `ctx.mail`/`ctx.email` `send`/`queue` call whose recipient field
2699
+ * (to/cc/bcc) is derived from the handler's `args` with no server-side
2700
+ * scoping.
2701
+ *
2702
+ * A recipient derived straight from request input turns the deployment into
2703
+ * an open relay / spam amplifier — any caller can direct mail to an arbitrary
2704
+ * address just by supplying it as an argument. The fix is to derive
2705
+ * recipients from server-trusted state (e.g. the authenticated user's own
2706
+ * record via `ctx.auth`), never straight from `args`; if user-chosen
2707
+ * recipients are a genuine product requirement, gate the path behind
2708
+ * authentication and rate limiting rather than leaving it open to any caller.
2709
+ *
2710
+ * Runs only when the codegen feeder supplies mail recipient-access evidence
2711
+ * (`context.mailRecipientAccesses`); a runtime caller flags nothing. One
2712
+ * finding per offending `ctx.mail`/`ctx.email` call — not per recipient field.
2713
+ */
2714
+ declare const mailRecipientFromRequestInput: Lint;
2715
+ /**
1098
2716
  * Flags a public procedure that reads a table for which at least one other
1099
2717
  * procedure declares a column mask (evidence the developer decided that table
1100
2718
  * carries sensitive columns), but whose own builder chain does NOT include
@@ -1133,6 +2751,69 @@ declare const indexReferencesUnknownField: Lint;
1133
2751
  */
1134
2752
  declare const maskUncoveredPiiColumn: Lint;
1135
2753
  /**
2754
+ * Flags a `mask(policies)` column whose strategy is the literal `"hash"` and
2755
+ * whose column name matches a PII heuristic (`email`, `ssn`, `phone`, …).
2756
+ *
2757
+ * Lunora's `"hash"` mask strategy is an unsalted 32-bit FNV-1a digest — a
2758
+ * stable pseudonym for grouping/joining, deliberately **not** a confidentiality
2759
+ * control. Its narrow (~2^32) output space makes it brute-force-recoverable by
2760
+ * the very caller it is meant to mask from, and identical inputs always
2761
+ * produce identical tokens, so it also leaks cross-row/cross-tenant equality.
2762
+ * Applying it to a PII column reads as protection but isn't; `"redact"` (drop
2763
+ * to `null`) is the strategy that actually hides the value.
2764
+ *
2765
+ * Runs only when the codegen feeder supplies strategy evidence
2766
+ * (`context.maskStrategies`); a runtime caller with no evidence flags nothing.
2767
+ * A `MaskFn` (custom, non-literal) strategy carries no static signal and is
2768
+ * never recorded by the feeder, so it never reaches this lint either.
2769
+ */
2770
+ declare const maskWeakHashStrategyOnPii: Lint;
2771
+ /**
2772
+ * Flags a public read that hydrates a masked table's rows in the clear through a
2773
+ * `with` relation.
2774
+ *
2775
+ * Column masking (`.use(mask(...))`) is applied per-procedure to the *top-level*
2776
+ * rows of the table named in a read. It does **not** descend into relations
2777
+ * hydrated via `with` — `ctx.db.posts.findMany({ with: { author: true } })`
2778
+ * returns each `author` fully unmasked even when the `users` table is masked
2779
+ * elsewhere. So a table whose columns you carefully mask on its own reads is
2780
+ * still served in the clear whenever an unprotected parent read pulls it in as a
2781
+ * relation.
2782
+ *
2783
+ * INFO, near-zero false positives by construction: it fires only when all of
2784
+ * (1) the enclosing read is public, (2) the read declares `with: { &lt;rel> }`,
2785
+ * (3) `&lt;rel>` resolves through the schema to a real target table, and (4) that
2786
+ * target table actually has masked columns (per the discovered mask evidence).
2787
+ * Absent any mask usage the lint is a no-op. Runs only when the codegen feeder
2788
+ * supplies `context.relationLoads`; a runtime caller flags nothing. One finding
2789
+ * per `(read, masked relation)` pair.
2790
+ */
2791
+ declare const maskedRelationLeakViaWith: Lint;
2792
+ /**
2793
+ * Flags a custom mutator whose authoritative `server` impl writes a row with
2794
+ * `ctx.db.replace(id, document)` — a whole-document overwrite.
2795
+ *
2796
+ * The local-first sync engine serializes mutators in the shard DO, so two
2797
+ * mutators that touch the *same row* but *different columns* both run to
2798
+ * completion — but only if each writes its own column. A `replace` overwrites
2799
+ * the entire row from the document the mutator assembled, so a concurrent edit
2800
+ * to another column (committed between this mutator's read and its write, or
2801
+ * carried as a pending optimistic overlay on a client) is silently clobbered:
2802
+ * the kind of "two offline edits to different fields fight each other" data loss
2803
+ * a column-level merge avoids. `ctx.db.patch(id, { onlyTheChangedField })`
2804
+ * merges at the column level instead, so independent field edits coexist.
2805
+ *
2806
+ * `WARN`, not `ERROR`: `replace` is legitimate when the mutator genuinely owns
2807
+ * the whole row (a full-form save, a state-machine transition that rewrites
2808
+ * every field). The lint just surfaces the column-clobber risk so a developer
2809
+ * reaches for `patch` by default on a synced table.
2810
+ *
2811
+ * **Evidence supply**: runs only when the codegen feeder supplies
2812
+ * `context.mutatorWrites` (each a `replace` call lifted from a mutator's inline
2813
+ * `server` body); absent for runtime callers, where the lint finds nothing.
2814
+ */
2815
+ declare const mutatorFullRowReplace: Lint;
2816
+ /**
1136
2817
  * Flags a non-deterministic API call inside a `query(...)` or `mutation(...)`
1137
2818
  * handler body.
1138
2819
  *
@@ -1153,6 +2834,133 @@ declare const maskUncoveredPiiColumn: Lint;
1153
2834
  */
1154
2835
  declare const nondeterministicQueryMutation: Lint;
1155
2836
  /**
2837
+ * Flags a public `query`/`mutation` whose handler gates a `ctx.db.get`/`patch`/`delete`
2838
+ * on a null-checked `ctx.db.normalizeId(table, id)` result, with no intervening
2839
+ * ownership predicate and no RLS coverage.
2840
+ *
2841
+ * `normalizeId` performs pure structural validation — it checks that a string is
2842
+ * shaped like a valid id for `table` and returns the branded id, but it **never reads
2843
+ * the database**. A non-null result therefore proves only that the id is well-formed,
2844
+ * never that the row exists or that the caller owns it. Treating "normalizeId returned
2845
+ * non-null" as authorization is an IDOR: any caller who supplies a syntactically valid
2846
+ * id of another user's row reaches it. This is an INFO-level nudge — the handler may be
2847
+ * intentionally public — it flags a place where the shape check is doing load-bearing
2848
+ * work it can't actually do.
2849
+ *
2850
+ * **Negative-proof gates** (bias toward silence — a false negative is cheaper than a
2851
+ * false positive that trains users to ignore the advisor): the feeder records a row
2852
+ * only when a null-gated normalized id reaches an id-first sink, and this lint
2853
+ * additionally requires (1) `visibility === "public"` (an internal procedure trusts
2854
+ * its server caller for authorization), (2) no `.use(rls(...))` on the builder chain,
2855
+ * (3) no ownership/identity mention anywhere in the handler (`mentionsOwnership` — any
2856
+ * `ctx.auth`/`ctx.identity` read or ownership-named identifier suppresses), and (4) the
2857
+ * table not covered by schema-required RLS. A handler that compares the loaded row's
2858
+ * `userId` to `ctx.auth.userId`, or a schema in `.rls("required")` mode, is never
2859
+ * flagged.
2860
+ *
2861
+ * Runs only when the codegen feeder supplies `context.normalizeIdAuthorizations`; a
2862
+ * runtime caller flags nothing.
2863
+ */
2864
+ declare const normalizeIdUsedAsAuthorization: Lint;
2865
+ /**
2866
+ * Nudges a `.public()` `query` whose handler returns raw table rows — with no
2867
+ * `.output(...)` projection and no `.use(mask(...))` — when that table carries
2868
+ * PII-named columns (`email`, `phone`, `ssn`, …).
2869
+ *
2870
+ * A public query is reachable by any client. Returning a table row verbatim ships
2871
+ * every column it holds — including PII the caller never needed — and silently
2872
+ * widens the exposed surface every time a column is added to the table later. An
2873
+ * explicit `.output(v.object({ … }))` projection (or a `.use(mask(...))` policy)
2874
+ * makes the exposed shape intentional and stops the next-added column from leaking
2875
+ * by default. This is an INFO-level nudge, not a defect: the query may be perfectly
2876
+ * fine — it flags a place worth a deliberate projection decision.
2877
+ *
2878
+ * **Low-FP gates**: the feeder records a row only when the handler returns the raw
2879
+ * read result itself (a hand-built object / array / `.map(...)` projection is not
2880
+ * recorded), and this lint additionally requires (1) `visibility === "public"`,
2881
+ * (2) no `.output(...)` and no `.use(mask(...))` on the builder chain, and (3) the
2882
+ * returned table to declare at least one PII-named column. A public query returning
2883
+ * a non-PII lookup table (`emojis`, `countries`) is never flagged.
2884
+ *
2885
+ * Runs only when the codegen feeder supplies `context.rawRowReturns`; a runtime
2886
+ * caller flags nothing.
2887
+ */
2888
+ declare const outputProjectionMissingOnPublicRead: Lint;
2889
+ /**
2890
+ * Flags a `ctx.db` write (`insert` / `replace` / `patch` / `insertManyUnsafe`)
2891
+ * that sets an ownership / identity column — `userId`, `ownerId`, `tenantId`, and
2892
+ * the like — from the handler's `args` instead of the server-trusted identity.
2893
+ *
2894
+ * The ownership column decides *who a row belongs to*. When its value comes from
2895
+ * request input (`ctx.db.insert("posts", { userId: args.userId })`), any caller
2896
+ * can claim to be anyone: pass a different `userId` / `tenantId` and the write
2897
+ * lands a row owned by another user or tenant — the classic act-as-any-user /
2898
+ * cross-tenant IDOR. The fix is to stamp the ownership column from `ctx.auth` /
2899
+ * `ctx.identity` on the server and never read it from `args`. A column set from
2900
+ * `ctx.*`, or to a fixed literal, is correct and not flagged.
2901
+ *
2902
+ * Runs only when the codegen feeder supplies owner-write evidence
2903
+ * (`context.ownerFieldWrites`); a runtime caller flags nothing. One finding per
2904
+ * offending identity-column write.
2905
+ */
2906
+ declare const ownerFieldFromArgsNotAuth: Lint;
2907
+ /**
2908
+ * Flags a `createPayment({...})` constructed without an `authorize` callback.
2909
+ *
2910
+ * The `authorize(referenceId)` hook is `@lunora/payment`'s access-control gate: it
2911
+ * runs before every charge/refund/subscription operation and decides whether the
2912
+ * current caller may act on that payment reference. Omit it and the guard is
2913
+ * effectively open — any caller who can reach a reference id (often a
2914
+ * client-supplied string) can drive money movement against someone else's
2915
+ * payment, a broken-access-control (IDOR) escalation on the most sensitive
2916
+ * surface an app has.
2917
+ *
2918
+ * The context accessor `ctx.payments` is authorization-scoped by default; this
2919
+ * lint targets the direct `createPayment(...)` factory, where the gate is
2920
+ * opt-in. Runs only when the codegen feeder supplies config-call evidence
2921
+ * (`context.configCalls`); a runtime caller flags nothing. Skips calls whose
2922
+ * config wasn't a static object literal (the key may be set on a config built
2923
+ * elsewhere). One finding per unguarded call.
2924
+ */
2925
+ declare const paymentCreateWithoutAuthorize: Lint;
2926
+ /**
2927
+ * Flags a payment webhook adapter (`createStripeAdapter` / `createPolarAdapter` /
2928
+ * `createAutumnAdapter` / `createDodoPaymentsAdapter`) configured with an
2929
+ * implausibly wide `webhookToleranceSeconds` replay window.
2930
+ *
2931
+ * The adapters reject a webhook whose signed timestamp is more than
2932
+ * `webhookToleranceSeconds` from now, so a captured-then-replayed signed payload
2933
+ * is refused once it ages past the window. The default (300s) is the recommended
2934
+ * clock-skew allowance; widening it to hours or days keeps stale, replayable
2935
+ * signed events valid long after capture, defeating the timestamp check and
2936
+ * re-opening the replay window the signature scheme exists to close.
2937
+ *
2938
+ * Runs only when the codegen feeder supplies adapter evidence
2939
+ * (`context.paymentWebhooks`); a runtime caller flags nothing. Fires only on a
2940
+ * statically-known numeric `webhookToleranceSeconds` literal above the ceiling —
2941
+ * a computed or env-sourced value is not evaluated, to keep the false-positive
2942
+ * rate low. One finding per matching adapter.
2943
+ */
2944
+ declare const paymentWebhookWideTolerance: Lint;
2945
+ /**
2946
+ * Flags a plaintext secret committed to `wrangler.jsonc`'s `vars` block.
2947
+ *
2948
+ * `vars` are **plaintext environment variables**: Wrangler bakes them into the
2949
+ * deployed Worker's bundle in cleartext, and `wrangler.jsonc` is checked into
2950
+ * source control — so a real API key, access key, private key, or token placed
2951
+ * there leaks two ways at once (every reader of the repo and the deployed bundle)
2952
+ * and rotating it means editing tracked config plus a redeploy. Secrets belong in
2953
+ * a Secrets Store binding (`ctx.secrets.get(name)`) or `wrangler secret put`, never
2954
+ * `vars`. The evidence comes from `@lunora/config`, which reads `wrangler.jsonc`
2955
+ * and applies the same secret-shape heuristics as `hardcoded_secret`, plus a
2956
+ * secret-suggestive key-name rule (`*_KEY` / `*_SECRET` / `*_TOKEN` / `*_PASSWORD`
2957
+ * / `*_DSN`), while skipping placeholders and public/publishable keys.
2958
+ *
2959
+ * Runs only when the config feeder supplies wrangler-variable evidence
2960
+ * (`context.wranglerVariables`); a runtime caller flags nothing. One finding per var.
2961
+ */
2962
+ declare const plaintextSecretInWranglerVariables: Lint;
2963
+ /**
1156
2964
  * Flags an RLS policy whose `table` names a table that does not exist in the
1157
2965
  * schema.
1158
2966
  *
@@ -1176,6 +2984,50 @@ declare const nondeterministicQueryMutation: Lint;
1176
2984
  */
1177
2985
  declare const policyReferencesUnknownTable: Lint;
1178
2986
  /**
2987
+ * Flags a `ctx.run`/`context.run` back into a Lunora function from inside a
2988
+ * `defineQueue` push handler or a `defineWorkflow` handler, when the dispatch's
2989
+ * args reference the handler's untrusted payload (`context.params` for a
2990
+ * workflow, a `for (… of batch.messages)` body for a queue) **and** the target
2991
+ * enforces row-level security.
2992
+ *
2993
+ * A queue/workflow handler runs under the deployment's **system identity** with
2994
+ * end-user RLS disabled — that is by design, so the handler can touch any row.
2995
+ * But the payload it receives is attacker-influenced: a queue body is whatever
2996
+ * was enqueued (often straight from a public mutation's `args`), and a
2997
+ * workflow's `params` are set by the `.create({ params })` caller. Forwarding
2998
+ * that payload into a function whose own protection is a *row policy* is a
2999
+ * confused-deputy: the policy that would have rejected the caller's request is
3000
+ * skipped because the handler, not the user, is now the principal. Any field
3001
+ * the payload controls that the target's RLS keys on (owner id, tenant id, row
3002
+ * id) becomes an act-as-any-user / cross-tenant write.
3003
+ *
3004
+ * The lint is deliberately narrow to stay false-positive-free: it fires **only**
3005
+ * when the resolved target (`api.&lt;file>.&lt;export>`) is found in the
3006
+ * RLS-procedure evidence with `usesRls: true`. A dispatch into a function that
3007
+ * does its own arg validation and carries no row policy (the common, correct
3008
+ * case — e.g. a welcome-message mutation with no `rls`) is not flagged. Runs
3009
+ * only when the codegen feeder supplies dispatch evidence
3010
+ * (`context.privilegedDispatches`); a runtime caller flags nothing.
3011
+ */
3012
+ declare const privilegedDispatchUnvalidatedPayload: Lint;
3013
+ /**
3014
+ * Flags a public procedure whose handler fans work out to a privileged,
3015
+ * cost-bearing dispatch surface without a rate-limit guard.
3016
+ *
3017
+ * `ctx.scheduler.runAfter` / `runAt`, a `ctx.queues.&lt;name>` producer send, and
3018
+ * `ctx.workflows.&lt;name>.create` all enqueue work that runs later under the
3019
+ * system identity (RLS disabled) and bills against the account. Triggered from a
3020
+ * `.public()` procedure with no rate limit, an anonymous caller can drive that
3021
+ * dispatch in a loop — a cost-amplification / denial-of-wallet vector, and a way
3022
+ * to flood a privileged async surface. The fix is to gate the public entry point
3023
+ * with a rate limit (or make it internal and trigger it from a guarded path).
3024
+ *
3025
+ * Runs only when the codegen feeder supplies procedure-protection evidence
3026
+ * (`context.procedureProtections`); a runtime caller flags nothing. One finding
3027
+ * per unguarded public fan-out procedure.
3028
+ */
3029
+ declare const privilegedFanoutFromPublicProcedure: Lint;
3030
+ /**
1179
3031
  * Flags a `v.any()` argument on a public procedure.
1180
3032
  *
1181
3033
  * `v.any()` disables validation: the field accepts arbitrary, untyped,
@@ -1206,10 +3058,55 @@ declare const publicArgumentUsesAny: Lint;
1206
3058
  */
1207
3059
  declare const publicMutationWithoutRatelimit: Lint;
1208
3060
  /**
3061
+ * Flags a `.public()` table that carries ownership/tenancy- or PII-named
3062
+ * columns, on a schema that requires RLS (`.rls("required")`).
3063
+ *
3064
+ * `.public()`'s name is misleading: it does NOT mean "this table holds public
3065
+ * data" — it means the OPPOSITE of that from an enforcement standpoint. It opts
3066
+ * one table OUT of the schema-wide `.rls("required")` enforcement, so its
3067
+ * `ctx.db` write path is never denied for missing RLS coverage. A table named
3068
+ * `.public()` that also carries `userId` / `email` / `ssn`-shaped columns reads
3069
+ * as "safe to expose" but is actually "exempt from the row-security guard" —
3070
+ * exactly the confusion the method name invites.
3071
+ *
3072
+ * **Low-FP gate**: only flagged when (1) the schema opted into
3073
+ * `.rls("required")` at all (`.public()` is a documented no-op otherwise — see
3074
+ * {@link https://lunora.sh}'s `.public()` docs), and (2) the table's declared
3075
+ * columns match the ownership/PII heuristic ({@link ownershipOrPiiColumns}). A
3076
+ * genuinely public lookup table (e.g. `emojis`, `countries`) with no such
3077
+ * columns is not flagged.
3078
+ *
3079
+ * **Pure schema evidence**: reads only `context.schema` — both `isPublic` and
3080
+ * `rlsMode` are forwarded by the runtime `fromServerSchema` and the codegen
3081
+ * `toAdvisorSchema` paths, so this lint runs identically from a live shard and
3082
+ * from codegen, with no feeder required.
3083
+ */
3084
+ declare const publicTableRlsOptoutConfusion: Lint;
3085
+ /**
3086
+ * Flags a declared queue — push or pull — that has no dead-letter queue.
3087
+ *
3088
+ * A Cloudflare Queues consumer retries a failing message up to `maxRetries`
3089
+ * times (default 3 — roughly four total delivery attempts); once that budget is
3090
+ * exhausted, a message with no `deadLetterQueue` is **deleted permanently** with
3091
+ * no record an operator can inspect. Routing exhausted messages to a DLQ turns
3092
+ * silent data loss into a backlog you can inspect, alert on, and replay. Hence
3093
+ * `WARN`/`INTERNAL`: an operator-facing reliability nudge, not a hard error — a
3094
+ * genuinely fire-and-forget queue may accept the loss.
3095
+ *
3096
+ * A queue that is itself some other queue's `deadLetterQueue` target is skipped:
3097
+ * a DLQ is a terminal sink and requiring it to have its own DLQ would recurse
3098
+ * forever (and mis-flag the best-practice scaffold, which pairs a queue with a
3099
+ * dedicated DLQ consumer).
3100
+ *
3101
+ * Only runs when the declaration feeder supplied evidence (`context.queues`
3102
+ * present); a runtime caller flags nothing.
3103
+ */
3104
+ declare const queueWithoutDlq: Lint;
3105
+ /**
1209
3106
  * Flags an R2 SQL `ctx.r2sql` access inside a `query(...)` or `mutation(...)`
1210
3107
  * handler body.
1211
3108
  *
1212
- * R2 SQL (`@lunora/r2sql`) queries Apache Iceberg tables over an **external**
3109
+ * R2 SQL (`@lunora/bindings/r2sql`) queries Apache Iceberg tables over an **external**
1213
3110
  * REST endpoint Lunora does not own — there is no Workers binding, every query
1214
3111
  * is an HTTPS round-trip. A `ctx.r2sql` query is therefore non-deterministic
1215
3112
  * (exactly like `fetch`), which breaks the determinism the coordinator relies on
@@ -1231,6 +3128,62 @@ declare const publicMutationWithoutRatelimit: Lint;
1231
3128
  */
1232
3129
  declare const r2sqlOutsideAction: Lint;
1233
3130
  /**
3131
+ * Flags a `new RateLimiter({...})` constructed without an explicit `store`.
3132
+ *
3133
+ * The default store is per-instance in-memory. On Cloudflare Workers each request
3134
+ * can land on a different isolate, so an in-memory counter never sums across
3135
+ * them: the limiter silently under-counts and, under real traffic, is close to a
3136
+ * no-op. A rate limit that doesn't actually limit gives a false sense of
3137
+ * protection against brute-force, enumeration, and cost-abuse — exactly the
3138
+ * attacks it was added to stop. The fix is a shared, durable store (a Durable
3139
+ * Object or KV-backed one) so every isolate reads and writes the same bucket.
3140
+ *
3141
+ * Runs only when the codegen feeder supplies config-call evidence
3142
+ * (`context.configCalls`); a runtime caller flags nothing. Skips calls whose
3143
+ * config wasn't a static object literal. One finding per limiter.
3144
+ */
3145
+ declare const ratelimitDefaultMemoryStore: Lint;
3146
+ /**
3147
+ * Flags a `rateLimit`/`dbRateLimit` middleware call (`@lunora/ratelimit`) whose
3148
+ * `key` selector is derived from the handler's `args` with no server-side
3149
+ * scoping.
3150
+ *
3151
+ * The middleware's `key` is `(ctx) => string | undefined` — a sub-key that
3152
+ * isolates the limit per caller. When the selector reads straight from `args`
3153
+ * (an email, a client-supplied id, …) instead of a server-trusted identity
3154
+ * (`ctx.auth.userId`) or the server-trusted `ctx.ip`, an attacker can rotate the
3155
+ * key on every request and never share a bucket with themselves, defeating the
3156
+ * limit entirely. A selector with no `args` reference at all — a fixed/global
3157
+ * bucket, including simply omitting `key` — is a *different*, fuzzier problem
3158
+ * (one caller can still exhaust a shared global bucket for everyone) and is
3159
+ * deliberately **not** flagged here, to keep this lint's false-positive rate
3160
+ * low: a global bucket is a legitimate choice for some limits (e.g. a
3161
+ * deployment-wide cost cap), so its absence alone is not a reliable signal.
3162
+ *
3163
+ * Runs only when the codegen feeder supplies rate-limit key-selector evidence
3164
+ * (`context.ratelimitKeySelectors`); a runtime caller flags nothing. One finding
3165
+ * per arg-derived, unscoped selector.
3166
+ */
3167
+ declare const ratelimitKeySpoofableOrGlobal: Lint;
3168
+ /**
3169
+ * Flags a `rateLimit`/`dbRateLimit`/`verifyTurnstileMiddleware` guard configured
3170
+ * `failOpen: true` on an auth/payment-sensitive procedure.
3171
+ *
3172
+ * `@lunora/ratelimit`'s `rateLimit`/`dbRateLimit` and `@lunora/auth`'s
3173
+ * `verifyTurnstileMiddleware` fail **closed** by default — a store outage or a
3174
+ * failed Turnstile siteverify rejects the request (503). Passing `failOpen: true`
3175
+ * inverts that: the middleware swallows the failure and admits the request. On a
3176
+ * sign-in / account-creation / password-reset / OTP / payment endpoint that turns
3177
+ * a transient limiter outage into an unthrottled brute-force / abuse window.
3178
+ *
3179
+ * Runs only when the codegen feeder supplies fail-open-guard evidence
3180
+ * (`context.failOpenGuards`); a runtime caller flags nothing. Deliberately narrow
3181
+ * — fires only when the options literal provably set `failOpen: true` AND the
3182
+ * guarded procedure's export name or rate-limit `name` matches an auth/payment
3183
+ * token, keeping the false-positive rate low. One finding per guard.
3184
+ */
3185
+ declare const ratelimitMiddlewareFailOpen: Lint;
3186
+ /**
1234
3187
  * A correctness lint covering the columns a relation wires together: the FK
1235
3188
  * `field` and the `references` column must each exist on their respective
1236
3189
  * tables, or the join can never resolve. Caught here at codegen time rather
@@ -1285,6 +3238,70 @@ declare const relationReferencesUnknownTable: Lint;
1285
3238
  */
1286
3239
  declare const rlsUncoveredTable: Lint;
1287
3240
  /**
3241
+ * Flags a replication shape whose `table` is a `.global()` table.
3242
+ *
3243
+ * Poke-live replication is a per-shard-DO property: the shard owns its SQLite
3244
+ * and a monotonic `__cdc_log`, so a write produces an ordered op the DO pokes to
3245
+ * every subscriber at the next flush. A `.global()` table lives outside the
3246
+ * shard DO's SQLite op-log (in a global backend — D1, or Hyperdrive-fronted
3247
+ * Postgres/MySQL) — so a shape over a global table cannot be poke-live. It is
3248
+ * served through the cross-shard tier: **coordinator/poll-refreshed, latency-
3249
+ * tiered**, not live. That is a real and supported tier (it is the recommended
3250
+ * answer for cross-shard reads — denormalize, or move the joined table to
3251
+ * `.global()` and read through the global backend), but its freshness semantics
3252
+ * differ from a sharded shape's, so the boundary is surfaced rather than hidden.
3253
+ *
3254
+ * `WARN`, not `ERROR`: a global-table shape is a legitimate design once you
3255
+ * accept the poll-refresh latency; the lint just makes the tier explicit so a
3256
+ * developer does not assume poke-live freshness.
3257
+ *
3258
+ * **Evidence supply**: runs only when the codegen feeder supplies
3259
+ * `context.shapes`; the table's tier comes from the schema's `shardKind`. A
3260
+ * shape whose table is unknown (caught by `shape_unknown_table`) or whose tier
3261
+ * the feeder didn't supply is skipped.
3262
+ */
3263
+ declare const shapeTargetsGlobalTable: Lint;
3264
+ /**
3265
+ * Flags a replication shape whose `table` names a table that does not exist in
3266
+ * the schema.
3267
+ *
3268
+ * `defineShape({ table: "messages", … })` binds a shape to a table by a plain
3269
+ * string. A live `subscribeShape("…")` resolves that shape server-side and runs
3270
+ * its membership query against the named table — so a typo, a stale name after a
3271
+ * rename, or a copy-paste mistake produces a shape that can never resolve a
3272
+ * rowset: the subscription seeds empty and then errors at the first flush
3273
+ * (`no such table`). This is a definite, build-time-detectable break, so it is
3274
+ * an `ERROR` — surfaced before the broken shape ever ships.
3275
+ *
3276
+ * **Evidence supply**: runs only when the codegen feeder supplies
3277
+ * `context.shapes`. A shape whose `table` wasn't a static string literal (no
3278
+ * resolvable name) is skipped rather than guessed at, so the lint under-reports
3279
+ * rather than raising false alarms.
3280
+ */
3281
+ declare const shapeUnknownTable: Lint;
3282
+ /**
3283
+ * Flags a public read that resurfaces soft-deleted rows via `includeDeleted` —
3284
+ * either hardcoded `true` or wired from the handler's `args`.
3285
+ *
3286
+ * A `.softDelete()` table hides deleted rows from list reads (`findMany` /
3287
+ * `findFirst`) unless the call passes `includeDeleted: true`. That opt-out is a
3288
+ * deliberate, privileged escape hatch (an admin trash view, a restore flow). On
3289
+ * a `.public()` read it becomes a leak: `includeDeleted: true` returns
3290
+ * soft-deleted rows to *every* caller, and `includeDeleted: args.showDeleted`
3291
+ * lets *any* caller flip the toggle per request — either way the "deleted" rows
3292
+ * a user believes are gone (and that your UI hides) are served straight back.
3293
+ *
3294
+ * INFO, near-zero false positives by construction: it fires only when all of
3295
+ * (1) the enclosing procedure is public, (2) the read's target is a schema table
3296
+ * that actually declares `.softDelete()`, and (3) `includeDeleted` is a hardcoded
3297
+ * `true` or arg-derived. A literal `false`, or an `includeDeleted` gated by a
3298
+ * server-trusted `ctx.*` value, is never recorded by the feeder, so a correct
3299
+ * admin-gated read is not flagged. Runs only when the codegen feeder supplies
3300
+ * `context.softDeleteReads`; a runtime caller flags nothing. One finding per
3301
+ * matching read.
3302
+ */
3303
+ declare const softDeleteIncludeDeletedFromArgs: Lint;
3304
+ /**
1288
3305
  * Flags a `ctx.sql` tagged-template that splices an unparameterized
1289
3306
  * string-building expression into the query.
1290
3307
  *
@@ -1301,6 +3318,107 @@ declare const rlsUncoveredTable: Lint;
1301
3318
  */
1302
3319
  declare const sqlInjectionRisk: Lint;
1303
3320
  /**
3321
+ * Flags a `ctx.storage.generateUploadUrl(key, …)` call whose options argument
3322
+ * omits `contentType`.
3323
+ *
3324
+ * `generateUploadUrl` mints a signed `PUT` URL the *client* uploads directly
3325
+ * to R2, bypassing `upload()`/`store()` entirely — including their
3326
+ * `allowedContentTypes`/`maxSize` guards, which this alias never sees. The one
3327
+ * guard `generateUploadUrl` itself offers is `contentType`: passing it pins
3328
+ * the `Content-Type` into the signature, so the signed URL only authorizes a
3329
+ * PUT with exactly that content-type. Omit it and the minted URL accepts any
3330
+ * content-type/size the client chooses, entirely unchecked server-side.
3331
+ *
3332
+ * Runs only when the codegen feeder supplies storage-upload evidence
3333
+ * (`context.storageUploads`); a runtime caller flags nothing. Skips calls
3334
+ * whose options argument wasn't statically analyzable (a variable, call
3335
+ * result, or a spread) — the key may be set on an object built elsewhere. One
3336
+ * finding per unpinned call.
3337
+ */
3338
+ declare const storageGenerateUploadUrlNoContentTypePin: Lint;
3339
+ /**
3340
+ * Flags a `ctx.storage.&lt;bucket>.&lt;method>(key, …)` whose R2 object key is derived
3341
+ * from the handler's `args` with no server-side scoping — an object-level IDOR.
3342
+ *
3343
+ * The bucket read/write/URL/delete methods (`get`, `put`, `delete`, `download`,
3344
+ * `store`, `getSignedUrl`, …) take the object key as their first argument. When
3345
+ * that key comes straight from request input (`ctx.storage.docs.get(args.key)`, or
3346
+ * a key built one hop earlier from `args`), any caller can name any object — reading,
3347
+ * overwriting, or deleting another user's file. The fix is to prefix the key with a
3348
+ * server-trusted identity (`` `${ctx.auth.userId}/…` ``) or to resolve the object
3349
+ * through a record the caller is known to own; a key that references `ctx` is treated
3350
+ * as scoped and is not flagged.
3351
+ *
3352
+ * Runs only when the codegen feeder supplies storage-key evidence
3353
+ * (`context.storageKeyAccesses`); a runtime caller flags nothing. One finding per
3354
+ * offending call.
3355
+ */
3356
+ declare const storageKeyFromUserArgs: Lint;
3357
+ /**
3358
+ * Flags a `ctx.storage.getPresignedUrl(...)` call, or a `getPresignedUrl`/
3359
+ * `getSignedUrl` call whose `expiresInSeconds` sits near the shared 7-day
3360
+ * signing ceiling.
3361
+ *
3362
+ * `getPresignedUrl` mints a native S3 SigV4 URL that resolves directly
3363
+ * against R2's S3 endpoint — the holder reaches the object straight off R2,
3364
+ * **bypassing the Worker entirely**, so any auth/RLS/rate-limit gate the app
3365
+ * enforces in its own handlers never runs for that request. That's the right
3366
+ * trade for genuinely public or bulk content where the app has no per-request
3367
+ * gating to apply; it's the wrong choice for private, per-user, or
3368
+ * policy-gated content, where `getSignedUrl` (worker-signed, resolves back
3369
+ * through the app) is the fit. Separately, either signer minting a long TTL
3370
+ * near the shared 7-day ceiling hands out a bearer credential that stays
3371
+ * valid almost as long as the platform allows — a leaked link (referrer,
3372
+ * logs, browser history) then grants access for nearly a week.
3373
+ *
3374
+ * Runs only when the codegen feeder supplies storage-upload evidence
3375
+ * (`context.storageUploads`); a runtime caller flags nothing. The
3376
+ * near-ceiling check only fires on a statically-known numeric
3377
+ * `expiresInSeconds` literal — a variable or computed expression is not
3378
+ * evaluated, to keep the false-positive rate low. One finding per matching
3379
+ * call.
3380
+ */
3381
+ declare const storagePresignedUrlForPrivateContent: Lint;
3382
+ /**
3383
+ * Flags a `ctx.storage.upload`/`store` call whose options argument omits
3384
+ * `allowedContentTypes`.
3385
+ *
3386
+ * `@lunora/storage`'s `upload`/`store` accept an `allowedContentTypes`
3387
+ * allowlist that, when set, rejects a mismatched (or missing) `contentType` —
3388
+ * the control that stops an uploader from storing `text/html` or
3389
+ * `image/svg+xml` and having it served back from `publicBaseUrl`, a classic
3390
+ * stored-XSS path against your own origin. Omitting the option leaves any
3391
+ * content-type acceptable. `generateUploadUrl`'s signed PUT has no
3392
+ * `allowedContentTypes` option at all (it bypasses `upload()`'s guards
3393
+ * entirely) — that gap is `storage_generate_upload_url_no_content_type_pin`'s
3394
+ * concern, not this lint's.
3395
+ *
3396
+ * Runs only when the codegen feeder supplies storage-upload evidence
3397
+ * (`context.storageUploads`); a runtime caller flags nothing. Skips calls
3398
+ * whose options argument wasn't statically analyzable (a variable, call
3399
+ * result, or a spread) — the key may be set on an object built elsewhere. One
3400
+ * finding per unguarded call.
3401
+ */
3402
+ declare const storageUploadWithoutContentTypeAllowlist: Lint;
3403
+ /**
3404
+ * Flags a `ctx.storage.upload`/`store` call whose options argument omits
3405
+ * `maxSize`.
3406
+ *
3407
+ * `@lunora/storage`'s `upload`/`store` accept a `maxSize` byte ceiling that
3408
+ * rejects an oversized `ArrayBuffer`/`Blob` up front, and pipes a
3409
+ * `ReadableStream` through a counting `TransformStream` that aborts once the
3410
+ * limit is exceeded — without it, a caller can push an unbounded body through
3411
+ * the Worker straight into R2, exhausting storage/billing (and, for a
3412
+ * streamed body, worker CPU/time) with no cap.
3413
+ *
3414
+ * Runs only when the codegen feeder supplies storage-upload evidence
3415
+ * (`context.storageUploads`); a runtime caller flags nothing. Skips calls
3416
+ * whose options argument wasn't statically analyzable (a variable, call
3417
+ * result, or a spread) — the key may be set on an object built elsewhere. One
3418
+ * finding per unbounded call.
3419
+ */
3420
+ declare const storageUploadWithoutMaxSize: Lint;
3421
+ /**
1304
3422
  * Flags a declared table that no function inserts into.
1305
3423
  *
1306
3424
  * Using `@lunora/codegen`'s write-side discovery (the analog of the read
@@ -1387,6 +3505,44 @@ declare const unindexedRelationTarget: Lint;
1387
3505
  */
1388
3506
  declare const userCreatingMutationWithoutCaptcha: Lint;
1389
3507
  /**
3508
+ * Flags a `ctx.vectors.query`/`upsert`/`upsertMany` call whose `namespace`
3509
+ * input is derived from the handler's `args` with no server-side scoping — a
3510
+ * tenant-partition escape.
3511
+ *
3512
+ * Vectorize namespaces partition a single index into isolated sub-collections
3513
+ * (typically one per tenant/user). When a namespace comes straight from
3514
+ * request input (`ctx.vectors.query(idx, { namespace: args.tenant })`, or a
3515
+ * value built one hop earlier from `args`), any caller can hand in another
3516
+ * tenant's namespace and read or poison that tenant's vectors. The fix is to
3517
+ * derive the namespace from a server-trusted identity (`` `${ctx.auth.orgId}` ``)
3518
+ * so a caller can only ever address their own partition — a namespace that
3519
+ * references `ctx` is treated as scoped and is not flagged.
3520
+ *
3521
+ * Runs only when the codegen feeder supplies vector-namespace evidence
3522
+ * (`context.vectorNamespaceAccesses`); a runtime caller flags nothing. One
3523
+ * finding per arg-derived, unscoped `ctx.vectors` call.
3524
+ */
3525
+ declare const vectorsNamespaceFromUserInput: Lint;
3526
+ /**
3527
+ * Flags a durable step name reused within one workflow.
3528
+ *
3529
+ * Cloudflare Workflows memoizes every `step.do` / `step.sleep` / `step.sleepUntil`
3530
+ * / `step.waitForEvent` call by its name: on replay the runtime returns the cached
3531
+ * result for a name it has already seen. Two distinct steps that share a name are
3532
+ * therefore a silent bug — the second call never runs its body and instead yields
3533
+ * the first's result, skipping the work (a charge, a write, an external wait)
3534
+ * without error. Hence `ERROR`/`INTERNAL`: it is a developer-facing correctness
3535
+ * defect in the workflow's own code, not a runtime-data nit.
3536
+ *
3537
+ * Only the first string-literal argument of each step call is compared; a step
3538
+ * named dynamically (`step.do(\`load-${id}\`, …)`) is omitted by the feeder, so a
3539
+ * deliberately-parameterized fan-out is never flagged. `ctx.runStep(stepDef, …)`
3540
+ * names (which come from `defineStep` in another file) are out of scope here.
3541
+ * Only runs when the declaration feeder supplied step evidence
3542
+ * (`workflow.steps` present); a runtime caller flags nothing.
3543
+ */
3544
+ declare const workflowDuplicateStepName: Lint;
3545
+ /**
1390
3546
  * A correctness lint: every `ctx.workflows.get("name")` call must reference a
1391
3547
  * workflow that exists — i.e. a `defineWorkflow` export in `lunora/workflows.ts`.
1392
3548
  * A `.get("x")` whose `"x"` resolves to no declared workflow is a typo or a
@@ -1448,4 +3604,4 @@ interface RunAdvisorOptions {
1448
3604
  * `static` lints at build time and defer `runtime` lints to a live shard.
1449
3605
  */
1450
3606
  declare const runAdvisor: (context: LintContext, options?: RunAdvisorOptions) => Finding[];
1451
- export { AE_METRIC_EVENTS, ALL_LINTS, type AdvisorAdminRoute, type AdvisorArgumentValidator, type AdvisorAuthApiCall, type AdvisorContainer, type AdvisorHyperdriveCall, type AdvisorIndex, type AdvisorIndexHit, type AdvisorInsertWrite, type AdvisorMaskProcedure, type AdvisorNondeterministicCall, type AdvisorProcedureProtection, type AdvisorQueryRead, type AdvisorR2sqlCall, type AdvisorRelation, type AdvisorRlsProcedure, type AdvisorSchema, type AdvisorSecretLiteral, type AdvisorShardTraffic, type AdvisorSqlInterpolation, type AdvisorTable, type AdvisorTableSample, type AdvisorTableScan, type AdvisorWorkflow, type AdvisorWorkflowCall, type AnalyticsMetricsOptions, type AnalyticsMetricsSource, type AnalyticsRuntimeMetrics, type Category, type Facing, type Finding, type Level, type Lint, type LintContext, type LintSource, RUNTIME_LINTS, RunAdvisorOptions, STATIC_LINTS, adminRouteWithoutGuard, authApiCallWithoutHeaders, circularFk, constraintValidator, containerOversizedInstance, containerPublicInternet, duplicateIndex, emptyIndex, filterWithoutIndex, fromServerSchema, hardcodedSecret, hotShard, hyperdriveOutsideAction, indexReferencesUnknownField, indexUtilization, loadAnalyticsRuntimeMetrics, maskUncoveredPiiColumn, nondeterministicQueryMutation, policyReferencesUnknownTable, publicArgumentUsesAny, publicMutationWithoutRatelimit, r2sqlOutsideAction, relationReferencesUnknownField, relationReferencesUnknownTable, rlsUncoveredTable, runAdvisor, sqlInjectionRisk, tableWithoutInsert, unboundedStringArgument, unindexedForeignKey, unindexedRelationTarget, userCreatingMutationWithoutCaptcha, workflowUnknownTarget, workflowUnused };
3607
+ export { AE_METRIC_EVENTS, ALL_LINTS, type AdvisorAdminRoute, type AdvisorAiRawRun, type AdvisorAiToolSideEffect, type AdvisorArgumentDerivedFetch, type AdvisorArgumentValidator, type AdvisorAuthApiCall, type AdvisorAuthConfig, type AdvisorBrowserUrlAccess, type AdvisorConfigCall, type AdvisorContainer, type AdvisorContainerKeyAccess, type AdvisorContainerOverride, type AdvisorFailOpenGuard, type AdvisorFlagSecurityDefault, type AdvisorHttpActionGuard, type AdvisorHttpHeaderWrite, type AdvisorHyperdriveCall, type AdvisorIdentityClaimRead, type AdvisorImageDeliveryUrlAccess, type AdvisorIndex, type AdvisorIndexHit, type AdvisorInsertWrite, type AdvisorKvKeyAccess, type AdvisorMailRecipientAccess, type AdvisorMaskProcedure, type AdvisorMaskStrategy, type AdvisorMutatorWrite, type AdvisorNondeterministicCall, type AdvisorNormalizeIdAuthorization, type AdvisorOwnerFieldWrite, type AdvisorPaymentWebhook, type AdvisorPrivilegedDispatch, type AdvisorProcedureProtection, type AdvisorQueryRead, type AdvisorQueue, type AdvisorQueueTuning, type AdvisorR2sqlCall, type AdvisorRatelimitKeySelector, type AdvisorRawRowReturn, type AdvisorRelation, type AdvisorRelationLoad, type AdvisorRlsProcedure, type AdvisorSchema, type AdvisorSecretLiteral, type AdvisorShape, type AdvisorShardTraffic, type AdvisorSoftDeleteRead, type AdvisorSqlInterpolation, type AdvisorStorageKeyAccess, type AdvisorStorageUpload, type AdvisorTable, type AdvisorTableSample, type AdvisorTableScan, type AdvisorVectorNamespaceAccess, type AdvisorWorkflow, type AdvisorWorkflowCall, type AdvisorWranglerVariable, type AnalyticsMetricsOptions, type AnalyticsMetricsSource, type AnalyticsRuntimeMetrics, type Category, type Facing, type Finding, type Level, type Lint, type LintContext, type LintSource, RUNTIME_LINTS, RunAdvisorOptions, STATIC_LINTS, actionFetchSsrf, adminRouteWithoutGuard, aiRawRunEscapeHatch, aiToolSideEffectPromptInjection, aiUnboundedGenerationPublic, allowUnauthenticatedShardAccessEnabled, authApiCallWithoutHeaders, authCsrfCheckDisabled, authEmailVerificationDisabled, authSecureCookiesDisabled, authSessionFreshageZero, authTrustedOriginsWildcard, browserAllowPrivateTargets, browserUserUrlWithoutAllowlist, circularFk, constraintValidator, containerInstanceKeyFromUserInput, containerOversizedInstance, containerPublicInternet, containerRuntimeEgressRelaxation, containerStartEnableInternetOverride, duplicateIndex, emptyIndex, externalSourceOnGlobal, externalSourceUnscoped, filterWithoutIndex, flagGatesSecurityWithUnsafeDefault, fromServerSchema, hardcodedSecret, hotShard, httpActionMissingAuthGuard, httpActionResponseHeaderInjection, hyperdriveOutsideAction, identityUndeclaredClaimTrusted, imagesUrlSourceFromUserInput, indexReferencesUnknownField, indexUtilization, insertManyUnsafeUserData, kvUnscopedUserKeyIdor, loadAnalyticsRuntimeMetrics, mailInboundDispatchWithoutVerify, mailRecipientFromRequestInput, maskUncoveredPiiColumn, maskWeakHashStrategyOnPii, maskedRelationLeakViaWith, mutatorFullRowReplace, nondeterministicQueryMutation, normalizeIdUsedAsAuthorization, outputProjectionMissingOnPublicRead, ownerFieldFromArgsNotAuth, paymentCreateWithoutAuthorize, paymentWebhookWideTolerance, plaintextSecretInWranglerVariables, policyReferencesUnknownTable, privilegedDispatchUnvalidatedPayload, privilegedFanoutFromPublicProcedure, publicArgumentUsesAny, publicMutationWithoutRatelimit, publicTableRlsOptoutConfusion, queueWithoutDlq, r2sqlOutsideAction, ratelimitDefaultMemoryStore, ratelimitKeySpoofableOrGlobal, ratelimitMiddlewareFailOpen, relationReferencesUnknownField, relationReferencesUnknownTable, rlsUncoveredTable, runAdvisor, shapeTargetsGlobalTable, shapeUnknownTable, softDeleteIncludeDeletedFromArgs, sqlInjectionRisk, storageGenerateUploadUrlNoContentTypePin, storageKeyFromUserArgs, storagePresignedUrlForPrivateContent, storageUploadWithoutContentTypeAllowlist, storageUploadWithoutMaxSize, tableWithoutInsert, unboundedStringArgument, unindexedForeignKey, unindexedRelationTarget, userCreatingMutationWithoutCaptcha, vectorsNamespaceFromUserInput, workflowDuplicateStepName, workflowUnknownTarget, workflowUnused };