@lunora/advisor 1.0.0-alpha.3 → 1.0.0-alpha.31

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 (76) 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 +3145 -940
  5. package/dist/index.d.ts +3145 -940
  6. package/dist/index.mjs +120 -13
  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/circularFk-CNcAVuYa.mjs +127 -0
  22. package/dist/packem_shared/{constraintValidator-Dr9Py3FD.mjs → constraintValidator-CxwtpJ6E.mjs} +24 -4
  23. package/dist/packem_shared/containerInstanceKeyFromUserInput-BUXj2J4w.mjs +19 -0
  24. package/dist/packem_shared/containerRuntimeEgressRelaxation-B0LlfmDA.mjs +26 -0
  25. package/dist/packem_shared/containerStartEnableInternetOverride-DDaHZQ1L.mjs +26 -0
  26. package/dist/packem_shared/dedupeCacheKeys-r5B7u_yq.mjs +13 -0
  27. package/dist/packem_shared/externalSourceIncrementalNoDeletePath-DvX9cRBD.mjs +45 -0
  28. package/dist/packem_shared/externalSourceOnGlobal-Bg-NfCX9.mjs +30 -0
  29. package/dist/packem_shared/externalSourceUnscoped-5vT-Bup3.mjs +44 -0
  30. package/dist/packem_shared/flagGatesSecurityWithUnsafeDefault-BhIs0shr.mjs +41 -0
  31. package/dist/packem_shared/{fromServerSchema-DinF1nph.mjs → fromServerSchema-BjAZdvJ6.mjs} +10 -0
  32. package/dist/packem_shared/{hardcodedSecret-W2pz1UZB.mjs → hardcodedSecret-Be-pKVdn.mjs} +3 -7
  33. package/dist/packem_shared/helpers-BySnKhVB.mjs +31 -0
  34. package/dist/packem_shared/hotShard-CkC7qpre.mjs +60 -0
  35. package/dist/packem_shared/httpActionMissingAuthGuard-CxipddNx.mjs +35 -0
  36. package/dist/packem_shared/httpActionResponseHeaderInjection-DOFS7pFT.mjs +39 -0
  37. package/dist/packem_shared/identityUndeclaredClaimTrusted-D8nXV2dd.mjs +32 -0
  38. package/dist/packem_shared/imagesUrlSourceFromUserInput-YcDQ0b_Y.mjs +19 -0
  39. package/dist/packem_shared/{indexReferencesUnknownField-DH0_dbUY.mjs → indexReferencesUnknownField-BSWNngxX.mjs} +1 -1
  40. package/dist/packem_shared/insertManyUnsafeUserData-Dn77XpmX.mjs +26 -0
  41. package/dist/packem_shared/kvUnscopedUserKeyIdor-YWwmfE8X.mjs +19 -0
  42. package/dist/packem_shared/mailInboundDispatchWithoutVerify-CHSRA8zz.mjs +28 -0
  43. package/dist/packem_shared/mailRecipientFromRequestInput-Cka2qu5J.mjs +26 -0
  44. package/dist/packem_shared/maskWeakHashStrategyOnPii-1c4q8Opf.mjs +35 -0
  45. package/dist/packem_shared/maskedRelationLeakViaWith-CPI4s0sl.mjs +76 -0
  46. package/dist/packem_shared/mutatorFullRowReplace-BJnNDaIV.mjs +26 -0
  47. package/dist/packem_shared/normalizeIdUsedAsAuthorization-BVPtCpzT.mjs +50 -0
  48. package/dist/packem_shared/outputProjectionMissingOnPublicRead-Bl5IMx0k.mjs +51 -0
  49. package/dist/packem_shared/ownerFieldFromArgsNotAuth-mOw3hE5z.mjs +26 -0
  50. package/dist/packem_shared/paymentCreateWithoutAuthorize-BYm4JLxo.mjs +26 -0
  51. package/dist/packem_shared/paymentWebhookWideTolerance-D9QQJMJq.mjs +35 -0
  52. package/dist/packem_shared/plaintextSecretInWranglerVariables-NKO4YkKf.mjs +35 -0
  53. package/dist/packem_shared/privilegedDispatchUnvalidatedPayload-5Forjckt.mjs +37 -0
  54. package/dist/packem_shared/privilegedFanoutFromPublicProcedure-D3dL01B8.mjs +26 -0
  55. package/dist/packem_shared/{publicMutationWithoutRatelimit-xBpJ6GWK.mjs → publicMutationWithoutRatelimit-DbIhgi7j.mjs} +2 -2
  56. package/dist/packem_shared/publicTableRlsOptoutConfusion-Bn71yoD4.mjs +38 -0
  57. package/dist/packem_shared/queueWithoutDlq-CSkGNb_0.mjs +41 -0
  58. package/dist/packem_shared/ratelimitDefaultMemoryStore-BISChG5C.mjs +26 -0
  59. package/dist/packem_shared/ratelimitKeySpoofableOrGlobal-DqlHYQQ3.mjs +26 -0
  60. package/dist/packem_shared/ratelimitMiddlewareFailOpen-CJgDCaUw.mjs +33 -0
  61. package/dist/packem_shared/{relationReferencesUnknownField-YznyXt_7.mjs → relationReferencesUnknownField-CjbLScJ1.mjs} +1 -1
  62. package/dist/packem_shared/shapeTargetsGlobalTable-DHrf4Koi.mjs +34 -0
  63. package/dist/packem_shared/shapeUnknownTable-C8aDWFoe.mjs +34 -0
  64. package/dist/packem_shared/softDeleteIncludeDeletedFromArgs-BLqDKrkM.mjs +38 -0
  65. package/dist/packem_shared/storageGenerateUploadUrlNoContentTypePin-Da4L9Ge8.mjs +26 -0
  66. package/dist/packem_shared/storageKeyFromUserArgs-B86elJgS.mjs +19 -0
  67. package/dist/packem_shared/storagePresignedUrlForPrivateContent-yGmb8uqz.mjs +47 -0
  68. package/dist/packem_shared/storageUploadWithoutContentTypeAllowlist-BV-gF1lT.mjs +27 -0
  69. package/dist/packem_shared/storageUploadWithoutMaxSize-DpxO59wU.mjs +27 -0
  70. package/dist/packem_shared/{userCreatingMutationWithoutCaptcha-CH31YsUZ.mjs → userCreatingMutationWithoutCaptcha-2DZtWIPb.mjs} +2 -2
  71. package/dist/packem_shared/vectorsNamespaceFromUserInput-CQhr5bVn.mjs +19 -0
  72. package/dist/packem_shared/workflowDuplicateStepName-ioBxPBCy.mjs +48 -0
  73. package/package.json +4 -3
  74. package/dist/packem_shared/circularFk-B2freHrP.mjs +0 -84
  75. package/dist/packem_shared/helpers-DNCkMWZQ.mjs +0 -4
  76. package/dist/packem_shared/hotShard-Ir5D0B6J.mjs +0 -48
package/dist/index.d.mts CHANGED
@@ -1,10 +1,10 @@
1
1
  import { Schema } from '@lunora/server';
2
2
  /**
3
- * One `httpRoute.<verb>("/admin/…")` REST route on an admin/privileged-looking
4
- * path, with whether its handler references an auth/admin guard — the input the
5
- * `admin_route_without_guard` lint consumes. Produced by the codegen feeder;
6
- * runtime callers don't supply it, so the lint finds nothing there.
7
- */
3
+ * One `httpRoute.<verb>("/admin/…")` REST route on an admin/privileged-looking
4
+ * path, with whether its handler references an auth/admin guard — the input the
5
+ * `admin_route_without_guard` lint consumes. Produced by the codegen feeder;
6
+ * runtime callers don't supply it, so the lint finds nothing there.
7
+ */
8
8
  interface AdvisorAdminRoute {
9
9
  /** The exported binding name of the route handler. */
10
10
  exportName: string;
@@ -18,13 +18,76 @@ interface AdvisorAdminRoute {
18
18
  usesGuard: boolean;
19
19
  }
20
20
  /**
21
- * One public procedure's argument validators reduced to the input-safety facts
22
- * the `public_arg_uses_any` and `unbounded_string_arg` lints consume: which args
23
- * are declared `v.any()` (unvalidated input) and which `v.string()` args carry no
24
- * length bound (a DoS / storage-abuse vector). Produced by the codegen feeder for
25
- * public procedures only; internal functions take server-trusted input. Runtime
26
- * callers don't supply it, so the lints find nothing there.
27
- */
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
+ /**
84
+ * One public procedure's argument validators reduced to the input-safety facts
85
+ * the `public_arg_uses_any` and `unbounded_string_arg` lints consume: which args
86
+ * are declared `v.any()` (unvalidated input) and which `v.string()` args carry no
87
+ * length bound (a DoS / storage-abuse vector). Produced by the codegen feeder for
88
+ * public procedures only; internal functions take server-trusted input. Runtime
89
+ * callers don't supply it, so the lints find nothing there.
90
+ */
28
91
  interface AdvisorArgumentValidator {
29
92
  /** Arg names declared as `v.any()`. */
30
93
  anyArgs: ReadonlyArray<string>;
@@ -38,10 +101,50 @@ interface AdvisorArgumentValidator {
38
101
  unboundedStringArgs: ReadonlyArray<string>;
39
102
  }
40
103
  /**
41
- * One `ctx.authApi.&lt;method>(...)` call discovered in a function body — the input
42
- * the `auth_api_call_without_headers` lint consumes. Produced by the codegen
43
- * feeder; runtime callers don't supply it, so the lint finds nothing there.
44
- */
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
+ /**
144
+ * One `ctx.authApi.&lt;method>(...)` call discovered in a function body — the input
145
+ * the `auth_api_call_without_headers` lint consumes. Produced by the codegen
146
+ * feeder; runtime callers don't supply it, so the lint finds nothing there.
147
+ */
45
148
  interface AdvisorAuthApiCall {
46
149
  /** The exported function performing the call (e.g. `createOrg`). */
47
150
  exportName: string;
@@ -55,20 +158,114 @@ interface AdvisorAuthApiCall {
55
158
  method: string;
56
159
  }
57
160
  /**
58
- * One container declaration discovered in `lunora/containers.ts` — the input
59
- * the `container_*` lints consume. Produced by the codegen feeder (which lifts
60
- * the static fields of each `defineContainer({...})` export); runtime callers
61
- * don't supply it, so the container lints simply find nothing there.
62
- *
63
- * A structural subset of codegen's `ContainerIR`, so the feeder can pass the
64
- * IR array straight through without conversion (mirrors how `AdvisorQueryRead`
65
- * tracks `QueryReadIR`).
66
- */
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
+ /**
255
+ * One container declaration discovered in `lunora/containers.ts` — the input
256
+ * the `container_*` lints consume. Produced by the codegen feeder (which lifts
257
+ * the static fields of each `defineContainer({...})` export); runtime callers
258
+ * don't supply it, so the container lints simply find nothing there.
259
+ *
260
+ * A structural subset of codegen's `ContainerIR`, so the feeder can pass the
261
+ * IR array straight through without conversion (mirrors how `AdvisorQueryRead`
262
+ * tracks `QueryReadIR`).
263
+ */
67
264
  interface AdvisorContainer {
68
265
  /**
69
- * Whether outbound internet was explicitly configured. `undefined` means
70
- * the field was omitted (platform default `true`) or wasn't a static literal.
71
- */
266
+ * Whether outbound internet was explicitly configured. `undefined` means
267
+ * the field was omitted (platform default `true`) or wasn't a static literal.
268
+ */
72
269
  enableInternet?: boolean;
73
270
  /** The `lunora/containers.ts` export name, e.g. `transcoder`. */
74
271
  exportName: string;
@@ -84,19 +281,121 @@ interface AdvisorContainer {
84
281
  sleepAfter?: number | string;
85
282
  }
86
283
  /**
87
- * One `ctx.sql` access discovered lexically inside a `query(...)` or
88
- * `mutation(...)` handler body — the input the `hyperdrive_outside_action` lint
89
- * consumes. Produced by the codegen feeder, which walks each exported function's
90
- * handler with ts-morph and records reads of the Hyperdrive `ctx.sql` surface
91
- * (`ctx.sql(...)`, `ctx.sql.query(...)`).
92
- *
93
- * Hyperdrive points at an **external** database Lunora does not own: a `ctx.sql`
94
- * call is a network round-trip with a mutable result (non-deterministic, like
95
- * `fetch`) and its writes are invisible to Lunora live queries. It therefore
96
- * belongs **only** in `action(...)` handlers. Calls inside `action(...)` are
97
- * intentionally **not** recorded — actions are the escape hatch. Runtime callers
98
- * don't supply this, so the lint finds nothing there.
99
- */
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
+ /**
386
+ * One `ctx.sql` access discovered lexically inside a `query(...)` or
387
+ * `mutation(...)` handler body — the input the `hyperdrive_outside_action` lint
388
+ * consumes. Produced by the codegen feeder, which walks each exported function's
389
+ * handler with ts-morph and records reads of the Hyperdrive `ctx.sql` surface
390
+ * (`ctx.sql(...)`, `ctx.sql.query(...)`).
391
+ *
392
+ * Hyperdrive points at an **external** database Lunora does not own: a `ctx.sql`
393
+ * call is a network round-trip with a mutable result (non-deterministic, like
394
+ * `fetch`) and its writes are invisible to Lunora live queries. It therefore
395
+ * belongs **only** in `action(...)` handlers. Calls inside `action(...)` are
396
+ * intentionally **not** recorded — actions are the escape hatch. Runtime callers
397
+ * don't supply this, so the lint finds nothing there.
398
+ */
100
399
  interface AdvisorHyperdriveCall {
101
400
  /** The accessed `ctx.sql` surface, e.g. `ctx.sql.query` / `ctx.sql`. */
102
401
  callee: string;
@@ -110,29 +409,64 @@ interface AdvisorHyperdriveCall {
110
409
  line: number;
111
410
  }
112
411
  /**
113
- * Observed read signal over a table the input the `index_utilization` runtime
114
- * lint consumes. Produced by the studio backend from each shard's recorded
115
- * metrics.
116
- *
117
- * `AdvisorTableScan` comes straight from the per-`(function, table)` full-scan
118
- * attribution the runtime already records (`__lunora_metrics_scans`, surfaced as
119
- * `FunctionCallStat.scannedTables`). Each entry is a table the app read with no
120
- * index a hot one points at a missing index.
121
- *
122
- * `AdvisorIndexHit` is the per-declared-index hit count. The runtime now records
123
- * this in the durable `__lunora_metrics_index` table (stamped on every index use
124
- * via `onIndexUse`) and surfaces it through the `getMetrics` admin RPC; the
125
- * studio sums the per-shard arrays and feeds them as `context.indexHits`, and the
126
- * lint flags a declared index with zero recorded reads as dead. When the feed is
127
- * absent (a static caller, or a shard that recorded nothing) the dead-index half
128
- * is a no-op and only the hot-scan half runs off the scan attribution.
129
- */
130
- /**
131
- * Per-table full-scan volume observed over the window a read that hit no
132
- * index. Sourced from `FunctionCallStat.scannedTables` aggregated across
133
- * functions and shards. Runtime callers supply this; static callers don't, so
134
- * the hot-scan half of the lint finds nothing there.
135
- */
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
+ /**
447
+ * Observed read signal over a table — the input the `index_utilization` runtime
448
+ * lint consumes. Produced by the studio backend from each shard's recorded
449
+ * metrics.
450
+ *
451
+ * `AdvisorTableScan` comes straight from the per-`(function, table)` full-scan
452
+ * attribution the runtime already records (`__lunora_metrics_scans`, surfaced as
453
+ * `FunctionCallStat.scannedTables`). Each entry is a table the app read with no
454
+ * index — a hot one points at a missing index.
455
+ *
456
+ * `AdvisorIndexHit` is the per-declared-index hit count. The runtime now records
457
+ * this in the durable `__lunora_metrics_index` table (stamped on every index use
458
+ * via `onIndexUse`) and surfaces it through the `getMetrics` admin RPC; the
459
+ * studio sums the per-shard arrays and feeds them as `context.indexHits`, and the
460
+ * lint flags a declared index with zero recorded reads as dead. When the feed is
461
+ * absent (a static caller, or a shard that recorded nothing) the dead-index half
462
+ * is a no-op and only the hot-scan half runs off the scan attribution.
463
+ */
464
+ /**
465
+ * Per-table full-scan volume observed over the window — a read that hit no
466
+ * index. Sourced from `FunctionCallStat.scannedTables` aggregated across
467
+ * functions and shards. Runtime callers supply this; static callers don't, so
468
+ * the hot-scan half of the lint finds nothing there.
469
+ */
136
470
  interface AdvisorTableScan {
137
471
  /** Total full-scans of `table` over the observed window. */
138
472
  scans: number;
@@ -140,17 +474,17 @@ interface AdvisorTableScan {
140
474
  table: string;
141
475
  }
142
476
  /**
143
- * Per-declared-index hit count observed over the window — how many recorded
144
- * reads used the index to narrow.
145
- *
146
- * Produced by the runtime: every index use (`onIndexUse` in the DO) bumps a
147
- * per-`(table, index)` counter in the durable `__lunora_metrics_index` table, the
148
- * complement of the full-*scan* attribution in `__lunora_metrics_scans`. The
149
- * `getMetrics` admin RPC surfaces it per shard; the studio sums the arrays across
150
- * shards and passes them as `context.indexHits`. A declared index that appears
151
- * with `reads: 0` (or is absent entirely after the schema reconciliation) is dead
152
- * for the window.
153
- */
477
+ * Per-declared-index hit count observed over the window — how many recorded
478
+ * reads used the index to narrow.
479
+ *
480
+ * Produced by the runtime: every index use (`onIndexUse` in the DO) bumps a
481
+ * per-`(table, index)` counter in the durable `__lunora_metrics_index` table, the
482
+ * complement of the full-*scan* attribution in `__lunora_metrics_scans`. The
483
+ * `getMetrics` admin RPC surfaces it per shard; the studio sums the arrays across
484
+ * shards and passes them as `context.indexHits`. A declared index that appears
485
+ * with `reads: 0` (or is absent entirely after the schema reconciliation) is dead
486
+ * for the window.
487
+ */
154
488
  interface AdvisorIndexHit {
155
489
  /** The declared index name. */
156
490
  index: string;
@@ -160,12 +494,12 @@ interface AdvisorIndexHit {
160
494
  table: string;
161
495
  }
162
496
  /**
163
- * One `ctx.db.insert("table", …)` write discovered in a function body — the
164
- * write-side analog of `AdvisorQueryRead`, the input the
165
- * `table_without_insert` lint consumes. Produced by the codegen feeder (which
166
- * attributes each insert to the exported function performing it); runtime callers
167
- * don't supply it, so the lint simply finds nothing there.
168
- */
497
+ * One `ctx.db.insert("table", …)` write discovered in a function body — the
498
+ * write-side analog of `AdvisorQueryRead`, the input the
499
+ * `table_without_insert` lint consumes. Produced by the codegen feeder (which
500
+ * attributes each insert to the exported function performing it); runtime callers
501
+ * don't supply it, so the lint simply finds nothing there.
502
+ */
169
503
  interface AdvisorInsertWrite {
170
504
  /** The exported function performing the insert (e.g. `send`). */
171
505
  exportName: string;
@@ -177,24 +511,67 @@ interface AdvisorInsertWrite {
177
511
  table: string;
178
512
  }
179
513
  /**
180
- * One procedure (query / mutation / action) discovered in the lunora source,
181
- * reduced to the facts the `mask_uncovered_pii_column` lint needs: whether the
182
- * procedure's builder chain includes `.use(mask(...))`, which `(table, column)`
183
- * pairs that mask declares, and which tables the procedure reads or writes.
184
- * Produced by the codegen feeder; runtime callers don't supply it, so the lint
185
- * finds nothing there. The column-level twin of `AdvisorRlsProcedure`.
186
- */
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
+ /**
557
+ * One procedure (query / mutation / action) discovered in the lunora source,
558
+ * reduced to the facts the `mask_uncovered_pii_column` lint needs: whether the
559
+ * procedure's builder chain includes `.use(mask(...))`, which `(table, column)`
560
+ * pairs that mask declares, and which tables the procedure reads or writes.
561
+ * Produced by the codegen feeder; runtime callers don't supply it, so the lint
562
+ * finds nothing there. The column-level twin of `AdvisorRlsProcedure`.
563
+ */
187
564
  interface AdvisorMaskProcedure {
188
565
  /** The exported binding name of the procedure (e.g. `listUsers`). */
189
566
  exportName: string;
190
567
  /** Source file relative to the lunora dir, no extension. */
191
568
  file: string;
192
569
  /**
193
- * The `(table, column)` pairs declared by the `mask(policies)` object passed
194
- * to `.use(mask(...))` in this procedure's builder chain. Empty when the
195
- * policies argument is not a statically-readable object literal
196
- * (conservative: `usesMask` is still `true`).
197
- */
570
+ * The `(table, column)` pairs declared by the `mask(policies)` object passed
571
+ * to `.use(mask(...))` in this procedure's builder chain. Empty when the
572
+ * policies argument is not a statically-readable object literal
573
+ * (conservative: `usesMask` is still `true`).
574
+ */
198
575
  maskColumns: ReadonlyArray<{
199
576
  column: string;
200
577
  table: string;
@@ -204,25 +581,71 @@ interface AdvisorMaskProcedure {
204
581
  /** Tables written by the procedure via `ctx.db.insert("table", …)` / `ctx.db.patch(...)` etc. */
205
582
  tablesWritten: ReadonlyArray<string>;
206
583
  /**
207
- * `true` when the procedure's builder chain includes `.use(mask(...))` — the
208
- * `mask` callee is identified by name from `@lunora/server`. `false` when no
209
- * `.use(mask(...))` is found in the chain (or the procedure uses the bare
210
- * `query({...})` factory form, which never carries a builder chain at all).
211
- */
584
+ * `true` when the procedure's builder chain includes `.use(mask(...))` — the
585
+ * `mask` callee is identified by name from `@lunora/server`. `false` when no
586
+ * `.use(mask(...))` is found in the chain (or the procedure uses the bare
587
+ * `query({...})` factory form, which never carries a builder chain at all).
588
+ */
212
589
  usesMask: boolean;
213
590
  /** `"internal"` when the procedure uses `internalQuery` / `internalMutation` / `internalAction`. */
214
591
  visibility: "internal" | "public";
215
592
  }
216
593
  /**
217
- * One non-deterministic API call discovered lexically inside a `query(...)` or
218
- * `mutation(...)` handler body — the input the `nondeterministic_query_mutation`
219
- * lint consumes. Produced by the codegen feeder, which walks each exported
220
- * function's handler with ts-morph and records calls to `Date.now`,
221
- * `Math.random`, `crypto.randomUUID`, `crypto.getRandomValues`, and `fetch`.
222
- * Calls inside `action(...)` handlers are intentionally **not** recorded — actions
223
- * are the determinism escape hatch. Runtime callers don't supply this, so the
224
- * lint finds nothing there.
225
- */
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
+ /**
640
+ * One non-deterministic API call discovered lexically inside a `query(...)` or
641
+ * `mutation(...)` handler body — the input the `nondeterministic_query_mutation`
642
+ * lint consumes. Produced by the codegen feeder, which walks each exported
643
+ * function's handler with ts-morph and records calls to `Date.now`,
644
+ * `Math.random`, `crypto.randomUUID`, `crypto.getRandomValues`, and `fetch`.
645
+ * Calls inside `action(...)` handlers are intentionally **not** recorded — actions
646
+ * are the determinism escape hatch. Runtime callers don't supply this, so the
647
+ * lint finds nothing there.
648
+ */
226
649
  interface AdvisorNondeterministicCall {
227
650
  /** The non-deterministic API invoked, e.g. `Date.now` / `Math.random` / `crypto.randomUUID` / `fetch`. */
228
651
  callee: string;
@@ -236,25 +659,135 @@ interface AdvisorNondeterministicCall {
236
659
  line: number;
237
660
  }
238
661
  /**
239
- * One procedure (query / mutation / action) reduced to the protective middlewares
240
- * its builder chain installs plus the behavioural facts that decide whether a
241
- * guard is expected the input the `public_mutation_without_ratelimit` and
242
- * `user_creating_mutation_without_captcha` lints consume. A `protectPublic({...})`
243
- * bundle is unwrapped by the feeder: its keys set `usesRateLimit`/`usesCaptcha`
244
- * exactly as the standalone `.use(...)` steps would. Produced by the codegen
245
- * feeder; runtime callers don't supply it, so the lints find nothing there.
246
- */
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
+ /**
766
+ * One procedure (query / mutation / action) reduced to the protective middlewares
767
+ * its builder chain installs plus the behavioural facts that decide whether a
768
+ * guard is expected — the input the `public_mutation_without_ratelimit` and
769
+ * `user_creating_mutation_without_captcha` lints consume. A `protectPublic({...})`
770
+ * bundle is unwrapped by the feeder: its keys set `usesRateLimit`/`usesCaptcha`
771
+ * exactly as the standalone `.use(...)` steps would. Produced by the codegen
772
+ * feeder; runtime callers don't supply it, so the lints find nothing there.
773
+ */
247
774
  interface AdvisorProcedureProtection {
248
775
  /** `true` when the handler references `ctx.mail` / `ctx.email` (sends mail). */
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. */
@@ -267,11 +800,11 @@ interface AdvisorProcedureProtection {
267
800
  writesUserTable: boolean;
268
801
  }
269
802
  /**
270
- * One query read discovered in a function body — the input the
271
- * `filter_without_index` lint consumes. Produced by the codegen feeder (which
272
- * parses `ctx.db.query("table")…` chains from the AST); runtime callers don't
273
- * supply it, so the lint simply finds nothing there.
274
- */
803
+ * One query read discovered in a function body — the input the
804
+ * `filter_without_index` lint consumes. Produced by the codegen feeder (which
805
+ * parses `ctx.db.query("table")…` chains from the AST); runtime callers don't
806
+ * supply it, so the lint simply finds nothing there.
807
+ */
275
808
  interface AdvisorQueryRead {
276
809
  /** Source file the read appears in (relative to the lunora dir, no extension). */
277
810
  file: string;
@@ -285,20 +818,59 @@ interface AdvisorQueryRead {
285
818
  table: string;
286
819
  }
287
820
  /**
288
- * One `ctx.r2sql` access discovered lexically inside a `query(...)` or
289
- * `mutation(...)` handler body the input the `r2sql_outside_action` lint
290
- * consumes. Produced by the codegen feeder, which walks each exported function's
291
- * handler with ts-morph and records reads of the R2 SQL `ctx.r2sql` surface
292
- * (`ctx.r2sql.query(...)`, `ctx.r2sql.from(...)`, …).
293
- *
294
- * R2 SQL queries Apache Iceberg tables over an **external** REST endpoint Lunora
295
- * does not own (there is no Workers binding): a `ctx.r2sql` call is a network
296
- * round-trip with a mutable result (non-deterministic, like `fetch`) and its
297
- * reads are invisible to Lunora live queries. It therefore belongs **only** in
298
- * `action(...)` handlers. Calls inside `action(...)` are intentionally **not**
299
- * recorded — actions are the escape hatch. Runtime callers don't supply this, so
300
- * the lint finds nothing there.
301
- */
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
+ /**
860
+ * One `ctx.r2sql` access discovered lexically inside a `query(...)` or
861
+ * `mutation(...)` handler body — the input the `r2sql_outside_action` lint
862
+ * consumes. Produced by the codegen feeder, which walks each exported function's
863
+ * handler with ts-morph and records reads of the R2 SQL `ctx.r2sql` surface
864
+ * (`ctx.r2sql.query(...)`, `ctx.r2sql.from(...)`, …).
865
+ *
866
+ * R2 SQL queries Apache Iceberg tables over an **external** REST endpoint Lunora
867
+ * does not own (there is no Workers binding): a `ctx.r2sql` call is a network
868
+ * round-trip with a mutable result (non-deterministic, like `fetch`) and its
869
+ * reads are invisible to Lunora live queries. It therefore belongs **only** in
870
+ * `action(...)` handlers. Calls inside `action(...)` are intentionally **not**
871
+ * recorded — actions are the escape hatch. Runtime callers don't supply this, so
872
+ * the lint finds nothing there.
873
+ */
302
874
  interface AdvisorR2sqlCall {
303
875
  /** The accessed `ctx.r2sql` surface, e.g. `ctx.r2sql.query` / `ctx.r2sql.from`. */
304
876
  callee: string;
@@ -312,99 +884,227 @@ interface AdvisorR2sqlCall {
312
884
  line: number;
313
885
  }
314
886
  /**
315
- * One procedure (query / mutation / action) discovered in the lunora source,
316
- * reduced to the facts the `rls_uncovered_table` lint needs: whether the
317
- * procedure's builder chain includes `.use(rls(...))`, and which tables the
318
- * procedure reads or writes. Produced by the codegen feeder; runtime callers
319
- * don't supply it, so the lint finds nothing there.
320
- */
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
+ /**
951
+ * One procedure (query / mutation / action) discovered in the lunora source,
952
+ * reduced to the facts the `rls_uncovered_table` lint needs: whether the
953
+ * procedure's builder chain includes `.use(rls(...))`, and which tables the
954
+ * procedure reads or writes. Produced by the codegen feeder; runtime callers
955
+ * don't supply it, so the lint finds nothing there.
956
+ */
321
957
  interface AdvisorRlsProcedure {
322
958
  /** The exported binding name of the procedure (e.g. `listDocuments`). */
323
959
  exportName: string;
324
960
  /** Source file relative to the lunora dir, no extension. */
325
961
  file: string;
326
962
  /**
327
- * Tables explicitly named in the `rls(policies)` array passed to `.use(rls(...))`
328
- * in this procedure's builder chain. Empty when the policies argument is not a
329
- * statically-readable array literal (conservative: `usesRls` is still `true`).
330
- */
963
+ * Tables explicitly named in the `rls(policies)` array passed to `.use(rls(...))`
964
+ * in this procedure's builder chain. Empty when the policies argument is not a
965
+ * statically-readable array literal (conservative: `usesRls` is still `true`).
966
+ */
331
967
  rlsTables: ReadonlyArray<string>;
332
968
  /** Tables read by the procedure via `ctx.db.query("table")` / `ctx.db.findMany(...)` etc. */
333
969
  tablesRead: ReadonlyArray<string>;
334
970
  /** Tables written by the procedure via `ctx.db.insert("table", …)` / `ctx.db.patch(...)` etc. */
335
971
  tablesWritten: ReadonlyArray<string>;
336
972
  /**
337
- * `true` when the procedure's builder chain includes `.use(rls(...))` — the
338
- * `rls` callee is identified by name from `@lunora/server`. `false` when no
339
- * `.use(rls(...))` is found in the chain (or the procedure uses the bare
340
- * `query({...})` factory form, which never carries a builder chain at all).
341
- */
973
+ * `true` when the procedure's builder chain includes `.use(rls(...))` — the
974
+ * `rls` callee is identified by name from `@lunora/server`. `false` when no
975
+ * `.use(rls(...))` is found in the chain (or the procedure uses the bare
976
+ * `query({...})` factory form, which never carries a builder chain at all).
977
+ */
342
978
  usesRls: boolean;
343
979
  /** `"internal"` when the procedure uses `internalQuery` / `internalMutation` / `internalAction`. */
344
980
  visibility: "internal" | "public";
345
981
  }
346
982
  /**
347
- * Normalized, feeder-agnostic view of a schema that lints run against. Both the
348
- * runtime `@lunora/server` {@link Schema} (record-shaped) and `@lunora/codegen`'s
349
- * `SchemaIR` (array-shaped, AST-derived) collapse to this same shape, so a lint
350
- * is written once and runs in either place. It carries only what the lints
351
- * read — tables, their columns, indexes, and relations.
352
- */
983
+ * Normalized, feeder-agnostic view of a schema that lints run against. Both the
984
+ * runtime `@lunora/server` {@link Schema} (record-shaped) and `@lunora/codegen`'s
985
+ * `SchemaIR` (array-shaped, AST-derived) collapse to this same shape, so a lint
986
+ * is written once and runs in either place. It carries only what the lints
987
+ * read — tables, their columns, indexes, and relations.
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. */
357
1002
  interface AdvisorTable {
358
1003
  /**
359
- * `true` when the table is written outside Lunora's discoverable insert path
360
- * — declared via `.externallyManaged()` (e.g. `@lunora/auth`'s better-auth
361
- * tables, `@lunora/ratelimit`'s store). Insert-path lints
362
- * (`table_without_insert`) skip such tables. Defaults to `false`.
363
- */
1004
+ * `true` when the table is written outside Lunora's discoverable insert path
1005
+ * — declared via `.externallyManaged()` (e.g. `@lunora/auth`'s better-auth
1006
+ * tables, `@lunora/ratelimit`'s store). Insert-path lints
1007
+ * (`table_without_insert`) skip such tables. Defaults to `false`.
1008
+ */
364
1009
  externallyManaged?: boolean;
365
1010
  /**
366
- * Declared column names (the `defineTable({...})` keys). Excludes the
367
- * framework-managed system fields `_id` / `_creationTime`, which every table
368
- * has implicitly — lints that resolve a column treat those as always valid.
369
- */
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
+ /**
1019
+ * Declared column names (the `defineTable({...})` keys). Excludes the
1020
+ * framework-managed system fields `_id` / `_creationTime`, which every table
1021
+ * has implicitly — lints that resolve a column treat those as always valid.
1022
+ */
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
  /**
376
- * Column names that are optional or nullable and therefore may legally hold
377
- * `null` / `undefined` in stored rows. Populated by {@link fromServerSchema}
378
- * from the runtime validator graph (`v.optional(...)` → kind `"optional"`;
379
- * `.nullable()` → `column.notNull === false`). When absent (e.g. from the
380
- * codegen feeder, which does not supply this field), constraint lints that
381
- * check NOT NULL should skip the check entirely or treat every field as
382
- * required (the codegen feeder never runs runtime lints anyway).
383
- */
1038
+ * Column names that are optional or nullable and therefore may legally hold
1039
+ * `null` / `undefined` in stored rows. Populated by {@link fromServerSchema}
1040
+ * from the runtime validator graph (`v.optional(...)` → kind `"optional"`;
1041
+ * `.nullable()` → `column.notNull === false`). When absent (e.g. from the
1042
+ * codegen feeder, which does not supply this field), constraint lints that
1043
+ * check NOT NULL should skip the check entirely or treat every field as
1044
+ * required (the codegen feeder never runs runtime lints anyway).
1045
+ */
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
- * One declared index, flattened across Lunora's index kinds so a single lint can
390
- * reason about every column an index touches. `kind` distinguishes the DSL that
391
- * declared it — only `index` (a btree secondary index) covers a foreign-key
392
- * equality lookup, so the FK lint filters on it. `fields` is every column the
393
- * index references (a secondary index's columns; a search index's text +
394
- * filter fields; a rank index's sort + partition fields; a vector index's
395
- * source field). `unique` is set only for unique secondary indexes.
396
- */
1072
+ * One declared index, flattened across Lunora's index kinds so a single lint can
1073
+ * reason about every column an index touches. `kind` distinguishes the DSL that
1074
+ * declared it — only `index` (a btree secondary index) covers a foreign-key
1075
+ * equality lookup, so the FK lint filters on it. `fields` is every column the
1076
+ * index references (a secondary index's columns; a search index's text +
1077
+ * filter fields; a rank index's sort + partition fields; a vector index's
1078
+ * source field). `unique` is set only for unique secondary indexes.
1079
+ */
397
1080
  interface AdvisorIndex {
398
1081
  fields: ReadonlyArray<string>;
399
1082
  kind: "index" | "rank" | "search" | "vector";
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 — one incremental delete-visibility path the `external_source_incremental_no_delete_path` lint accepts. */
1089
+ hasReconcile?: boolean;
1090
+ /** `true` when a `softDeleteColumn` was given — the other incremental delete-visibility path. */
1091
+ hasSoftDelete?: boolean;
1092
+ /** `true` when a `tenantBy` mapper was given — the tenant-isolation boundary. */
1093
+ hasTenantBy: boolean;
1094
+ /** Delete-detection mode literal, when given (`"full-pull"` or `"incremental"`). */
1095
+ mode?: string;
1096
+ /**
1097
+ * `true` when `.source(...)` was declared but its config wasn't a static object
1098
+ * literal, so `hasTenantBy` (and the rest) couldn't be read. Only the codegen
1099
+ * feeder can hit this; the runtime feeder always holds the real config.
1100
+ */
1101
+ unanalyzable?: boolean;
1102
+ }
403
1103
  /**
404
- * One declared relation. For a `one` relation the FK column `field` lives on
405
- * the holding table; for `many` it lives on the target. `name` is the accessor
406
- * the relation is loaded under.
407
- */
1104
+ * One declared relation. For a `one` relation the FK column `field` lives on
1105
+ * the holding table; for `many` it lives on the target. `name` is the accessor
1106
+ * the relation is loaded under.
1107
+ */
408
1108
  interface AdvisorRelation {
409
1109
  field: string;
410
1110
  kind: "many" | "one";
@@ -414,21 +1114,21 @@ interface AdvisorRelation {
414
1114
  table: string;
415
1115
  }
416
1116
  /**
417
- * Adapt the runtime `@lunora/server` {@link Schema} into an {@link AdvisorSchema}.
418
- * Runtime callers (the studio backend, a live shard) hold the real schema
419
- * object; this collapses its record-keyed `tables`/`relationMap` into the array
420
- * form lints consume and flattens the per-kind index arrays into one list. The
421
- * codegen feeder builds the same shape from its AST IR independently (it never
422
- * imports `@lunora/server`).
423
- */
1117
+ * Adapt the runtime `@lunora/server` {@link Schema} into an {@link AdvisorSchema}.
1118
+ * Runtime callers (the studio backend, a live shard) hold the real schema
1119
+ * object; this collapses its record-keyed `tables`/`relationMap` into the array
1120
+ * form lints consume and flattens the per-kind index arrays into one list. The
1121
+ * codegen feeder builds the same shape from its AST IR independently (it never
1122
+ * imports `@lunora/server`).
1123
+ */
424
1124
  declare const fromServerSchema: (schema: Schema) => AdvisorSchema;
425
1125
  /**
426
- * One secret-shaped string literal discovered in the lunora source — the input
427
- * the `hardcoded_secret` lint consumes. The full value is never carried; only a
428
- * redacted {@link AdvisorSecretLiteral.preview}. Produced by the codegen feeder
429
- * (complementing the pre-commit `vis secrets` scan); runtime callers don't supply
430
- * it, so the lint finds nothing there.
431
- */
1126
+ * One secret-shaped string literal discovered in the lunora source — the input
1127
+ * the `hardcoded_secret` lint consumes. The full value is never carried; only a
1128
+ * redacted {@link AdvisorSecretLiteral.preview}. Produced by the codegen feeder
1129
+ * (complementing the pre-commit `vis secrets` scan); runtime callers don't supply
1130
+ * it, so the lint finds nothing there.
1131
+ */
432
1132
  interface AdvisorSecretLiteral {
433
1133
  /** Source file relative to the lunora dir, no extension. */
434
1134
  file: string;
@@ -440,43 +1140,91 @@ interface AdvisorSecretLiteral {
440
1140
  preview: string;
441
1141
  }
442
1142
  /**
443
- * One shard's observed traffic share the input the `hot_shard` runtime lint
444
- * consumes. Produced by the studio backend, which fans out over a sharded
445
- * function's shards and reads each shard's recorded request volume from the
446
- * durable `__lunora_metrics` accumulator (`SUM(calls)`) or, equivalently, the
447
- * per-shard request-log count. Codegen and other static callers don't supply
448
- * it, so the lint simply finds nothing there.
449
- *
450
- * The lint is a pure function over its context, so it can't fan out over shards
451
- * itself; the caller does the cross-shard read and hands the aggregated
452
- * distribution here, exactly as the codegen feeder hands `AdvisorQueryRead`s for
453
- * the static query lints.
454
- */
1143
+ * A replication shape declared via `defineShape({ table, where, columns? })` in
1144
+ * `lunora/shapes.ts` (the local-first sync engine's partial-replication unit).
1145
+ * The `shape_*` lints cross-reference each shape's {@link AdvisorShape.table}
1146
+ * against the declared schema to flag a shape targeting an unknown table or a
1147
+ * `.global()` table (which replicates through the latency-tiered D1 poll path,
1148
+ * not the poke-live op-log). Supplied by the codegen feeder, which lifts only
1149
+ * the export name + the static `table` literal; absent for runtime callers,
1150
+ * where the shape lints find nothing.
1151
+ */
1152
+ interface AdvisorShape {
1153
+ /** Export binding name — the shape's registry key (e.g. `channelMessages`). */
1154
+ exportName: string;
1155
+ /** File the shape is declared in (relative, for the operator to open). */
1156
+ file: string;
1157
+ /**
1158
+ * The `table` string literal the shape replicates from, or `undefined` when
1159
+ * the feeder could not read it as a plain string literal — tier-sensitive
1160
+ * lints skip a shape with no resolvable table rather than guessing.
1161
+ */
1162
+ table?: string;
1163
+ }
1164
+ /**
1165
+ * One shard's observed traffic share — the input the `hot_shard` runtime lint
1166
+ * consumes. Produced by the studio backend, which fans out over a sharded
1167
+ * function's shards and reads each shard's recorded request volume from the
1168
+ * durable `__lunora_metrics` accumulator (`SUM(calls)`) — or, equivalently, the
1169
+ * per-shard request-log count. Codegen and other static callers don't supply
1170
+ * it, so the lint simply finds nothing there.
1171
+ *
1172
+ * The lint is a pure function over its context, so it can't fan out over shards
1173
+ * itself; the caller does the cross-shard read and hands the aggregated
1174
+ * distribution here, exactly as the codegen feeder hands `AdvisorQueryRead`s for
1175
+ * the static query lints.
1176
+ */
455
1177
  interface AdvisorShardTraffic {
456
1178
  /**
457
- * The sharded function group these shards belong to, when the caller scopes
458
- * the distribution to one `.shardBy(...)` function. Used only to name the
459
- * finding; empty when the traffic is the whole deployment's shard set.
460
- */
1179
+ * The sharded function group these shards belong to, when the caller scopes
1180
+ * the distribution to one `.shardBy(...)` function. Used only to name the
1181
+ * finding; empty when the traffic is the whole deployment's shard set.
1182
+ */
461
1183
  group?: string;
462
1184
  /** Total requests (function dispatches) recorded against this shard over the observed window. */
463
1185
  requests: number;
464
1186
  /**
465
- * The shard key (the Durable Object id name) traffic was attributed to —
466
- * a user / tenant / room id, depending on the `.shardBy(...)` key. Empty for
467
- * the unnamed root DO.
468
- */
1187
+ * The shard key (the Durable Object id name) traffic was attributed to —
1188
+ * a user / tenant / room id, depending on the `.shardBy(...)` key. Empty for
1189
+ * the unnamed root DO.
1190
+ */
469
1191
  shardKey: string;
470
1192
  }
471
1193
  /**
472
- * One `ctx.sql` tagged-template interpolation that splices an unparameterized
473
- * string-building expression into the query the input the `sql_injection_risk`
474
- * lint consumes. A `${…}` placeholder that simply names a value is bound as a
475
- * parameter by the Hyperdrive driver and is *not* recorded; only in-place string
476
- * construction (`"… " + raw`, a nested template literal) reaches here. Produced by
477
- * the codegen feeder; runtime callers don't supply it, so the lint finds nothing
478
- * there.
479
- */
1194
+ * One `ctx.db.&lt;table>.findMany({ includeDeleted })` list read whose
1195
+ * `includeDeleted` is either a hardcoded `true` or derived from the handler's
1196
+ * `args` the shared input for the `soft_delete_include_deleted_from_args`
1197
+ * lint. `includeDeleted` resurfaces rows a `.softDelete()` table would otherwise
1198
+ * hide from list reads; on a public read that means any caller (arg-derived) or
1199
+ * every caller (hardcoded `true`) can see soft-deleted rows. Produced by the
1200
+ * codegen feeder; runtime callers don't supply it, so the lint finds nothing
1201
+ * there. Structurally identical to `@lunora/codegen`'s `SoftDeleteReadIR`.
1202
+ */
1203
+ interface AdvisorSoftDeleteRead {
1204
+ /** The exported binding name of the procedure performing the read. */
1205
+ exportName: string;
1206
+ /** Source file relative to the lunora dir, no extension. */
1207
+ file: string;
1208
+ /** `true` when `includeDeleted` was derived from the handler's `args` (any caller can flip it). */
1209
+ fromArgs: boolean;
1210
+ /** `true` when `includeDeleted` was a hardcoded `true` literal (always resurfaces soft-deleted rows). */
1211
+ hardcodedTrue: boolean;
1212
+ /** 1-based line of the read call. */
1213
+ line: number;
1214
+ /** Table read, or `""` when the table couldn't be statically resolved. */
1215
+ table: string;
1216
+ /** `"internal"` for `internalQuery` / `internalMutation` / `internalAction`. */
1217
+ visibility: "internal" | "public";
1218
+ }
1219
+ /**
1220
+ * One `ctx.sql` tagged-template interpolation that splices an unparameterized
1221
+ * string-building expression into the query — the input the `sql_injection_risk`
1222
+ * lint consumes. A `${…}` placeholder that simply names a value is bound as a
1223
+ * parameter by the Hyperdrive driver and is *not* recorded; only in-place string
1224
+ * construction (`"… " + raw`, a nested template literal) reaches here. Produced by
1225
+ * the codegen feeder; runtime callers don't supply it, so the lint finds nothing
1226
+ * there.
1227
+ */
480
1228
  interface AdvisorSqlInterpolation {
481
1229
  /** The exported binding name of the procedure performing the `ctx.sql` call. */
482
1230
  exportName: string;
@@ -486,22 +1234,71 @@ interface AdvisorSqlInterpolation {
486
1234
  line: number;
487
1235
  }
488
1236
  /**
489
- * A bounded sample of rows from one table, fed into the constraint-validator
490
- * lint by the studio backend (via `readTablePage`). The cap prevents unbounded
491
- * scans while still catching obvious violations on small-to-medium tables.
492
- *
493
- * The studio notes the cap to the operator when the row count exceeds it
494
- * (`truncated: true`), so violations on rows beyond the sample window are not
495
- * silently missed the finding description mentions the cap.
496
- */
1237
+ * One `ctx.storage.&lt;bucket>.&lt;method>(key, …)` call whose R2 object key is derived
1238
+ * from the handler's `args` with no server-side scoping — the input the
1239
+ * `storage_key_from_user_args` lint consumes. An object key taken straight from
1240
+ * request input lets any caller read, overwrite, or delete another user's object
1241
+ * (object-level IDOR). A key prefixed with a server-trusted identity (a `ctx.*`
1242
+ * value such as `` `${ctx.auth.userId}/…` ``) is treated as scoped and is *not*
1243
+ * recorded; only an arg-derived, `ctx`-free key reaches here. Produced by the
1244
+ * codegen feeder; runtime callers don't supply it, so the lint finds nothing there.
1245
+ */
1246
+ interface AdvisorStorageKeyAccess {
1247
+ /** The exported binding name of the procedure performing the storage call. */
1248
+ exportName: string;
1249
+ /** Source file relative to the lunora dir, no extension. */
1250
+ file: string;
1251
+ /** 1-based line of the storage call, or `0` when unknown. */
1252
+ line: number;
1253
+ /** The bucket method invoked with the arg-derived key, e.g. `get` / `put` / `delete` / `download`. */
1254
+ method: string;
1255
+ }
1256
+ /**
1257
+ * One tracked `ctx.storage.&lt;bucket>.&lt;method>(...)` upload/signing call — the
1258
+ * shared input for the storage config-hygiene security lints
1259
+ * (`storage_upload_without_content_type_allowlist`, `storage_upload_without_max_size`,
1260
+ * `storage_generate_upload_url_no_content_type_pin`, `storage_presigned_url_for_private_content`).
1261
+ * `upload`/`store` carry the `UploadOptions` guards (`allowedContentTypes` /
1262
+ * `maxSize`); `generateUploadUrl` carries the signed-PUT `contentType` pin;
1263
+ * `getPresignedUrl`/`getSignedUrl` carry a statically-known `expiresInSeconds`
1264
+ * literal. `presentKeys` is empty (and `expiresInSeconds` unset) when the
1265
+ * options argument was absent, a non-literal, or a spread — see `analyzable`.
1266
+ * Produced by the codegen feeder; runtime callers don't supply it, so the
1267
+ * lints find nothing there.
1268
+ */
1269
+ interface AdvisorStorageUpload {
1270
+ /** `true` when the call's options-object argument (or its deliberate absence) was statically resolvable. */
1271
+ analyzable: boolean;
1272
+ /** Numeric literal value of an `expiresInSeconds` option, when statically known (`getSignedUrl` / `getPresignedUrl` only). */
1273
+ expiresInSeconds?: number;
1274
+ /** The exported binding name of the procedure performing the call. */
1275
+ exportName: string;
1276
+ /** Source file relative to the lunora dir, no extension. */
1277
+ file: string;
1278
+ /** 1-based line of the call, or `0` when unknown. */
1279
+ line: number;
1280
+ /** The `ctx.storage` method invoked. */
1281
+ method: "generateUploadUrl" | "getPresignedUrl" | "getSignedUrl" | "store" | "upload";
1282
+ /** Options-object keys present at the call site (empty when not `analyzable`, or when no options argument was passed). */
1283
+ presentKeys: string[];
1284
+ }
1285
+ /**
1286
+ * A bounded sample of rows from one table, fed into the constraint-validator
1287
+ * lint by the studio backend (via `readTablePage`). The cap prevents unbounded
1288
+ * scans while still catching obvious violations on small-to-medium tables.
1289
+ *
1290
+ * The studio notes the cap to the operator when the row count exceeds it
1291
+ * (`truncated: true`), so violations on rows beyond the sample window are not
1292
+ * silently missed — the finding description mentions the cap.
1293
+ */
497
1294
  interface AdvisorTableSample {
498
1295
  /** The cap applied; equals `rows.length` when not truncated. */
499
1296
  readonly cap: number;
500
1297
  /**
501
- * The row ids of every existing row in this table (bounded to `cap`), used
502
- * for FK referential-integrity checks: if a FK value does not appear in the
503
- * target table's `existingIds`, it is a dangling reference.
504
- */
1298
+ * The row ids of every existing row in this table (bounded to `cap`), used
1299
+ * for FK referential-integrity checks: if a FK value does not appear in the
1300
+ * target table's `existingIds`, it is a dangling reference.
1301
+ */
505
1302
  readonly existingIds: ReadonlySet<string>;
506
1303
  /** Sampled rows (up to `cap`). Each row includes `_id` and all declared columns. */
507
1304
  readonly rows: ReadonlyArray<Record<string, unknown>>;
@@ -511,22 +1308,61 @@ interface AdvisorTableSample {
511
1308
  readonly truncated: boolean;
512
1309
  }
513
1310
  /**
514
- * The two workflow-shaped inputs the `workflow_*` lints consume, produced by the
515
- * codegen feeder. {@link AdvisorWorkflow} is the declaration side (one per
516
- * `defineWorkflow` export in `lunora/workflows.ts`); {@link AdvisorWorkflowCall}
517
- * is the use side (one per `ctx.workflows.get("name")` call discovered in a
518
- * function body). Runtime callers don't supply either, so the workflow lints
519
- * simply find nothing there.
520
- *
521
- * Both are structural subsets of codegen's `WorkflowIR` / `WorkflowCallIR`, so
522
- * the feeder passes the IR arrays straight through without conversion (mirrors
523
- * how `AdvisorContainer` tracks `ContainerIR` and `AdvisorInsertWrite` tracks
524
- * `InsertWriteIR`).
525
- */
1311
+ * One `ctx.vectors.&lt;method>(indexName, input)` call whose `input.namespace` is
1312
+ * derived from the handler's `args` with no server-side scoping — the input the
1313
+ * `vectors_namespace_from_user_input` lint consumes. A Vectorize namespace
1314
+ * partitions one index into isolated sub-collections, so a namespace taken
1315
+ * straight from request input lets any caller read or poison another tenant's
1316
+ * vectors. A fixed literal namespace, or one prefixed with a server-trusted
1317
+ * identity (`` `${ctx.auth.orgId}` `` — references `ctx`, so treated as
1318
+ * scoped), is not recorded; only an arg-derived, unscoped namespace reaches
1319
+ * here. Produced by the codegen feeder; runtime callers don't supply it, so
1320
+ * the lint finds nothing there.
1321
+ */
1322
+ interface AdvisorVectorNamespaceAccess {
1323
+ /** The exported binding name of the procedure performing the `ctx.vectors` access. */
1324
+ exportName: string;
1325
+ /** Source file relative to the lunora dir, no extension. */
1326
+ file: string;
1327
+ /** 1-based line of the `ctx.vectors` call, or `0` when unknown. */
1328
+ line: number;
1329
+ /** The `ctx.vectors` method invoked: `query` / `upsert` / `upsertMany`. */
1330
+ method: string;
1331
+ }
1332
+ /**
1333
+ * The two workflow-shaped inputs the `workflow_*` lints consume, produced by the
1334
+ * codegen feeder. {@link AdvisorWorkflow} is the declaration side (one per
1335
+ * `defineWorkflow` export in `lunora/workflows.ts`); {@link AdvisorWorkflowCall}
1336
+ * is the use side (one per `ctx.workflows.get("name")` call discovered in a
1337
+ * function body). Runtime callers don't supply either, so the workflow lints
1338
+ * simply find nothing there.
1339
+ *
1340
+ * Both are structural subsets of codegen's `WorkflowIR` / `WorkflowCallIR`, so
1341
+ * the feeder passes the IR arrays straight through without conversion (mirrors
1342
+ * how `AdvisorContainer` tracks `ContainerIR` and `AdvisorInsertWrite` tracks
1343
+ * `InsertWriteIR`).
1344
+ */
1345
+ /** One durable step call lifted from a workflow handler body — the input the duplicate-step-name lint compares. Structural subset of codegen's `WorkflowStepIR`. */
1346
+ interface AdvisorWorkflowStep {
1347
+ /** 1-based line of the durable step call, or `0` when unknown. */
1348
+ line: number;
1349
+ /** The native step method invoked: `do` / `sleep` / `sleepUntil` / `waitForEvent`. */
1350
+ method: string;
1351
+ /** The step's static label (the first string-literal argument). */
1352
+ name: string;
1353
+ }
526
1354
  /** One workflow declared via a `defineWorkflow()` export in `lunora/workflows.ts`. */
527
1355
  interface AdvisorWorkflow {
528
1356
  /** The `lunora/workflows.ts` export name, e.g. `orderPipeline`. */
529
1357
  exportName: string;
1358
+ /**
1359
+ * The durable step labels discovered in the handler body, in source order —
1360
+ * the duplicate-step-name input. Cloudflare memoizes a step by its name, so a
1361
+ * name used twice makes the second call silently return the first's cached
1362
+ * result. Supplied by the codegen feeder; `undefined` for runtime callers,
1363
+ * where the lint finds nothing.
1364
+ */
1365
+ steps?: ReadonlyArray<AdvisorWorkflowStep>;
530
1366
  }
531
1367
  /** One `ctx.workflows.get("name")` call discovered in a function body. */
532
1368
  interface AdvisorWorkflowCall {
@@ -540,41 +1376,58 @@ interface AdvisorWorkflowCall {
540
1376
  workflow: string;
541
1377
  }
542
1378
  /**
543
- * Severity of a finding, mirroring splinter's `level`. `ERROR` is a definite
544
- * problem, `WARN` a likely one, `INFO` an advisory nudge.
545
- */
1379
+ * One committed `wrangler.jsonc` `vars` entry whose value is a plaintext secret —
1380
+ * the input the `plaintext_secret_in_wrangler_vars` lint consumes. The full value
1381
+ * is never carried; only a redacted {@link AdvisorWranglerVariable.preview}.
1382
+ * Produced by `@lunora/config` (which reads `wrangler.jsonc`) and threaded through
1383
+ * codegen; runtime callers don't supply it, so the lint finds nothing there.
1384
+ */
1385
+ interface AdvisorWranglerVariable {
1386
+ /** The `wrangler.jsonc` file the var was read from, relative to the project root. */
1387
+ file: string;
1388
+ /** The offending `vars` key (e.g. `STRIPE_SECRET_KEY`). */
1389
+ key: string;
1390
+ /** Heuristic that matched, e.g. `stripe_live_key` / `private_key` / `secret_named_var`. */
1391
+ kind: string;
1392
+ /** Redacted preview (first few chars + length) — never the full secret. */
1393
+ preview: string;
1394
+ }
1395
+ /**
1396
+ * Severity of a finding, mirroring splinter's `level`. `ERROR` is a definite
1397
+ * problem, `WARN` a likely one, `INFO` an advisory nudge.
1398
+ */
546
1399
  type Level = "ERROR" | "INFO" | "WARN";
547
1400
  /**
548
- * Who the finding concerns, mirroring splinter's `facing`. `EXTERNAL` findings
549
- * affect clients of the app (performance/security a user can feel); `INTERNAL`
550
- * ones are operator-only hygiene.
551
- */
1401
+ * Who the finding concerns, mirroring splinter's `facing`. `EXTERNAL` findings
1402
+ * affect clients of the app (performance/security a user can feel); `INTERNAL`
1403
+ * ones are operator-only hygiene.
1404
+ */
552
1405
  type Facing = "EXTERNAL" | "INTERNAL";
553
1406
  /**
554
- * Concern bucket a lint belongs to. `SCHEMA` covers shape/correctness nits that
555
- * are neither a perf nor a security issue (missing primary key, duplicate
556
- * index). `PERFORMANCE` and `SECURITY` match splinter's two categories.
557
- */
1407
+ * Concern bucket a lint belongs to. `SCHEMA` covers shape/correctness nits that
1408
+ * are neither a perf nor a security issue (missing primary key, duplicate
1409
+ * index). `PERFORMANCE` and `SECURITY` match splinter's two categories.
1410
+ */
558
1411
  type Category = "PERFORMANCE" | "SCHEMA" | "SECURITY";
559
1412
  /**
560
- * Where a lint draws its evidence from.
561
- *
562
- * `static` runs against the declared {@link AdvisorSchema} alone (tables,
563
- * indexes, relations) — deterministic, runnable at codegen/build time, and
564
- * catches a problem _before_ it ships. This is the edge Lunora has over a
565
- * live-DB-only advisor like Supabase's.
566
- *
567
- * `runtime` needs observed signal from a running shard (full-scan attribution,
568
- * function call stats). Added in a later slice; the context grows optional
569
- * fields the runtime lints read.
570
- */
1413
+ * Where a lint draws its evidence from.
1414
+ *
1415
+ * `static` runs against the declared {@link AdvisorSchema} alone (tables,
1416
+ * indexes, relations) — deterministic, runnable at codegen/build time, and
1417
+ * catches a problem _before_ it ships. This is the edge Lunora has over a
1418
+ * live-DB-only advisor like Supabase's.
1419
+ *
1420
+ * `runtime` needs observed signal from a running shard (full-scan attribution,
1421
+ * function call stats). Added in a later slice; the context grows optional
1422
+ * fields the runtime lints read.
1423
+ */
571
1424
  type LintSource = "runtime" | "static";
572
1425
  /**
573
- * One emitted advisory, shaped after splinter's lint-view row so the studio
574
- * Advisors table can render any lint uniformly. `cacheKey` is a stable,
575
- * content-derived id used to dedup across runs and to let an operator dismiss a
576
- * specific finding without silencing the whole lint.
577
- */
1426
+ * One emitted advisory, shaped after splinter's lint-view row so the studio
1427
+ * Advisors table can render any lint uniformly. `cacheKey` is a stable,
1428
+ * content-derived id used to dedup across runs and to let an operator dismiss a
1429
+ * specific finding without silencing the whole lint.
1430
+ */
578
1431
  interface Finding {
579
1432
  /** Stable identifier for dedup/dismissal across runs. */
580
1433
  cacheKey: string;
@@ -598,173 +1451,483 @@ interface Finding {
598
1451
  title: string;
599
1452
  }
600
1453
  /**
601
- * Everything a lint may inspect. Static lints read only {@link LintContext.schema};
602
- * runtime lints will additionally read observed-signal fields added here later.
603
- */
1454
+ * Everything a lint may inspect. Static lints read only {@link LintContext.schema};
1455
+ * runtime lints will additionally read observed-signal fields added here later.
1456
+ */
604
1457
  interface LintContext {
605
1458
  /**
606
- * `httpRoute.&lt;verb>("/admin/…")` routes on admin/privileged-looking paths and
607
- * whether each references an auth/admin guard — the `admin_route_without_guard`
608
- * input. Supplied by the codegen feeder; absent for runtime callers, where the
609
- * lint finds nothing.
610
- */
1459
+ * `httpRoute.&lt;verb>("/admin/…")` routes on admin/privileged-looking paths and
1460
+ * whether each references an auth/admin guard — the `admin_route_without_guard`
1461
+ * input. Supplied by the codegen feeder; absent for runtime callers, where the
1462
+ * lint finds nothing.
1463
+ */
611
1464
  adminRoutes?: ReadonlyArray<AdvisorAdminRoute>;
612
1465
  /**
613
- * Per-public-procedure argument validators that weaken input safety the
614
- * `public_arg_uses_any` (`v.any()` args) and `unbounded_string_arg` (length-less
615
- * `v.string()` args) input. Supplied by the codegen feeder for public procedures
616
- * only; absent for runtime callers, where the lints find nothing.
617
- */
1466
+ * `ctx.ai.run(model, …)` calls whose model-id argument is derived from the
1467
+ * handler's `args` with no server-side scoping — the `ai_raw_run_escape_hatch`
1468
+ * input. `ctx.ai.run` is the raw Workers AI passthrough, so an arg-derived model
1469
+ * id lets any caller select an arbitrary model, bypassing the typed
1470
+ * `ctx.ai.model(...)` + AI-SDK layer's cap/schema (an arg-derived `inputs`
1471
+ * argument is normal usage and is not recorded). Supplied by the codegen feeder;
1472
+ * absent for runtime callers, where the lint finds nothing.
1473
+ */
1474
+ aiRawRuns?: ReadonlyArray<AdvisorAiRawRun>;
1475
+ /**
1476
+ * `generateText` / `streamText` calls whose model-callable `tools` reach a
1477
+ * privileged side effect (DB write / function dispatch / outbound
1478
+ * fetch/mail/queue) — the `ai_tool_side_effect_prompt_injection` input. Each
1479
+ * row's `userInputDerived` says whether the model input flows from `args`; the
1480
+ * lint fires only when it does. Supplied by the codegen feeder; absent for
1481
+ * runtime callers, where the lint finds nothing.
1482
+ */
1483
+ aiToolSideEffects?: ReadonlyArray<AdvisorAiToolSideEffect>;
1484
+ /**
1485
+ * `ctx.fetch(url, …)` calls inside actions whose URL argument is derived from
1486
+ * the handler's `args` — the `action_fetch_ssrf` input. `ctx.fetch` has no
1487
+ * host allowlist, so a URL built from request input is a server-side request
1488
+ * forgery vector. Supplied by the codegen feeder; absent for runtime callers,
1489
+ * where the lint finds nothing.
1490
+ */
1491
+ argumentDerivedFetches?: ReadonlyArray<AdvisorArgumentDerivedFetch>;
1492
+ /**
1493
+ * Per-public-procedure argument validators that weaken input safety — the
1494
+ * `public_arg_uses_any` (`v.any()` args) and `unbounded_string_arg` (length-less
1495
+ * `v.string()` args) input. Supplied by the codegen feeder for public procedures
1496
+ * only; absent for runtime callers, where the lints find nothing.
1497
+ */
618
1498
  argValidators?: ReadonlyArray<AdvisorArgumentValidator>;
619
1499
  /**
620
- * `ctx.authApi.&lt;method>(...)` calls discovered in function bodies (the
621
- * `auth_api_call_without_headers` input). Supplied by the codegen feeder; absent
622
- * for runtime callers, where the lint finds nothing.
623
- */
1500
+ * `ctx.authApi.&lt;method>(...)` calls discovered in function bodies (the
1501
+ * `auth_api_call_without_headers` input). Supplied by the codegen feeder; absent
1502
+ * for runtime callers, where the lint finds nothing.
1503
+ */
624
1504
  authApiCalls?: ReadonlyArray<AdvisorAuthApiCall>;
625
1505
  /**
626
- * Containers declared in `lunora/containers.ts` — the `container_*` lint
627
- * input. Supplied by the codegen feeder; absent for runtime callers, where
628
- * the container lints find nothing.
629
- */
1506
+ * Per-`createAuth({...})`-call configuration snapshots — the shared input for
1507
+ * the five `auth_*` security lints (`auth_trusted_origins_wildcard`,
1508
+ * `auth_csrf_check_disabled`, `auth_secure_cookies_disabled`,
1509
+ * `auth_email_verification_disabled`, `auth_session_freshage_zero`). Each
1510
+ * carries whether the call's config object literal was statically analyzable
1511
+ * and, when it was, the handful of nested facts the lints check (a
1512
+ * `trustedOrigins` wildcard, `advanced.disableCSRFCheck`/`useSecureCookies`,
1513
+ * `emailAndPassword.enabled`/`requireEmailVerification`,
1514
+ * `session.freshAge === 0`). Supplied by the codegen feeder; absent for
1515
+ * runtime callers, where the auth-config lints find nothing.
1516
+ */
1517
+ authConfigs?: ReadonlyArray<AdvisorAuthConfig>;
1518
+ /**
1519
+ * `ctx.browser.&lt;method>(url, …)` calls whose navigation URL is derived from the
1520
+ * handler's `args` with no server-side scoping — the
1521
+ * `browser_user_url_without_allowlist` input. `@lunora/browser` blocks
1522
+ * private/internal targets by default, but a request-supplied public URL can
1523
+ * still be an open-proxy / SSRF vector; the lint suppresses findings when a
1524
+ * `createBrowser` config-call is hardened with `allowedHosts` or `resolveDns`.
1525
+ * Supplied by the codegen feeder; absent for runtime callers, where the lint
1526
+ * finds nothing.
1527
+ */
1528
+ browserUrlAccesses?: ReadonlyArray<AdvisorBrowserUrlAccess>;
1529
+ /**
1530
+ * Factory/constructor calls in `lunora/` whose config object literal a
1531
+ * security lint inspects for a present-or-absent key — the shared input for
1532
+ * the config-call security lints (payment authorize, inbound-mail verify,
1533
+ * rate-limit store, browser private-targets). Supplied by the codegen feeder;
1534
+ * absent for runtime callers, where the config-call lints find nothing.
1535
+ */
1536
+ configCalls?: ReadonlyArray<AdvisorConfigCall>;
1537
+ /**
1538
+ * `ctx.containers.&lt;name>.get(key, …)` calls whose instance key is derived from
1539
+ * the handler's `args` with no server-side scoping — the
1540
+ * `container_instance_key_from_user_input` input. Each container definition's
1541
+ * `.get(name)` accessor routes to one instance per key, so an arg-derived key lets
1542
+ * any caller reach another tenant's container (cross-tenant IDOR). A key scoped by
1543
+ * a server-trusted `ctx.*` value, or a fixed literal, is not recorded. Supplied by
1544
+ * the codegen feeder; absent for runtime callers, where the lint finds nothing.
1545
+ */
1546
+ containerKeyAccesses?: ReadonlyArray<AdvisorContainerKeyAccess>;
1547
+ /**
1548
+ * Runtime container-override calls — a `.start({ enableInternet: true, … })`
1549
+ * launch override, or a `.egress.&lt;method>(...)` runtime firewall mutation — the
1550
+ * `container_start_enable_internet_override` and `container_runtime_egress_relaxation`
1551
+ * lint input. Supplied by the codegen feeder; absent for runtime callers, where
1552
+ * those lints find nothing.
1553
+ */
1554
+ containerOverrides?: ReadonlyArray<AdvisorContainerOverride>;
1555
+ /**
1556
+ * Containers declared in `lunora/containers.ts` — the `container_*` lint
1557
+ * input. Supplied by the codegen feeder; absent for runtime callers, where
1558
+ * the container lints find nothing.
1559
+ */
630
1560
  containers?: ReadonlyArray<AdvisorContainer>;
631
1561
  /**
632
- * Hyperdrive `ctx.sql` accesses discovered lexically inside `query`/`mutation`
633
- * handler bodies the `hyperdrive_outside_action` input. Supplied by the
634
- * codegen feeder, which omits `action` handlers (where `ctx.sql` is the typed,
635
- * intended surface); absent for runtime callers, where the lint finds nothing.
636
- */
1562
+ * `rateLimit`/`dbRateLimit` (`@lunora/ratelimit`) and `verifyTurnstileMiddleware`
1563
+ * (`@lunora/auth`) middleware calls, each with whether its options literal set
1564
+ * `failOpen: true` and the rate-limit `name` the
1565
+ * `ratelimit_middleware_fail_open` input. These guards fail closed by default; a
1566
+ * `failOpen: true` admits every request during a limiter/siteverify outage, so
1567
+ * the lint fires when a fail-open guard protects an auth/payment-sensitive
1568
+ * procedure. Supplied by the codegen feeder; absent for runtime callers, where
1569
+ * the lint finds nothing.
1570
+ */
1571
+ failOpenGuards?: ReadonlyArray<AdvisorFailOpenGuard>;
1572
+ /**
1573
+ * `ctx.flags.boolean(key, default)` reads with a statically-known string key and
1574
+ * boolean-literal default — the `flag_gates_security_with_unsafe_default` input.
1575
+ * OpenFeature returns the default when the provider errors, so a fail-open
1576
+ * default on a security-shaped key (an `enforce`/`rls`/`gate`/`lockdown`
1577
+ * protection defaulting `false`, or an `allow`/`permit`/`bypass` permission
1578
+ * defaulting `true`) silently opens access during an outage. Supplied by the
1579
+ * codegen feeder; absent for runtime callers, where the lint finds nothing.
1580
+ */
1581
+ flagSecurityDefaults?: ReadonlyArray<AdvisorFlagSecurityDefault>;
1582
+ /**
1583
+ * `httpAction`/`httpRoute` handlers that perform a side effect
1584
+ * (`ctx.runMutation` / `ctx.runAction` / a `ctx.db` write) from the HTTP edge,
1585
+ * with whether each reads `ctx.auth` — the `http_action_missing_auth_guard`
1586
+ * input. Supplied by the codegen feeder; absent for runtime callers, where the
1587
+ * lint finds nothing.
1588
+ */
1589
+ httpActionGuards?: ReadonlyArray<AdvisorHttpActionGuard>;
1590
+ /**
1591
+ * Response-header writes, inside `httpAction` handlers, whose value is derived
1592
+ * from raw request input (`request.headers`/URL/query/body) with no CR/LF
1593
+ * sanitizer — the `http_action_response_header_injection` input. Supplied by the
1594
+ * codegen feeder; absent for runtime callers, where the lint finds nothing.
1595
+ */
1596
+ httpHeaderWrites?: ReadonlyArray<AdvisorHttpHeaderWrite>;
1597
+ /**
1598
+ * Hyperdrive `ctx.sql` accesses discovered lexically inside `query`/`mutation`
1599
+ * handler bodies — the `hyperdrive_outside_action` input. Supplied by the
1600
+ * codegen feeder, which omits `action` handlers (where `ctx.sql` is the typed,
1601
+ * intended surface); absent for runtime callers, where the lint finds nothing.
1602
+ */
637
1603
  hyperdriveCalls?: ReadonlyArray<AdvisorHyperdriveCall>;
638
1604
  /**
639
- * Per-declared-index hit counts observed at runtime (the dead-index half of
640
- * the `index_utilization` lint input). Supplied by the studio backend, which
641
- * sums the per-`(table, index)` reads each shard records in the durable
642
- * `__lunora_metrics_index` table and surfaces through the `getMetrics` admin
643
- * RPC (see {@link AdvisorIndexHit}). Absent for static callers, where the
644
- * dead-index check finds nothing.
645
- */
1605
+ * `&lt;receiver>.identity.&lt;key>` claim reads (RLS/mask policy `auth`, or
1606
+ * `ctx.auth`/`context.auth`) the `identity_undeclared_claim_trusted` input.
1607
+ * `defineIdentity` validates only declared claims and forwards undeclared ones
1608
+ * verbatim, so each row's `declared` flag says whether `key` is in the contract
1609
+ * (or the always-present `userId`); the lint fires on the undeclared reads.
1610
+ * Supplied by the codegen feeder — and only when a resolvable `defineIdentity`
1611
+ * contract exists; absent for runtime callers, where the lint finds nothing.
1612
+ */
1613
+ identityClaimReads?: ReadonlyArray<AdvisorIdentityClaimRead>;
1614
+ /**
1615
+ * `buildImageDeliveryUrl({ key, … })` calls (`@lunora/bindings/images`) whose
1616
+ * `key` — the CDN transform's source image, an absolute URL or an
1617
+ * origin-relative key — is derived from the handler's `args` with no
1618
+ * server-side scoping — the `images_url_source_from_user_input` input.
1619
+ * `ctx.images.transform`/`info` take image bytes, never a URL, so they are
1620
+ * not sinks; only the `key` of `buildImageDeliveryUrl` accepts a URL-or-key
1621
+ * source and is inspected. An arg-derived key lets any caller point the
1622
+ * CDN's `/cdn-cgi/image/` transform at an attacker-chosen origin (SSRF /
1623
+ * open proxy). A fixed literal, or a key scoped by a server-trusted `ctx.*`
1624
+ * value, is not recorded. Supplied by the codegen feeder; absent for
1625
+ * runtime callers, where the lint finds nothing.
1626
+ */
1627
+ imageDeliveryUrlAccesses?: ReadonlyArray<AdvisorImageDeliveryUrlAccess>;
1628
+ /**
1629
+ * Per-declared-index hit counts observed at runtime (the dead-index half of
1630
+ * the `index_utilization` lint input). Supplied by the studio backend, which
1631
+ * sums the per-`(table, index)` reads each shard records in the durable
1632
+ * `__lunora_metrics_index` table and surfaces through the `getMetrics` admin
1633
+ * RPC (see {@link AdvisorIndexHit}). Absent for static callers, where the
1634
+ * dead-index check finds nothing.
1635
+ */
646
1636
  indexHits?: ReadonlyArray<AdvisorIndexHit>;
647
1637
  /**
648
- * Insert writes discovered in function bodies (the `table_without_insert`
649
- * input). Supplied by the codegen feeder; absent for runtime callers, where
650
- * the write-shaped lints simply find nothing.
651
- */
1638
+ * Insert writes discovered in function bodies (the `table_without_insert`
1639
+ * input). Supplied by the codegen feeder; absent for runtime callers, where
1640
+ * the write-shaped lints simply find nothing.
1641
+ */
652
1642
  inserts?: ReadonlyArray<AdvisorInsertWrite>;
653
1643
  /**
654
- * Per-procedure column-masking usage discovered in function bodies (the
655
- * `mask_uncovered_pii_column` input). Carries whether each procedure's builder
656
- * chain includes `.use(mask(...))`, which `(table, column)` pairs its mask
657
- * policy declares, and which tables the procedure reads/writes. Supplied by
658
- * the codegen feeder; absent for runtime callers, where the lint finds
659
- * nothing.
660
- */
1644
+ * `ctx.kv.&lt;method>(key, …)` calls whose namespace key is derived from the
1645
+ * handler's `args` with no server-side scoping the `kv_unscoped_user_key_idor`
1646
+ * input. Workers KV is one flat namespace, so a key taken straight from request
1647
+ * input lets any caller read/overwrite/delete another user's entry (IDOR). Only
1648
+ * arg-derived, unscoped keys are recorded (a fixed literal or a
1649
+ * `${ctx.auth.userId}:…` prefix is not). Supplied by the codegen feeder; absent
1650
+ * for runtime callers, where the lint finds nothing.
1651
+ */
1652
+ kvKeyAccesses?: ReadonlyArray<AdvisorKvKeyAccess>;
1653
+ /**
1654
+ * `ctx.mail`/`ctx.email` `send`/`queue` calls whose `to`/`cc`/`bcc` recipient is
1655
+ * derived from the handler's `args` with no server-side scoping — the
1656
+ * `mail_recipient_from_request_input` input. A recipient taken straight from
1657
+ * request input turns the deployment into an open relay / spam amplifier (any
1658
+ * caller can direct mail to an arbitrary address). A recipient scoped by a
1659
+ * server-trusted `ctx.*` value, or a fixed literal, is not recorded. Supplied by
1660
+ * the codegen feeder; absent for runtime callers, where the lint finds nothing.
1661
+ */
1662
+ mailRecipientAccesses?: ReadonlyArray<AdvisorMailRecipientAccess>;
1663
+ /**
1664
+ * Per-procedure column-masking usage discovered in function bodies (the
1665
+ * `mask_uncovered_pii_column` input). Carries whether each procedure's builder
1666
+ * chain includes `.use(mask(...))`, which `(table, column)` pairs its mask
1667
+ * policy declares, and which tables the procedure reads/writes. Supplied by
1668
+ * the codegen feeder; absent for runtime callers, where the lint finds
1669
+ * nothing.
1670
+ */
661
1671
  maskProcedures?: ReadonlyArray<AdvisorMaskProcedure>;
662
1672
  /**
663
- * Non-deterministic API calls (`Date.now`, `Math.random`,
664
- * `crypto.randomUUID`, `crypto.getRandomValues`, `fetch`) discovered lexically
665
- * inside `query`/`mutation` handler bodies the `nondeterministic_query_mutation`
666
- * input. Supplied by the codegen feeder, which omits `action` handlers (their
667
- * non-determinism is intentional); absent for runtime callers, where the lint
668
- * finds nothing.
669
- */
1673
+ * Masked columns whose `mask(policies)` strategy is a statically-known
1674
+ * literal (the `mask_weak_hash_strategy_on_pii` input). One row per masked
1675
+ * column, with the `"hash"` / `"redact"` strategy literal attached; a
1676
+ * `MaskFn` (custom, non-literal) strategy is never recorded. Supplied by
1677
+ * the codegen feeder; absent for runtime callers, where the lint finds
1678
+ * nothing.
1679
+ */
1680
+ maskStrategies?: ReadonlyArray<AdvisorMaskStrategy>;
1681
+ /**
1682
+ * Whole-row `ctx.db.replace(id, document)` writes lifted from custom
1683
+ * mutators' authoritative `server` impls (the `mutator_full_row_replace`
1684
+ * input). Each `replace` overwrites the entire row, clobbering a concurrent
1685
+ * edit to a different column on a synced table. Supplied by the codegen
1686
+ * feeder; absent for runtime callers, where the lint finds nothing.
1687
+ */
1688
+ mutatorWrites?: ReadonlyArray<AdvisorMutatorWrite>;
1689
+ /**
1690
+ * Non-deterministic API calls (`Date.now`, `Math.random`,
1691
+ * `crypto.randomUUID`, `crypto.getRandomValues`, `fetch`) discovered lexically
1692
+ * inside `query`/`mutation` handler bodies — the `nondeterministic_query_mutation`
1693
+ * input. Supplied by the codegen feeder, which omits `action` handlers (their
1694
+ * non-determinism is intentional); absent for runtime callers, where the lint
1695
+ * finds nothing.
1696
+ */
670
1697
  nondeterministicCalls?: ReadonlyArray<AdvisorNondeterministicCall>;
671
1698
  /**
672
- * Per-procedure protective-middleware snapshots the
673
- * `public_mutation_without_ratelimit` and `user_creating_mutation_without_captcha`
674
- * input. Records which `.use(...)` guards (`rateLimit`, captcha, `rls`, `mask`,
675
- * the `protectPublic` bundle) each procedure carries and whether it writes a
676
- * user table or sends mail. Supplied by the codegen feeder; absent for runtime
677
- * callers, where the lints find nothing.
678
- */
1699
+ * `query`/`mutation` handlers that gate a `ctx.db.get`/`patch`/`delete` on a
1700
+ * null-checked `ctx.db.normalizeId(table, id)` result — the
1701
+ * `normalize_id_used_as_authorization` input. `normalizeId` validates an id's
1702
+ * structural shape only (it never reads the database), so gating access on a
1703
+ * non-null result is an IDOR. The lint keeps only public procedures with no
1704
+ * `.use(rls(...))` and no ownership/identity mention, then joins `table` against
1705
+ * the schema's RLS mode before flagging. Supplied by the codegen feeder; absent
1706
+ * for runtime callers, where the lint finds nothing.
1707
+ */
1708
+ normalizeIdAuthorizations?: ReadonlyArray<AdvisorNormalizeIdAuthorization>;
1709
+ /**
1710
+ * `ctx.db` writes (`insert` / `replace` / `patch` / `insertManyUnsafe`) that set
1711
+ * an ownership / identity column (`userId`, `ownerId`, `tenantId`, …) from the
1712
+ * handler's `args` instead of the server-trusted identity — the
1713
+ * `owner_field_from_args_not_auth` input. The ownership column decides who a row
1714
+ * belongs to, so an arg-derived value lets any caller write rows owned by another
1715
+ * user or tenant (act-as-any-user / cross-tenant IDOR). A column stamped from
1716
+ * `ctx.*`, or a fixed literal, is not recorded. Supplied by the codegen feeder;
1717
+ * absent for runtime callers, where the lint finds nothing.
1718
+ */
1719
+ ownerFieldWrites?: ReadonlyArray<AdvisorOwnerFieldWrite>;
1720
+ /**
1721
+ * Payment webhook-adapter constructions (`createStripeAdapter` /
1722
+ * `createPolarAdapter` / `createAutumnAdapter` / `createDodoPaymentsAdapter`) — the payment-webhook wide-tolerance lint's input. Each row's
1723
+ * `toleranceSeconds` is the statically-known `webhookToleranceSeconds` replay
1724
+ * window (default 300s); the lint fires only above a conservative ceiling, where
1725
+ * the endpoint would accept stale, replayable signed payloads. Supplied by the
1726
+ * codegen feeder; absent for runtime callers, where the lint finds nothing.
1727
+ */
1728
+ paymentWebhooks?: ReadonlyArray<AdvisorPaymentWebhook>;
1729
+ /**
1730
+ * Payload-derived privileged dispatches — the `privileged_dispatch_unvalidated_payload`
1731
+ * input. Each is a `ctx.run`/`context.run` back into a Lunora function from inside a
1732
+ * `defineQueue` push handler or a `defineWorkflow` handler, whose args reference the
1733
+ * handler's untrusted payload (`context.params` for a workflow, a `for (… of
1734
+ * batch.messages)` body for a queue). Both handler kinds run under the system identity
1735
+ * (RLS disabled), so the lint joins the resolved target against `rlsProcedures` and
1736
+ * fires only when the target enforces a row policy. Supplied by the codegen feeder;
1737
+ * absent for runtime callers, where the lint finds nothing.
1738
+ */
1739
+ privilegedDispatches?: ReadonlyArray<AdvisorPrivilegedDispatch>;
1740
+ /**
1741
+ * Per-procedure protective-middleware snapshots — the
1742
+ * `public_mutation_without_ratelimit` and `user_creating_mutation_without_captcha`
1743
+ * input. Records which `.use(...)` guards (`rateLimit`, captcha, `rls`, `mask`,
1744
+ * the `protectPublic` bundle) each procedure carries and whether it writes a
1745
+ * user table or sends mail. Supplied by the codegen feeder; absent for runtime
1746
+ * callers, where the lints find nothing.
1747
+ */
679
1748
  procedureProtections?: ReadonlyArray<AdvisorProcedureProtection>;
680
1749
  /**
681
- * Query reads discovered in function bodies (the `filter_without_index`
682
- * input). Supplied by the codegen feeder; absent for runtime callers, where
683
- * the query-shaped lints simply find nothing.
684
- */
1750
+ * Query reads discovered in function bodies (the `filter_without_index`
1751
+ * input). Supplied by the codegen feeder; absent for runtime callers, where
1752
+ * the query-shaped lints simply find nothing.
1753
+ */
685
1754
  queries?: ReadonlyArray<AdvisorQueryRead>;
686
1755
  /**
687
- * R2 SQL `ctx.r2sql` accesses discovered lexically inside `query`/`mutation`
688
- * handler bodies the `r2sql_outside_action` input. Supplied by the codegen
689
- * feeder, which omits `action` handlers (where `ctx.r2sql` is the typed,
690
- * intended surface); absent for runtime callers, where the lint finds nothing.
691
- */
1756
+ * Queues declared via `defineQueue` exports in `lunora/queues.ts` the
1757
+ * declaration-side input for the `queue_*` lints (`queue_without_dlq`).
1758
+ * Supplied by the codegen feeder; absent for runtime callers, where the
1759
+ * queue lints find nothing.
1760
+ */
1761
+ queues?: ReadonlyArray<AdvisorQueue>;
1762
+ /**
1763
+ * R2 SQL `ctx.r2sql` accesses discovered lexically inside `query`/`mutation`
1764
+ * handler bodies — the `r2sql_outside_action` input. Supplied by the codegen
1765
+ * feeder, which omits `action` handlers (where `ctx.r2sql` is the typed,
1766
+ * intended surface); absent for runtime callers, where the lint finds nothing.
1767
+ */
692
1768
  r2sqlCalls?: ReadonlyArray<AdvisorR2sqlCall>;
693
1769
  /**
694
- * Per-procedure RLS usage discovered in function bodies (the
695
- * `rls_uncovered_table` input). Carries whether each procedure's builder chain
696
- * includes `.use(rls(...))`, which tables the procedure reads/writes, and which
697
- * tables its RLS policy array names. Supplied by the codegen feeder; absent for
698
- * runtime callers, where the lint finds nothing.
699
- */
1770
+ * `rateLimit`/`dbRateLimit` middleware calls (`@lunora/ratelimit`) whose
1771
+ * `key` selector is derived from the handler's `args` with no server-side
1772
+ * scoping the `ratelimit_key_spoofable_or_global` input. A key an
1773
+ * attacker controls lets them rotate it per request and bypass the limit
1774
+ * entirely, defeating its purpose. A selector scoped by `ctx` (e.g.
1775
+ * `ctx.auth.userId`, `ctx.ip`), or one with no `args` reference at all (a
1776
+ * fixed/global bucket), is not recorded. Supplied by the codegen feeder;
1777
+ * absent for runtime callers, where the lint finds nothing.
1778
+ */
1779
+ ratelimitKeySelectors?: ReadonlyArray<AdvisorRatelimitKeySelector>;
1780
+ /**
1781
+ * `query` handlers that `return` the raw rows of a table (a `ctx.db` row read
1782
+ * or `ctx.db.query(...)` fluent chain, returned directly or through one local
1783
+ * `const` hop, with no hand-built projection) — the
1784
+ * `output_projection_missing_on_public_read` input. The lint keeps only public
1785
+ * queries with no `.output(...)`/mask on the chain, then joins `table` against
1786
+ * the schema's PII-named columns before nudging. Supplied by the codegen
1787
+ * feeder; absent for runtime callers, where the lint finds nothing.
1788
+ */
1789
+ rawRowReturns?: ReadonlyArray<AdvisorRawRowReturn>;
1790
+ /**
1791
+ * `ctx.db.&lt;table>.findMany({ with: { &lt;rel> } })` relation-hydrating list reads
1792
+ * — the `masked_relation_leak_via_with` input. Column masking is applied to a
1793
+ * read's top-level rows but does not descend into `with`-hydrated relations,
1794
+ * so a masked table surfaced only through a `with` on an unprotected public
1795
+ * read is returned in the clear. Supplied by the codegen feeder; absent for
1796
+ * runtime callers, where the lint finds nothing.
1797
+ */
1798
+ relationLoads?: ReadonlyArray<AdvisorRelationLoad>;
1799
+ /**
1800
+ * Per-procedure RLS usage discovered in function bodies (the
1801
+ * `rls_uncovered_table` input). Carries whether each procedure's builder chain
1802
+ * includes `.use(rls(...))`, which tables the procedure reads/writes, and which
1803
+ * tables its RLS policy array names. Supplied by the codegen feeder; absent for
1804
+ * runtime callers, where the lint finds nothing.
1805
+ */
700
1806
  rlsProcedures?: ReadonlyArray<AdvisorRlsProcedure>;
701
1807
  /** The declared schema under audit, normalized to the feeder-agnostic {@link AdvisorSchema}. */
702
1808
  schema: AdvisorSchema;
703
1809
  /**
704
- * Secret-shaped string literals discovered in the lunora source — the
705
- * `hardcoded_secret` input. Each carries only a redacted preview, never the
706
- * full value. Supplied by the codegen feeder; absent for runtime callers,
707
- * where the lint finds nothing.
708
- */
1810
+ * Secret-shaped string literals discovered in the lunora source — the
1811
+ * `hardcoded_secret` input. Each carries only a redacted preview, never the
1812
+ * full value. Supplied by the codegen feeder; absent for runtime callers,
1813
+ * where the lint finds nothing.
1814
+ */
709
1815
  secretLiterals?: ReadonlyArray<AdvisorSecretLiteral>;
710
1816
  /**
711
- * Per-shard observed traffic the `hot_shard` lint input. Supplied by the
712
- * studio backend, which fans out over a sharded function's shards and reads
713
- * each shard's recorded request volume from the durable `__lunora_metrics`
714
- * accumulator. Absent for static callers, where the lint finds nothing.
715
- */
1817
+ * Replication shapes declared via `defineShape` in `lunora/shapes.ts` the
1818
+ * `shape_unknown_table` and `shape_targets_global_table` lint input. Each
1819
+ * carries the export name and its static `table` literal, cross-referenced
1820
+ * against {@link LintContext.schema}. Supplied by the codegen feeder; absent
1821
+ * for runtime callers, where the shape lints find nothing.
1822
+ */
1823
+ shapes?: ReadonlyArray<AdvisorShape>;
1824
+ /**
1825
+ * Per-shard observed traffic — the `hot_shard` lint input. Supplied by the
1826
+ * studio backend, which fans out over a sharded function's shards and reads
1827
+ * each shard's recorded request volume from the durable `__lunora_metrics`
1828
+ * accumulator. Absent for static callers, where the lint finds nothing.
1829
+ */
716
1830
  shardTraffic?: ReadonlyArray<AdvisorShardTraffic>;
717
1831
  /**
718
- * `ctx.sql` tagged-template interpolations that splice an unparameterized
719
- * string-building expression into the query the `sql_injection_risk` input.
720
- * Supplied by the codegen feeder; absent for runtime callers, where the lint
721
- * finds nothing.
722
- */
1832
+ * `ctx.db.&lt;table>.findMany({ includeDeleted })` list reads whose
1833
+ * `includeDeleted` is a hardcoded `true` or derived from the handler's
1834
+ * `args` the `soft_delete_include_deleted_from_args` input. On a public
1835
+ * read of a `.softDelete()` table this resurfaces soft-deleted rows to any
1836
+ * caller (arg-derived) or every caller (hardcoded). Supplied by the codegen
1837
+ * feeder; absent for runtime callers, where the lint finds nothing.
1838
+ */
1839
+ softDeleteReads?: ReadonlyArray<AdvisorSoftDeleteRead>;
1840
+ /**
1841
+ * `ctx.sql` tagged-template interpolations that splice an unparameterized
1842
+ * string-building expression into the query — the `sql_injection_risk` input.
1843
+ * Supplied by the codegen feeder; absent for runtime callers, where the lint
1844
+ * finds nothing.
1845
+ */
723
1846
  sqlInterpolations?: ReadonlyArray<AdvisorSqlInterpolation>;
724
1847
  /**
725
- * Bounded row samples per table the `constraint_validator` lint input.
726
- * Supplied by the studio backend, which reads up to the configured row cap
727
- * from each table via `readTablePage` and assembles the existing-id set for
728
- * FK referential-integrity checks. Absent for static callers or codegen
729
- * feeders, where the constraint lint simply finds nothing.
730
- *
731
- * Each entry carries `existingIds` (every `_id` in the sample window) so
732
- * FK columns can be cross-checked across tables in O(1) per value. When
733
- * `truncated` is `true`, violations on rows beyond the cap are not reported
734
- * — the finding description notes the sample cap so the operator understands
735
- * the bounded window.
736
- */
1848
+ * `ctx.storage.&lt;bucket>.&lt;method>(key, …)` calls whose R2 object key is derived
1849
+ * from the handler's `args` with no server-side scoping the
1850
+ * `storage_key_from_user_args` input. The bucket read/write/URL/delete methods
1851
+ * key by their first argument, so an arg-derived key is object-level IDOR
1852
+ * (read/overwrite/delete anyone's object). A key referencing a server-trusted
1853
+ * `ctx.*` value (e.g. `${ctx.auth.userId}/…`) is treated as scoped and not
1854
+ * recorded. Supplied by the codegen feeder; absent for runtime callers, where
1855
+ * the lint finds nothing.
1856
+ */
1857
+ storageKeyAccesses?: ReadonlyArray<AdvisorStorageKeyAccess>;
1858
+ /**
1859
+ * Tracked `ctx.storage.&lt;bucket>.&lt;method>(...)` upload/signing calls — the
1860
+ * shared input for the storage config-hygiene lints
1861
+ * (`storage_upload_without_content_type_allowlist`, `storage_upload_without_max_size`,
1862
+ * `storage_generate_upload_url_no_content_type_pin`,
1863
+ * `storage_presigned_url_for_private_content`). Each row carries the method
1864
+ * invoked, which options-object keys were present, and (for the two URL
1865
+ * signers) a statically-known `expiresInSeconds` literal. Supplied by the
1866
+ * codegen feeder; absent for runtime callers, where these lints find
1867
+ * nothing.
1868
+ */
1869
+ storageUploads?: ReadonlyArray<AdvisorStorageUpload>;
1870
+ /**
1871
+ * Bounded row samples per table — the `constraint_validator` lint input.
1872
+ * Supplied by the studio backend, which reads up to the configured row cap
1873
+ * from each table via `readTablePage` and assembles the existing-id set for
1874
+ * FK referential-integrity checks. Absent for static callers or codegen
1875
+ * feeders, where the constraint lint simply finds nothing.
1876
+ *
1877
+ * Each entry carries `existingIds` (every `_id` in the sample window) so
1878
+ * FK columns can be cross-checked across tables in O(1) per value. When
1879
+ * `truncated` is `true`, violations on rows beyond the cap are not reported
1880
+ * — the finding description notes the sample cap so the operator understands
1881
+ * the bounded window.
1882
+ */
737
1883
  tableSamples?: ReadonlyArray<AdvisorTableSample>;
738
1884
  /**
739
- * Per-table full-scan volume observed at runtime (the hot-scan half of the
740
- * `index_utilization` lint input). Sourced from the per-`(function, table)`
741
- * full-scan attribution the runtime records (`__lunora_metrics_scans`,
742
- * surfaced as `FunctionCallStat.scannedTables`), aggregated across functions
743
- * and shards. Absent for static callers, where the lint finds nothing.
744
- */
1885
+ * Per-table full-scan volume observed at runtime (the hot-scan half of the
1886
+ * `index_utilization` lint input). Sourced from the per-`(function, table)`
1887
+ * full-scan attribution the runtime records (`__lunora_metrics_scans`,
1888
+ * surfaced as `FunctionCallStat.scannedTables`), aggregated across functions
1889
+ * and shards. Absent for static callers, where the lint finds nothing.
1890
+ */
745
1891
  tableScans?: ReadonlyArray<AdvisorTableScan>;
746
1892
  /**
747
- * `ctx.workflows.get("name")` call sites discovered in function bodies the
748
- * use-side input the `workflow_unused` and `workflow_unknown_target` lints
749
- * cross-reference against {@link LintContext.workflows}. Supplied by the
750
- * codegen feeder; absent for runtime callers, where the workflow lints find
751
- * nothing.
752
- */
1893
+ * `ctx.vectors.&lt;method>(index, { namespace, })` calls whose `namespace` is
1894
+ * derived from the handler's `args` with no server-side scoping — the
1895
+ * `vectors_namespace_from_user_input` input. A Vectorize namespace partitions one
1896
+ * index into isolated sub-collections, so an arg-derived namespace lets any caller
1897
+ * read or poison another tenant's vectors. A namespace scoped by a server-trusted
1898
+ * `ctx.*` value, or a fixed literal, is not recorded. Supplied by the codegen
1899
+ * feeder; absent for runtime callers, where the lint finds nothing.
1900
+ */
1901
+ vectorNamespaceAccesses?: ReadonlyArray<AdvisorVectorNamespaceAccess>;
1902
+ /**
1903
+ * `ctx.workflows.get("name")` call sites discovered in function bodies — the
1904
+ * use-side input the `workflow_unused` and `workflow_unknown_target` lints
1905
+ * cross-reference against {@link LintContext.workflows}. Supplied by the
1906
+ * codegen feeder; absent for runtime callers, where the workflow lints find
1907
+ * nothing.
1908
+ */
753
1909
  workflowCalls?: ReadonlyArray<AdvisorWorkflowCall>;
754
1910
  /**
755
- * Workflows declared via `defineWorkflow` exports in `lunora/workflows.ts` —
756
- * the declaration-side input for the `workflow_*` lints. Supplied by the
757
- * codegen feeder; absent for runtime callers, where the workflow lints find
758
- * nothing.
759
- */
1911
+ * Workflows declared via `defineWorkflow` exports in `lunora/workflows.ts` —
1912
+ * the declaration-side input for the `workflow_*` lints. Supplied by the
1913
+ * codegen feeder; absent for runtime callers, where the workflow lints find
1914
+ * nothing.
1915
+ */
760
1916
  workflows?: ReadonlyArray<AdvisorWorkflow>;
1917
+ /**
1918
+ * Committed `wrangler.jsonc` `vars` entries holding plaintext secrets — the
1919
+ * input for the `plaintext_secret_in_wrangler_vars` lint. Supplied by
1920
+ * `@lunora/config` (which reads `wrangler.jsonc`) via the codegen pass-through;
1921
+ * absent for runtime callers, where the lint finds nothing.
1922
+ */
1923
+ wranglerVariables?: ReadonlyArray<AdvisorWranglerVariable>;
761
1924
  }
762
1925
  /**
763
- * A single advisory rule. `run` is pure over its {@link LintContext} so lints are
764
- * trivially testable and order-independent. Each rule owns the static metadata
765
- * (`name`/`title`/…) that its findings inherit, keeping individual `Finding`
766
- * construction to just the per-occurrence `detail`/`metadata`/`cacheKey`.
767
- */
1926
+ * A single advisory rule. `run` is pure over its {@link LintContext} so lints are
1927
+ * trivially testable and order-independent. Each rule owns the static metadata
1928
+ * (`name`/`title`/…) that its findings inherit, keeping individual `Finding`
1929
+ * construction to just the per-occurrence `detail`/`metadata`/`cacheKey`.
1930
+ */
768
1931
  interface Lint {
769
1932
  /** Concern buckets every finding from this lint carries. */
770
1933
  categories: Category[];
@@ -786,20 +1949,20 @@ interface Lint {
786
1949
  title: string;
787
1950
  }
788
1951
  /**
789
- * Minimal structural view of the `@lunora/analytics` SQL client — just its
790
- * `query(sql)` method. Kept structural (not an `import type` from
791
- * `@lunora/analytics`) so the advisor needn't depend on the analytics package;
792
- * the real `AnalyticsSqlClient` satisfies it, as does a plain test double.
793
- */
1952
+ * Minimal structural view of the `@lunora/bindings/analytics` SQL client — just its
1953
+ * `query(sql)` method. Kept structural (not an `import type` from
1954
+ * `@lunora/bindings/analytics`) so the advisor needn't depend on the analytics package;
1955
+ * the real `AnalyticsSqlClient` satisfies it, as does a plain test double.
1956
+ */
794
1957
  interface AnalyticsMetricsSource {
795
1958
  query: (sql: string) => Promise<{
796
1959
  rows: ReadonlyArray<Record<string, unknown>>;
797
1960
  }>;
798
1961
  }
799
1962
  /**
800
- * The AE event-name + dimension-column contract the runtime writes and this
801
- * reader reads. `blob1` is the event name; dimensions start at `blob2`.
802
- */
1963
+ * The AE event-name + dimension-column contract the runtime writes and this
1964
+ * reader reads. `blob1` is the event name; dimensions start at `blob2`.
1965
+ */
803
1966
  declare const AE_METRIC_EVENTS: {
804
1967
  /** `lunora.index.hit` — one row per `(table, index)` use. `blob2`=table, `blob3`=index. */
805
1968
  readonly indexHit: {
@@ -824,21 +1987,21 @@ interface AnalyticsMetricsOptions {
824
1987
  /** The AE dataset (the wrangler `analytics_engine_datasets[].dataset`) to read from. */
825
1988
  dataset: string;
826
1989
  /**
827
- * Declared index names per table, used to synthesise the `reads: 0` rows the
828
- * `index_utilization` dead-index half needs. AE only stores rows for indexes
829
- * that were *used*, so a never-hit index has no AE row at all; supplying the
830
- * declared set lets the reader emit an explicit `reads: 0` entry for any
831
- * declared index absent from the AE hit feed. Omit it to report only the
832
- * positive hit counts AE returns.
833
- */
1990
+ * Declared index names per table, used to synthesise the `reads: 0` rows the
1991
+ * `index_utilization` dead-index half needs. AE only stores rows for indexes
1992
+ * that were *used*, so a never-hit index has no AE row at all; supplying the
1993
+ * declared set lets the reader emit an explicit `reads: 0` entry for any
1994
+ * declared index absent from the AE hit feed. Omit it to report only the
1995
+ * positive hit counts AE returns.
1996
+ */
834
1997
  declaredIndexes?: ReadonlyArray<{
835
1998
  index: string;
836
1999
  table: string;
837
2000
  }>;
838
2001
  /**
839
- * Restrict the shard-traffic read to one sharded-function group (`blob3`).
840
- * Omit to read the whole deployment's shard set.
841
- */
2002
+ * Restrict the shard-traffic read to one sharded-function group (`blob3`).
2003
+ * Omit to read the whole deployment's shard set.
2004
+ */
842
2005
  group?: string;
843
2006
  }
844
2007
  /** The runtime-lint input arrays this module reconstructs from AE. */
@@ -848,590 +2011,1632 @@ interface AnalyticsRuntimeMetrics {
848
2011
  tableScans: AdvisorTableScan[];
849
2012
  }
850
2013
  /**
851
- * Reconstruct the runtime-lint input arrays (`shardTraffic` / `tableScans` /
852
- * `indexHits`) from the Analytics Engine SQL API. The three reads run
853
- * concurrently; each degrades to an empty array on a query failure, so a
854
- * partially-misconfigured read path still returns what it can.
855
- *
856
- * Feed the result into a {@link LintContext} alongside the declared schema:
857
- *
858
- * ```ts
859
- * const metrics = await loadAnalyticsRuntimeMetrics(client, { dataset: "ANALYTICS" });
860
- * runAdvisor({ schema, ...metrics }, { source: "runtime" });
861
- * ```
862
- */
2014
+ * Reconstruct the runtime-lint input arrays (`shardTraffic` / `tableScans` /
2015
+ * `indexHits`) from the Analytics Engine SQL API. The three reads run
2016
+ * concurrently; each degrades to an empty array on a query failure, so a
2017
+ * partially-misconfigured read path still returns what it can.
2018
+ *
2019
+ * Feed the result into a {@link LintContext} alongside the declared schema:
2020
+ *
2021
+ * ```ts
2022
+ * const metrics = await loadAnalyticsRuntimeMetrics(client, { dataset: "ANALYTICS" });
2023
+ * runAdvisor({ schema, ...metrics }, { source: "runtime" });
2024
+ * ```
2025
+ */
863
2026
  declare const loadAnalyticsRuntimeMetrics: (source: AnalyticsMetricsSource, options: AnalyticsMetricsOptions) => Promise<AnalyticsRuntimeMetrics>;
864
2027
  /**
865
- * Constraint validator flag rows that violate declared FK / NOT NULL / UNIQUE
866
- * constraints by cross-checking sampled row data against the schema.
867
- *
868
- * This lint reads the `context.tableSamples` feed (bounded row samples supplied
869
- * by the studio backend via `readTablePage`) and the declared schema. Three
870
- * families of check run over each sample:
871
- *
872
- * FK referential integrity: for every `one` relation the holding table declares,
873
- * check that each sampled row's FK column value appears in the target table's
874
- * sampled id set. A dangling value means no target row exists for the reference.
875
- *
876
- * NOT NULL / non-optional columns: the lint surfaces rows with null/undefined in
877
- * declared fields inserted before a column was added or via raw import.
878
- *
879
- * UNIQUE index violations: for each declared unique secondary index, check the
880
- * sampled rows for duplicate values across the index's columns.
881
- *
882
- * All checks are bounded by the cap in each sample; the lint never triggers an
883
- * additional read. When a sample is truncated, findings note the caveat.
884
- */
2028
+ * Disambiguate {@link Finding}s that share a `cacheKey`.
2029
+ *
2030
+ * A `cacheKey` is the studio's dedup / dismissal id (see {@link Finding.cacheKey}):
2031
+ * two findings with the same key collapse to one row, and a single dismissal
2032
+ * silences both. A lint keyed on `name:file:line` (the argument-derived sinks,
2033
+ * `sql_injection_risk`, `kv_unscoped_user_key_idor`, `hardcoded_secret`, …) can
2034
+ * legitimately emit two occurrences on one physical source line, so without a
2035
+ * within-line discriminator the second finding is silently hidden.
2036
+ *
2037
+ * This suffixes the second-and-later occurrence of any repeated key with
2038
+ * `:&lt;n>` (`:2`, `:3`, …), leaving the first occurrence unsuffixed so existing
2039
+ * single-occurrence keys stay stable across runs. Order is preserved. Keys are
2040
+ * lint-name-prefixed, so this never merges across lints.
2041
+ */
2042
+ declare const dedupeCacheKeys: (findings: ReadonlyArray<Finding>) => Finding[];
2043
+ /**
2044
+ * Constraint validator — flag rows that violate declared FK / NOT NULL / UNIQUE
2045
+ * constraints by cross-checking sampled row data against the schema.
2046
+ *
2047
+ * This lint reads the `context.tableSamples` feed (bounded row samples supplied
2048
+ * by the studio backend via `readTablePage`) and the declared schema. Three
2049
+ * families of check run over each sample:
2050
+ *
2051
+ * FK referential integrity: for every `one` relation the holding table declares,
2052
+ * check that each sampled row's FK column value appears in the target table's
2053
+ * sampled id set. A dangling value means no target row exists for the reference.
2054
+ *
2055
+ * NOT NULL / non-optional columns: the lint surfaces rows with null/undefined in
2056
+ * declared fields — inserted before a column was added or via raw import.
2057
+ *
2058
+ * UNIQUE index violations: for each declared unique secondary index, check the
2059
+ * sampled rows for duplicate values across the index's columns.
2060
+ *
2061
+ * All checks are bounded by the cap in each sample; the lint never triggers an
2062
+ * additional read. When a sample is truncated, findings note the caveat.
2063
+ */
885
2064
  declare const constraintValidator: Lint;
886
2065
  /**
887
- * `hot_shard` — flag a shard whose request share is disproportionately high.
888
- *
889
- * Sharding (`.shardBy(key)`) spreads state and load across many Durable Objects
890
- * by user / tenant / room. Its whole value is *even* distribution: when one
891
- * shard absorbs a dominant fraction of traffic, that single DO becomes the
892
- * bottleneck (one request stream, one SQLite, one WS fan-out) while its siblings
893
- * idle — the hot-key skew sharding is meant to avoid. That usually means the
894
- * shard key has too little cardinality, or one entity is unusually busy and
895
- * needs its own split.
896
- *
897
- * The per-shard request volume comes from the runtime feeder
898
- * (`context.shardTraffic`): the studio backend fans out over the function's
899
- * shards and reads each shard's recorded `__lunora_metrics` call total. The lint
900
- * is pure over that distribution, so it only fires once the window has more than
901
- * one shard and enough total requests (`MIN_TOTAL_REQUESTS`) for the proportion
902
- * to be trustworthy.
903
- */
2066
+ * `hot_shard` — flag a shard whose request share is disproportionately high.
2067
+ *
2068
+ * Sharding (`.shardBy(key)`) spreads state and load across many Durable Objects
2069
+ * by user / tenant / room. Its whole value is *even* distribution: when one
2070
+ * shard absorbs a dominant fraction of traffic, that single DO becomes the
2071
+ * bottleneck (one request stream, one SQLite, one WS fan-out) while its siblings
2072
+ * idle — the hot-key skew sharding is meant to avoid. That usually means the
2073
+ * shard key has too little cardinality, or one entity is unusually busy and
2074
+ * needs its own split.
2075
+ *
2076
+ * The per-shard request volume comes from the runtime feeder
2077
+ * (`context.shardTraffic`): the studio backend fans out over the function's
2078
+ * shards and reads each shard's recorded `__lunora_metrics` call total. The lint
2079
+ * is pure over that distribution, so it only fires once the window has more than
2080
+ * one shard and enough total requests (`MIN_TOTAL_REQUESTS`) for the proportion
2081
+ * to be trustworthy.
2082
+ */
904
2083
  declare const hotShard: Lint;
905
2084
  /**
906
- * `index_utilization` — flag indexes the workload doesn't pay for. Two
907
- * complementary checks over recorded reads.
908
- *
909
- * Dead index — a declared index that recorded reads never used. An unused index
910
- * is pure overhead: every write maintains it, every byte of storage holds it,
911
- * and nothing reads through it. Fired off the per-index hit feed
912
- * (`context.indexHits`); a declared index whose recorded `reads` is `0` is dead.
913
- * The runtime records this in the durable `__lunora_metrics_index` table (every
914
- * index use stamps a per-`(table, index)` counter via `onIndexUse`) and surfaces
915
- * it through the `getMetrics` admin RPC; the studio sums the per-shard arrays
916
- * into `context.indexHits` (see `AdvisorIndexHit`). The counter is cumulative and
917
- * never decays, so a non-zero index never reverts to "dead" — `reads: 0` means
918
- * the index has not been used once since the counter was created.
919
- *
920
- * Hot unindexed scan — a table read hot with no index at all. Fired off the
921
- * full-scan attribution the runtime does record (`context.tableScans`, sourced
922
- * from `__lunora_metrics_scans` / `FunctionCallStat.scannedTables`): a table
923
- * whose scan count clears `HOT_SCAN_THRESHOLD` is one the app keeps
924
- * full-scanning, the runtime-confirmed counterpart to the static
925
- * `filter_without_index` advisory.
926
- */
2085
+ * `index_utilization` — flag indexes the workload doesn't pay for. Two
2086
+ * complementary checks over recorded reads.
2087
+ *
2088
+ * Dead index — a declared index that recorded reads never used. An unused index
2089
+ * is pure overhead: every write maintains it, every byte of storage holds it,
2090
+ * and nothing reads through it. Fired off the per-index hit feed
2091
+ * (`context.indexHits`); a declared index whose recorded `reads` is `0` is dead.
2092
+ * The runtime records this in the durable `__lunora_metrics_index` table (every
2093
+ * index use stamps a per-`(table, index)` counter via `onIndexUse`) and surfaces
2094
+ * it through the `getMetrics` admin RPC; the studio sums the per-shard arrays
2095
+ * into `context.indexHits` (see `AdvisorIndexHit`). The counter is cumulative and
2096
+ * never decays, so a non-zero index never reverts to "dead" — `reads: 0` means
2097
+ * the index has not been used once since the counter was created.
2098
+ *
2099
+ * Hot unindexed scan — a table read hot with no index at all. Fired off the
2100
+ * full-scan attribution the runtime does record (`context.tableScans`, sourced
2101
+ * from `__lunora_metrics_scans` / `FunctionCallStat.scannedTables`): a table
2102
+ * whose scan count clears `HOT_SCAN_THRESHOLD` is one the app keeps
2103
+ * full-scanning, the runtime-confirmed counterpart to the static
2104
+ * `filter_without_index` advisory.
2105
+ */
927
2106
  declare const indexUtilization: Lint;
928
2107
  /**
929
- * Flags an `httpRoute` on an admin/privileged-looking path whose handler shows no
930
- * auth/admin guard.
931
- *
932
- * REST routes (unlike queries/mutations) aren't covered by RLS they run whatever
933
- * the handler does, so an `/admin/*` (or `/internal/*`, `/_*`) route with no
934
- * session/admin check is an open privilege door: anyone who can reach the URL can
935
- * invoke it. The handler must assert an authenticated, authorized caller
936
- * (`ctx.auth` / `getSession` / a `requireAdmin`-style guard) before doing
937
- * privileged work.
938
- *
939
- * Detection is heuristic: the feeder records whether the handler body references
940
- * any known guard token. Runs only when the codegen feeder supplies route evidence
941
- * (`context.adminRoutes`); a runtime caller flags nothing.
942
- */
2108
+ * Flags an action's `ctx.fetch(url, …)` whose URL is derived from the handler's
2109
+ * `args` — a server-side request forgery (SSRF) vector.
2110
+ *
2111
+ * `ctx.fetch` is the action-only outbound-request escape hatch, and it applies no
2112
+ * host allowlist: whatever URL it is handed, it fetches. When that URL comes from
2113
+ * request input (`ctx.fetch(args.url)`, a template embedding `args.*`, or a URL
2114
+ * built one hop earlier from `args`), a caller can point the worker at the cloud
2115
+ * metadata endpoint (`169.254.169.254`) or an internal service and read the
2116
+ * response — classic SSRF. The fix is to validate the URL against an allowlist of
2117
+ * expected hosts before fetching, and to reject private / link-local targets.
2118
+ *
2119
+ * Runs only when the codegen feeder supplies fetch-taint evidence
2120
+ * (`context.argumentDerivedFetches`); a runtime caller flags nothing. One finding per
2121
+ * arg-derived `ctx.fetch` call.
2122
+ */
2123
+ declare const actionFetchSsrf: Lint;
2124
+ /**
2125
+ * Flags an `httpRoute` on an admin/privileged-looking path whose handler shows no
2126
+ * auth/admin guard.
2127
+ *
2128
+ * REST routes (unlike queries/mutations) aren't covered by RLS — they run whatever
2129
+ * the handler does, so an `/admin/*` (or `/internal/*`, `/_*`) route with no
2130
+ * session/admin check is an open privilege door: anyone who can reach the URL can
2131
+ * invoke it. The handler must assert an authenticated, authorized caller
2132
+ * (`ctx.auth` / `getSession` / a `requireAdmin`-style guard) before doing
2133
+ * privileged work.
2134
+ *
2135
+ * Detection is heuristic: the feeder records whether the handler body references
2136
+ * any known guard token. Runs only when the codegen feeder supplies route evidence
2137
+ * (`context.adminRoutes`); a runtime caller flags nothing.
2138
+ */
943
2139
  declare const adminRouteWithoutGuard: Lint;
944
2140
  /**
945
- * Flags a `ctx.authApi.&lt;method>(...)` call whose argument object omits `headers`.
946
- *
947
- * `@lunora/auth`'s `withAuthPlugins` middleware attaches the full privileged
948
- * better-auth API to `ctx.authApi` — `banUser`, `setRole`, impersonation,
949
- * `createOrganization`, `removeMember`, etc. better-auth authorizes these calls
950
- * from the caller's session carried in the `headers` you pass. Called
951
- * **without** `headers`, better-auth treats the invocation as a trusted
952
- * server-to-server call and **skips session authorization entirely**. So a
953
- * header-less `ctx.authApi.banUser({ body })` runs with full privileges
954
- * regardless of who the caller is an authorization bypass.
955
- *
956
- * This lint runs when the codegen feeder has supplied call evidence
957
- * (`context.authApiCalls` present); a runtime caller with no evidence flags
958
- * nothing rather than raising false alarms.
959
- */
2141
+ * Flags a `ctx.ai.run(model, …)` call whose model-id argument is derived from
2142
+ * the handler's `args` with no server-side scoping — arbitrary model
2143
+ * selection that bypasses the typed AI-SDK layer.
2144
+ *
2145
+ * `ctx.ai.run` is the raw Workers AI binding passthrough (void-style
2146
+ * `ai.run`), bypassing `ctx.ai.model(...)` plus the AI SDK functions
2147
+ * (`generateText`, `streamText`, …) entirely no output cap, no schema. When
2148
+ * the model id comes straight from request input (`ctx.ai.run(args.model,
2149
+ * …)`, or one built one hop earlier from `args`), any caller can pick which
2150
+ * model runs, sidestepping whatever the typed path would have enforced. Only
2151
+ * the model argument (`arguments[0]`) is inspected; an arg-derived `inputs`
2152
+ * (`arguments[1]`) is normal, expected usage and is never flagged. A model
2153
+ * scoped by a server-trusted `ctx.*` value is treated as scoped and not
2154
+ * flagged.
2155
+ *
2156
+ * Runs only when the codegen feeder supplies raw-run evidence
2157
+ * (`context.aiRawRuns`); a runtime caller flags nothing. One finding per
2158
+ * arg-derived, unscoped `ctx.ai.run` call.
2159
+ */
2160
+ declare const aiRawRunEscapeHatch: Lint;
2161
+ /**
2162
+ * Flags a `generateText` / `streamText` call whose model input is user-derived
2163
+ * **and** whose model-callable `tools` reach a privileged side effect.
2164
+ *
2165
+ * The AI SDK lets the model call a `tool({ execute })` to take an action. When
2166
+ * that tool's `execute` performs a real side effect — a DB write, a function
2167
+ * dispatch (`ctx.run`), or an outbound send (fetch / mail / queue) — and the
2168
+ * model's prompt / messages carry user-supplied text, an injected instruction in
2169
+ * that text can steer the model into firing the side effect. The model becomes a
2170
+ * confused deputy: attacker-authored words drive privileged actions. This is the
2171
+ * canonical LLM prompt-injection-to-tool-call hazard.
2172
+ *
2173
+ * Runs only when the codegen feeder supplies generation evidence
2174
+ * (`context.aiToolSideEffects`); a runtime caller flags nothing. Fires only when
2175
+ * the model input is derived from the handler's `args` — a fully server-authored
2176
+ * prompt driving a side-effecting tool is not flagged. Deliberately narrow (an
2177
+ * inherently heuristic rule): one finding per matching call.
2178
+ */
2179
+ declare const aiToolSideEffectPromptInjection: Lint;
2180
+ /**
2181
+ * Flags a public procedure that runs an AI text/object generation
2182
+ * (`generateText` / `streamText` / `generateObject` / `streamObject`) with no
2183
+ * `maxOutputTokens` bound in its config.
2184
+ *
2185
+ * Each generation call bills against the account's Workers AI / provider budget
2186
+ * in proportion to the tokens produced. Left unbounded and reachable from a
2187
+ * `.public()` procedure, an anonymous caller can request arbitrarily long
2188
+ * completions in a loop — a denial-of-wallet vector — and can also tie up worker
2189
+ * CPU/time on long streams. The fix is to cap output with `maxOutputTokens` (and
2190
+ * ideally rate-limit the entry point).
2191
+ *
2192
+ * Only a call whose config is a visible object literal is judged; a hoisted or
2193
+ * spread config is statically opaque and left un-flagged (fail-open) to avoid a
2194
+ * false positive. Runs only when the codegen feeder supplies procedure-protection
2195
+ * evidence (`context.procedureProtections`); a runtime caller flags nothing.
2196
+ */
2197
+ declare const aiUnboundedGenerationPublic: Lint;
2198
+ /**
2199
+ * Flags an app that opts into `allowUnauthenticatedShardAccess: true` while its
2200
+ * schema has an RLS gap (no `.rls("required")`, or a `.public()` table).
2201
+ *
2202
+ * `allowUnauthenticatedShardAccess` (a `WorkerOptions` field consumed by
2203
+ * `createWorker(...)`) turns off the fail-closed default that denies a shard
2204
+ * lookup for a request with no verified identity. That is a deliberate, opt-in
2205
+ * posture switch — appropriate for a public/anonymous-first shard resolver — but
2206
+ * combined with a schema that never enforces `.rls("required")` (or that leaves
2207
+ * a table `.public()`, i.e. exempt from it), an unauthenticated caller can shard-hop
2208
+ * and read another tenant's rows with no row-security guard behind the door.
2209
+ *
2210
+ * **Evidence and coverage gap**: this reads `context.configCalls`, fed by the
2211
+ * codegen `discover-config-calls.ts` feeder's `.extend(fn)` callback-shape
2212
+ * support — it only sees the setting when a `lunora/`-local file calls the
2213
+ * generated `defineApp()...extend(() => ({ allowUnauthenticatedShardAccess:
2214
+ * true }))` escape hatch (the pattern the `nuxt` / `analog` templates use in
2215
+ * `lunora/server.ts`). An app that sets the same field via `@lunora/vite`'s
2216
+ * `LunoraPluginOptions` (`vite.config.ts`) or a hand-authored worker entry
2217
+ * outside `lunora/` (the `sveltekit` / `astro` / `react-router` /
2218
+ * `tanstack-start` template style) is invisible to this lint — a coverage gap,
2219
+ * not a false negative this lint claims to catch.
2220
+ *
2221
+ * Runs only when the codegen feeder supplies config-call evidence; a runtime
2222
+ * caller flags nothing. One finding per opted-in `.extend(...)` call site.
2223
+ */
2224
+ declare const allowUnauthenticatedShardAccessEnabled: Lint;
2225
+ /**
2226
+ * Flags a `ctx.authApi.&lt;method>(...)` call whose argument object omits `headers`.
2227
+ *
2228
+ * `@lunora/auth`'s `withAuthPlugins` middleware attaches the full privileged
2229
+ * better-auth API to `ctx.authApi` — `banUser`, `setRole`, impersonation,
2230
+ * `createOrganization`, `removeMember`, etc. better-auth authorizes these calls
2231
+ * from the caller's session carried in the `headers` you pass. Called
2232
+ * **without** `headers`, better-auth treats the invocation as a trusted
2233
+ * server-to-server call and **skips session authorization entirely**. So a
2234
+ * header-less `ctx.authApi.banUser({ body })` runs with full privileges
2235
+ * regardless of who the caller is — an authorization bypass.
2236
+ *
2237
+ * This lint runs when the codegen feeder has supplied call evidence
2238
+ * (`context.authApiCalls` present); a runtime caller with no evidence flags
2239
+ * nothing rather than raising false alarms.
2240
+ */
960
2241
  declare const authApiCallWithoutHeaders: Lint;
961
2242
  /**
962
- * Detect FK cycles in the declared relation graph via a DFS.
963
- *
964
- * A "circular FK" exists when a chain of `one` relations forms a loop — for
965
- * example `A.authorId B`, `B.ownerId C`, `C.postId → A`. Such cycles can
966
- * cause unexpected behavior during DELETE operations: a CASCADE chain may loop
967
- * forever (or deadlock), and even a RESTRICT cycle prevents deletion of any row
968
- * in the loop without temporarily disabling constraints.
969
- *
970
- * Only `one` relations are followed because they are the side that owns the FK
971
- * column (the `field` lives on the holding table). `many` relations point back
972
- * to the same edge from the opposite side and would cause every edge to be
973
- * double-counted; skipping them gives the correct directed graph.
974
- *
975
- * A single-table self-reference (`A.parentId A`) is **not** reported: a
976
- * self-referential FK is the canonical, intentional shape for trees/hierarchies
977
- * (categories, org charts, threaded comments), so flagging every such schema
978
- * would be noise. Only multi-table cycles — the ones the description illustrates
979
- * — are surfaced.
980
- *
981
- * Each unique cycle is reported once: the cycle path is canonicalized to its
982
- * lexicographically smallest rotation so two DFS traversals that enter the same
983
- * ring at different nodes emit the same cacheKey and detail. A representative
984
- * cycle is reported for each distinct simple cycle in the graph; overlapping or
985
- * chord cycles that share interior nodes are each detected independently.
986
- */
2243
+ * Flags a `createAuth({...})` call whose `advanced.disableCSRFCheck` is
2244
+ * explicitly `true`.
2245
+ *
2246
+ * better-auth's CSRF check validates the request's origin against
2247
+ * `trustedOrigins` before applying a state-changing auth mutation (sign-in,
2248
+ * sign-up, session revocation, …). `disableCSRFCheck: true` turns that check
2249
+ * off outright, so a cross-site request riding the browser's ambient auth
2250
+ * cookie is processed the same as a same-origin one — the standard CSRF
2251
+ * exposure.
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 authCsrfCheckDisabled: Lint;
2260
+ /**
2261
+ * Flags a `createAuth({...})` call with `emailAndPassword.enabled: true` and no
2262
+ * `emailAndPassword.requireEmailVerification: true`.
2263
+ *
2264
+ * better-auth defaults `requireEmailVerification` off, so an email/password
2265
+ * account is usable signed in, able to act the moment it's created, before
2266
+ * the caller has proven ownership of the email address. That lets an attacker
2267
+ * sign up with a victim's email (or an address they don't control) and operate
2268
+ * the account immediately, and it weakens any downstream flow (password reset,
2269
+ * account recovery) that assumes a verified address.
2270
+ *
2271
+ * Runs only when the codegen feeder supplies auth-config evidence
2272
+ * (`context.authConfigs`), and only for an analyzable config (a static,
2273
+ * spread-free object literal); an opaque config could set the key elsewhere
2274
+ * and is skipped rather than guessed at. One finding per matching `createAuth`
2275
+ * call.
2276
+ */
2277
+ declare const authEmailVerificationDisabled: Lint;
2278
+ /**
2279
+ * Flags a `createAuth({...})` call whose `advanced.useSecureCookies` is
2280
+ * explicitly `false`.
2281
+ *
2282
+ * `@lunora/auth` defaults `useSecureCookies` ON unless the deployment's
2283
+ * `baseURL` is provably plain `http://` (a local dev origin) — see
2284
+ * `hardenAuthOptions` in `packages/auth/src/create-auth.ts`. An explicit
2285
+ * `useSecureCookies: false` overrides that secure-by-default posture, so the
2286
+ * session cookie ships without the `Secure` attribute even on an HTTPS
2287
+ * deployment — it is then sent over any plaintext connection an attacker can
2288
+ * coerce (mixed-content requests, a downgraded subdomain), exposing the
2289
+ * session.
2290
+ *
2291
+ * Runs only when the codegen feeder supplies auth-config evidence
2292
+ * (`context.authConfigs`), and only for an analyzable config (a static,
2293
+ * spread-free object literal); an opaque config could set the key elsewhere
2294
+ * and is skipped rather than guessed at. One finding per matching `createAuth`
2295
+ * call.
2296
+ */
2297
+ declare const authSecureCookiesDisabled: Lint;
2298
+ /**
2299
+ * Flags a `createAuth({...})` call whose `session.freshAge` is explicitly the
2300
+ * literal `0`.
2301
+ *
2302
+ * `freshAge` is the window better-auth treats a session as "recently
2303
+ * re-authenticated" for sensitive operations (changing the password, adding a
2304
+ * passkey, revoking other sessions, …) that gate on a fresh session rather than
2305
+ * merely a valid one. Setting it to `0` disables that recent-reauth check
2306
+ * entirely — every sensitive operation is treated as fresh regardless of how
2307
+ * old the session is, so a long-lived stolen session (or token) can perform
2308
+ * them without ever proving the caller still controls the credentials.
2309
+ *
2310
+ * Runs only when the codegen feeder supplies auth-config evidence
2311
+ * (`context.authConfigs`), and only for an analyzable config (a static,
2312
+ * spread-free object literal); an opaque config could set the key elsewhere
2313
+ * and is skipped rather than guessed at. One finding per matching `createAuth`
2314
+ * call.
2315
+ */
2316
+ declare const authSessionFreshageZero: Lint;
2317
+ /**
2318
+ * Flags a `createAuth({...})` call whose `trustedOrigins` array literal
2319
+ * contains a `"*"` entry.
2320
+ *
2321
+ * better-auth's `trustedOrigins` is the allowlist its CSRF/origin validation
2322
+ * checks every state-changing request against. A `"*"` entry disables that
2323
+ * check entirely — any origin is accepted, which is exactly the protection
2324
+ * Lunora leans on to keep cross-site requests from riding an authenticated
2325
+ * cookie.
2326
+ *
2327
+ * Runs only when the codegen feeder supplies auth-config evidence
2328
+ * (`context.authConfigs`), and only for an analyzable config (a static,
2329
+ * spread-free object literal); an opaque config could set `trustedOrigins`
2330
+ * elsewhere and is skipped rather than guessed at. One finding per matching
2331
+ * `createAuth` call.
2332
+ */
2333
+ declare const authTrustedOriginsWildcard: Lint;
2334
+ /**
2335
+ * Flags a `createBrowser({ allowPrivateTargets: true })`.
2336
+ *
2337
+ * `@lunora/browser` blocks navigation to private / internal / loopback addresses
2338
+ * by default — that guard is what stops a browser action from being turned into a
2339
+ * server-side request forgery (SSRF) tool that reaches cloud metadata endpoints
2340
+ * (`169.254.169.254`), internal services, or `localhost`. Setting
2341
+ * `allowPrivateTargets: true` disables it wholesale. Combined with a
2342
+ * request-supplied URL that is the classic SSRF-to-metadata exfiltration path.
2343
+ *
2344
+ * Runs only when the codegen feeder supplies config-call evidence
2345
+ * (`context.configCalls`); a runtime caller flags nothing. One finding per
2346
+ * opted-out browser.
2347
+ */
2348
+ declare const browserAllowPrivateTargets: Lint;
2349
+ /**
2350
+ * Flags a `ctx.browser.&lt;method>(url, …)` call whose navigation URL is derived
2351
+ * from the handler's `args` with no server-side scoping — and no hardened
2352
+ * `createBrowser` allowlist to contain it.
2353
+ *
2354
+ * `@lunora/browser` blocks navigation to private/internal/loopback addresses by
2355
+ * default, but that guard only stops SSRF to *internal* targets. A
2356
+ * request-supplied *public* URL still turns the headless browser into an
2357
+ * open-proxy / request-forgery tool: any caller can make the deployment fetch
2358
+ * an arbitrary third-party URL (SSRF to public cloud APIs that trust the egress
2359
+ * IP, data exfiltration through the fetched URL), and — without pinned DNS — a
2360
+ * public hostname can rebind to an internal address after the guard's check.
2361
+ * The containment is an `allowedHosts` allowlist on `createBrowser`, or a
2362
+ * pinned `resolveDns`. This lint therefore suppresses all findings when the
2363
+ * config-call evidence shows a `createBrowser` hardened with either key; only
2364
+ * an unhardened browser reaching an arg-derived URL is flagged.
2365
+ *
2366
+ * Runs only when the codegen feeder supplies browser URL-access evidence
2367
+ * (`context.browserUrlAccesses`); a runtime caller flags nothing. One finding
2368
+ * per arg-derived, unscoped `ctx.browser` navigation.
2369
+ */
2370
+ declare const browserUserUrlWithoutAllowlist: Lint;
2371
+ /**
2372
+ * Detect FK cycles in the declared relation graph.
2373
+ *
2374
+ * A "circular FK" exists when a chain of `one` relations forms a loop — for
2375
+ * example `A.authorId → B`, `B.ownerId → C`, `C.postId → A`. Such cycles can
2376
+ * cause unexpected behavior during DELETE operations: a CASCADE chain may loop
2377
+ * forever (or deadlock), and even a RESTRICT cycle prevents deletion of any row
2378
+ * in the loop without temporarily disabling constraints.
2379
+ *
2380
+ * Only `one` relations are followed because they are the side that owns the FK
2381
+ * column (the `field` lives on the holding table). `many` relations point back
2382
+ * to the same edge from the opposite side and would cause every edge to be
2383
+ * double-counted; skipping them gives the correct directed graph.
2384
+ *
2385
+ * A single-table self-reference (`A.parentId → A`) is **not** reported: a
2386
+ * self-referential FK is the canonical, intentional shape for trees/hierarchies
2387
+ * (categories, org charts, threaded comments), so flagging every such schema
2388
+ * would be noise. Only multi-table cycles — the ones the description illustrates
2389
+ * — are surfaced.
2390
+ *
2391
+ * Cycle enumeration uses **Johnson's algorithm** rather than a naive
2392
+ * enumerate-all-paths DFS. A plain path-DFS re-walks every simple path in the
2393
+ * graph, which is worst-case exponential even on a fully **acyclic** schema
2394
+ * (reconverging FK fan-out — many tables referencing shared parents in a chain —
2395
+ * makes codegen appear to hang). Johnson blocks a vertex once a fruitless
2396
+ * subtree is exhausted and only unblocks it when a new cycle through it is
2397
+ * found, so it runs in `O((V + E)(C + 1))` for `C` elementary circuits — no
2398
+ * blowup on acyclic input. Each circuit is enumerated exactly once from its
2399
+ * lowest-indexed member (vertices are ordered lexicographically), so overlapping
2400
+ * / chord cycles that share interior nodes are each detected independently. The
2401
+ * emitted cycle is still canonicalized to its lexicographically smallest
2402
+ * rotation for a stable cacheKey.
2403
+ */
987
2404
  declare const circularFk: Lint;
988
2405
  /**
989
- * Flags a container declared on a large instance type. The big `standard-3` /
990
- * `standard-4` sizes (and large custom shapes) are billed on their provisioned
991
- * memory + disk for the whole time an instance runs, so an over-provisioned
992
- * container is a standing cost. Informational — a real workload may need it —
993
- * but worth surfacing so the choice is deliberate.
994
- */
2406
+ * Flags a `ctx.containers.&lt;exportName>.get(name, …)` call whose instance key
2407
+ * is derived from the handler's `args` with no server-side scoping — a
2408
+ * cross-tenant container IDOR.
2409
+ *
2410
+ * A container definition's `.get(name)` accessor routes to one Durable
2411
+ * Object-backed container instance per `name` — one container per entity
2412
+ * (user, room, job…). When the key comes straight from request input
2413
+ * (`ctx.containers.app.get(args.id)`, a template embedding `args.*`, or a key
2414
+ * built one hop earlier from `args`), any caller can hand in another tenant's
2415
+ * key and reach that tenant's container instance. The fix is to derive the
2416
+ * key from a server-trusted identity (`` `${ctx.auth.userId}` ``) or a record
2417
+ * the caller owns — a key that references `ctx` is treated as scoped and is
2418
+ * not flagged. `.any()`/`.pool()` are not sinks (they take no key).
2419
+ *
2420
+ * Runs only when the codegen feeder supplies container key-access evidence
2421
+ * (`context.containerKeyAccesses`); a runtime caller flags nothing. One
2422
+ * finding per arg-derived, unscoped `ctx.containers.*.get` call.
2423
+ */
2424
+ declare const containerInstanceKeyFromUserInput: Lint;
2425
+ /**
2426
+ * Flags a container declared on a large instance type. The big `standard-3` /
2427
+ * `standard-4` sizes (and large custom shapes) are billed on their provisioned
2428
+ * memory + disk for the whole time an instance runs, so an over-provisioned
2429
+ * container is a standing cost. Informational — a real workload may need it —
2430
+ * but worth surfacing so the choice is deliberate.
2431
+ */
995
2432
  declare const containerOversizedInstance: Lint;
996
2433
  /**
997
- * Flags a container that leaves outbound internet access at the platform
998
- * default (`enableInternet: true`). Egress is billed per GB and an open
999
- * outbound path widens the attack surface, so a container that doesn't call
1000
- * external services should set `enableInternet: false` explicitly. We can't
1001
- * tell from config whether egress is actually used, so this is an INFO nudge to
1002
- * make the choice deliberate, not an error.
1003
- *
1004
- * Only fires when the field was omitted (or a non-literal we couldn't read) —
1005
- * an explicit `enableInternet: true` is treated as a deliberate opt-in and left
1006
- * alone.
1007
- */
2434
+ * Flags a container that leaves outbound internet access at the platform
2435
+ * default (`enableInternet: true`). Egress is billed per GB and an open
2436
+ * outbound path widens the attack surface, so a container that doesn't call
2437
+ * external services should set `enableInternet: false` explicitly. We can't
2438
+ * tell from config whether egress is actually used, so this is an INFO nudge to
2439
+ * make the choice deliberate, not an error.
2440
+ *
2441
+ * Only fires when the field was omitted (or a non-literal we couldn't read) —
2442
+ * an explicit `enableInternet: true` is treated as a deliberate opt-in and left
2443
+ * alone.
2444
+ */
1008
2445
  declare const containerPublicInternet: Lint;
1009
- /**
1010
- * Lunora port of splinter's `0009_duplicate_index`.
1011
- *
1012
- * A btree secondary index is redundant when another index already serves every
1013
- * lookup it does — i.e. its columns are a leading prefix of the other's
1014
- * (SQLite's leftmost-prefix rule means `["a", "b"]` already covers `["a"]`).
1015
- * Exact duplicates are the degenerate case. A redundant index is pure overhead:
1016
- * extra storage and a write amplified on every insert/update/delete.
1017
- *
1018
- * Only `kind: "index"` participates search/rank/vector indexes are distinct
1019
- * structures, never redundant with a btree. A `unique` index is never reported
1020
- * as redundant even when its columns are a prefix: it enforces a constraint the
1021
- * covering index does not, so dropping it would change behavior.
1022
- */
2446
+ declare const containerRuntimeEgressRelaxation: Lint;
2447
+ declare const containerStartEnableInternetOverride: Lint;
2448
+ /**
2449
+ * Lunora port of splinter's `0009_duplicate_index`.
2450
+ *
2451
+ * A btree secondary index is redundant when another index already serves every
2452
+ * lookup it does i.e. its columns are a leading prefix of the other's
2453
+ * (SQLite's leftmost-prefix rule means `["a", "b"]` already covers `["a"]`).
2454
+ * Exact duplicates are the degenerate case. A redundant index is pure overhead:
2455
+ * extra storage and a write amplified on every insert/update/delete.
2456
+ *
2457
+ * Only `kind: "index"` participates search/rank/vector indexes are distinct
2458
+ * structures, never redundant with a btree. A `unique` index is never reported
2459
+ * as redundant even when its columns are a prefix: it enforces a constraint the
2460
+ * covering index does not, so dropping it would change behavior.
2461
+ */
1023
2462
  declare const duplicateIndex: Lint;
1024
2463
  /**
1025
- * Flags a secondary index declared with no columns (`.index("x", [])`). The
1026
- * `.index(name, fields)` builder types `fields` as `string[]`, not
1027
- * `keyof Shape[]`, so an empty array slips past the compiler — but an index over
1028
- * zero columns indexes nothing and can never narrow a read. Almost always a
1029
- * leftover from a refactor. (Search / rank / vector indexes always carry at
1030
- * least one field by construction, so only `kind: "index"` is checked.)
1031
- */
2464
+ * Flags a secondary index declared with no columns (`.index("x", [])`). The
2465
+ * `.index(name, fields)` builder types `fields` as `string[]`, not
2466
+ * `keyof Shape[]`, so an empty array slips past the compiler — but an index over
2467
+ * zero columns indexes nothing and can never narrow a read. Almost always a
2468
+ * leftover from a refactor. (Search / rank / vector indexes always carry at
2469
+ * least one field by construction, so only `kind: "index"` is checked.)
2470
+ */
1032
2471
  declare const emptyIndex: Lint;
1033
2472
  /**
1034
- * Flags a query read that calls `.filter()` without first narrowing with
1035
- * `.withIndex()` / `.withSearchIndex()`. Such a read loads *every* row of the
1036
- * table and applies the predicate in memory — a full table scan that degrades
1037
- * linearly as the table grows. The healthy pattern is `.withIndex(...)` to
1038
- * narrow, then `.filter(...)` only for predicates the index can't express
1039
- * (which is why an indexed read with a trailing `.filter()` is NOT flagged).
1040
- *
1041
- * The query reads come from the codegen feeder, which parses
1042
- * `ctx.db.query("table")…` chains out of function bodies. Runtime callers supply
1043
- * no `queries`, so this lint is a no-op there.
1044
- */
2473
+ * Flags a `.source({ mode: "incremental" })` table that declares neither a
2474
+ * `reconcileEveryMs` sweep nor a `softDeleteColumn` (plan 136).
2475
+ *
2476
+ * Incremental ingest pulls only rows past a watermark, so an upstream **delete**
2477
+ * is invisible to it the deleted row simply stops appearing in the changed-rows
2478
+ * slice, and the locally-materialized copy lingers forever. Over time the table
2479
+ * fills with phantom rows that no longer exist upstream, which `defineShape` then
2480
+ * serves to clients as live data. That is silent data corruption, so it is an
2481
+ * `ERROR` (STOP) that fails the build.
2482
+ *
2483
+ * The fix is a declared delete-visibility path: `reconcileEveryMs` (a periodic
2484
+ * full-pull sweep that GCs vanished rows) or `softDeleteColumn` (an upstream
2485
+ * tombstone column the incremental pull returns, turned into a local delete).
2486
+ *
2487
+ * `defineSchema` throws on this exact condition too — this lint is the build-time
2488
+ * mirror (same belt-and-suspenders as `external_source_unscoped`), and it also
2489
+ * covers the `unanalyzable` config case the runtime guard can't see.
2490
+ *
2491
+ * **Evidence supply**: reads `table.externalSource.{mode,hasReconcile,hasSoftDelete}`
2492
+ * (the codegen feeder captures them from `.source({...})`; the runtime feeder
2493
+ * derives them). A non-incremental source, or one with either delete path, is skipped.
2494
+ */
2495
+ declare const externalSourceIncrementalNoDeletePath: Lint;
2496
+ /**
2497
+ * Flags a table that is both `.source(...)` and `.global()`.
2498
+ *
2499
+ * The two are contradictory. `.global()` already places a table in an external
2500
+ * store (D1, or a Hyperdrive-fronted Postgres/MySQL) that Lunora owns the schema
2501
+ * for and reads through the global backend. `.source(...)` declares the table as
2502
+ * **materialized from** an external database into a shard DO's SQLite by the
2503
+ * ingest poll loop. A table cannot simultaneously live in the global tier and be
2504
+ * polled into per-shard SQLite — the ingest loop has no DO-local table to write,
2505
+ * and the global backend has no poll loop. This is a definite misconfiguration,
2506
+ * so it is an `ERROR`.
2507
+ *
2508
+ * **Evidence supply**: reads `table.externalSource` + `table.shardKind`. Skipped
2509
+ * unless both a sourced declaration and the `global` tier are present.
2510
+ */
2511
+ declare const externalSourceOnGlobal: Lint;
2512
+ /**
2513
+ * Flags a `.source(...)` + `.shardBy(...)` table that has no `tenantBy` mapper.
2514
+ *
2515
+ * Per-shard SQLite isolation only controls *where* materialized rows land — not
2516
+ * what* the ingest query pulls. A sourced + sharded table whose `tenantBy` is
2517
+ * absent runs the same unscoped membership query on every tenant's Durable
2518
+ * Object, so each agent replicates the **entire** multitenant table into its own
2519
+ * SQLite (and then to its clients via `defineShape`). That is a cross-tenant data
2520
+ * leak, not a performance nit — so it is an `ERROR` that fails the build.
2521
+ *
2522
+ * `tenantBy(shardKey)` is the boundary: it binds this DO's shard key into the
2523
+ * query's parameters so the tenant can only ever pull its own rows.
2524
+ *
2525
+ * **Evidence supply**: reads `table.externalSource` (the codegen feeder captures
2526
+ * it from `.source({...})`; the runtime feeder derives it). A table without a
2527
+ * sourced declaration, or one not sharded, is skipped.
2528
+ */
2529
+ declare const externalSourceUnscoped: Lint;
2530
+ /**
2531
+ * Flags a query read that calls `.filter()` without first narrowing with
2532
+ * `.withIndex()` / `.withSearchIndex()`. Such a read loads *every* row of the
2533
+ * table and applies the predicate in memory — a full table scan that degrades
2534
+ * linearly as the table grows. The healthy pattern is `.withIndex(...)` to
2535
+ * narrow, then `.filter(...)` only for predicates the index can't express
2536
+ * (which is why an indexed read with a trailing `.filter()` is NOT flagged).
2537
+ *
2538
+ * The query reads come from the codegen feeder, which parses
2539
+ * `ctx.db.query("table")…` chains out of function bodies. Runtime callers supply
2540
+ * no `queries`, so this lint is a no-op there.
2541
+ */
1045
2542
  declare const filterWithoutIndex: Lint;
1046
2543
  /**
1047
- * Flags a secret-shaped string literal checked into the lunora source.
1048
- *
1049
- * A live API key, access key, private key, or high-entropy token committed to the
1050
- * codebase leaks the moment the repo is cloned, forked, or its history is read —
1051
- * and rotating it means a redeploy. Secrets belong in `.dev.vars` locally and
1052
- * `wrangler secret put` in production, read at runtime via `env`. This lint
1053
- * surfaces the same class of finding the pre-commit `vis secrets` gate catches,
1054
- * inside the studio Advisors table.
1055
- *
1056
- * Runs only when the codegen feeder supplies secret evidence
1057
- * (`context.secretLiterals`); a runtime caller flags nothing. One finding per
1058
- * literal.
1059
- */
2544
+ * Flags a `ctx.flags.boolean(key, default)` read on a security-shaped key whose
2545
+ * fail-open default selects the *permissive* branch.
2546
+ *
2547
+ * OpenFeature returns the caller-supplied default when the provider errors, so a
2548
+ * flag read is a security decision that fails to its default. When the key names
2549
+ * a protection (`enforce*`/`rls*`/`gate*`/`lockdown*`) and defaults `false`, or
2550
+ * names a permission/bypass (`allow*`/`permit*`/`bypass*`) and defaults `true`,
2551
+ * a flag-backend outage silently disables the protection or grants the
2552
+ * permission for every request.
2553
+ *
2554
+ * Runs only when the codegen feeder supplies flag-default evidence
2555
+ * (`context.flagSecurityDefaults`); a runtime caller flags nothing. Deliberately
2556
+ * narrow — matched on security-shaped key tokens with an unambiguous polarity
2557
+ * plus a boolean-literal default; keys whose polarity is indeterminate (a bare
2558
+ * `auth`/`admin`) or contradictory are skipped to keep the false-positive rate
2559
+ * low. One finding per read.
2560
+ */
2561
+ declare const flagGatesSecurityWithUnsafeDefault: Lint;
2562
+ /**
2563
+ * Flags a secret-shaped string literal checked into the lunora source.
2564
+ *
2565
+ * A live API key, access key, private key, or high-entropy token committed to the
2566
+ * codebase leaks the moment the repo is cloned, forked, or its history is read —
2567
+ * and rotating it means a redeploy. Secrets belong in `.dev.vars` locally and
2568
+ * `wrangler secret put` in production, read at runtime via `env`. This lint
2569
+ * surfaces the same class of finding the pre-commit `vis secrets` gate catches,
2570
+ * inside the studio Advisors table.
2571
+ *
2572
+ * Runs only when the codegen feeder supplies secret evidence
2573
+ * (`context.secretLiterals`); a runtime caller flags nothing. One finding per
2574
+ * literal.
2575
+ */
1060
2576
  declare const hardcodedSecret: Lint;
1061
2577
  /**
1062
- * Flags a Hyperdrive `ctx.sql` access inside a `query(...)` or `mutation(...)`
1063
- * handler body.
1064
- *
1065
- * Hyperdrive (`@lunora/hyperdrive`) points at an **external** Postgres/MySQL
1066
- * database Lunora does not own. A `ctx.sql` query is a network round-trip with a
1067
- * mutable resultnon-deterministic, exactly like `fetch` so it breaks the
1068
- * determinism the coordinator relies on when it re-runs a query on subscription
1069
- * re-evaluation or a mutation on OCC retry. Worse, external writes are invisible
1070
- * to the DO/SQLite change-feed, so a subscription will never re-fire on them.
1071
- * `ctx.sql` is therefore wired onto `ActionCtx` **only** and belongs exclusively
1072
- * in `action(...)` handlers; using it in a query/mutation is the same class of
1073
- * bug as `fetch`/`Date.now`.
1074
- *
1075
- * This is the enforcement teeth behind the action-only rule — runtime
1076
- * enforcement is still absent (see `MEMORY.md` "Query/mutation determinism not
1077
- * enforced"), so the lint is the guardrail.
1078
- *
1079
- * This lint runs when the codegen feeder has supplied access evidence
1080
- * (`context.hyperdriveCalls` present); a runtime caller with no evidence flags
1081
- * nothing rather than raising false alarms. The feeder records accesses only
1082
- * inside `query`/`mutation` handlers, so `action(...)` bodies never reach here.
1083
- */
2578
+ * Flags an `httpAction`/`httpRoute` handler that performs a side effect
2579
+ * (`ctx.runMutation` / `ctx.runAction` / a `ctx.db` write) but never reads
2580
+ * `ctx.auth`.
2581
+ *
2582
+ * Unlike `query`/`mutation`/`action` procedures which run under a resolved
2583
+ * identity and RLS a raw HTTP handler is reached directly from the public
2584
+ * internet with no framework-supplied auth step. `HttpActionCtx` still exposes
2585
+ * `ctx.auth` (`getIdentity()` / `userId`), but nothing forces the handler to
2586
+ * consult it. A handler that mutates state or dispatches an action without ever
2587
+ * touching `ctx.auth` is an unauthenticated write endpoint: any anonymous caller
2588
+ * can drive the side effect, and the downstream `runMutation`/`runAction`/`db`
2589
+ * write runs with whatever ambient authority the handler carries — bypassing the
2590
+ * identity/RLS checks the rest of the app relies on. Distinct from
2591
+ * `admin_route_without_guard`, which covers Studio/admin-path routes.
2592
+ *
2593
+ * Runs only when the codegen feeder supplies HTTP-handler evidence
2594
+ * (`context.httpActionGuards`); a runtime caller flags nothing. The feeder only
2595
+ * records handlers that already perform a side effect and whose `ctx` binding was
2596
+ * statically resolvable (a named-function or wrapped handler is skipped,
2597
+ * fail-safe), so this lint just filters to those that never read `ctx.auth`. One
2598
+ * finding per handler.
2599
+ */
2600
+ declare const httpActionMissingAuthGuard: Lint;
2601
+ /**
2602
+ * Flags an `httpAction` handler that writes a response-header value derived from
2603
+ * raw request input (`request.headers`, `request.url`/query, `await
2604
+ * request.json()`) with no CR/LF sanitizer.
2605
+ *
2606
+ * A `Request`-derived string placed verbatim into a response header lets a caller
2607
+ * smuggle carriage-return/line-feed sequences (`\r\n`) into the response — injecting
2608
+ * additional headers (`Set-Cookie`, `Location`, CORS) or splitting the response body
2609
+ * (HTTP response splitting / header injection). Unlike `query`/`mutation` handlers,
2610
+ * a raw `httpAction` builds its own `Response`, so nothing forces the value through
2611
+ * the framework's `isSafeHeaderValue` CR/LF guard.
2612
+ *
2613
+ * Runs only when the codegen feeder supplies header-write evidence
2614
+ * (`context.httpHeaderWrites`); a runtime caller flags nothing. The feeder records
2615
+ * a site only when its value is request-tainted AND unguarded — a value routed
2616
+ * through `isSafeHeaderValue`, `encodeURIComponent`/`encodeURI`, a numeric coercion
2617
+ * (`Number`/`parseInt`/`parseFloat`), or `btoa` is treated as safe and never
2618
+ * recorded. One finding per unsafe header write.
2619
+ */
2620
+ declare const httpActionResponseHeaderInjection: Lint;
2621
+ /**
2622
+ * Flags a Hyperdrive `ctx.sql` access inside a `query(...)` or `mutation(...)`
2623
+ * handler body.
2624
+ *
2625
+ * Hyperdrive (`@lunora/hyperdrive`) points at an **external** Postgres/MySQL
2626
+ * database Lunora does not own. A `ctx.sql` query is a network round-trip with a
2627
+ * mutable result — non-deterministic, exactly like `fetch` — so it breaks the
2628
+ * determinism the coordinator relies on when it re-runs a query on subscription
2629
+ * re-evaluation or a mutation on OCC retry. Worse, external writes are invisible
2630
+ * to the DO/SQLite change-feed, so a subscription will never re-fire on them.
2631
+ * `ctx.sql` is therefore wired onto `ActionCtx` **only** and belongs exclusively
2632
+ * in `action(...)` handlers; using it in a query/mutation is the same class of
2633
+ * bug as `fetch`/`Date.now`.
2634
+ *
2635
+ * This is the enforcement teeth behind the action-only rule — runtime
2636
+ * enforcement is still absent (see `MEMORY.md` "Query/mutation determinism not
2637
+ * enforced"), so the lint is the guardrail.
2638
+ *
2639
+ * This lint runs when the codegen feeder has supplied access evidence
2640
+ * (`context.hyperdriveCalls` present); a runtime caller with no evidence flags
2641
+ * nothing rather than raising false alarms. The feeder records accesses only
2642
+ * inside `query`/`mutation` handlers, so `action(...)` bodies never reach here.
2643
+ */
1084
2644
  declare const hyperdriveOutsideAction: Lint;
1085
2645
  /**
1086
- * A correctness lint with no splinter analogue it exploits Lunora's static
1087
- * edge: the schema is fully declared, so a typo'd index column is catchable at
1088
- * codegen time rather than surfacing as a runtime error or a silently
1089
- * never-matching index.
1090
- *
1091
- * Every index (secondary / search / rank / vector) names the columns it covers;
1092
- * each must be a declared column of the table (or a system field). A reference
1093
- * to an unknown column is almost always a typo or a column that was renamed
1094
- * without updating the index.
1095
- */
2646
+ * Flags an authorization read of an identity claim that is **not** in the app's
2647
+ * declared `defineIdentity({ ... })` contract.
2648
+ *
2649
+ * `defineIdentity` is the trust boundary for `ctx.auth.identity`: the worker
2650
+ * validates a resolver's returned claims against the *declared* validators, but
2651
+ * by design forwards any **undeclared** claims through verbatim, unchecked.
2652
+ * So a policy predicate or authorize hook that reads `auth.identity.&lt;key>` for a
2653
+ * `&lt;key>` the contract never declares is trusting a value the runtime never
2654
+ * validated a claim an attacker's token can carry with an arbitrary value.
2655
+ * Reading `userId` (always declared) or any declared claim is fine.
2656
+ *
2657
+ * Runs only when the codegen feeder supplies claim-read evidence
2658
+ * (`context.identityClaimReads`) — which it does only when a resolvable
2659
+ * `defineIdentity` contract exists — so an app with no typed identity contract,
2660
+ * or a runtime caller, flags nothing. One finding per undeclared read.
2661
+ */
2662
+ declare const identityUndeclaredClaimTrusted: Lint;
2663
+ /**
2664
+ * Flags a `buildImageDeliveryUrl({ key, … })` call (`@lunora/bindings/images`)
2665
+ * whose `key` is derived from the handler's `args` with no server-side scoping.
2666
+ *
2667
+ * `key` is the CDN transform's source image — an absolute URL, or an
2668
+ * origin-relative key under the account's own store — that `buildImageDeliveryUrl`
2669
+ * splices into the `/cdn-cgi/image/…` delivery URL. `ctx.images.transform`/`info`
2670
+ * take image *bytes*, never a URL, so they carry no equivalent risk and are not
2671
+ * flagged. An arg-derived `key` lets any caller point the CDN's on-the-fly
2672
+ * transform at an attacker-chosen origin (SSRF / open proxy against whatever
2673
+ * that origin trusts the CDN's egress IP for) or at an arbitrary key under the
2674
+ * account's own store.
2675
+ *
2676
+ * Runs only when the codegen feeder supplies image-delivery-URL evidence
2677
+ * (`context.imageDeliveryUrlAccesses`); a runtime caller flags nothing. One
2678
+ * finding per arg-derived, unscoped `key`.
2679
+ */
2680
+ declare const imagesUrlSourceFromUserInput: Lint;
2681
+ /**
2682
+ * A correctness lint with no splinter analogue — it exploits Lunora's static
2683
+ * edge: the schema is fully declared, so a typo'd index column is catchable at
2684
+ * codegen time rather than surfacing as a runtime error or a silently
2685
+ * never-matching index.
2686
+ *
2687
+ * Every index (secondary / search / rank / vector) names the columns it covers;
2688
+ * each must be a declared column of the table (or a system field). A reference
2689
+ * to an unknown column is almost always a typo or a column that was renamed
2690
+ * without updating the index.
2691
+ */
1096
2692
  declare const indexReferencesUnknownField: Lint;
1097
2693
  /**
1098
- * Flags a public procedure that reads a table for which at least one other
1099
- * procedure declares a column mask (evidence the developer decided that table
1100
- * carries sensitive columns), but whose own builder chain does NOT include
1101
- * `.use(mask(...))`.
1102
- *
1103
- * Lunora masking is **opt-in per procedure**: a `mask(policies)` object only
1104
- * redacts columns inside procedures whose builder chain includes
1105
- * `.use(mask(policies))`. A procedure without it returns the raw column value
1106
- * even when another procedure in the same app declares that column maskable.
1107
- * This is the "one procedure masks `users.email`, another leaks it" failure
1108
- * mode, the column-level sibling of `rls_uncovered_table`.
1109
- *
1110
- * **Granularity**: the lint is table-granular, not column-precise. Statically
1111
- * proving that a procedure *returns* a specific masked column would need
1112
- * return-shape analysis that is infeasible over the IR; instead the lint flags a
1113
- * public procedure that *reads* a mask-covered table without any
1114
- * `.use(mask(...))` of its own, and the finding lists the masked columns the
1115
- * developer flagged elsewhere. Only reads are considered — masking is a
1116
- * read/return-path concern, so writes never trigger it.
1117
- *
1118
- * **Scope**: only `public` procedures are flagged. `internal*` procedures
1119
- * (e.g. `internalQuery`, `internalMutation`) intentionally bypass masking, so
1120
- * flagging them would produce only noise. Remediation text notes this exemption.
1121
- *
1122
- * **Evidence supply**: this lint runs only when the codegen feeder has supplied
1123
- * `context.maskProcedures`; a runtime caller with no evidence flags nothing
1124
- * rather than raising false alarms.
1125
- *
1126
- * **Conservative policy detection**: when a procedure calls `mask(policies)`
1127
- * with a non-literal object (a variable reference), the feeder cannot statically
1128
- * enumerate the masked `(table, column)` pairs. In that case the procedure is
1129
- * still marked `usesMask: true` (so it is NOT itself flagged), but its pairs
1130
- * contribute nothing to the masked-column map. The lint may under-report (false
1131
- * negatives) when policies are extracted into named constants, but never
1132
- * over-reports (no false positives).
1133
- */
2694
+ * Flags a public procedure whose handler calls `ctx.db.insertManyUnsafe(...)`.
2695
+ *
2696
+ * `insertManyUnsafe` is the bulk-insert escape hatch: it writes rows straight to
2697
+ * storage, bypassing the per-row argument validators AND the schema's insert
2698
+ * triggers (which is where server-trusted columns, ownership stamping, and RLS
2699
+ * write checks live). That is acceptable for a seed script or an internal import
2700
+ * fed server-trusted rows but on a `.public()` procedure the rows are shaped
2701
+ * from request input, so the bypass lets a caller write columns they should never
2702
+ * control and skip every trigger-enforced invariant. The fix is to use the
2703
+ * validated `ctx.db.insert(...)` path, or keep the unsafe bulk write in an
2704
+ * internal function fed only server-built rows.
2705
+ *
2706
+ * Runs only when the codegen feeder supplies procedure-protection evidence
2707
+ * (`context.procedureProtections`); a runtime caller flags nothing. One finding
2708
+ * per public procedure using the unsafe bulk insert.
2709
+ */
2710
+ declare const insertManyUnsafeUserData: Lint;
2711
+ /**
2712
+ * Flags a `ctx.kv` read/write whose namespace key is derived from the handler's
2713
+ * `args` with no server-side scoping — a namespace-level insecure direct object
2714
+ * reference (IDOR).
2715
+ *
2716
+ * Workers KV is a single flat namespace with no per-caller isolation. When a key
2717
+ * comes straight from request input (`ctx.kv.get(args.key)`, a template embedding
2718
+ * `args.*`, or a key built one hop earlier from `args`), any caller can hand in
2719
+ * another user's key and read, overwrite, or delete that user's entry. The fix is
2720
+ * to prefix every key with a server-trusted identity (`` `${ctx.auth.userId}:…` ``)
2721
+ * so a caller can only ever address their own entries — a key that references
2722
+ * `ctx` is treated as scoped and is not flagged. `list` is not a sink (it takes a
2723
+ * prefix, not a per-entry key).
2724
+ *
2725
+ * Runs only when the codegen feeder supplies KV key-access evidence
2726
+ * (`context.kvKeyAccesses`); a runtime caller flags nothing. One finding per
2727
+ * arg-derived, unscoped `ctx.kv` call.
2728
+ */
2729
+ declare const kvUnscopedUserKeyIdor: Lint;
2730
+ /**
2731
+ * Flags a `createInboundEmailHandler({...})` built without a `verify` hook.
2732
+ *
2733
+ * An inbound email handler receives mail from the public internet, where the
2734
+ * `from` address is trivially spoofable and the DKIM/SPF/DMARC verdicts are only
2735
+ * meaningful if the handler actually checks them. The `verify` hook is where that
2736
+ * check belongs. Omit it and the handler trusts every message it receives — and
2737
+ * when the handler dispatches into a Lunora function (which runs under the admin
2738
+ * bearer, with RLS disabled), a forged sender can drive privileged writes: a
2739
+ * confused-deputy escalation reachable by anyone who can send an email.
2740
+ *
2741
+ * Runs only when the codegen feeder supplies config-call evidence
2742
+ * (`context.configCalls`); a runtime caller flags nothing. Skips calls whose
2743
+ * config wasn't a static object literal. One finding per unverified handler.
2744
+ */
2745
+ declare const mailInboundDispatchWithoutVerify: Lint;
2746
+ /**
2747
+ * Flags a `ctx.mail`/`ctx.email` `send`/`queue` call whose recipient field
2748
+ * (to/cc/bcc) is derived from the handler's `args` with no server-side
2749
+ * scoping.
2750
+ *
2751
+ * A recipient derived straight from request input turns the deployment into
2752
+ * an open relay / spam amplifier — any caller can direct mail to an arbitrary
2753
+ * address just by supplying it as an argument. The fix is to derive
2754
+ * recipients from server-trusted state (e.g. the authenticated user's own
2755
+ * record via `ctx.auth`), never straight from `args`; if user-chosen
2756
+ * recipients are a genuine product requirement, gate the path behind
2757
+ * authentication and rate limiting rather than leaving it open to any caller.
2758
+ *
2759
+ * Runs only when the codegen feeder supplies mail recipient-access evidence
2760
+ * (`context.mailRecipientAccesses`); a runtime caller flags nothing. One
2761
+ * finding per offending `ctx.mail`/`ctx.email` call — not per recipient field.
2762
+ */
2763
+ declare const mailRecipientFromRequestInput: Lint;
2764
+ /**
2765
+ * Flags a public procedure that reads a table for which at least one other
2766
+ * procedure declares a column mask (evidence the developer decided that table
2767
+ * carries sensitive columns), but whose own builder chain does NOT include
2768
+ * `.use(mask(...))`.
2769
+ *
2770
+ * Lunora masking is **opt-in per procedure**: a `mask(policies)` object only
2771
+ * redacts columns inside procedures whose builder chain includes
2772
+ * `.use(mask(policies))`. A procedure without it returns the raw column value —
2773
+ * even when another procedure in the same app declares that column maskable.
2774
+ * This is the "one procedure masks `users.email`, another leaks it" failure
2775
+ * mode, the column-level sibling of `rls_uncovered_table`.
2776
+ *
2777
+ * **Granularity**: the lint is table-granular, not column-precise. Statically
2778
+ * proving that a procedure *returns* a specific masked column would need
2779
+ * return-shape analysis that is infeasible over the IR; instead the lint flags a
2780
+ * public procedure that *reads* a mask-covered table without any
2781
+ * `.use(mask(...))` of its own, and the finding lists the masked columns the
2782
+ * developer flagged elsewhere. Only reads are considered — masking is a
2783
+ * read/return-path concern, so writes never trigger it.
2784
+ *
2785
+ * **Scope**: only `public` procedures are flagged. `internal*` procedures
2786
+ * (e.g. `internalQuery`, `internalMutation`) intentionally bypass masking, so
2787
+ * flagging them would produce only noise. Remediation text notes this exemption.
2788
+ *
2789
+ * **Evidence supply**: this lint runs only when the codegen feeder has supplied
2790
+ * `context.maskProcedures`; a runtime caller with no evidence flags nothing
2791
+ * rather than raising false alarms.
2792
+ *
2793
+ * **Conservative policy detection**: when a procedure calls `mask(policies)`
2794
+ * with a non-literal object (a variable reference), the feeder cannot statically
2795
+ * enumerate the masked `(table, column)` pairs. In that case the procedure is
2796
+ * still marked `usesMask: true` (so it is NOT itself flagged), but its pairs
2797
+ * contribute nothing to the masked-column map. The lint may under-report (false
2798
+ * negatives) when policies are extracted into named constants, but never
2799
+ * over-reports (no false positives).
2800
+ */
1134
2801
  declare const maskUncoveredPiiColumn: Lint;
1135
2802
  /**
1136
- * Flags a non-deterministic API call inside a `query(...)` or `mutation(...)`
1137
- * handler body.
1138
- *
1139
- * Lunora queries and mutations must be **deterministic**: the coordinator may
1140
- * re-run a mutation on optimistic-concurrency (OCC) retry and a query on
1141
- * subscription re-evaluation, so a handler that reads wall-clock time, draws
1142
- * randomness, or hits the network can produce different results on each run
1143
- * breaking read-your-writes, cache invalidation, and replayable history.
1144
- * `Date.now`, `Math.random`, `crypto.randomUUID`, `crypto.getRandomValues`, and
1145
- * `fetch` are therefore disallowed in query/mutation handlers and belong in an
1146
- * `action(...)`, which runs exactly once and may use ambient/non-deterministic
1147
- * APIs freely (pass the result into a mutation as an argument).
1148
- *
1149
- * This lint runs when the codegen feeder has supplied call evidence
1150
- * (`context.nondeterministicCalls` present); a runtime caller with no evidence
1151
- * flags nothing rather than raising false alarms. The feeder records calls only
1152
- * inside `query`/`mutation` handlers, so `action(...)` bodies never reach here.
1153
- */
2803
+ * Flags a `mask(policies)` column whose strategy is the literal `"hash"` and
2804
+ * whose column name matches a PII heuristic (`email`, `ssn`, `phone`, …).
2805
+ *
2806
+ * Lunora's `"hash"` mask strategy is an unsalted 32-bit FNV-1a digest — a
2807
+ * stable pseudonym for grouping/joining, deliberately **not** a confidentiality
2808
+ * control. Its narrow (~2^32) output space makes it brute-force-recoverable by
2809
+ * the very caller it is meant to mask from, and identical inputs always
2810
+ * produce identical tokens, so it also leaks cross-row/cross-tenant equality.
2811
+ * Applying it to a PII column reads as protection but isn't; `"redact"` (drop
2812
+ * to `null`) is the strategy that actually hides the value.
2813
+ *
2814
+ * Runs only when the codegen feeder supplies strategy evidence
2815
+ * (`context.maskStrategies`); a runtime caller with no evidence flags nothing.
2816
+ * A `MaskFn` (custom, non-literal) strategy carries no static signal and is
2817
+ * never recorded by the feeder, so it never reaches this lint either.
2818
+ */
2819
+ declare const maskWeakHashStrategyOnPii: Lint;
2820
+ /**
2821
+ * Flags a public read that hydrates a masked table's rows in the clear through a
2822
+ * `with` relation.
2823
+ *
2824
+ * Column masking (`.use(mask(...))`) is applied per-procedure to the *top-level*
2825
+ * rows of the table named in a read. It does **not** descend into relations
2826
+ * hydrated via `with` — `ctx.db.posts.findMany({ with: { author: true } })`
2827
+ * returns each `author` fully unmasked even when the `users` table is masked
2828
+ * elsewhere. So a table whose columns you carefully mask on its own reads is
2829
+ * still served in the clear whenever an unprotected parent read pulls it in as a
2830
+ * relation.
2831
+ *
2832
+ * INFO, near-zero false positives by construction: it fires only when all of
2833
+ * (1) the enclosing read is public, (2) the read declares `with: { &lt;rel> }`,
2834
+ * (3) `&lt;rel>` resolves through the schema to a real target table, and (4) that
2835
+ * target table actually has masked columns (per the discovered mask evidence).
2836
+ * Absent any mask usage the lint is a no-op. Runs only when the codegen feeder
2837
+ * supplies `context.relationLoads`; a runtime caller flags nothing. One finding
2838
+ * per `(read, masked relation)` pair.
2839
+ */
2840
+ declare const maskedRelationLeakViaWith: Lint;
2841
+ /**
2842
+ * Flags a custom mutator whose authoritative `server` impl writes a row with
2843
+ * `ctx.db.replace(id, document)` — a whole-document overwrite.
2844
+ *
2845
+ * The local-first sync engine serializes mutators in the shard DO, so two
2846
+ * mutators that touch the *same row* but *different columns* both run to
2847
+ * completion — but only if each writes its own column. A `replace` overwrites
2848
+ * the entire row from the document the mutator assembled, so a concurrent edit
2849
+ * to another column (committed between this mutator's read and its write, or
2850
+ * carried as a pending optimistic overlay on a client) is silently clobbered:
2851
+ * the kind of "two offline edits to different fields fight each other" data loss
2852
+ * a column-level merge avoids. `ctx.db.patch(id, { onlyTheChangedField })`
2853
+ * merges at the column level instead, so independent field edits coexist.
2854
+ *
2855
+ * `WARN`, not `ERROR`: `replace` is legitimate when the mutator genuinely owns
2856
+ * the whole row (a full-form save, a state-machine transition that rewrites
2857
+ * every field). The lint just surfaces the column-clobber risk so a developer
2858
+ * reaches for `patch` by default on a synced table.
2859
+ *
2860
+ * **Evidence supply**: runs only when the codegen feeder supplies
2861
+ * `context.mutatorWrites` (each a `replace` call lifted from a mutator's inline
2862
+ * `server` body); absent for runtime callers, where the lint finds nothing.
2863
+ */
2864
+ declare const mutatorFullRowReplace: Lint;
2865
+ /**
2866
+ * Flags a non-deterministic API call inside a `query(...)` or `mutation(...)`
2867
+ * handler body.
2868
+ *
2869
+ * Lunora queries and mutations must be **deterministic**: the coordinator may
2870
+ * re-run a mutation on optimistic-concurrency (OCC) retry and a query on
2871
+ * subscription re-evaluation, so a handler that reads wall-clock time, draws
2872
+ * randomness, or hits the network can produce different results on each run —
2873
+ * breaking read-your-writes, cache invalidation, and replayable history.
2874
+ * `Date.now`, `Math.random`, `crypto.randomUUID`, `crypto.getRandomValues`, and
2875
+ * `fetch` are therefore disallowed in query/mutation handlers and belong in an
2876
+ * `action(...)`, which runs exactly once and may use ambient/non-deterministic
2877
+ * APIs freely (pass the result into a mutation as an argument).
2878
+ *
2879
+ * This lint runs when the codegen feeder has supplied call evidence
2880
+ * (`context.nondeterministicCalls` present); a runtime caller with no evidence
2881
+ * flags nothing rather than raising false alarms. The feeder records calls only
2882
+ * inside `query`/`mutation` handlers, so `action(...)` bodies never reach here.
2883
+ */
1154
2884
  declare const nondeterministicQueryMutation: Lint;
1155
2885
  /**
1156
- * Flags an RLS policy whose `table` names a table that does not exist in the
1157
- * schema.
1158
- *
1159
- * A policy is bound to a table by a plain string (`definePolicy({ table:
1160
- * "documents", })`). The `rls()` middleware only applies a policy to reads and
1161
- * writes of that exact table name so a typo, a stale name after a rename, or a
1162
- * copy-paste mistake produces a policy that silently matches **nothing**. The
1163
- * table the developer believes is gated is left completely ungated, which is a
1164
- * security gap, not a mere dead-code wart: every read of the real table returns
1165
- * unrestricted rows and every write is allowed.
1166
- *
1167
- * This is strictly worse than `rls_uncovered_table` (a procedure forgetting the
1168
- * middleware): here the middleware *is* wired up, the policy *is* in the list,
1169
- * and it still does nothingthe failure is invisible at every call site.
1170
- *
1171
- * **Evidence supply**: like `rls_uncovered_table`, this runs only when the
1172
- * codegen feeder supplies `context.rlsProcedures`. The covered-table names come
1173
- * from each procedure's statically-read `rls(policies)` array (`rlsTables`); a
1174
- * policies argument that isn't a literal array contributes no names, so the lint
1175
- * under-reports rather than raising false alarms.
1176
- */
2886
+ * Flags a public `query`/`mutation` whose handler gates a `ctx.db.get`/`patch`/`delete`
2887
+ * on a null-checked `ctx.db.normalizeId(table, id)` result, with no intervening
2888
+ * ownership predicate and no RLS coverage.
2889
+ *
2890
+ * `normalizeId` performs pure structural validation it checks that a string is
2891
+ * shaped like a valid id for `table` and returns the branded id, but it **never reads
2892
+ * the database**. A non-null result therefore proves only that the id is well-formed,
2893
+ * never that the row exists or that the caller owns it. Treating "normalizeId returned
2894
+ * non-null" as authorization is an IDOR: any caller who supplies a syntactically valid
2895
+ * id of another user's row reaches it. This is an INFO-level nudge — the handler may be
2896
+ * intentionally public — it flags a place where the shape check is doing load-bearing
2897
+ * work it can't actually do.
2898
+ *
2899
+ * **Negative-proof gates** (bias toward silencea false negative is cheaper than a
2900
+ * false positive that trains users to ignore the advisor): the feeder records a row
2901
+ * only when a null-gated normalized id reaches an id-first sink, and this lint
2902
+ * additionally requires (1) `visibility === "public"` (an internal procedure trusts
2903
+ * its server caller for authorization), (2) no `.use(rls(...))` on the builder chain,
2904
+ * (3) no ownership/identity mention anywhere in the handler (`mentionsOwnership` any
2905
+ * `ctx.auth`/`ctx.identity` read or ownership-named identifier suppresses), and (4) the
2906
+ * table not covered by schema-required RLS. A handler that compares the loaded row's
2907
+ * `userId` to `ctx.auth.userId`, or a schema in `.rls("required")` mode, is never
2908
+ * flagged.
2909
+ *
2910
+ * Runs only when the codegen feeder supplies `context.normalizeIdAuthorizations`; a
2911
+ * runtime caller flags nothing.
2912
+ */
2913
+ declare const normalizeIdUsedAsAuthorization: Lint;
2914
+ /**
2915
+ * Nudges a `.public()` `query` whose handler returns raw table rows — with no
2916
+ * `.output(...)` projection and no `.use(mask(...))` — when that table carries
2917
+ * PII-named columns (`email`, `phone`, `ssn`, …).
2918
+ *
2919
+ * A public query is reachable by any client. Returning a table row verbatim ships
2920
+ * every column it holds — including PII the caller never needed — and silently
2921
+ * widens the exposed surface every time a column is added to the table later. An
2922
+ * explicit `.output(v.object({ … }))` projection (or a `.use(mask(...))` policy)
2923
+ * makes the exposed shape intentional and stops the next-added column from leaking
2924
+ * by default. This is an INFO-level nudge, not a defect: the query may be perfectly
2925
+ * fine — it flags a place worth a deliberate projection decision.
2926
+ *
2927
+ * **Low-FP gates**: the feeder records a row only when the handler returns the raw
2928
+ * read result itself (a hand-built object / array / `.map(...)` projection is not
2929
+ * recorded), and this lint additionally requires (1) `visibility === "public"`,
2930
+ * (2) no `.output(...)` and no `.use(mask(...))` on the builder chain, and (3) the
2931
+ * returned table to declare at least one PII-named column. A public query returning
2932
+ * a non-PII lookup table (`emojis`, `countries`) is never flagged.
2933
+ *
2934
+ * Runs only when the codegen feeder supplies `context.rawRowReturns`; a runtime
2935
+ * caller flags nothing.
2936
+ */
2937
+ declare const outputProjectionMissingOnPublicRead: Lint;
2938
+ /**
2939
+ * Flags a `ctx.db` write (`insert` / `replace` / `patch` / `insertManyUnsafe`)
2940
+ * that sets an ownership / identity column — `userId`, `ownerId`, `tenantId`, and
2941
+ * the like — from the handler's `args` instead of the server-trusted identity.
2942
+ *
2943
+ * The ownership column decides *who a row belongs to*. When its value comes from
2944
+ * request input (`ctx.db.insert("posts", { userId: args.userId })`), any caller
2945
+ * can claim to be anyone: pass a different `userId` / `tenantId` and the write
2946
+ * lands a row owned by another user or tenant — the classic act-as-any-user /
2947
+ * cross-tenant IDOR. The fix is to stamp the ownership column from `ctx.auth` /
2948
+ * `ctx.identity` on the server and never read it from `args`. A column set from
2949
+ * `ctx.*`, or to a fixed literal, is correct and not flagged.
2950
+ *
2951
+ * Runs only when the codegen feeder supplies owner-write evidence
2952
+ * (`context.ownerFieldWrites`); a runtime caller flags nothing. One finding per
2953
+ * offending identity-column write.
2954
+ */
2955
+ declare const ownerFieldFromArgsNotAuth: Lint;
2956
+ /**
2957
+ * Flags a `createPayment({...})` constructed without an `authorize` callback.
2958
+ *
2959
+ * The `authorize(referenceId)` hook is `@lunora/payment`'s access-control gate: it
2960
+ * runs before every charge/refund/subscription operation and decides whether the
2961
+ * current caller may act on that payment reference. Omit it and the guard is
2962
+ * effectively open — any caller who can reach a reference id (often a
2963
+ * client-supplied string) can drive money movement against someone else's
2964
+ * payment, a broken-access-control (IDOR) escalation on the most sensitive
2965
+ * surface an app has.
2966
+ *
2967
+ * The context accessor `ctx.payments` is authorization-scoped by default; this
2968
+ * lint targets the direct `createPayment(...)` factory, where the gate is
2969
+ * opt-in. Runs only when the codegen feeder supplies config-call evidence
2970
+ * (`context.configCalls`); a runtime caller flags nothing. Skips calls whose
2971
+ * config wasn't a static object literal (the key may be set on a config built
2972
+ * elsewhere). One finding per unguarded call.
2973
+ */
2974
+ declare const paymentCreateWithoutAuthorize: Lint;
2975
+ /**
2976
+ * Flags a payment webhook adapter (`createStripeAdapter` / `createPolarAdapter` /
2977
+ * `createAutumnAdapter` / `createDodoPaymentsAdapter`) configured with an
2978
+ * implausibly wide `webhookToleranceSeconds` replay window.
2979
+ *
2980
+ * The adapters reject a webhook whose signed timestamp is more than
2981
+ * `webhookToleranceSeconds` from now, so a captured-then-replayed signed payload
2982
+ * is refused once it ages past the window. The default (300s) is the recommended
2983
+ * clock-skew allowance; widening it to hours or days keeps stale, replayable
2984
+ * signed events valid long after capture, defeating the timestamp check and
2985
+ * re-opening the replay window the signature scheme exists to close.
2986
+ *
2987
+ * Runs only when the codegen feeder supplies adapter evidence
2988
+ * (`context.paymentWebhooks`); a runtime caller flags nothing. Fires only on a
2989
+ * statically-known numeric `webhookToleranceSeconds` literal above the ceiling —
2990
+ * a computed or env-sourced value is not evaluated, to keep the false-positive
2991
+ * rate low. One finding per matching adapter.
2992
+ */
2993
+ declare const paymentWebhookWideTolerance: Lint;
2994
+ /**
2995
+ * Flags a plaintext secret committed to `wrangler.jsonc`'s `vars` block.
2996
+ *
2997
+ * `vars` are **plaintext environment variables**: Wrangler bakes them into the
2998
+ * deployed Worker's bundle in cleartext, and `wrangler.jsonc` is checked into
2999
+ * source control — so a real API key, access key, private key, or token placed
3000
+ * there leaks two ways at once (every reader of the repo and the deployed bundle)
3001
+ * and rotating it means editing tracked config plus a redeploy. Secrets belong in
3002
+ * a Secrets Store binding (`ctx.secrets.get(name)`) or `wrangler secret put`, never
3003
+ * `vars`. The evidence comes from `@lunora/config`, which reads `wrangler.jsonc`
3004
+ * and applies the same secret-shape heuristics as `hardcoded_secret`, plus a
3005
+ * secret-suggestive key-name rule (`*_KEY` / `*_SECRET` / `*_TOKEN` / `*_PASSWORD`
3006
+ * / `*_DSN`), while skipping placeholders and public/publishable keys.
3007
+ *
3008
+ * Runs only when the config feeder supplies wrangler-variable evidence
3009
+ * (`context.wranglerVariables`); a runtime caller flags nothing. One finding per var.
3010
+ */
3011
+ declare const plaintextSecretInWranglerVariables: Lint;
3012
+ /**
3013
+ * Flags an RLS policy whose `table` names a table that does not exist in the
3014
+ * schema.
3015
+ *
3016
+ * A policy is bound to a table by a plain string (`definePolicy({ table:
3017
+ * "documents", … })`). The `rls()` middleware only applies a policy to reads and
3018
+ * writes of that exact table name — so a typo, a stale name after a rename, or a
3019
+ * copy-paste mistake produces a policy that silently matches **nothing**. The
3020
+ * table the developer believes is gated is left completely ungated, which is a
3021
+ * security gap, not a mere dead-code wart: every read of the real table returns
3022
+ * unrestricted rows and every write is allowed.
3023
+ *
3024
+ * This is strictly worse than `rls_uncovered_table` (a procedure forgetting the
3025
+ * middleware): here the middleware *is* wired up, the policy *is* in the list,
3026
+ * and it still does nothing — the failure is invisible at every call site.
3027
+ *
3028
+ * **Evidence supply**: like `rls_uncovered_table`, this runs only when the
3029
+ * codegen feeder supplies `context.rlsProcedures`. The covered-table names come
3030
+ * from each procedure's statically-read `rls(policies)` array (`rlsTables`); a
3031
+ * policies argument that isn't a literal array contributes no names, so the lint
3032
+ * under-reports rather than raising false alarms.
3033
+ */
1177
3034
  declare const policyReferencesUnknownTable: Lint;
1178
3035
  /**
1179
- * Flags a `v.any()` argument on a public procedure.
1180
- *
1181
- * `v.any()` disables validation: the field accepts arbitrary, untyped,
1182
- * arbitrarily-large input straight from an untrusted client. That defeats the
1183
- * end-to-end type safety Lunora exists to provide and opens the door to injection,
1184
- * prototype pollution, and oversized-payload abuse. Public input should be a
1185
- * precise validator (`v.object`, `v.string`, `v.union`, …).
1186
- *
1187
- * Runs only when the codegen feeder supplies arg evidence
1188
- * (`context.argValidators`, public procedures only); a runtime caller flags
1189
- * nothing. One finding per offending arg.
1190
- */
3036
+ * Flags a `ctx.run`/`context.run` back into a Lunora function from inside a
3037
+ * `defineQueue` push handler or a `defineWorkflow` handler, when the dispatch's
3038
+ * args reference the handler's untrusted payload (`context.params` for a
3039
+ * workflow, a `for (… of batch.messages)` body for a queue) **and** the target
3040
+ * enforces row-level security.
3041
+ *
3042
+ * A queue/workflow handler runs under the deployment's **system identity** with
3043
+ * end-user RLS disabled — that is by design, so the handler can touch any row.
3044
+ * But the payload it receives is attacker-influenced: a queue body is whatever
3045
+ * was enqueued (often straight from a public mutation's `args`), and a
3046
+ * workflow's `params` are set by the `.create({ params })` caller. Forwarding
3047
+ * that payload into a function whose own protection is a *row policy* is a
3048
+ * confused-deputy: the policy that would have rejected the caller's request is
3049
+ * skipped because the handler, not the user, is now the principal. Any field
3050
+ * the payload controls that the target's RLS keys on (owner id, tenant id, row
3051
+ * id) becomes an act-as-any-user / cross-tenant write.
3052
+ *
3053
+ * The lint is deliberately narrow to stay false-positive-free: it fires **only**
3054
+ * when the resolved target (`api.&lt;file>.&lt;export>`) is found in the
3055
+ * RLS-procedure evidence with `usesRls: true`. A dispatch into a function that
3056
+ * does its own arg validation and carries no row policy (the common, correct
3057
+ * case — e.g. a welcome-message mutation with no `rls`) is not flagged. Runs
3058
+ * only when the codegen feeder supplies dispatch evidence
3059
+ * (`context.privilegedDispatches`); a runtime caller flags nothing.
3060
+ */
3061
+ declare const privilegedDispatchUnvalidatedPayload: Lint;
3062
+ /**
3063
+ * Flags a public procedure whose handler fans work out to a privileged,
3064
+ * cost-bearing dispatch surface without a rate-limit guard.
3065
+ *
3066
+ * `ctx.scheduler.runAfter` / `runAt`, a `ctx.queues.&lt;name>` producer send, and
3067
+ * `ctx.workflows.&lt;name>.create` all enqueue work that runs later under the
3068
+ * system identity (RLS disabled) and bills against the account. Triggered from a
3069
+ * `.public()` procedure with no rate limit, an anonymous caller can drive that
3070
+ * dispatch in a loop — a cost-amplification / denial-of-wallet vector, and a way
3071
+ * to flood a privileged async surface. The fix is to gate the public entry point
3072
+ * with a rate limit (or make it internal and trigger it from a guarded path).
3073
+ *
3074
+ * Runs only when the codegen feeder supplies procedure-protection evidence
3075
+ * (`context.procedureProtections`); a runtime caller flags nothing. One finding
3076
+ * per unguarded public fan-out procedure.
3077
+ */
3078
+ declare const privilegedFanoutFromPublicProcedure: Lint;
3079
+ /**
3080
+ * Flags a `v.any()` argument on a public procedure.
3081
+ *
3082
+ * `v.any()` disables validation: the field accepts arbitrary, untyped,
3083
+ * arbitrarily-large input straight from an untrusted client. That defeats the
3084
+ * end-to-end type safety Lunora exists to provide and opens the door to injection,
3085
+ * prototype pollution, and oversized-payload abuse. Public input should be a
3086
+ * precise validator (`v.object`, `v.string`, `v.union`, …).
3087
+ *
3088
+ * Runs only when the codegen feeder supplies arg evidence
3089
+ * (`context.argValidators`, public procedures only); a runtime caller flags
3090
+ * nothing. One finding per offending arg.
3091
+ */
1191
3092
  declare const publicArgumentUsesAny: Lint;
1192
3093
  /**
1193
- * Flags a public `mutation`/`action` whose builder chain installs no rate limit.
1194
- *
1195
- * Every publicly-callable write is a flood target: without a `rateLimit`
1196
- * middleware a single client can hammer it to exhaust D1 writes, send-mail quota,
1197
- * or paid credits, and brute-force auth-shaped endpoints (login / reset / OTP).
1198
- * Lunora ships `rateLimit()` (`@lunora/ratelimit`) and the `protectPublic({...})`
1199
- * bundle for exactly this; this lint fires when neither is present on a public
1200
- * write.
1201
- *
1202
- * Runs only when the codegen feeder supplies protection evidence
1203
- * (`context.procedureProtections`); a runtime caller with no evidence flags
1204
- * nothing. `query` is read-only and excluded; internal functions are
1205
- * server-called and excluded.
1206
- */
3094
+ * Flags a public `mutation`/`action` whose builder chain installs no rate limit.
3095
+ *
3096
+ * Every publicly-callable write is a flood target: without a `rateLimit`
3097
+ * middleware a single client can hammer it to exhaust D1 writes, send-mail quota,
3098
+ * or paid credits, and brute-force auth-shaped endpoints (login / reset / OTP).
3099
+ * Lunora ships `rateLimit()` (`@lunora/ratelimit`) and the `protectPublic({...})`
3100
+ * bundle for exactly this; this lint fires when neither is present on a public
3101
+ * write.
3102
+ *
3103
+ * Runs only when the codegen feeder supplies protection evidence
3104
+ * (`context.procedureProtections`); a runtime caller with no evidence flags
3105
+ * nothing. `query` is read-only and excluded; internal functions are
3106
+ * server-called and excluded.
3107
+ */
1207
3108
  declare const publicMutationWithoutRatelimit: Lint;
1208
3109
  /**
1209
- * Flags an R2 SQL `ctx.r2sql` access inside a `query(...)` or `mutation(...)`
1210
- * handler body.
1211
- *
1212
- * R2 SQL (`@lunora/r2sql`) queries Apache Iceberg tables over an **external**
1213
- * REST endpoint Lunora does not own there is no Workers binding, every query
1214
- * is an HTTPS round-trip. A `ctx.r2sql` query is therefore non-deterministic
1215
- * (exactly like `fetch`), which breaks the determinism the coordinator relies on
1216
- * when it re-runs a query on subscription re-evaluation or a mutation on OCC
1217
- * retry. And R2 SQL reads are invisible to the DO/SQLite change-feed, so a
1218
- * subscription will never re-fire on them. `ctx.r2sql` is therefore wired onto
1219
- * `ActionCtx` **only** and belongs exclusively in `action(...)` handlers; using
1220
- * it in a query/mutation is the same class of bug as `fetch`/`Date.now`.
1221
- *
1222
- * This mirrors `hyperdrive_outside_action` the action-only enforcement teeth
1223
- * for external, non-reactive I/O. Runtime enforcement is still absent (see
1224
- * `MEMORY.md` "Query/mutation determinism not enforced"), so the lint is the
1225
- * guardrail.
1226
- *
1227
- * This lint runs when the codegen feeder has supplied access evidence
1228
- * (`context.r2sqlCalls` present); a runtime caller with no evidence flags nothing
1229
- * rather than raising false alarms. The feeder records accesses only inside
1230
- * `query`/`mutation` handlers, so `action(...)` bodies never reach here.
1231
- */
3110
+ * Flags a `.public()` table that carries ownership/tenancy- or PII-named
3111
+ * columns, on a schema that requires RLS (`.rls("required")`).
3112
+ *
3113
+ * `.public()`'s name is misleading: it does NOT mean "this table holds public
3114
+ * data" it means the OPPOSITE of that from an enforcement standpoint. It opts
3115
+ * one table OUT of the schema-wide `.rls("required")` enforcement, so its
3116
+ * `ctx.db` write path is never denied for missing RLS coverage. A table named
3117
+ * `.public()` that also carries `userId` / `email` / `ssn`-shaped columns reads
3118
+ * as "safe to expose" but is actually "exempt from the row-security guard"
3119
+ * exactly the confusion the method name invites.
3120
+ *
3121
+ * **Low-FP gate**: only flagged when (1) the schema opted into
3122
+ * `.rls("required")` at all (`.public()` is a documented no-op otherwise — see
3123
+ * {@link https://lunora.sh}'s `.public()` docs), and (2) the table's declared
3124
+ * columns match the ownership/PII heuristic ({@link ownershipOrPiiColumns}). A
3125
+ * genuinely public lookup table (e.g. `emojis`, `countries`) with no such
3126
+ * columns is not flagged.
3127
+ *
3128
+ * **Pure schema evidence**: reads only `context.schema` both `isPublic` and
3129
+ * `rlsMode` are forwarded by the runtime `fromServerSchema` and the codegen
3130
+ * `toAdvisorSchema` paths, so this lint runs identically from a live shard and
3131
+ * from codegen, with no feeder required.
3132
+ */
3133
+ declare const publicTableRlsOptoutConfusion: Lint;
3134
+ /**
3135
+ * Flags a declared queue — push or pull — that has no dead-letter queue.
3136
+ *
3137
+ * A Cloudflare Queues consumer retries a failing message up to `maxRetries`
3138
+ * times (default 3 — roughly four total delivery attempts); once that budget is
3139
+ * exhausted, a message with no `deadLetterQueue` is **deleted permanently** with
3140
+ * no record an operator can inspect. Routing exhausted messages to a DLQ turns
3141
+ * silent data loss into a backlog you can inspect, alert on, and replay. Hence
3142
+ * `WARN`/`INTERNAL`: an operator-facing reliability nudge, not a hard error — a
3143
+ * genuinely fire-and-forget queue may accept the loss.
3144
+ *
3145
+ * A queue that is itself some other queue's `deadLetterQueue` target is skipped:
3146
+ * a DLQ is a terminal sink and requiring it to have its own DLQ would recurse
3147
+ * forever (and mis-flag the best-practice scaffold, which pairs a queue with a
3148
+ * dedicated DLQ consumer).
3149
+ *
3150
+ * Only runs when the declaration feeder supplied evidence (`context.queues`
3151
+ * present); a runtime caller flags nothing.
3152
+ */
3153
+ declare const queueWithoutDlq: Lint;
3154
+ /**
3155
+ * Flags an R2 SQL `ctx.r2sql` access inside a `query(...)` or `mutation(...)`
3156
+ * handler body.
3157
+ *
3158
+ * R2 SQL (`@lunora/bindings/r2sql`) queries Apache Iceberg tables over an **external**
3159
+ * REST endpoint Lunora does not own — there is no Workers binding, every query
3160
+ * is an HTTPS round-trip. A `ctx.r2sql` query is therefore non-deterministic
3161
+ * (exactly like `fetch`), which breaks the determinism the coordinator relies on
3162
+ * when it re-runs a query on subscription re-evaluation or a mutation on OCC
3163
+ * retry. And R2 SQL reads are invisible to the DO/SQLite change-feed, so a
3164
+ * subscription will never re-fire on them. `ctx.r2sql` is therefore wired onto
3165
+ * `ActionCtx` **only** and belongs exclusively in `action(...)` handlers; using
3166
+ * it in a query/mutation is the same class of bug as `fetch`/`Date.now`.
3167
+ *
3168
+ * This mirrors `hyperdrive_outside_action` — the action-only enforcement teeth
3169
+ * for external, non-reactive I/O. Runtime enforcement is still absent (see
3170
+ * `MEMORY.md` "Query/mutation determinism not enforced"), so the lint is the
3171
+ * guardrail.
3172
+ *
3173
+ * This lint runs when the codegen feeder has supplied access evidence
3174
+ * (`context.r2sqlCalls` present); a runtime caller with no evidence flags nothing
3175
+ * rather than raising false alarms. The feeder records accesses only inside
3176
+ * `query`/`mutation` handlers, so `action(...)` bodies never reach here.
3177
+ */
1232
3178
  declare const r2sqlOutsideAction: Lint;
1233
3179
  /**
1234
- * A correctness lint covering the columns a relation wires together: the FK
1235
- * `field` and the `references` column must each exist on their respective
1236
- * tables, or the join can never resolve. Caught here at codegen time rather
1237
- * than as a runtime failure.
1238
- */
3180
+ * Flags a `new RateLimiter({...})` constructed without an explicit `store`.
3181
+ *
3182
+ * The default store is per-instance in-memory. On Cloudflare Workers each request
3183
+ * can land on a different isolate, so an in-memory counter never sums across
3184
+ * them: the limiter silently under-counts and, under real traffic, is close to a
3185
+ * no-op. A rate limit that doesn't actually limit gives a false sense of
3186
+ * protection against brute-force, enumeration, and cost-abuse — exactly the
3187
+ * attacks it was added to stop. The fix is a shared, durable store (a Durable
3188
+ * Object or KV-backed one) so every isolate reads and writes the same bucket.
3189
+ *
3190
+ * Runs only when the codegen feeder supplies config-call evidence
3191
+ * (`context.configCalls`); a runtime caller flags nothing. Skips calls whose
3192
+ * config wasn't a static object literal. One finding per limiter.
3193
+ */
3194
+ declare const ratelimitDefaultMemoryStore: Lint;
3195
+ /**
3196
+ * Flags a `rateLimit`/`dbRateLimit` middleware call (`@lunora/ratelimit`) whose
3197
+ * `key` selector is derived from the handler's `args` with no server-side
3198
+ * scoping.
3199
+ *
3200
+ * The middleware's `key` is `(ctx) => string | undefined` — a sub-key that
3201
+ * isolates the limit per caller. When the selector reads straight from `args`
3202
+ * (an email, a client-supplied id, …) instead of a server-trusted identity
3203
+ * (`ctx.auth.userId`) or the server-trusted `ctx.ip`, an attacker can rotate the
3204
+ * key on every request and never share a bucket with themselves, defeating the
3205
+ * limit entirely. A selector with no `args` reference at all — a fixed/global
3206
+ * bucket, including simply omitting `key` — is a *different*, fuzzier problem
3207
+ * (one caller can still exhaust a shared global bucket for everyone) and is
3208
+ * deliberately **not** flagged here, to keep this lint's false-positive rate
3209
+ * low: a global bucket is a legitimate choice for some limits (e.g. a
3210
+ * deployment-wide cost cap), so its absence alone is not a reliable signal.
3211
+ *
3212
+ * Runs only when the codegen feeder supplies rate-limit key-selector evidence
3213
+ * (`context.ratelimitKeySelectors`); a runtime caller flags nothing. One finding
3214
+ * per arg-derived, unscoped selector.
3215
+ */
3216
+ declare const ratelimitKeySpoofableOrGlobal: Lint;
3217
+ /**
3218
+ * Flags a `rateLimit`/`dbRateLimit`/`verifyTurnstileMiddleware` guard configured
3219
+ * `failOpen: true` on an auth/payment-sensitive procedure.
3220
+ *
3221
+ * `@lunora/ratelimit`'s `rateLimit`/`dbRateLimit` and `@lunora/auth`'s
3222
+ * `verifyTurnstileMiddleware` fail **closed** by default — a store outage or a
3223
+ * failed Turnstile siteverify rejects the request (503). Passing `failOpen: true`
3224
+ * inverts that: the middleware swallows the failure and admits the request. On a
3225
+ * sign-in / account-creation / password-reset / OTP / payment endpoint that turns
3226
+ * a transient limiter outage into an unthrottled brute-force / abuse window.
3227
+ *
3228
+ * Runs only when the codegen feeder supplies fail-open-guard evidence
3229
+ * (`context.failOpenGuards`); a runtime caller flags nothing. Deliberately narrow
3230
+ * — fires only when the options literal provably set `failOpen: true` AND the
3231
+ * guarded procedure's export name or rate-limit `name` matches an auth/payment
3232
+ * token, keeping the false-positive rate low. One finding per guard.
3233
+ */
3234
+ declare const ratelimitMiddlewareFailOpen: Lint;
3235
+ /**
3236
+ * A correctness lint covering the columns a relation wires together: the FK
3237
+ * `field` and the `references` column must each exist on their respective
3238
+ * tables, or the join can never resolve. Caught here at codegen time rather
3239
+ * than as a runtime failure.
3240
+ */
1239
3241
  declare const relationReferencesUnknownField: Lint;
1240
3242
  /**
1241
- * A correctness lint: every relation declared via `.relations((r) => …)` names a
1242
- * target table, which must exist in the schema. A target that resolves to no
1243
- * table is a typo or a reference to a table that was removed/renamed — the
1244
- * relation can never load. Caught here at codegen time rather than at runtime.
1245
- *
1246
- * (Extension tables are already namespaced and their relation targets rewritten
1247
- * by the time the schema reaches a lint, so a surviving unknown target is a real
1248
- * miss, not an unresolved cross-package reference.)
1249
- */
3243
+ * A correctness lint: every relation declared via `.relations((r) => …)` names a
3244
+ * target table, which must exist in the schema. A target that resolves to no
3245
+ * table is a typo or a reference to a table that was removed/renamed — the
3246
+ * relation can never load. Caught here at codegen time rather than at runtime.
3247
+ *
3248
+ * (Extension tables are already namespaced and their relation targets rewritten
3249
+ * by the time the schema reaches a lint, so a surviving unknown target is a real
3250
+ * miss, not an unresolved cross-package reference.)
3251
+ */
1250
3252
  declare const relationReferencesUnknownTable: Lint;
1251
3253
  /**
1252
- * Flags a public procedure that reads or writes a table named in at least one
1253
- * other procedure's `rls(policies)` list, but whose own builder chain does NOT
1254
- * include `.use(rls(...))`.
1255
- *
1256
- * Lunora RLS is **opt-in per procedure**: a policy list only takes effect
1257
- * inside procedures whose builder chain includes `.use(rls(policies))`. A
1258
- * procedure without it sees the raw, unwrapped `ctx.db` and silently bypasses
1259
- * every policy in the list — even when another procedure in the same app
1260
- * declares that table as policy-gated.
1261
- *
1262
- * The lint surfaces the most dangerous subclass of this failure mode: a table
1263
- * that the developer explicitly decided to gate with RLS (evidenced by naming
1264
- * it in at least one procedure's policy list) is nonetheless accessible
1265
- * without restriction from a procedure that forgot the `.use(rls(...))` call.
1266
- *
1267
- * **Scope**: only `public` procedures are flagged. `internal*` procedures
1268
- * (e.g. `internalQuery`, `internalMutation`) are intentional server-side
1269
- * helpers that legitimately bypass the user-facing RLS gate, so flagging them
1270
- * would produce only noise. Remediation text notes this exemption so authors
1271
- * know to use `internalQuery`/`internalMutation`/`internalAction` when they
1272
- * truly need unwrapped access.
1273
- *
1274
- * **Evidence supply**: this lint runs only when the codegen feeder has supplied
1275
- * `context.rlsProcedures`; a runtime caller with no evidence flags nothing
1276
- * rather than raising false alarms.
1277
- *
1278
- * **Conservative policy-table detection**: when a procedure calls
1279
- * `rls(policies)` with a non-literal array (a variable reference), the feeder
1280
- * cannot statically enumerate the covered tables. In that case the procedure is
1281
- * still marked `usesRls: true` (so it is NOT itself flagged), but its tables
1282
- * contribute nothing to `policyCoveredTables`. This means the lint may
1283
- * under-report (false negatives) when policies are extracted into named
1284
- * constants, but it never over-reports (no false positives).
1285
- */
3254
+ * Flags a public procedure that reads or writes a table named in at least one
3255
+ * other procedure's `rls(policies)` list, but whose own builder chain does NOT
3256
+ * include `.use(rls(...))`.
3257
+ *
3258
+ * Lunora RLS is **opt-in per procedure**: a policy list only takes effect
3259
+ * inside procedures whose builder chain includes `.use(rls(policies))`. A
3260
+ * procedure without it sees the raw, unwrapped `ctx.db` and silently bypasses
3261
+ * every policy in the list — even when another procedure in the same app
3262
+ * declares that table as policy-gated.
3263
+ *
3264
+ * The lint surfaces the most dangerous subclass of this failure mode: a table
3265
+ * that the developer explicitly decided to gate with RLS (evidenced by naming
3266
+ * it in at least one procedure's policy list) is nonetheless accessible
3267
+ * without restriction from a procedure that forgot the `.use(rls(...))` call.
3268
+ *
3269
+ * **Scope**: only `public` procedures are flagged. `internal*` procedures
3270
+ * (e.g. `internalQuery`, `internalMutation`) are intentional server-side
3271
+ * helpers that legitimately bypass the user-facing RLS gate, so flagging them
3272
+ * would produce only noise. Remediation text notes this exemption so authors
3273
+ * know to use `internalQuery`/`internalMutation`/`internalAction` when they
3274
+ * truly need unwrapped access.
3275
+ *
3276
+ * **Evidence supply**: this lint runs only when the codegen feeder has supplied
3277
+ * `context.rlsProcedures`; a runtime caller with no evidence flags nothing
3278
+ * rather than raising false alarms.
3279
+ *
3280
+ * **Conservative policy-table detection**: when a procedure calls
3281
+ * `rls(policies)` with a non-literal array (a variable reference), the feeder
3282
+ * cannot statically enumerate the covered tables. In that case the procedure is
3283
+ * still marked `usesRls: true` (so it is NOT itself flagged), but its tables
3284
+ * contribute nothing to `policyCoveredTables`. This means the lint may
3285
+ * under-report (false negatives) when policies are extracted into named
3286
+ * constants, but it never over-reports (no false positives).
3287
+ */
1286
3288
  declare const rlsUncoveredTable: Lint;
1287
3289
  /**
1288
- * Flags a `ctx.sql` tagged-template that splices an unparameterized
1289
- * string-building expression into the query.
1290
- *
1291
- * The Hyperdrive `ctx.sql\`…\`` driver binds each `${value}` placeholder as a
1292
- * query parameter safe by construction. But a placeholder that *builds* a string
1293
- * in place (`ctx.sql\`… ${"WHERE name='" + name + "'"}\``, or a nested template
1294
- * literal) splices raw, attacker-controlled text into the SQL, reopening classic
1295
- * SQL injection. The fix is always to pass the value through a placeholder so the
1296
- * driver parameterizes it.
1297
- *
1298
- * Runs only when the codegen feeder supplies interpolation evidence
1299
- * (`context.sqlInterpolations`); a runtime caller flags nothing. One finding per
1300
- * interpolation.
1301
- */
3290
+ * Flags a replication shape whose `table` is a `.global()` table.
3291
+ *
3292
+ * Poke-live replication is a per-shard-DO property: the shard owns its SQLite
3293
+ * and a monotonic `__cdc_log`, so a write produces an ordered op the DO pokes to
3294
+ * every subscriber at the next flush. A `.global()` table lives outside the
3295
+ * shard DO's SQLite op-log (in a global backend D1, or Hyperdrive-fronted
3296
+ * Postgres/MySQL) so a shape over a global table cannot be poke-live. It is
3297
+ * served through the cross-shard tier: **coordinator/poll-refreshed, latency-
3298
+ * tiered**, not live. That is a real and supported tier (it is the recommended
3299
+ * answer for cross-shard reads — denormalize, or move the joined table to
3300
+ * `.global()` and read through the global backend), but its freshness semantics
3301
+ * differ from a sharded shape's, so the boundary is surfaced rather than hidden.
3302
+ *
3303
+ * `WARN`, not `ERROR`: a global-table shape is a legitimate design once you
3304
+ * accept the poll-refresh latency; the lint just makes the tier explicit so a
3305
+ * developer does not assume poke-live freshness.
3306
+ *
3307
+ * **Evidence supply**: runs only when the codegen feeder supplies
3308
+ * `context.shapes`; the table's tier comes from the schema's `shardKind`. A
3309
+ * shape whose table is unknown (caught by `shape_unknown_table`) or whose tier
3310
+ * the feeder didn't supply is skipped.
3311
+ */
3312
+ declare const shapeTargetsGlobalTable: Lint;
3313
+ /**
3314
+ * Flags a replication shape whose `table` names a table that does not exist in
3315
+ * the schema.
3316
+ *
3317
+ * `defineShape({ table: "messages", … })` binds a shape to a table by a plain
3318
+ * string. A live `subscribeShape("…")` resolves that shape server-side and runs
3319
+ * its membership query against the named table — so a typo, a stale name after a
3320
+ * rename, or a copy-paste mistake produces a shape that can never resolve a
3321
+ * rowset: the subscription seeds empty and then errors at the first flush
3322
+ * (`no such table`). This is a definite, build-time-detectable break, so it is
3323
+ * an `ERROR` — surfaced before the broken shape ever ships.
3324
+ *
3325
+ * **Evidence supply**: runs only when the codegen feeder supplies
3326
+ * `context.shapes`. A shape whose `table` wasn't a static string literal (no
3327
+ * resolvable name) is skipped rather than guessed at, so the lint under-reports
3328
+ * rather than raising false alarms.
3329
+ */
3330
+ declare const shapeUnknownTable: Lint;
3331
+ /**
3332
+ * Flags a public read that resurfaces soft-deleted rows via `includeDeleted` —
3333
+ * either hardcoded `true` or wired from the handler's `args`.
3334
+ *
3335
+ * A `.softDelete()` table hides deleted rows from list reads (`findMany` /
3336
+ * `findFirst`) unless the call passes `includeDeleted: true`. That opt-out is a
3337
+ * deliberate, privileged escape hatch (an admin trash view, a restore flow). On
3338
+ * a `.public()` read it becomes a leak: `includeDeleted: true` returns
3339
+ * soft-deleted rows to *every* caller, and `includeDeleted: args.showDeleted`
3340
+ * lets *any* caller flip the toggle per request — either way the "deleted" rows
3341
+ * a user believes are gone (and that your UI hides) are served straight back.
3342
+ *
3343
+ * INFO, near-zero false positives by construction: it fires only when all of
3344
+ * (1) the enclosing procedure is public, (2) the read's target is a schema table
3345
+ * that actually declares `.softDelete()`, and (3) `includeDeleted` is a hardcoded
3346
+ * `true` or arg-derived. A literal `false`, or an `includeDeleted` gated by a
3347
+ * server-trusted `ctx.*` value, is never recorded by the feeder, so a correct
3348
+ * admin-gated read is not flagged. Runs only when the codegen feeder supplies
3349
+ * `context.softDeleteReads`; a runtime caller flags nothing. One finding per
3350
+ * matching read.
3351
+ */
3352
+ declare const softDeleteIncludeDeletedFromArgs: Lint;
3353
+ /**
3354
+ * Flags a `ctx.sql` tagged-template that splices an unparameterized
3355
+ * string-building expression into the query.
3356
+ *
3357
+ * The Hyperdrive `ctx.sql\`…\`` driver binds each `${value}` placeholder as a
3358
+ * query parameter — safe by construction. But a placeholder that *builds* a string
3359
+ * in place (`ctx.sql\`… ${"WHERE name='" + name + "'"}\``, or a nested template
3360
+ * literal) splices raw, attacker-controlled text into the SQL, reopening classic
3361
+ * SQL injection. The fix is always to pass the value through a placeholder so the
3362
+ * driver parameterizes it.
3363
+ *
3364
+ * Runs only when the codegen feeder supplies interpolation evidence
3365
+ * (`context.sqlInterpolations`); a runtime caller flags nothing. One finding per
3366
+ * interpolation.
3367
+ */
1302
3368
  declare const sqlInjectionRisk: Lint;
1303
3369
  /**
1304
- * Flags a declared table that no function inserts into.
1305
- *
1306
- * Using `@lunora/codegen`'s write-side discovery (the analog of the read
1307
- * discovery that feeds `filter_without_index`), this lint cross-references every
1308
- * schema table against the set of tables some exported function writes via
1309
- * `ctx.db.insert("&lt;table>", …)`. A table with no such write either is dead schema
1310
- * or is populated through a path the static analysis can't see — a migration/seed,
1311
- * cross-region replication, the `ctx.orm.insert(...)` builder, or a trusted
1312
- * snapshot import. Hence `INFO`/`INTERNAL`: a nudge to confirm intent, not an error.
1313
- *
1314
- * A table declared `.externallyManaged()` is skipped — that flag is the explicit
1315
- * acknowledgement that its rows are written outside Lunora (an adapter/migration/
1316
- * middleware), so `@lunora/auth`'s better-auth tables and `@lunora/ratelimit`'s
1317
- * store never flag here.
1318
- *
1319
- * Only runs when the write feeder supplied evidence (`context.inserts` present);
1320
- * a runtime caller with no insert signal flags nothing rather than every table.
1321
- */
3370
+ * Flags a `ctx.storage.generateUploadUrl(key, …)` call whose options argument
3371
+ * omits `contentType`.
3372
+ *
3373
+ * `generateUploadUrl` mints a signed `PUT` URL the *client* uploads directly
3374
+ * to R2, bypassing `upload()`/`store()` entirely including their
3375
+ * `allowedContentTypes`/`maxSize` guards, which this alias never sees. The one
3376
+ * guard `generateUploadUrl` itself offers is `contentType`: passing it pins
3377
+ * the `Content-Type` into the signature, so the signed URL only authorizes a
3378
+ * PUT with exactly that content-type. Omit it and the minted URL accepts any
3379
+ * content-type/size the client chooses, entirely unchecked server-side.
3380
+ *
3381
+ * Runs only when the codegen feeder supplies storage-upload evidence
3382
+ * (`context.storageUploads`); a runtime caller flags nothing. Skips calls
3383
+ * whose options argument wasn't statically analyzable (a variable, call
3384
+ * result, or a spread) — the key may be set on an object built elsewhere. One
3385
+ * finding per unpinned call.
3386
+ */
3387
+ declare const storageGenerateUploadUrlNoContentTypePin: Lint;
3388
+ /**
3389
+ * Flags a `ctx.storage.&lt;bucket>.&lt;method>(key, …)` whose R2 object key is derived
3390
+ * from the handler's `args` with no server-side scoping — an object-level IDOR.
3391
+ *
3392
+ * The bucket read/write/URL/delete methods (`get`, `put`, `delete`, `download`,
3393
+ * `store`, `getSignedUrl`, …) take the object key as their first argument. When
3394
+ * that key comes straight from request input (`ctx.storage.docs.get(args.key)`, or
3395
+ * a key built one hop earlier from `args`), any caller can name any object — reading,
3396
+ * overwriting, or deleting another user's file. The fix is to prefix the key with a
3397
+ * server-trusted identity (`` `${ctx.auth.userId}/…` ``) or to resolve the object
3398
+ * through a record the caller is known to own; a key that references `ctx` is treated
3399
+ * as scoped and is not flagged.
3400
+ *
3401
+ * Runs only when the codegen feeder supplies storage-key evidence
3402
+ * (`context.storageKeyAccesses`); a runtime caller flags nothing. One finding per
3403
+ * offending call.
3404
+ */
3405
+ declare const storageKeyFromUserArgs: Lint;
3406
+ /**
3407
+ * Flags a `ctx.storage.getPresignedUrl(...)` call, or a `getPresignedUrl`/
3408
+ * `getSignedUrl` call whose `expiresInSeconds` sits near the shared 7-day
3409
+ * signing ceiling.
3410
+ *
3411
+ * `getPresignedUrl` mints a native S3 SigV4 URL that resolves directly
3412
+ * against R2's S3 endpoint — the holder reaches the object straight off R2,
3413
+ * **bypassing the Worker entirely**, so any auth/RLS/rate-limit gate the app
3414
+ * enforces in its own handlers never runs for that request. That's the right
3415
+ * trade for genuinely public or bulk content where the app has no per-request
3416
+ * gating to apply; it's the wrong choice for private, per-user, or
3417
+ * policy-gated content, where `getSignedUrl` (worker-signed, resolves back
3418
+ * through the app) is the fit. Separately, either signer minting a long TTL
3419
+ * near the shared 7-day ceiling hands out a bearer credential that stays
3420
+ * valid almost as long as the platform allows — a leaked link (referrer,
3421
+ * logs, browser history) then grants access for nearly a week.
3422
+ *
3423
+ * Runs only when the codegen feeder supplies storage-upload evidence
3424
+ * (`context.storageUploads`); a runtime caller flags nothing. The
3425
+ * near-ceiling check only fires on a statically-known numeric
3426
+ * `expiresInSeconds` literal — a variable or computed expression is not
3427
+ * evaluated, to keep the false-positive rate low. One finding per matching
3428
+ * call.
3429
+ */
3430
+ declare const storagePresignedUrlForPrivateContent: Lint;
3431
+ /**
3432
+ * Flags a `ctx.storage.upload`/`store` call whose options argument omits
3433
+ * `allowedContentTypes`.
3434
+ *
3435
+ * `@lunora/storage`'s `upload`/`store` accept an `allowedContentTypes`
3436
+ * allowlist that, when set, rejects a mismatched (or missing) `contentType` —
3437
+ * the control that stops an uploader from storing `text/html` or
3438
+ * `image/svg+xml` and having it served back from `publicBaseUrl`, a classic
3439
+ * stored-XSS path against your own origin. Omitting the option leaves any
3440
+ * content-type acceptable. `generateUploadUrl`'s signed PUT has no
3441
+ * `allowedContentTypes` option at all (it bypasses `upload()`'s guards
3442
+ * entirely) — that gap is `storage_generate_upload_url_no_content_type_pin`'s
3443
+ * concern, not this lint's.
3444
+ *
3445
+ * Runs only when the codegen feeder supplies storage-upload evidence
3446
+ * (`context.storageUploads`); a runtime caller flags nothing. Skips calls
3447
+ * whose options argument wasn't statically analyzable (a variable, call
3448
+ * result, or a spread) — the key may be set on an object built elsewhere. One
3449
+ * finding per unguarded call.
3450
+ */
3451
+ declare const storageUploadWithoutContentTypeAllowlist: Lint;
3452
+ /**
3453
+ * Flags a `ctx.storage.upload`/`store` call whose options argument omits
3454
+ * `maxSize`.
3455
+ *
3456
+ * `@lunora/storage`'s `upload`/`store` accept a `maxSize` byte ceiling that
3457
+ * rejects an oversized `ArrayBuffer`/`Blob` up front, and pipes a
3458
+ * `ReadableStream` through a counting `TransformStream` that aborts once the
3459
+ * limit is exceeded — without it, a caller can push an unbounded body through
3460
+ * the Worker straight into R2, exhausting storage/billing (and, for a
3461
+ * streamed body, worker CPU/time) with no cap.
3462
+ *
3463
+ * Runs only when the codegen feeder supplies storage-upload evidence
3464
+ * (`context.storageUploads`); a runtime caller flags nothing. Skips calls
3465
+ * whose options argument wasn't statically analyzable (a variable, call
3466
+ * result, or a spread) — the key may be set on an object built elsewhere. One
3467
+ * finding per unbounded call.
3468
+ */
3469
+ declare const storageUploadWithoutMaxSize: Lint;
3470
+ /**
3471
+ * Flags a declared table that no function inserts into.
3472
+ *
3473
+ * Using `@lunora/codegen`'s write-side discovery (the analog of the read
3474
+ * discovery that feeds `filter_without_index`), this lint cross-references every
3475
+ * schema table against the set of tables some exported function writes via
3476
+ * `ctx.db.insert("&lt;table>", …)`. A table with no such write either is dead schema
3477
+ * or is populated through a path the static analysis can't see — a migration/seed,
3478
+ * cross-region replication, the `ctx.orm.insert(...)` builder, or a trusted
3479
+ * snapshot import. Hence `INFO`/`INTERNAL`: a nudge to confirm intent, not an error.
3480
+ *
3481
+ * A table declared `.externallyManaged()` is skipped — that flag is the explicit
3482
+ * acknowledgement that its rows are written outside Lunora (an adapter/migration/
3483
+ * middleware), so `@lunora/auth`'s better-auth tables and `@lunora/ratelimit`'s
3484
+ * store never flag here.
3485
+ *
3486
+ * Only runs when the write feeder supplied evidence (`context.inserts` present);
3487
+ * a runtime caller with no insert signal flags nothing rather than every table.
3488
+ */
1322
3489
  declare const tableWithoutInsert: Lint;
1323
3490
  /**
1324
- * Flags a public `v.string()` argument with no length bound.
1325
- *
1326
- * A string field that accepts an unbounded value lets a client send megabytes of
1327
- * text per request — inflating storage, blowing the row/document size budget, and
1328
- * driving CPU/memory on every handler that processes it. A `.check()`/`.meta()`
1329
- * max-length bound caps the blast radius. Advisory (INFO): a deliberately-open
1330
- * free-text field is sometimes legitimate, so this nudges rather than blocks.
1331
- *
1332
- * Runs only when the codegen feeder supplies arg evidence
1333
- * (`context.argValidators`, public procedures only); a runtime caller flags
1334
- * nothing. One finding per offending arg.
1335
- */
3491
+ * Flags a public `v.string()` argument with no length bound.
3492
+ *
3493
+ * A string field that accepts an unbounded value lets a client send megabytes of
3494
+ * text per request — inflating storage, blowing the row/document size budget, and
3495
+ * driving CPU/memory on every handler that processes it. A `.check()`/`.meta()`
3496
+ * max-length bound caps the blast radius. Advisory (INFO): a deliberately-open
3497
+ * free-text field is sometimes legitimate, so this nudges rather than blocks.
3498
+ *
3499
+ * Runs only when the codegen feeder supplies arg evidence
3500
+ * (`context.argValidators`, public procedures only); a runtime caller flags
3501
+ * nothing. One finding per offending arg.
3502
+ */
1336
3503
  declare const unboundedStringArgument: Lint;
1337
3504
  /**
1338
- * Lunora port of splinter's `0001_unindexed_foreign_keys`.
1339
- *
1340
- * A `one` (many-to-one) relation declares a foreign-key column (`relation.field`)
1341
- * on the holder table pointing at the target's `references` column. If no index
1342
- * leads with that column, every read that filters or joins on the FK degrades to
1343
- * a full table scan — the canonical silent performance cliff as a table grows.
1344
- *
1345
- * Coverage follows SQLite's leftmost-prefix rule: a composite index
1346
- * `["authorId", "createdAt"]` covers lookups on `authorId`, so the FK is
1347
- * satisfied when it is the *leading* column of any declared index. `many`
1348
- * relations are skipped here — their FK lives on the opposite table and is
1349
- * caught when that table's own `one` side is audited.
1350
- */
3505
+ * Lunora port of splinter's `0001_unindexed_foreign_keys`.
3506
+ *
3507
+ * A `one` (many-to-one) relation declares a foreign-key column (`relation.field`)
3508
+ * on the holder table pointing at the target's `references` column. If no index
3509
+ * leads with that column, every read that filters or joins on the FK degrades to
3510
+ * a full table scan — the canonical silent performance cliff as a table grows.
3511
+ *
3512
+ * Coverage follows SQLite's leftmost-prefix rule: a composite index
3513
+ * `["authorId", "createdAt"]` covers lookups on `authorId`, so the FK is
3514
+ * satisfied when it is the *leading* column of any declared index. `many`
3515
+ * relations are skipped here — their FK lives on the opposite table and is
3516
+ * caught when that table's own `one` side is audited.
3517
+ */
1351
3518
  declare const unindexedForeignKey: Lint;
1352
3519
  /**
1353
- * The to-many counterpart of `unindexed_foreign_key`.
1354
- *
1355
- * A `many` relation declares its foreign-key column (`relation.field`) on the
1356
- * target table — `users.posts = r.many("posts", { field: "authorId" })` puts
1357
- * `authorId` on `posts`. A relation predicate over that relation (`{ posts: {
1358
- * some|none|every: W } }` in a `where`/RLS policy) and a `with:` child load both
1359
- * resolve by querying the target table on that FK column, so an unindexed FK
1360
- * there is the same silent full-scan cliff `unindexed_foreign_key` warns about —
1361
- * just on the other side of the relation.
1362
- *
1363
- * `unindexed_foreign_key` only audits a table's own `one` relations, so it
1364
- * catches this column **only when the target table declares the inverse `one`
1365
- * relation** (`posts.author = r.one("users", { field: "authorId" })`). A
1366
- * one-directional `many` (declared on the parent, with no inverse `one` on the
1367
- * child) slips through — that exact gap is this lint's job. To stay strictly
1368
- * complementary it skips any FK the target already covers via its own `one`
1369
- * relation (reported there) and only fires on the otherwise-unaudited column.
1370
- */
3520
+ * The to-many counterpart of `unindexed_foreign_key`.
3521
+ *
3522
+ * A `many` relation declares its foreign-key column (`relation.field`) on the
3523
+ * target table — `users.posts = r.many("posts", { field: "authorId" })` puts
3524
+ * `authorId` on `posts`. A relation predicate over that relation (`{ posts: {
3525
+ * some|none|every: W } }` in a `where`/RLS policy) and a `with:` child load both
3526
+ * resolve by querying the target table on that FK column, so an unindexed FK
3527
+ * there is the same silent full-scan cliff `unindexed_foreign_key` warns about —
3528
+ * just on the other side of the relation.
3529
+ *
3530
+ * `unindexed_foreign_key` only audits a table's own `one` relations, so it
3531
+ * catches this column **only when the target table declares the inverse `one`
3532
+ * relation** (`posts.author = r.one("users", { field: "authorId" })`). A
3533
+ * one-directional `many` (declared on the parent, with no inverse `one` on the
3534
+ * child) slips through — that exact gap is this lint's job. To stay strictly
3535
+ * complementary it skips any FK the target already covers via its own `one`
3536
+ * relation (reported there) and only fires on the otherwise-unaudited column.
3537
+ */
1371
3538
  declare const unindexedRelationTarget: Lint;
1372
3539
  /**
1373
- * Flags a public `mutation`/`action` that creates a user/session or sends mail but
1374
- * installs no CAPTCHA / bot check.
1375
- *
1376
- * Endpoints that mint accounts or trigger emails are the classic automated-abuse
1377
- * surface: credential-stuffing sign-ups, mailbox-flooding "forgot password" loops,
1378
- * and disposable-account farming. A server-verified human check (Turnstile) in
1379
- * front of them is the defense. Lunora ships `verifyTurnstile()` (`@lunora/auth`)
1380
- * and the `protectPublic({ captcha })` bundle; this lint fires when a public
1381
- * procedure writes a user/session/account-shaped table (or references `ctx.mail`)
1382
- * with no captcha middleware.
1383
- *
1384
- * Runs only when the codegen feeder supplies protection evidence
1385
- * (`context.procedureProtections`); a runtime caller with no evidence flags
1386
- * nothing.
1387
- */
3540
+ * Flags a public `mutation`/`action` that creates a user/session or sends mail but
3541
+ * installs no CAPTCHA / bot check.
3542
+ *
3543
+ * Endpoints that mint accounts or trigger emails are the classic automated-abuse
3544
+ * surface: credential-stuffing sign-ups, mailbox-flooding "forgot password" loops,
3545
+ * and disposable-account farming. A server-verified human check (Turnstile) in
3546
+ * front of them is the defense. Lunora ships `verifyTurnstile()` (`@lunora/auth`)
3547
+ * and the `protectPublic({ captcha })` bundle; this lint fires when a public
3548
+ * procedure writes a user/session/account-shaped table (or references `ctx.mail`)
3549
+ * with no captcha middleware.
3550
+ *
3551
+ * Runs only when the codegen feeder supplies protection evidence
3552
+ * (`context.procedureProtections`); a runtime caller with no evidence flags
3553
+ * nothing.
3554
+ */
1388
3555
  declare const userCreatingMutationWithoutCaptcha: Lint;
1389
3556
  /**
1390
- * A correctness lint: every `ctx.workflows.get("name")` call must reference a
1391
- * workflow that exists i.e. a `defineWorkflow` export in `lunora/workflows.ts`.
1392
- * A `.get("x")` whose `"x"` resolves to no declared workflow is a typo or a
1393
- * reference to a workflow that was removed/renamed; codegen wires the typed
1394
- * `ctx.workflows` accessor off the declared set, so the call throws at runtime.
1395
- * Caught here at codegen time instead.
1396
- *
1397
- * Calls with a non-literal name (`workflow === ""`) are skipped they can't be
1398
- * statically resolved, so they're neither confirmed-unknown here nor counted as
1399
- * a typo. Only runs when both feeders supplied evidence (declared workflows and
1400
- * discovered calls); a runtime caller flags nothing.
1401
- */
3557
+ * Flags a `ctx.vectors.query`/`upsert`/`upsertMany` call whose `namespace`
3558
+ * input is derived from the handler's `args` with no server-side scoping — a
3559
+ * tenant-partition escape.
3560
+ *
3561
+ * Vectorize namespaces partition a single index into isolated sub-collections
3562
+ * (typically one per tenant/user). When a namespace comes straight from
3563
+ * request input (`ctx.vectors.query(idx, { namespace: args.tenant })`, or a
3564
+ * value built one hop earlier from `args`), any caller can hand in another
3565
+ * tenant's namespace and read or poison that tenant's vectors. The fix is to
3566
+ * derive the namespace from a server-trusted identity (`` `${ctx.auth.orgId}` ``)
3567
+ * so a caller can only ever address their own partition — a namespace that
3568
+ * references `ctx` is treated as scoped and is not flagged.
3569
+ *
3570
+ * Runs only when the codegen feeder supplies vector-namespace evidence
3571
+ * (`context.vectorNamespaceAccesses`); a runtime caller flags nothing. One
3572
+ * finding per arg-derived, unscoped `ctx.vectors` call.
3573
+ */
3574
+ declare const vectorsNamespaceFromUserInput: Lint;
3575
+ /**
3576
+ * Flags a durable step name reused within one workflow.
3577
+ *
3578
+ * Cloudflare Workflows memoizes every `step.do` / `step.sleep` / `step.sleepUntil`
3579
+ * / `step.waitForEvent` call by its name: on replay the runtime returns the cached
3580
+ * result for a name it has already seen. Two distinct steps that share a name are
3581
+ * therefore a silent bug — the second call never runs its body and instead yields
3582
+ * the first's result, skipping the work (a charge, a write, an external wait)
3583
+ * without error. Hence `ERROR`/`INTERNAL`: it is a developer-facing correctness
3584
+ * defect in the workflow's own code, not a runtime-data nit.
3585
+ *
3586
+ * Only the first string-literal argument of each step call is compared; a step
3587
+ * named dynamically (`step.do(\`load-${id}\`, …)`) is omitted by the feeder, so a
3588
+ * deliberately-parameterized fan-out is never flagged. `ctx.runStep(stepDef, …)`
3589
+ * names (which come from `defineStep` in another file) are out of scope here.
3590
+ * Only runs when the declaration feeder supplied step evidence
3591
+ * (`workflow.steps` present); a runtime caller flags nothing.
3592
+ */
3593
+ declare const workflowDuplicateStepName: Lint;
3594
+ /**
3595
+ * A correctness lint: every `ctx.workflows.get("name")` call must reference a
3596
+ * workflow that exists — i.e. a `defineWorkflow` export in `lunora/workflows.ts`.
3597
+ * A `.get("x")` whose `"x"` resolves to no declared workflow is a typo or a
3598
+ * reference to a workflow that was removed/renamed; codegen wires the typed
3599
+ * `ctx.workflows` accessor off the declared set, so the call throws at runtime.
3600
+ * Caught here at codegen time instead.
3601
+ *
3602
+ * Calls with a non-literal name (`workflow === ""`) are skipped — they can't be
3603
+ * statically resolved, so they're neither confirmed-unknown here nor counted as
3604
+ * a typo. Only runs when both feeders supplied evidence (declared workflows and
3605
+ * discovered calls); a runtime caller flags nothing.
3606
+ */
1402
3607
  declare const workflowUnknownTarget: Lint;
1403
3608
  /**
1404
- * Flags a declared workflow that nothing starts.
1405
- *
1406
- * Cross-references every `defineWorkflow` export against the set of workflow
1407
- * names some function references via `ctx.workflows.get("&lt;name>")`. A workflow
1408
- * with no such call is either dead code (declared, deployed as a billable
1409
- * `WorkflowEntrypoint`, never triggered) or is started through a path the static
1410
- * analysis can't see — the Cloudflare REST API, a `wrangler` invocation, or a
1411
- * cross-service binding. Hence `INFO`/`INTERNAL`: a nudge to confirm intent.
1412
- *
1413
- * Suppressed entirely when any call uses a non-literal name
1414
- * (`ctx.workflows.get(someVariable)`), because a dynamic dispatch could target
1415
- * any declared workflow — flagging "unused" workflows then would be a false
1416
- * positive. Only runs when the declaration feeder supplied evidence
1417
- * (`context.workflows` present); a runtime caller flags nothing.
1418
- */
3609
+ * Flags a declared workflow that nothing starts.
3610
+ *
3611
+ * Cross-references every `defineWorkflow` export against the set of workflow
3612
+ * names some function references via `ctx.workflows.get("&lt;name>")`. A workflow
3613
+ * with no such call is either dead code (declared, deployed as a billable
3614
+ * `WorkflowEntrypoint`, never triggered) or is started through a path the static
3615
+ * analysis can't see — the Cloudflare REST API, a `wrangler` invocation, or a
3616
+ * cross-service binding. Hence `INFO`/`INTERNAL`: a nudge to confirm intent.
3617
+ *
3618
+ * Suppressed entirely when any call uses a non-literal name
3619
+ * (`ctx.workflows.get(someVariable)`), because a dynamic dispatch could target
3620
+ * any declared workflow — flagging "unused" workflows then would be a false
3621
+ * positive. Only runs when the declaration feeder supplied evidence
3622
+ * (`context.workflows` present); a runtime caller flags nothing.
3623
+ */
1419
3624
  declare const workflowUnused: Lint;
1420
3625
  /**
1421
- * Every lint that runs against the declared schema (and, for
1422
- * `filter_without_index`, the discovered query reads) — no running shard
1423
- * required. Correctness lints (`*_unknown_*`, `empty_index`) come first so a
1424
- * broken schema's errors surface above the performance advisories.
1425
- */
3626
+ * Every lint that runs against the declared schema (and, for
3627
+ * `filter_without_index`, the discovered query reads) — no running shard
3628
+ * required. Correctness lints (`*_unknown_*`, `empty_index`) come first so a
3629
+ * broken schema's errors surface above the performance advisories.
3630
+ */
1426
3631
  declare const STATIC_LINTS: ReadonlyArray<Lint>;
1427
3632
  /**
1428
- * Every lint that needs observed runtime signal (recorded metrics) rather than
1429
- * just the declared schema. They read the feeder-supplied
1430
- * {@link LintContext.shardTraffic} / {@link LintContext.tableScans} /
1431
- * {@link LintContext.indexHits}; absent that signal (a static caller) each is a
1432
- * no-op. Run them with `runAdvisor(ctx, { source: "runtime" })` against a live
1433
- * deployment's aggregated metrics.
1434
- */
3633
+ * Every lint that needs observed runtime signal (recorded metrics) rather than
3634
+ * just the declared schema. They read the feeder-supplied
3635
+ * {@link LintContext.shardTraffic} / {@link LintContext.tableScans} /
3636
+ * {@link LintContext.indexHits}; absent that signal (a static caller) each is a
3637
+ * no-op. Run them with `runAdvisor(ctx, { source: "runtime" })` against a live
3638
+ * deployment's aggregated metrics.
3639
+ */
1435
3640
  declare const RUNTIME_LINTS: ReadonlyArray<Lint>;
1436
3641
  /** The default lint set: the static lints, then the runtime lints. A caller filters by `source` to run one tier. */
1437
3642
  declare const ALL_LINTS: ReadonlyArray<Lint>;
@@ -1443,9 +3648,9 @@ interface RunAdvisorOptions {
1443
3648
  source?: LintSource;
1444
3649
  }
1445
3650
  /**
1446
- * Run lints against a context and return their findings in lint-declaration
1447
- * order. Filtering by {@link RunAdvisorOptions.source} lets a caller run only
1448
- * `static` lints at build time and defer `runtime` lints to a live shard.
1449
- */
3651
+ * Run lints against a context and return their findings in lint-declaration
3652
+ * order. Filtering by {@link RunAdvisorOptions.source} lets a caller run only
3653
+ * `static` lints at build time and defer `runtime` lints to a live shard.
3654
+ */
1450
3655
  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 };
3656
+ 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, dedupeCacheKeys, duplicateIndex, emptyIndex, externalSourceIncrementalNoDeletePath, 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 };