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