@lunora/advisor 0.0.0 → 1.0.0-alpha.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE.md +105 -0
- package/README.md +130 -9
- package/__assets__/package-og.svg +14 -0
- package/dist/index.d.mts +1451 -0
- package/dist/index.d.ts +1451 -0
- package/dist/index.mjs +79 -0
- package/dist/packem_shared/AE_METRIC_EVENTS-DexctYv6.mjs +85 -0
- package/dist/packem_shared/adminRouteWithoutGuard-UUGBkAjU.mjs +33 -0
- package/dist/packem_shared/authApiCallWithoutHeaders-BeJhCZaf.mjs +38 -0
- package/dist/packem_shared/circularFk-B2freHrP.mjs +84 -0
- package/dist/packem_shared/constraintValidator-Dr9Py3FD.mjs +186 -0
- package/dist/packem_shared/containerOversizedInstance-5U1VKPRM.mjs +36 -0
- package/dist/packem_shared/containerPublicInternet-CuNerJE5.mjs +30 -0
- package/dist/packem_shared/duplicateIndex-BOublMSt.mjs +57 -0
- package/dist/packem_shared/emptyIndex-BX8EuEY7.mjs +32 -0
- package/dist/packem_shared/filterWithoutIndex-BYVeJaSs.mjs +31 -0
- package/dist/packem_shared/finding-Dm_zvzS1.mjs +16 -0
- package/dist/packem_shared/fk-index-IUK1ukgs.mjs +7 -0
- package/dist/packem_shared/fromServerSchema-DinF1nph.mjs +50 -0
- package/dist/packem_shared/hardcodedSecret-W2pz1UZB.mjs +35 -0
- package/dist/packem_shared/helpers-DNCkMWZQ.mjs +4 -0
- package/dist/packem_shared/hotShard-Ir5D0B6J.mjs +48 -0
- package/dist/packem_shared/hyperdriveOutsideAction-BgZqX7Xg.mjs +30 -0
- package/dist/packem_shared/indexReferencesUnknownField-DH0_dbUY.mjs +36 -0
- package/dist/packem_shared/indexUtilization-B5DMQ3bI.mjs +45 -0
- package/dist/packem_shared/maskUncoveredPiiColumn-DjGIPG6M.mjs +61 -0
- package/dist/packem_shared/nondeterministicQueryMutation-GXES1fLp.mjs +35 -0
- package/dist/packem_shared/policyReferencesUnknownTable-DtaIEovd.mjs +38 -0
- package/dist/packem_shared/publicArgumentUsesAny-C71b2NCf.mjs +32 -0
- package/dist/packem_shared/publicMutationWithoutRatelimit-xBpJ6GWK.mjs +36 -0
- package/dist/packem_shared/r2sqlOutsideAction-CtqxvMuV.mjs +30 -0
- package/dist/packem_shared/relationReferencesUnknownField-YznyXt_7.mjs +54 -0
- package/dist/packem_shared/relationReferencesUnknownTable-DrorpKYe.mjs +33 -0
- package/dist/packem_shared/rlsUncoveredTable-CxEfZ5eZ.mjs +56 -0
- package/dist/packem_shared/sqlInjectionRisk-zwytYGLt.mjs +26 -0
- package/dist/packem_shared/tableWithoutInsert-CbbaYIP4.mjs +34 -0
- package/dist/packem_shared/unboundedStringArgument-DThg2-wt.mjs +32 -0
- package/dist/packem_shared/unindexedForeignKey-BgJbKyqK.mjs +45 -0
- package/dist/packem_shared/unindexedRelationTarget-D6eyj6Xx.mjs +53 -0
- package/dist/packem_shared/userCreatingMutationWithoutCaptcha-CH31YsUZ.mjs +42 -0
- package/dist/packem_shared/workflowUnknownTarget-Cdd7WhKQ.mjs +34 -0
- package/dist/packem_shared/workflowUnused-D0jHxdz9.mjs +38 -0
- package/package.json +40 -17
package/dist/index.d.mts
ADDED
|
@@ -0,0 +1,1451 @@
|
|
|
1
|
+
import { Schema } from '@lunora/server';
|
|
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
|
+
*/
|
|
8
|
+
interface AdvisorAdminRoute {
|
|
9
|
+
/** The exported binding name of the route handler. */
|
|
10
|
+
exportName: string;
|
|
11
|
+
/** Source file relative to the lunora dir, no extension. */
|
|
12
|
+
file: string;
|
|
13
|
+
/** HTTP verb the route binds to (uppercased), e.g. `"POST"`. */
|
|
14
|
+
method: string;
|
|
15
|
+
/** The route path, e.g. `/admin/users`. */
|
|
16
|
+
path: string;
|
|
17
|
+
/** `true` when the handler references an auth/session/admin guard. */
|
|
18
|
+
usesGuard: boolean;
|
|
19
|
+
}
|
|
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
|
+
*/
|
|
28
|
+
interface AdvisorArgumentValidator {
|
|
29
|
+
/** Arg names declared as `v.any()`. */
|
|
30
|
+
anyArgs: ReadonlyArray<string>;
|
|
31
|
+
/** The exported binding name of the procedure (e.g. `updateProfile`). */
|
|
32
|
+
exportName: string;
|
|
33
|
+
/** Source file relative to the lunora dir, no extension. */
|
|
34
|
+
file: string;
|
|
35
|
+
/** 1-based line of the registration call, or `0` when unknown. */
|
|
36
|
+
line: number;
|
|
37
|
+
/** Arg names declared as `v.string()` with no statically-visible max-length bound. */
|
|
38
|
+
unboundedStringArgs: ReadonlyArray<string>;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* One `ctx.authApi.<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
|
+
*/
|
|
45
|
+
interface AdvisorAuthApiCall {
|
|
46
|
+
/** The exported function performing the call (e.g. `createOrg`). */
|
|
47
|
+
exportName: string;
|
|
48
|
+
/** Source file the call appears in (relative to the lunora dir, no extension). */
|
|
49
|
+
file: string;
|
|
50
|
+
/** True when the call's argument object includes a `headers` property. */
|
|
51
|
+
hasHeaders: boolean;
|
|
52
|
+
/** 1-based line of the call, or `0` when unknown. */
|
|
53
|
+
line: number;
|
|
54
|
+
/** The better-auth method invoked (e.g. `banUser`); empty when not statically known. */
|
|
55
|
+
method: string;
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
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
|
+
*/
|
|
67
|
+
interface AdvisorContainer {
|
|
68
|
+
/**
|
|
69
|
+
* Whether outbound internet was explicitly configured. `undefined` means
|
|
70
|
+
* the field was omitted (platform default `true`) or wasn't a static literal.
|
|
71
|
+
*/
|
|
72
|
+
enableInternet?: boolean;
|
|
73
|
+
/** The `lunora/containers.ts` export name, e.g. `transcoder`. */
|
|
74
|
+
exportName: string;
|
|
75
|
+
/** Declared `instanceType`: a named size, or a custom `{ vcpu, memoryMib, diskMb }`. */
|
|
76
|
+
instanceType?: string | {
|
|
77
|
+
diskMb?: number;
|
|
78
|
+
memoryMib?: number;
|
|
79
|
+
vcpu?: number;
|
|
80
|
+
};
|
|
81
|
+
/** Declared `maxInstances` cap, when present. */
|
|
82
|
+
maxInstances?: number;
|
|
83
|
+
/** Declared `sleepAfter` value, when a static literal. */
|
|
84
|
+
sleepAfter?: number | string;
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
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
|
+
*/
|
|
100
|
+
interface AdvisorHyperdriveCall {
|
|
101
|
+
/** The accessed `ctx.sql` surface, e.g. `ctx.sql.query` / `ctx.sql`. */
|
|
102
|
+
callee: string;
|
|
103
|
+
/** The exported function performing the access (e.g. `listCustomers`). */
|
|
104
|
+
exportName: string;
|
|
105
|
+
/** Source file the access appears in (relative to the lunora dir, no extension). */
|
|
106
|
+
file: string;
|
|
107
|
+
/** Which procedure kind the access lives in — only `query`/`mutation` are flagged; actions are exempt. */
|
|
108
|
+
kind: "mutation" | "query";
|
|
109
|
+
/** 1-based line of the access, or `0` when unknown. */
|
|
110
|
+
line: number;
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
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
|
+
*/
|
|
136
|
+
interface AdvisorTableScan {
|
|
137
|
+
/** Total full-scans of `table` over the observed window. */
|
|
138
|
+
scans: number;
|
|
139
|
+
/** The full-scanned table. */
|
|
140
|
+
table: string;
|
|
141
|
+
}
|
|
142
|
+
/**
|
|
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
|
+
*/
|
|
154
|
+
interface AdvisorIndexHit {
|
|
155
|
+
/** The declared index name. */
|
|
156
|
+
index: string;
|
|
157
|
+
/** Recorded reads that used this index to narrow over the observed window. */
|
|
158
|
+
reads: number;
|
|
159
|
+
/** The table the index is declared on. */
|
|
160
|
+
table: string;
|
|
161
|
+
}
|
|
162
|
+
/**
|
|
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
|
+
*/
|
|
169
|
+
interface AdvisorInsertWrite {
|
|
170
|
+
/** The exported function performing the insert (e.g. `send`). */
|
|
171
|
+
exportName: string;
|
|
172
|
+
/** Source file the insert appears in (relative to the lunora dir, no extension). */
|
|
173
|
+
file: string;
|
|
174
|
+
/** 1-based line of the `insert(...)` call, or `0` when unknown. */
|
|
175
|
+
line: number;
|
|
176
|
+
/** The inserted table; empty when the `insert(...)` argument is not a string literal. */
|
|
177
|
+
table: string;
|
|
178
|
+
}
|
|
179
|
+
/**
|
|
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
|
+
*/
|
|
187
|
+
interface AdvisorMaskProcedure {
|
|
188
|
+
/** The exported binding name of the procedure (e.g. `listUsers`). */
|
|
189
|
+
exportName: string;
|
|
190
|
+
/** Source file relative to the lunora dir, no extension. */
|
|
191
|
+
file: string;
|
|
192
|
+
/**
|
|
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
|
+
*/
|
|
198
|
+
maskColumns: ReadonlyArray<{
|
|
199
|
+
column: string;
|
|
200
|
+
table: string;
|
|
201
|
+
}>;
|
|
202
|
+
/** Tables read by the procedure via `ctx.db.query("table")` / `ctx.db.findMany(...)` etc. */
|
|
203
|
+
tablesRead: ReadonlyArray<string>;
|
|
204
|
+
/** Tables written by the procedure via `ctx.db.insert("table", …)` / `ctx.db.patch(...)` etc. */
|
|
205
|
+
tablesWritten: ReadonlyArray<string>;
|
|
206
|
+
/**
|
|
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
|
+
*/
|
|
212
|
+
usesMask: boolean;
|
|
213
|
+
/** `"internal"` when the procedure uses `internalQuery` / `internalMutation` / `internalAction`. */
|
|
214
|
+
visibility: "internal" | "public";
|
|
215
|
+
}
|
|
216
|
+
/**
|
|
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
|
+
*/
|
|
226
|
+
interface AdvisorNondeterministicCall {
|
|
227
|
+
/** The non-deterministic API invoked, e.g. `Date.now` / `Math.random` / `crypto.randomUUID` / `fetch`. */
|
|
228
|
+
callee: string;
|
|
229
|
+
/** The exported function performing the call (e.g. `sendMessage`). */
|
|
230
|
+
exportName: string;
|
|
231
|
+
/** Source file the call appears in (relative to the lunora dir, no extension). */
|
|
232
|
+
file: string;
|
|
233
|
+
/** Which procedure kind the call lives in — only `query`/`mutation` handlers are non-deterministic; actions are exempt. */
|
|
234
|
+
kind: "mutation" | "query";
|
|
235
|
+
/** 1-based line of the call, or `0` when unknown. */
|
|
236
|
+
line: number;
|
|
237
|
+
}
|
|
238
|
+
/**
|
|
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
|
+
*/
|
|
247
|
+
interface AdvisorProcedureProtection {
|
|
248
|
+
/** `true` when the handler references `ctx.mail` / `ctx.email` (sends mail). */
|
|
249
|
+
callsMail: boolean;
|
|
250
|
+
/** The exported binding name of the procedure (e.g. `signUp`). */
|
|
251
|
+
exportName: string;
|
|
252
|
+
/** Source file relative to the lunora dir, no extension. */
|
|
253
|
+
file: string;
|
|
254
|
+
/** Registration kind — `query` is read-only; `mutation`/`action` are write-shaped. */
|
|
255
|
+
kind: "action" | "mutation" | "query";
|
|
256
|
+
/** `true` when the chain carries `.use(verifyTurnstile(...))` or a `protectPublic({ captcha })` bundle. */
|
|
257
|
+
usesCaptcha: boolean;
|
|
258
|
+
/** `true` when the chain carries `.use(mask(...))`. */
|
|
259
|
+
usesMask: boolean;
|
|
260
|
+
/** `true` when the chain carries `.use(rateLimit(...))` or a `protectPublic({ rateLimit })` bundle. */
|
|
261
|
+
usesRateLimit: boolean;
|
|
262
|
+
/** `true` when the chain carries `.use(rls(...))`. */
|
|
263
|
+
usesRls: boolean;
|
|
264
|
+
/** `"internal"` when the procedure uses `internalQuery` / `internalMutation` / `internalAction`. */
|
|
265
|
+
visibility: "internal" | "public";
|
|
266
|
+
/** `true` when the handler inserts into a user/session/account-shaped table. */
|
|
267
|
+
writesUserTable: boolean;
|
|
268
|
+
}
|
|
269
|
+
/**
|
|
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
|
+
*/
|
|
275
|
+
interface AdvisorQueryRead {
|
|
276
|
+
/** Source file the read appears in (relative to the lunora dir, no extension). */
|
|
277
|
+
file: string;
|
|
278
|
+
/** True when the chain calls `.filter(...)`. */
|
|
279
|
+
hasFilter: boolean;
|
|
280
|
+
/** True when the chain narrows with `.withIndex(...)` or `.withSearchIndex(...)`. */
|
|
281
|
+
hasIndex: boolean;
|
|
282
|
+
/** 1-based line of the `query(...)` call, or `0` when unknown. */
|
|
283
|
+
line: number;
|
|
284
|
+
/** The queried table; empty when the `query(...)` argument is not a string literal. */
|
|
285
|
+
table: string;
|
|
286
|
+
}
|
|
287
|
+
/**
|
|
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
|
+
*/
|
|
302
|
+
interface AdvisorR2sqlCall {
|
|
303
|
+
/** The accessed `ctx.r2sql` surface, e.g. `ctx.r2sql.query` / `ctx.r2sql.from`. */
|
|
304
|
+
callee: string;
|
|
305
|
+
/** The exported function performing the access (e.g. `topPerRegion`). */
|
|
306
|
+
exportName: string;
|
|
307
|
+
/** Source file the access appears in (relative to the lunora dir, no extension). */
|
|
308
|
+
file: string;
|
|
309
|
+
/** Which procedure kind the access lives in — only `query`/`mutation` are flagged; actions are exempt. */
|
|
310
|
+
kind: "mutation" | "query";
|
|
311
|
+
/** 1-based line of the access, or `0` when unknown. */
|
|
312
|
+
line: number;
|
|
313
|
+
}
|
|
314
|
+
/**
|
|
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
|
+
*/
|
|
321
|
+
interface AdvisorRlsProcedure {
|
|
322
|
+
/** The exported binding name of the procedure (e.g. `listDocuments`). */
|
|
323
|
+
exportName: string;
|
|
324
|
+
/** Source file relative to the lunora dir, no extension. */
|
|
325
|
+
file: string;
|
|
326
|
+
/**
|
|
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
|
+
*/
|
|
331
|
+
rlsTables: ReadonlyArray<string>;
|
|
332
|
+
/** Tables read by the procedure via `ctx.db.query("table")` / `ctx.db.findMany(...)` etc. */
|
|
333
|
+
tablesRead: ReadonlyArray<string>;
|
|
334
|
+
/** Tables written by the procedure via `ctx.db.insert("table", …)` / `ctx.db.patch(...)` etc. */
|
|
335
|
+
tablesWritten: ReadonlyArray<string>;
|
|
336
|
+
/**
|
|
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
|
+
*/
|
|
342
|
+
usesRls: boolean;
|
|
343
|
+
/** `"internal"` when the procedure uses `internalQuery` / `internalMutation` / `internalAction`. */
|
|
344
|
+
visibility: "internal" | "public";
|
|
345
|
+
}
|
|
346
|
+
/**
|
|
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
|
+
*/
|
|
353
|
+
interface AdvisorSchema {
|
|
354
|
+
tables: ReadonlyArray<AdvisorTable>;
|
|
355
|
+
}
|
|
356
|
+
/** A table plus the column/index/relation metadata lints inspect. */
|
|
357
|
+
interface AdvisorTable {
|
|
358
|
+
/**
|
|
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
|
+
*/
|
|
364
|
+
externallyManaged?: boolean;
|
|
365
|
+
/**
|
|
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
|
+
*/
|
|
370
|
+
fields: ReadonlyArray<string>;
|
|
371
|
+
/** Every declared index, across all kinds (secondary / search / rank / vector). */
|
|
372
|
+
indexes: ReadonlyArray<AdvisorIndex>;
|
|
373
|
+
/** Table name. */
|
|
374
|
+
name: string;
|
|
375
|
+
/**
|
|
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
|
+
*/
|
|
384
|
+
optionalFields?: ReadonlySet<string>;
|
|
385
|
+
/** Declared relations (`.relations((r) => …)`). */
|
|
386
|
+
relations: ReadonlyArray<AdvisorRelation>;
|
|
387
|
+
}
|
|
388
|
+
/**
|
|
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
|
+
*/
|
|
397
|
+
interface AdvisorIndex {
|
|
398
|
+
fields: ReadonlyArray<string>;
|
|
399
|
+
kind: "index" | "rank" | "search" | "vector";
|
|
400
|
+
name: string;
|
|
401
|
+
unique?: boolean;
|
|
402
|
+
}
|
|
403
|
+
/**
|
|
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
|
+
*/
|
|
408
|
+
interface AdvisorRelation {
|
|
409
|
+
field: string;
|
|
410
|
+
kind: "many" | "one";
|
|
411
|
+
name: string;
|
|
412
|
+
onDelete?: "cascade" | "restrict" | "set null";
|
|
413
|
+
references: string;
|
|
414
|
+
table: string;
|
|
415
|
+
}
|
|
416
|
+
/**
|
|
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
|
+
*/
|
|
424
|
+
declare const fromServerSchema: (schema: Schema) => AdvisorSchema;
|
|
425
|
+
/**
|
|
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
|
+
*/
|
|
432
|
+
interface AdvisorSecretLiteral {
|
|
433
|
+
/** Source file relative to the lunora dir, no extension. */
|
|
434
|
+
file: string;
|
|
435
|
+
/** Heuristic that matched, e.g. `stripe_live_key` / `aws_access_key` / `private_key` / `high_entropy`. */
|
|
436
|
+
kind: string;
|
|
437
|
+
/** 1-based line of the literal, or `0` when unknown. */
|
|
438
|
+
line: number;
|
|
439
|
+
/** Redacted preview (first few chars + length) — never the full secret. */
|
|
440
|
+
preview: string;
|
|
441
|
+
}
|
|
442
|
+
/**
|
|
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
|
+
*/
|
|
455
|
+
interface AdvisorShardTraffic {
|
|
456
|
+
/**
|
|
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
|
+
*/
|
|
461
|
+
group?: string;
|
|
462
|
+
/** Total requests (function dispatches) recorded against this shard over the observed window. */
|
|
463
|
+
requests: number;
|
|
464
|
+
/**
|
|
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
|
+
*/
|
|
469
|
+
shardKey: string;
|
|
470
|
+
}
|
|
471
|
+
/**
|
|
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
|
+
*/
|
|
480
|
+
interface AdvisorSqlInterpolation {
|
|
481
|
+
/** The exported binding name of the procedure performing the `ctx.sql` call. */
|
|
482
|
+
exportName: string;
|
|
483
|
+
/** Source file relative to the lunora dir, no extension. */
|
|
484
|
+
file: string;
|
|
485
|
+
/** 1-based line of the interpolation, or `0` when unknown. */
|
|
486
|
+
line: number;
|
|
487
|
+
}
|
|
488
|
+
/**
|
|
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
|
+
*/
|
|
497
|
+
interface AdvisorTableSample {
|
|
498
|
+
/** The cap applied; equals `rows.length` when not truncated. */
|
|
499
|
+
readonly cap: number;
|
|
500
|
+
/**
|
|
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
|
+
*/
|
|
505
|
+
readonly existingIds: ReadonlySet<string>;
|
|
506
|
+
/** Sampled rows (up to `cap`). Each row includes `_id` and all declared columns. */
|
|
507
|
+
readonly rows: ReadonlyArray<Record<string, unknown>>;
|
|
508
|
+
/** The table's name. */
|
|
509
|
+
readonly table: string;
|
|
510
|
+
/** Whether more rows exist beyond the cap. */
|
|
511
|
+
readonly truncated: boolean;
|
|
512
|
+
}
|
|
513
|
+
/**
|
|
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
|
+
*/
|
|
526
|
+
/** One workflow declared via a `defineWorkflow()` export in `lunora/workflows.ts`. */
|
|
527
|
+
interface AdvisorWorkflow {
|
|
528
|
+
/** The `lunora/workflows.ts` export name, e.g. `orderPipeline`. */
|
|
529
|
+
exportName: string;
|
|
530
|
+
}
|
|
531
|
+
/** One `ctx.workflows.get("name")` call discovered in a function body. */
|
|
532
|
+
interface AdvisorWorkflowCall {
|
|
533
|
+
/** The exported function performing the call (e.g. `create`). */
|
|
534
|
+
exportName: string;
|
|
535
|
+
/** Source file the call appears in (relative to the lunora dir, no extension). */
|
|
536
|
+
file: string;
|
|
537
|
+
/** 1-based line of the `get(...)` call, or `0` when unknown. */
|
|
538
|
+
line: number;
|
|
539
|
+
/** The referenced workflow export name; empty when the `get(...)` argument is not a string literal. */
|
|
540
|
+
workflow: string;
|
|
541
|
+
}
|
|
542
|
+
/**
|
|
543
|
+
* Severity of a finding, mirroring splinter's `level`. `ERROR` is a definite
|
|
544
|
+
* problem, `WARN` a likely one, `INFO` an advisory nudge.
|
|
545
|
+
*/
|
|
546
|
+
type Level = "ERROR" | "INFO" | "WARN";
|
|
547
|
+
/**
|
|
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
|
+
*/
|
|
552
|
+
type Facing = "EXTERNAL" | "INTERNAL";
|
|
553
|
+
/**
|
|
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
|
+
*/
|
|
558
|
+
type Category = "PERFORMANCE" | "SCHEMA" | "SECURITY";
|
|
559
|
+
/**
|
|
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
|
+
*/
|
|
571
|
+
type LintSource = "runtime" | "static";
|
|
572
|
+
/**
|
|
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
|
+
*/
|
|
578
|
+
interface Finding {
|
|
579
|
+
/** Stable identifier for dedup/dismissal across runs. */
|
|
580
|
+
cacheKey: string;
|
|
581
|
+
/** The lint's concern buckets (usually one). */
|
|
582
|
+
categories: Category[];
|
|
583
|
+
/** Human-readable explanation of the rule in general terms. */
|
|
584
|
+
description: string;
|
|
585
|
+
/** The specific violation message for _this_ occurrence. */
|
|
586
|
+
detail: string;
|
|
587
|
+
/** Who the finding concerns. */
|
|
588
|
+
facing: Facing;
|
|
589
|
+
/** Severity. */
|
|
590
|
+
level: Level;
|
|
591
|
+
/** Structured context (table, field, index, …) for the UI and deep links. */
|
|
592
|
+
metadata: Record<string, unknown>;
|
|
593
|
+
/** The lint id that produced this finding, e.g. `unindexed_foreign_key`. */
|
|
594
|
+
name: string;
|
|
595
|
+
/** How to fix it — a doc URL or short imperative guidance. */
|
|
596
|
+
remediation: string;
|
|
597
|
+
/** Short headline for the finding. */
|
|
598
|
+
title: string;
|
|
599
|
+
}
|
|
600
|
+
/**
|
|
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
|
+
*/
|
|
604
|
+
interface LintContext {
|
|
605
|
+
/**
|
|
606
|
+
* `httpRoute.<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
|
+
*/
|
|
611
|
+
adminRoutes?: ReadonlyArray<AdvisorAdminRoute>;
|
|
612
|
+
/**
|
|
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
|
+
*/
|
|
618
|
+
argValidators?: ReadonlyArray<AdvisorArgumentValidator>;
|
|
619
|
+
/**
|
|
620
|
+
* `ctx.authApi.<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
|
+
*/
|
|
624
|
+
authApiCalls?: ReadonlyArray<AdvisorAuthApiCall>;
|
|
625
|
+
/**
|
|
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
|
+
*/
|
|
630
|
+
containers?: ReadonlyArray<AdvisorContainer>;
|
|
631
|
+
/**
|
|
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
|
+
*/
|
|
637
|
+
hyperdriveCalls?: ReadonlyArray<AdvisorHyperdriveCall>;
|
|
638
|
+
/**
|
|
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
|
+
*/
|
|
646
|
+
indexHits?: ReadonlyArray<AdvisorIndexHit>;
|
|
647
|
+
/**
|
|
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
|
+
*/
|
|
652
|
+
inserts?: ReadonlyArray<AdvisorInsertWrite>;
|
|
653
|
+
/**
|
|
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
|
+
*/
|
|
661
|
+
maskProcedures?: ReadonlyArray<AdvisorMaskProcedure>;
|
|
662
|
+
/**
|
|
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
|
+
*/
|
|
670
|
+
nondeterministicCalls?: ReadonlyArray<AdvisorNondeterministicCall>;
|
|
671
|
+
/**
|
|
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
|
+
*/
|
|
679
|
+
procedureProtections?: ReadonlyArray<AdvisorProcedureProtection>;
|
|
680
|
+
/**
|
|
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
|
+
*/
|
|
685
|
+
queries?: ReadonlyArray<AdvisorQueryRead>;
|
|
686
|
+
/**
|
|
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
|
+
*/
|
|
692
|
+
r2sqlCalls?: ReadonlyArray<AdvisorR2sqlCall>;
|
|
693
|
+
/**
|
|
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
|
+
*/
|
|
700
|
+
rlsProcedures?: ReadonlyArray<AdvisorRlsProcedure>;
|
|
701
|
+
/** The declared schema under audit, normalized to the feeder-agnostic {@link AdvisorSchema}. */
|
|
702
|
+
schema: AdvisorSchema;
|
|
703
|
+
/**
|
|
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
|
+
*/
|
|
709
|
+
secretLiterals?: ReadonlyArray<AdvisorSecretLiteral>;
|
|
710
|
+
/**
|
|
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
|
+
*/
|
|
716
|
+
shardTraffic?: ReadonlyArray<AdvisorShardTraffic>;
|
|
717
|
+
/**
|
|
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
|
+
*/
|
|
723
|
+
sqlInterpolations?: ReadonlyArray<AdvisorSqlInterpolation>;
|
|
724
|
+
/**
|
|
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
|
+
*/
|
|
737
|
+
tableSamples?: ReadonlyArray<AdvisorTableSample>;
|
|
738
|
+
/**
|
|
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
|
+
*/
|
|
745
|
+
tableScans?: ReadonlyArray<AdvisorTableScan>;
|
|
746
|
+
/**
|
|
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
|
+
*/
|
|
753
|
+
workflowCalls?: ReadonlyArray<AdvisorWorkflowCall>;
|
|
754
|
+
/**
|
|
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
|
+
*/
|
|
760
|
+
workflows?: ReadonlyArray<AdvisorWorkflow>;
|
|
761
|
+
}
|
|
762
|
+
/**
|
|
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
|
+
*/
|
|
768
|
+
interface Lint {
|
|
769
|
+
/** Concern buckets every finding from this lint carries. */
|
|
770
|
+
categories: Category[];
|
|
771
|
+
/** General-purpose description shared by every finding. */
|
|
772
|
+
description: string;
|
|
773
|
+
/** Default audience for this lint's findings. */
|
|
774
|
+
facing: Facing;
|
|
775
|
+
/** Default severity for this lint's findings. */
|
|
776
|
+
level: Level;
|
|
777
|
+
/** Unique lint id, snake_case (e.g. `unindexed_foreign_key`). */
|
|
778
|
+
name: string;
|
|
779
|
+
/** Fix guidance shared by every finding. */
|
|
780
|
+
remediation: string;
|
|
781
|
+
/** Produce zero or more findings for the given context. */
|
|
782
|
+
run: (context: LintContext) => Finding[];
|
|
783
|
+
/** Evidence source — see {@link LintSource}. */
|
|
784
|
+
source: LintSource;
|
|
785
|
+
/** Short headline shared by every finding. */
|
|
786
|
+
title: string;
|
|
787
|
+
}
|
|
788
|
+
/**
|
|
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
|
+
*/
|
|
794
|
+
interface AnalyticsMetricsSource {
|
|
795
|
+
query: (sql: string) => Promise<{
|
|
796
|
+
rows: ReadonlyArray<Record<string, unknown>>;
|
|
797
|
+
}>;
|
|
798
|
+
}
|
|
799
|
+
/**
|
|
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
|
+
*/
|
|
803
|
+
declare const AE_METRIC_EVENTS: {
|
|
804
|
+
/** `lunora.index.hit` — one row per `(table, index)` use. `blob2`=table, `blob3`=index. */
|
|
805
|
+
readonly indexHit: {
|
|
806
|
+
readonly event: "lunora.index.hit";
|
|
807
|
+
readonly index: "blob3";
|
|
808
|
+
readonly table: "blob2";
|
|
809
|
+
};
|
|
810
|
+
/** `lunora.shard.request` — one row per shard dispatch. `blob2`=shardKey, `blob3`=group. */
|
|
811
|
+
readonly shardRequest: {
|
|
812
|
+
readonly event: "lunora.shard.request";
|
|
813
|
+
readonly group: "blob3";
|
|
814
|
+
readonly shardKey: "blob2";
|
|
815
|
+
};
|
|
816
|
+
/** `lunora.table.scan` — one row per full-scan. `blob2`=table. */
|
|
817
|
+
readonly tableScan: {
|
|
818
|
+
readonly event: "lunora.table.scan";
|
|
819
|
+
readonly table: "blob2";
|
|
820
|
+
};
|
|
821
|
+
};
|
|
822
|
+
/** Options for the AE-backed runtime-metrics feeder. */
|
|
823
|
+
interface AnalyticsMetricsOptions {
|
|
824
|
+
/** The AE dataset (the wrangler `analytics_engine_datasets[].dataset`) to read from. */
|
|
825
|
+
dataset: string;
|
|
826
|
+
/**
|
|
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
|
+
*/
|
|
834
|
+
declaredIndexes?: ReadonlyArray<{
|
|
835
|
+
index: string;
|
|
836
|
+
table: string;
|
|
837
|
+
}>;
|
|
838
|
+
/**
|
|
839
|
+
* Restrict the shard-traffic read to one sharded-function group (`blob3`).
|
|
840
|
+
* Omit to read the whole deployment's shard set.
|
|
841
|
+
*/
|
|
842
|
+
group?: string;
|
|
843
|
+
}
|
|
844
|
+
/** The runtime-lint input arrays this module reconstructs from AE. */
|
|
845
|
+
interface AnalyticsRuntimeMetrics {
|
|
846
|
+
indexHits: AdvisorIndexHit[];
|
|
847
|
+
shardTraffic: AdvisorShardTraffic[];
|
|
848
|
+
tableScans: AdvisorTableScan[];
|
|
849
|
+
}
|
|
850
|
+
/**
|
|
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
|
+
*/
|
|
863
|
+
declare const loadAnalyticsRuntimeMetrics: (source: AnalyticsMetricsSource, options: AnalyticsMetricsOptions) => Promise<AnalyticsRuntimeMetrics>;
|
|
864
|
+
/**
|
|
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
|
+
*/
|
|
885
|
+
declare const constraintValidator: Lint;
|
|
886
|
+
/**
|
|
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
|
+
*/
|
|
904
|
+
declare const hotShard: Lint;
|
|
905
|
+
/**
|
|
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
|
+
*/
|
|
927
|
+
declare const indexUtilization: Lint;
|
|
928
|
+
/**
|
|
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
|
+
*/
|
|
943
|
+
declare const adminRouteWithoutGuard: Lint;
|
|
944
|
+
/**
|
|
945
|
+
* Flags a `ctx.authApi.<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
|
+
*/
|
|
960
|
+
declare const authApiCallWithoutHeaders: Lint;
|
|
961
|
+
/**
|
|
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
|
+
*/
|
|
987
|
+
declare const circularFk: Lint;
|
|
988
|
+
/**
|
|
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
|
+
*/
|
|
995
|
+
declare const containerOversizedInstance: Lint;
|
|
996
|
+
/**
|
|
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
|
+
*/
|
|
1008
|
+
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
|
+
*/
|
|
1023
|
+
declare const duplicateIndex: Lint;
|
|
1024
|
+
/**
|
|
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
|
+
*/
|
|
1032
|
+
declare const emptyIndex: Lint;
|
|
1033
|
+
/**
|
|
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
|
+
*/
|
|
1045
|
+
declare const filterWithoutIndex: Lint;
|
|
1046
|
+
/**
|
|
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
|
+
*/
|
|
1060
|
+
declare const hardcodedSecret: Lint;
|
|
1061
|
+
/**
|
|
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 result — non-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
|
+
*/
|
|
1084
|
+
declare const hyperdriveOutsideAction: Lint;
|
|
1085
|
+
/**
|
|
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
|
+
*/
|
|
1096
|
+
declare const indexReferencesUnknownField: Lint;
|
|
1097
|
+
/**
|
|
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
|
+
*/
|
|
1134
|
+
declare const maskUncoveredPiiColumn: Lint;
|
|
1135
|
+
/**
|
|
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
|
+
*/
|
|
1154
|
+
declare const nondeterministicQueryMutation: Lint;
|
|
1155
|
+
/**
|
|
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 nothing — the 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
|
+
*/
|
|
1177
|
+
declare const policyReferencesUnknownTable: Lint;
|
|
1178
|
+
/**
|
|
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
|
+
*/
|
|
1191
|
+
declare const publicArgumentUsesAny: Lint;
|
|
1192
|
+
/**
|
|
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
|
+
*/
|
|
1207
|
+
declare const publicMutationWithoutRatelimit: Lint;
|
|
1208
|
+
/**
|
|
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
|
+
*/
|
|
1232
|
+
declare const r2sqlOutsideAction: Lint;
|
|
1233
|
+
/**
|
|
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
|
+
*/
|
|
1239
|
+
declare const relationReferencesUnknownField: Lint;
|
|
1240
|
+
/**
|
|
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
|
+
*/
|
|
1250
|
+
declare const relationReferencesUnknownTable: Lint;
|
|
1251
|
+
/**
|
|
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
|
+
*/
|
|
1286
|
+
declare const rlsUncoveredTable: Lint;
|
|
1287
|
+
/**
|
|
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
|
+
*/
|
|
1302
|
+
declare const sqlInjectionRisk: Lint;
|
|
1303
|
+
/**
|
|
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("<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
|
+
*/
|
|
1322
|
+
declare const tableWithoutInsert: Lint;
|
|
1323
|
+
/**
|
|
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
|
+
*/
|
|
1336
|
+
declare const unboundedStringArgument: Lint;
|
|
1337
|
+
/**
|
|
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
|
+
*/
|
|
1351
|
+
declare const unindexedForeignKey: Lint;
|
|
1352
|
+
/**
|
|
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
|
+
*/
|
|
1371
|
+
declare const unindexedRelationTarget: Lint;
|
|
1372
|
+
/**
|
|
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
|
+
*/
|
|
1388
|
+
declare const userCreatingMutationWithoutCaptcha: Lint;
|
|
1389
|
+
/**
|
|
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
|
+
*/
|
|
1402
|
+
declare const workflowUnknownTarget: Lint;
|
|
1403
|
+
/**
|
|
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("<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
|
+
*/
|
|
1419
|
+
declare const workflowUnused: Lint;
|
|
1420
|
+
/**
|
|
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
|
+
*/
|
|
1426
|
+
declare const STATIC_LINTS: ReadonlyArray<Lint>;
|
|
1427
|
+
/**
|
|
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
|
+
*/
|
|
1435
|
+
declare const RUNTIME_LINTS: ReadonlyArray<Lint>;
|
|
1436
|
+
/** The default lint set: the static lints, then the runtime lints. A caller filters by `source` to run one tier. */
|
|
1437
|
+
declare const ALL_LINTS: ReadonlyArray<Lint>;
|
|
1438
|
+
/** Options for {@link runAdvisor}. */
|
|
1439
|
+
interface RunAdvisorOptions {
|
|
1440
|
+
/** Lints to run (default: {@link ALL_LINTS}). */
|
|
1441
|
+
lints?: ReadonlyArray<Lint>;
|
|
1442
|
+
/** Restrict to a single evidence source — e.g. `"static"` at codegen time. */
|
|
1443
|
+
source?: LintSource;
|
|
1444
|
+
}
|
|
1445
|
+
/**
|
|
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
|
+
*/
|
|
1450
|
+
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 };
|