@lunora/advisor 1.0.0-alpha.31 → 1.0.0-alpha.33
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.mts +255 -2
- package/dist/index.d.ts +255 -2
- package/dist/index.mjs +16 -2
- package/dist/packem_shared/exportSinkMisconfigured-897sVMUa.mjs +46 -0
- package/dist/packem_shared/{fromServerSchema-BjAZdvJ6.mjs → fromServerSchema-CVnbSf8j.mjs} +10 -0
- package/dist/packem_shared/geoIndexFieldNotGeopoint-CcaInLhY.mjs +40 -0
- package/dist/packem_shared/geoIndexUnused-D7I0UyND.mjs +40 -0
- package/dist/packem_shared/notifyMissingPushConfig-Ca9EzRzy.mjs +27 -0
- package/dist/packem_shared/notifySendOutsideAction-BUZSIZa4.mjs +30 -0
- package/dist/packem_shared/signupMutationWithoutDisposableGating-Dnbol2CS.mjs +39 -0
- package/dist/packem_shared/ttlFieldNotTimestamp-Dpaj63Z6.mjs +36 -0
- package/package.json +3 -3
package/dist/index.d.mts
CHANGED
|
@@ -280,6 +280,39 @@ interface AdvisorContainer {
|
|
|
280
280
|
/** Declared `sleepAfter` value, when a static literal. */
|
|
281
281
|
sleepAfter?: number | string;
|
|
282
282
|
}
|
|
283
|
+
/**
|
|
284
|
+
* One CDC export-sink construction discovered in a function body — the input the
|
|
285
|
+
* `export_sink_misconfigured` lint consumes. Produced by the codegen feeder,
|
|
286
|
+
* which walks the lunora source for the three sink factories the export tap
|
|
287
|
+
* (plan 170) ships: `defineExportSink({ name, deliver })`,
|
|
288
|
+
* `webhookExportSink({ name, url, … })`, and `r2Sink({ name, bucket, … })`.
|
|
289
|
+
*
|
|
290
|
+
* A sink missing a required field can never deliver a change batch — a webhook
|
|
291
|
+
* with no `url` would POST to `undefined`, an R2 sink with no `bucket` binding has
|
|
292
|
+
* nowhere to write, and a custom sink with no `deliver` has no delivery path. The
|
|
293
|
+
* runtime `defineExportSink` guard throws for a missing `name`/`deliver`, but the
|
|
294
|
+
* built-in `webhookExportSink`/`r2Sink` don't validate `url`/`bucket`, so catching
|
|
295
|
+
* the misconfiguration statically beats a silently-dead export tap at runtime.
|
|
296
|
+
* Runtime callers don't supply this, so the lint finds nothing there.
|
|
297
|
+
*/
|
|
298
|
+
interface AdvisorExportSink {
|
|
299
|
+
/**
|
|
300
|
+
* True when the factory's config argument is a statically analyzable object
|
|
301
|
+
* literal. A non-literal config (a variable, a spread) is not decidable, so
|
|
302
|
+
* the lint skips it rather than raising a false alarm.
|
|
303
|
+
*/
|
|
304
|
+
analyzable: boolean;
|
|
305
|
+
/** Present keys whose value is an empty-string literal (`""`) — treated as missing. */
|
|
306
|
+
emptyKeys: string[];
|
|
307
|
+
/** Which sink factory was called. */
|
|
308
|
+
factory: "defineExportSink" | "r2Sink" | "webhookExportSink";
|
|
309
|
+
/** Source file the construction appears in (relative to the lunora dir, no extension). */
|
|
310
|
+
file: string;
|
|
311
|
+
/** 1-based line of the factory call, or `0` when unknown. */
|
|
312
|
+
line: number;
|
|
313
|
+
/** Config keys present on the object literal, regardless of value. */
|
|
314
|
+
presentKeys: string[];
|
|
315
|
+
}
|
|
283
316
|
/**
|
|
284
317
|
* One rate-limit / Turnstile middleware call — the `ratelimit_middleware_fail_open`
|
|
285
318
|
* lint input. `rateLimit`/`dbRateLimit` (`@lunora/ratelimit`) and
|
|
@@ -328,6 +361,27 @@ interface AdvisorFlagSecurityDefault {
|
|
|
328
361
|
/** 1-based line of the `ctx.flags.boolean` call, or `0` when unknown. */
|
|
329
362
|
line: number;
|
|
330
363
|
}
|
|
364
|
+
/**
|
|
365
|
+
* One `withGeoIndex("name", …)` read discovered in a function body — the use-side
|
|
366
|
+
* input the `geo_index_unused` lint cross-references against the declared geo
|
|
367
|
+
* indexes in the lint context's schema. Produced by the codegen feeder, which
|
|
368
|
+
* walks the lunora source for `ctx.db.query("t").withGeoIndex(name, …)` /
|
|
369
|
+
* `ctx.db.<table>.withGeoIndex(name, …)` reads and records each referenced index
|
|
370
|
+
* name. Runtime callers don't supply it, so the lint finds nothing there.
|
|
371
|
+
*
|
|
372
|
+
* A `.geoIndex(name, { field })` maintains a geohash companion column on every
|
|
373
|
+
* write; if no handler ever reads it via `withGeoIndex(name, …)` the companion is
|
|
374
|
+
* dead overhead (maintained on every write, read by nothing) — the geo analogue
|
|
375
|
+
* of a dead regular index.
|
|
376
|
+
*/
|
|
377
|
+
interface AdvisorGeoIndexUsage {
|
|
378
|
+
/** Source file the read appears in (relative to the lunora dir, no extension). */
|
|
379
|
+
file: string;
|
|
380
|
+
/** The referenced geo-index name; empty when the `withGeoIndex(...)` argument is not a string literal. */
|
|
381
|
+
indexName: string;
|
|
382
|
+
/** 1-based line of the `withGeoIndex(...)` call, or `0` when unknown. */
|
|
383
|
+
line: number;
|
|
384
|
+
}
|
|
331
385
|
/**
|
|
332
386
|
* One discovered `httpAction`/`httpRoute` handler that performs a side effect
|
|
333
387
|
* (`ctx.runMutation` / `ctx.runAction` / a `ctx.db.{insert,patch,replace,delete,
|
|
@@ -688,6 +742,48 @@ interface AdvisorNormalizeIdAuthorization {
|
|
|
688
742
|
/** `"internal"` for `internalQuery`/`internalMutation`; `"public"` for `query`/`mutation`. */
|
|
689
743
|
visibility: "internal" | "public";
|
|
690
744
|
}
|
|
745
|
+
/**
|
|
746
|
+
* One `ctx.notify` / `ctx.push` send discovered lexically inside a `query(...)`
|
|
747
|
+
* or `mutation(...)` handler body — the input the `notify_send_outside_action`
|
|
748
|
+
* lint consumes. Produced by the codegen feeder, which walks each exported
|
|
749
|
+
* function's handler with ts-morph and records the `@lunora/notify` send surface
|
|
750
|
+
* (`ctx.notify.send`, `ctx.notify.chat/inApp/webhook`, `ctx.push.send`,
|
|
751
|
+
* `ctx.push.broadcast`).
|
|
752
|
+
*
|
|
753
|
+
* A notification send is external I/O (a `fetch` to a push service / FCM): it is
|
|
754
|
+
* non-deterministic like `fetch`, so it breaks the determinism the coordinator
|
|
755
|
+
* relies on when re-running a query on subscription re-evaluation or a mutation on
|
|
756
|
+
* OCC retry (a retried mutation would re-send). It therefore belongs **only** in
|
|
757
|
+
* `action(...)` handlers. Calls inside `action(...)` are intentionally **not**
|
|
758
|
+
* recorded — actions are the escape hatch. Runtime callers don't supply this, so
|
|
759
|
+
* the lint finds nothing there.
|
|
760
|
+
*/
|
|
761
|
+
interface AdvisorNotifyCall {
|
|
762
|
+
/** The send surface accessed, e.g. `ctx.push.broadcast` / `ctx.notify.send`. */
|
|
763
|
+
callee: string;
|
|
764
|
+
/** The exported function performing the send (e.g. `sendWelcome`). */
|
|
765
|
+
exportName: string;
|
|
766
|
+
/** Source file the send appears in (relative to the lunora dir, no extension). */
|
|
767
|
+
file: string;
|
|
768
|
+
/** Which procedure kind the send lives in — only `query`/`mutation` are flagged; actions are exempt. */
|
|
769
|
+
kind: "mutation" | "query";
|
|
770
|
+
/** 1-based line of the send, or `0` when unknown. */
|
|
771
|
+
line: number;
|
|
772
|
+
}
|
|
773
|
+
/**
|
|
774
|
+
* Whether an app uses `ctx.push` and which push channels `defineNotify(...)`
|
|
775
|
+
* configures — the input the `notify_missing_push_config` lint consumes. Produced
|
|
776
|
+
* by the codegen feeder from the resolved `lunora/notify.ts` definition and the
|
|
777
|
+
* discovered `ctx.push` usage; absent for runtime callers.
|
|
778
|
+
*/
|
|
779
|
+
interface AdvisorNotifyConfig {
|
|
780
|
+
/** Whether `defineNotify` wired the FCM channel. */
|
|
781
|
+
hasFcm: boolean;
|
|
782
|
+
/** Whether `defineNotify` wired the Web Push channel. */
|
|
783
|
+
hasWebPush: boolean;
|
|
784
|
+
/** Whether any handler sends a push (`ctx.push.send` / `ctx.push.broadcast`). */
|
|
785
|
+
usesPush: boolean;
|
|
786
|
+
}
|
|
691
787
|
/**
|
|
692
788
|
* One `ctx.db` write (`insert` / `replace` / `patch` / `insertManyUnsafe`) that
|
|
693
789
|
* sets an ownership / identity column — `userId`, `ownerId`, `tenantId`, and the
|
|
@@ -786,6 +882,8 @@ interface AdvisorProcedureProtection {
|
|
|
786
882
|
unboundedAiGeneration: boolean;
|
|
787
883
|
/** `true` when the chain carries `.use(verifyTurnstile(...))` or a `protectPublic({ captcha })` bundle. */
|
|
788
884
|
usesCaptcha: boolean;
|
|
885
|
+
/** `true` when the chain carries `.use(emailGateMiddleware(...))` from `@lunora/auth`. Read by the `signup_mutation_without_disposable_gating` lint (paired with public visibility + a user-table write). */
|
|
886
|
+
usesEmailGate: boolean;
|
|
789
887
|
/** `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
888
|
usesInsertManyUnsafe: boolean;
|
|
791
889
|
/** `true` when the chain carries `.use(mask(...))`. */
|
|
@@ -1000,6 +1098,14 @@ interface AdvisorSchema {
|
|
|
1000
1098
|
}
|
|
1001
1099
|
/** A table plus the column/index/relation metadata lints inspect. */
|
|
1002
1100
|
interface AdvisorTable {
|
|
1101
|
+
/**
|
|
1102
|
+
* Effective validator kind per declared column (a `v.optional(...)` is
|
|
1103
|
+
* unwrapped to its inner kind). Read by the schema-type lints
|
|
1104
|
+
* (`ttl_field_not_timestamp`, `geo_index_field_not_geopoint`) to check a
|
|
1105
|
+
* referenced column's type. Optional — a feeder that doesn't track column
|
|
1106
|
+
* kinds omits it, and the type lints then skip the check.
|
|
1107
|
+
*/
|
|
1108
|
+
columnKinds?: Record<string, string>;
|
|
1003
1109
|
/**
|
|
1004
1110
|
* `true` when the table is written outside Lunora's discoverable insert path
|
|
1005
1111
|
* — declared via `.externallyManaged()` (e.g. `@lunora/auth`'s better-auth
|
|
@@ -1067,6 +1173,15 @@ interface AdvisorTable {
|
|
|
1067
1173
|
softDelete?: {
|
|
1068
1174
|
field: string;
|
|
1069
1175
|
};
|
|
1176
|
+
/**
|
|
1177
|
+
* Set when the table declared `.ttl(field, { after? })`. Read by the
|
|
1178
|
+
* `ttl_field_not_timestamp` lint to confirm the expiry column is time-typed.
|
|
1179
|
+
* Optional — a feeder that doesn't track TTL omits it.
|
|
1180
|
+
*/
|
|
1181
|
+
ttl?: {
|
|
1182
|
+
after?: number;
|
|
1183
|
+
field: string;
|
|
1184
|
+
};
|
|
1070
1185
|
}
|
|
1071
1186
|
/**
|
|
1072
1187
|
* One declared index, flattened across Lunora's index kinds so a single lint can
|
|
@@ -1079,7 +1194,7 @@ interface AdvisorTable {
|
|
|
1079
1194
|
*/
|
|
1080
1195
|
interface AdvisorIndex {
|
|
1081
1196
|
fields: ReadonlyArray<string>;
|
|
1082
|
-
kind: "index" | "rank" | "search" | "vector";
|
|
1197
|
+
kind: "geo" | "index" | "rank" | "search" | "vector";
|
|
1083
1198
|
name: string;
|
|
1084
1199
|
unique?: boolean;
|
|
1085
1200
|
}
|
|
@@ -1558,6 +1673,16 @@ interface LintContext {
|
|
|
1558
1673
|
* the container lints find nothing.
|
|
1559
1674
|
*/
|
|
1560
1675
|
containers?: ReadonlyArray<AdvisorContainer>;
|
|
1676
|
+
/**
|
|
1677
|
+
* CDC export-sink constructions (`defineExportSink` / `webhookExportSink` /
|
|
1678
|
+
* `r2Sink`) discovered in function bodies — the `export_sink_misconfigured`
|
|
1679
|
+
* input. Each carries which config keys were present (and which were an empty
|
|
1680
|
+
* string), so the lint can flag a sink missing a required field (a webhook
|
|
1681
|
+
* with no `url`, an R2 sink with no `bucket`, a sink with no `name`/`deliver`).
|
|
1682
|
+
* Supplied by the codegen feeder; absent for runtime callers, where the lint
|
|
1683
|
+
* finds nothing.
|
|
1684
|
+
*/
|
|
1685
|
+
exportSinks?: ReadonlyArray<AdvisorExportSink>;
|
|
1561
1686
|
/**
|
|
1562
1687
|
* `rateLimit`/`dbRateLimit` (`@lunora/ratelimit`) and `verifyTurnstileMiddleware`
|
|
1563
1688
|
* (`@lunora/auth`) middleware calls, each with whether its options literal set
|
|
@@ -1579,6 +1704,15 @@ interface LintContext {
|
|
|
1579
1704
|
* codegen feeder; absent for runtime callers, where the lint finds nothing.
|
|
1580
1705
|
*/
|
|
1581
1706
|
flagSecurityDefaults?: ReadonlyArray<AdvisorFlagSecurityDefault>;
|
|
1707
|
+
/**
|
|
1708
|
+
* `withGeoIndex("name", …)` reads discovered in function bodies — the use-side
|
|
1709
|
+
* input the `geo_index_unused` lint cross-references against the declared geo
|
|
1710
|
+
* indexes in {@link LintContext.schema}. A declared `.geoIndex(name, …)` with
|
|
1711
|
+
* no matching read is dead overhead (its geohash companion is maintained on
|
|
1712
|
+
* every write and read by nothing). Supplied by the codegen feeder; absent for
|
|
1713
|
+
* runtime callers, where the lint finds nothing.
|
|
1714
|
+
*/
|
|
1715
|
+
geoIndexUsages?: ReadonlyArray<AdvisorGeoIndexUsage>;
|
|
1582
1716
|
/**
|
|
1583
1717
|
* `httpAction`/`httpRoute` handlers that perform a side effect
|
|
1584
1718
|
* (`ctx.runMutation` / `ctx.runAction` / a `ctx.db` write) from the HTTP edge,
|
|
@@ -1706,6 +1840,21 @@ interface LintContext {
|
|
|
1706
1840
|
* for runtime callers, where the lint finds nothing.
|
|
1707
1841
|
*/
|
|
1708
1842
|
normalizeIdAuthorizations?: ReadonlyArray<AdvisorNormalizeIdAuthorization>;
|
|
1843
|
+
/**
|
|
1844
|
+
* `ctx.notify` / `ctx.push` sends discovered lexically inside `query`/`mutation`
|
|
1845
|
+
* handler bodies — the `notify_send_outside_action` input. Supplied by the
|
|
1846
|
+
* codegen feeder, which omits `action` handlers (where these facades are the
|
|
1847
|
+
* typed, intended surface); absent for runtime callers, where the lint finds
|
|
1848
|
+
* nothing.
|
|
1849
|
+
*/
|
|
1850
|
+
notifyCalls?: ReadonlyArray<AdvisorNotifyCall>;
|
|
1851
|
+
/**
|
|
1852
|
+
* Whether the app uses `ctx.push` and which push channels `defineNotify(...)`
|
|
1853
|
+
* wires — the `notify_missing_push_config` input. Supplied by the codegen
|
|
1854
|
+
* feeder from the resolved `lunora/notify.ts` definition; absent for runtime
|
|
1855
|
+
* callers, where the lint finds nothing.
|
|
1856
|
+
*/
|
|
1857
|
+
notifyConfig?: AdvisorNotifyConfig;
|
|
1709
1858
|
/**
|
|
1710
1859
|
* `ctx.db` writes (`insert` / `replace` / `patch` / `insertManyUnsafe`) that set
|
|
1711
1860
|
* an ownership / identity column (`userId`, `ownerId`, `tenantId`, …) from the
|
|
@@ -2469,6 +2618,24 @@ declare const duplicateIndex: Lint;
|
|
|
2469
2618
|
* least one field by construction, so only `kind: "index"` is checked.)
|
|
2470
2619
|
*/
|
|
2471
2620
|
declare const emptyIndex: Lint;
|
|
2621
|
+
/**
|
|
2622
|
+
* Flags a misconfigured CDC export sink (plan 170) — a `defineExportSink` /
|
|
2623
|
+
* `webhookExportSink` / `r2Sink` construction with a required config field
|
|
2624
|
+
* missing or set to an empty string.
|
|
2625
|
+
*
|
|
2626
|
+
* The runtime `defineExportSink` guard throws for a missing `name`/`deliver`, but
|
|
2627
|
+
* the built-in `webhookExportSink` / `r2Sink` never validate their `url` /
|
|
2628
|
+
* `bucket`, so a webhook sink with no URL or an R2 sink with no bucket binding
|
|
2629
|
+
* ships and silently fails to drain — the export tap advances no cursor and the
|
|
2630
|
+
* warehouse never sees a row. Catching it at codegen time beats a dead tap in
|
|
2631
|
+
* production.
|
|
2632
|
+
*
|
|
2633
|
+
* A non-literal config (a variable, a spread that could supply the key) is not
|
|
2634
|
+
* statically decidable, so those constructions are skipped rather than flagged.
|
|
2635
|
+
* Only runs when the codegen feeder supplied evidence (`context.exportSinks`
|
|
2636
|
+
* present); a runtime caller flags nothing.
|
|
2637
|
+
*/
|
|
2638
|
+
declare const exportSinkMisconfigured: Lint;
|
|
2472
2639
|
/**
|
|
2473
2640
|
* Flags a `.source({ mode: "incremental" })` table that declares neither a
|
|
2474
2641
|
* `reconcileEveryMs` sweep nor a `softDeleteColumn` (plan 136).
|
|
@@ -2559,6 +2726,33 @@ declare const filterWithoutIndex: Lint;
|
|
|
2559
2726
|
* low. One finding per read.
|
|
2560
2727
|
*/
|
|
2561
2728
|
declare const flagGatesSecurityWithUnsafeDefault: Lint;
|
|
2729
|
+
/**
|
|
2730
|
+
* A correctness lint exploiting Lunora's static edge: a `.geoIndex(name, { field })`
|
|
2731
|
+
* maintains a geohash companion over a `v.geoPoint()` column, and
|
|
2732
|
+
* `withGeoIndex(...).near()/.within()` reads its `lat`/`lng`. If `field` is any
|
|
2733
|
+
* other type, the companion can't be built from `{ lat, lng }` and the index is
|
|
2734
|
+
* dead — it can never answer a geo query. Catching it at codegen time beats a
|
|
2735
|
+
* silently-empty proximity result at runtime.
|
|
2736
|
+
*/
|
|
2737
|
+
declare const geoIndexFieldNotGeopoint: Lint;
|
|
2738
|
+
/**
|
|
2739
|
+
* Flags a declared `.geoIndex(name, { field })` that no handler queries via
|
|
2740
|
+
* `withGeoIndex(name, …)`.
|
|
2741
|
+
*
|
|
2742
|
+
* A geo index maintains a geohash companion column on the row: every write stamps
|
|
2743
|
+
* it, every byte of storage holds it. If nothing ever reads it through
|
|
2744
|
+
* `withGeoIndex(name, q => q.near(…) | q.within(…))` the companion is pure dead
|
|
2745
|
+
* overhead — the geo analogue of a dead regular index. Cross-references every geo
|
|
2746
|
+
* index in the schema against the set of index names some handler references via
|
|
2747
|
+
* `withGeoIndex("<name>")`.
|
|
2748
|
+
*
|
|
2749
|
+
* Suppressed entirely when any usage passes a non-literal name
|
|
2750
|
+
* (`withGeoIndex(someVariable, …)`), because a dynamic reference could target any
|
|
2751
|
+
* declared geo index — flagging "unused" then would be a false positive. Only
|
|
2752
|
+
* runs when the usage feeder supplied evidence (`context.geoIndexUsages`
|
|
2753
|
+
* present); a runtime caller with no evidence flags nothing.
|
|
2754
|
+
*/
|
|
2755
|
+
declare const geoIndexUnused: Lint;
|
|
2562
2756
|
/**
|
|
2563
2757
|
* Flags a secret-shaped string literal checked into the lunora source.
|
|
2564
2758
|
*
|
|
@@ -2911,6 +3105,38 @@ declare const nondeterministicQueryMutation: Lint;
|
|
|
2911
3105
|
* runtime caller flags nothing.
|
|
2912
3106
|
*/
|
|
2913
3107
|
declare const normalizeIdUsedAsAuthorization: Lint;
|
|
3108
|
+
/**
|
|
3109
|
+
* Flags an app that sends push notifications (`ctx.push.send` / `ctx.push.broadcast`)
|
|
3110
|
+
* but whose `defineNotify(...)` wires **neither** the Web Push nor the FCM channel.
|
|
3111
|
+
*
|
|
3112
|
+
* With no push channel configured, every `ctx.push` send fails at runtime (the
|
|
3113
|
+
* routing push provider has nothing to dispatch to) — a silent, deploy-time-only
|
|
3114
|
+
* misconfiguration. Web Push additionally needs the `VAPID_*` secrets and FCM the
|
|
3115
|
+
* `FCM_*` secrets; those are scaffolded into `.dev.vars` from `@lunora/config`'s
|
|
3116
|
+
* package-secrets registry, but the channel must still be wired in `defineNotify`.
|
|
3117
|
+
*
|
|
3118
|
+
* This lint runs when the codegen feeder has supplied config evidence
|
|
3119
|
+
* (`context.notifyConfig` present); a runtime caller with no evidence flags
|
|
3120
|
+
* nothing.
|
|
3121
|
+
*/
|
|
3122
|
+
declare const notifyMissingPushConfig: Lint;
|
|
3123
|
+
/**
|
|
3124
|
+
* Flags a `@lunora/notify` send (`ctx.notify.*` / `ctx.push.*`) inside a
|
|
3125
|
+
* `query(...)` or `mutation(...)` handler body.
|
|
3126
|
+
*
|
|
3127
|
+
* A notification send is external I/O — a `fetch` to a Web Push service or FCM.
|
|
3128
|
+
* Like `fetch`/`ctx.sql`, it is non-deterministic, so it breaks the determinism
|
|
3129
|
+
* the coordinator relies on when it re-runs a query on subscription re-evaluation
|
|
3130
|
+
* or a mutation on OCC retry — a retried mutation would fire the notification
|
|
3131
|
+
* again (duplicate pushes). `ctx.notify` / `ctx.push` are therefore wired onto
|
|
3132
|
+
* `ActionCtx` only and belong exclusively in `action(...)` handlers.
|
|
3133
|
+
*
|
|
3134
|
+
* This lint runs when the codegen feeder has supplied send evidence
|
|
3135
|
+
* (`context.notifyCalls` present); a runtime caller with no evidence flags
|
|
3136
|
+
* nothing rather than raising false alarms. The feeder records sends only inside
|
|
3137
|
+
* `query`/`mutation` handlers, so `action(...)` bodies never reach here.
|
|
3138
|
+
*/
|
|
3139
|
+
declare const notifySendOutsideAction: Lint;
|
|
2914
3140
|
/**
|
|
2915
3141
|
* Nudges a `.public()` `query` whose handler returns raw table rows — with no
|
|
2916
3142
|
* `.output(...)` projection and no `.use(mask(...))` — when that table carries
|
|
@@ -3328,6 +3554,24 @@ declare const shapeTargetsGlobalTable: Lint;
|
|
|
3328
3554
|
* rather than raising false alarms.
|
|
3329
3555
|
*/
|
|
3330
3556
|
declare const shapeUnknownTable: Lint;
|
|
3557
|
+
/**
|
|
3558
|
+
* Flags a public `mutation`/`action` that creates a user/session/account row but
|
|
3559
|
+
* installs no email-domain gate.
|
|
3560
|
+
*
|
|
3561
|
+
* Account-minting endpoints are the classic throwaway-signup surface: disposable
|
|
3562
|
+
* mailboxes farm free trials, evade bans, and pollute the user table. Lunora
|
|
3563
|
+
* ships `emailGateMiddleware` (`@lunora/auth/email-guard`) — pure-data on the
|
|
3564
|
+
* default (edge-safe) path — that rejects disposable domains at signup. This lint
|
|
3565
|
+
* fires when a public procedure writes a user/session/account-shaped table with
|
|
3566
|
+
* no email gate, pairing with the existing `user_creating_mutation_without_captcha`
|
|
3567
|
+
* lint (a CAPTCHA stops bots; the email gate stops throwaway domains — both are
|
|
3568
|
+
* worth having).
|
|
3569
|
+
*
|
|
3570
|
+
* Runs only when the codegen feeder supplies protection evidence
|
|
3571
|
+
* (`context.procedureProtections`); a runtime caller with no evidence flags
|
|
3572
|
+
* nothing.
|
|
3573
|
+
*/
|
|
3574
|
+
declare const signupMutationWithoutDisposableGating: Lint;
|
|
3331
3575
|
/**
|
|
3332
3576
|
* Flags a public read that resurfaces soft-deleted rows via `includeDeleted` —
|
|
3333
3577
|
* either hardcoded `true` or wired from the handler's `args`.
|
|
@@ -3487,6 +3731,15 @@ declare const storageUploadWithoutMaxSize: Lint;
|
|
|
3487
3731
|
* a runtime caller with no insert signal flags nothing rather than every table.
|
|
3488
3732
|
*/
|
|
3489
3733
|
declare const tableWithoutInsert: Lint;
|
|
3734
|
+
/**
|
|
3735
|
+
* A correctness lint with no splinter analogue — Lunora's static edge again: the
|
|
3736
|
+
* `.ttl(field)` policy names an epoch-millisecond expiry column, and the DO alarm
|
|
3737
|
+
* sweep compares `field < now`. If `field` is a non-time column (a string token, a
|
|
3738
|
+
* boolean flag, an object), the comparison is meaningless and rows either never
|
|
3739
|
+
* expire or expire unpredictably — a bug the fully-declared schema catches at
|
|
3740
|
+
* codegen time rather than at 3am when the sweep silently misbehaves.
|
|
3741
|
+
*/
|
|
3742
|
+
declare const ttlFieldNotTimestamp: Lint;
|
|
3490
3743
|
/**
|
|
3491
3744
|
* Flags a public `v.string()` argument with no length bound.
|
|
3492
3745
|
*
|
|
@@ -3653,4 +3906,4 @@ interface RunAdvisorOptions {
|
|
|
3653
3906
|
* `static` lints at build time and defer `runtime` lints to a live shard.
|
|
3654
3907
|
*/
|
|
3655
3908
|
declare const runAdvisor: (context: LintContext, options?: RunAdvisorOptions) => Finding[];
|
|
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 };
|
|
3909
|
+
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 AdvisorExportSink, type AdvisorFailOpenGuard, type AdvisorFlagSecurityDefault, type AdvisorGeoIndexUsage, 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 AdvisorNotifyCall, type AdvisorNotifyConfig, 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, exportSinkMisconfigured, externalSourceIncrementalNoDeletePath, externalSourceOnGlobal, externalSourceUnscoped, filterWithoutIndex, flagGatesSecurityWithUnsafeDefault, fromServerSchema, geoIndexFieldNotGeopoint, geoIndexUnused, hardcodedSecret, hotShard, httpActionMissingAuthGuard, httpActionResponseHeaderInjection, hyperdriveOutsideAction, identityUndeclaredClaimTrusted, imagesUrlSourceFromUserInput, indexReferencesUnknownField, indexUtilization, insertManyUnsafeUserData, kvUnscopedUserKeyIdor, loadAnalyticsRuntimeMetrics, mailInboundDispatchWithoutVerify, mailRecipientFromRequestInput, maskUncoveredPiiColumn, maskWeakHashStrategyOnPii, maskedRelationLeakViaWith, mutatorFullRowReplace, nondeterministicQueryMutation, normalizeIdUsedAsAuthorization, notifyMissingPushConfig, notifySendOutsideAction, outputProjectionMissingOnPublicRead, ownerFieldFromArgsNotAuth, paymentCreateWithoutAuthorize, paymentWebhookWideTolerance, plaintextSecretInWranglerVariables, policyReferencesUnknownTable, privilegedDispatchUnvalidatedPayload, privilegedFanoutFromPublicProcedure, publicArgumentUsesAny, publicMutationWithoutRatelimit, publicTableRlsOptoutConfusion, queueWithoutDlq, r2sqlOutsideAction, ratelimitDefaultMemoryStore, ratelimitKeySpoofableOrGlobal, ratelimitMiddlewareFailOpen, relationReferencesUnknownField, relationReferencesUnknownTable, rlsUncoveredTable, runAdvisor, shapeTargetsGlobalTable, shapeUnknownTable, signupMutationWithoutDisposableGating, softDeleteIncludeDeletedFromArgs, sqlInjectionRisk, storageGenerateUploadUrlNoContentTypePin, storageKeyFromUserArgs, storagePresignedUrlForPrivateContent, storageUploadWithoutContentTypeAllowlist, storageUploadWithoutMaxSize, tableWithoutInsert, ttlFieldNotTimestamp, unboundedStringArgument, unindexedForeignKey, unindexedRelationTarget, userCreatingMutationWithoutCaptcha, vectorsNamespaceFromUserInput, workflowDuplicateStepName, workflowUnknownTarget, workflowUnused };
|
package/dist/index.d.ts
CHANGED
|
@@ -280,6 +280,39 @@ interface AdvisorContainer {
|
|
|
280
280
|
/** Declared `sleepAfter` value, when a static literal. */
|
|
281
281
|
sleepAfter?: number | string;
|
|
282
282
|
}
|
|
283
|
+
/**
|
|
284
|
+
* One CDC export-sink construction discovered in a function body — the input the
|
|
285
|
+
* `export_sink_misconfigured` lint consumes. Produced by the codegen feeder,
|
|
286
|
+
* which walks the lunora source for the three sink factories the export tap
|
|
287
|
+
* (plan 170) ships: `defineExportSink({ name, deliver })`,
|
|
288
|
+
* `webhookExportSink({ name, url, … })`, and `r2Sink({ name, bucket, … })`.
|
|
289
|
+
*
|
|
290
|
+
* A sink missing a required field can never deliver a change batch — a webhook
|
|
291
|
+
* with no `url` would POST to `undefined`, an R2 sink with no `bucket` binding has
|
|
292
|
+
* nowhere to write, and a custom sink with no `deliver` has no delivery path. The
|
|
293
|
+
* runtime `defineExportSink` guard throws for a missing `name`/`deliver`, but the
|
|
294
|
+
* built-in `webhookExportSink`/`r2Sink` don't validate `url`/`bucket`, so catching
|
|
295
|
+
* the misconfiguration statically beats a silently-dead export tap at runtime.
|
|
296
|
+
* Runtime callers don't supply this, so the lint finds nothing there.
|
|
297
|
+
*/
|
|
298
|
+
interface AdvisorExportSink {
|
|
299
|
+
/**
|
|
300
|
+
* True when the factory's config argument is a statically analyzable object
|
|
301
|
+
* literal. A non-literal config (a variable, a spread) is not decidable, so
|
|
302
|
+
* the lint skips it rather than raising a false alarm.
|
|
303
|
+
*/
|
|
304
|
+
analyzable: boolean;
|
|
305
|
+
/** Present keys whose value is an empty-string literal (`""`) — treated as missing. */
|
|
306
|
+
emptyKeys: string[];
|
|
307
|
+
/** Which sink factory was called. */
|
|
308
|
+
factory: "defineExportSink" | "r2Sink" | "webhookExportSink";
|
|
309
|
+
/** Source file the construction appears in (relative to the lunora dir, no extension). */
|
|
310
|
+
file: string;
|
|
311
|
+
/** 1-based line of the factory call, or `0` when unknown. */
|
|
312
|
+
line: number;
|
|
313
|
+
/** Config keys present on the object literal, regardless of value. */
|
|
314
|
+
presentKeys: string[];
|
|
315
|
+
}
|
|
283
316
|
/**
|
|
284
317
|
* One rate-limit / Turnstile middleware call — the `ratelimit_middleware_fail_open`
|
|
285
318
|
* lint input. `rateLimit`/`dbRateLimit` (`@lunora/ratelimit`) and
|
|
@@ -328,6 +361,27 @@ interface AdvisorFlagSecurityDefault {
|
|
|
328
361
|
/** 1-based line of the `ctx.flags.boolean` call, or `0` when unknown. */
|
|
329
362
|
line: number;
|
|
330
363
|
}
|
|
364
|
+
/**
|
|
365
|
+
* One `withGeoIndex("name", …)` read discovered in a function body — the use-side
|
|
366
|
+
* input the `geo_index_unused` lint cross-references against the declared geo
|
|
367
|
+
* indexes in the lint context's schema. Produced by the codegen feeder, which
|
|
368
|
+
* walks the lunora source for `ctx.db.query("t").withGeoIndex(name, …)` /
|
|
369
|
+
* `ctx.db.<table>.withGeoIndex(name, …)` reads and records each referenced index
|
|
370
|
+
* name. Runtime callers don't supply it, so the lint finds nothing there.
|
|
371
|
+
*
|
|
372
|
+
* A `.geoIndex(name, { field })` maintains a geohash companion column on every
|
|
373
|
+
* write; if no handler ever reads it via `withGeoIndex(name, …)` the companion is
|
|
374
|
+
* dead overhead (maintained on every write, read by nothing) — the geo analogue
|
|
375
|
+
* of a dead regular index.
|
|
376
|
+
*/
|
|
377
|
+
interface AdvisorGeoIndexUsage {
|
|
378
|
+
/** Source file the read appears in (relative to the lunora dir, no extension). */
|
|
379
|
+
file: string;
|
|
380
|
+
/** The referenced geo-index name; empty when the `withGeoIndex(...)` argument is not a string literal. */
|
|
381
|
+
indexName: string;
|
|
382
|
+
/** 1-based line of the `withGeoIndex(...)` call, or `0` when unknown. */
|
|
383
|
+
line: number;
|
|
384
|
+
}
|
|
331
385
|
/**
|
|
332
386
|
* One discovered `httpAction`/`httpRoute` handler that performs a side effect
|
|
333
387
|
* (`ctx.runMutation` / `ctx.runAction` / a `ctx.db.{insert,patch,replace,delete,
|
|
@@ -688,6 +742,48 @@ interface AdvisorNormalizeIdAuthorization {
|
|
|
688
742
|
/** `"internal"` for `internalQuery`/`internalMutation`; `"public"` for `query`/`mutation`. */
|
|
689
743
|
visibility: "internal" | "public";
|
|
690
744
|
}
|
|
745
|
+
/**
|
|
746
|
+
* One `ctx.notify` / `ctx.push` send discovered lexically inside a `query(...)`
|
|
747
|
+
* or `mutation(...)` handler body — the input the `notify_send_outside_action`
|
|
748
|
+
* lint consumes. Produced by the codegen feeder, which walks each exported
|
|
749
|
+
* function's handler with ts-morph and records the `@lunora/notify` send surface
|
|
750
|
+
* (`ctx.notify.send`, `ctx.notify.chat/inApp/webhook`, `ctx.push.send`,
|
|
751
|
+
* `ctx.push.broadcast`).
|
|
752
|
+
*
|
|
753
|
+
* A notification send is external I/O (a `fetch` to a push service / FCM): it is
|
|
754
|
+
* non-deterministic like `fetch`, so it breaks the determinism the coordinator
|
|
755
|
+
* relies on when re-running a query on subscription re-evaluation or a mutation on
|
|
756
|
+
* OCC retry (a retried mutation would re-send). It therefore belongs **only** in
|
|
757
|
+
* `action(...)` handlers. Calls inside `action(...)` are intentionally **not**
|
|
758
|
+
* recorded — actions are the escape hatch. Runtime callers don't supply this, so
|
|
759
|
+
* the lint finds nothing there.
|
|
760
|
+
*/
|
|
761
|
+
interface AdvisorNotifyCall {
|
|
762
|
+
/** The send surface accessed, e.g. `ctx.push.broadcast` / `ctx.notify.send`. */
|
|
763
|
+
callee: string;
|
|
764
|
+
/** The exported function performing the send (e.g. `sendWelcome`). */
|
|
765
|
+
exportName: string;
|
|
766
|
+
/** Source file the send appears in (relative to the lunora dir, no extension). */
|
|
767
|
+
file: string;
|
|
768
|
+
/** Which procedure kind the send lives in — only `query`/`mutation` are flagged; actions are exempt. */
|
|
769
|
+
kind: "mutation" | "query";
|
|
770
|
+
/** 1-based line of the send, or `0` when unknown. */
|
|
771
|
+
line: number;
|
|
772
|
+
}
|
|
773
|
+
/**
|
|
774
|
+
* Whether an app uses `ctx.push` and which push channels `defineNotify(...)`
|
|
775
|
+
* configures — the input the `notify_missing_push_config` lint consumes. Produced
|
|
776
|
+
* by the codegen feeder from the resolved `lunora/notify.ts` definition and the
|
|
777
|
+
* discovered `ctx.push` usage; absent for runtime callers.
|
|
778
|
+
*/
|
|
779
|
+
interface AdvisorNotifyConfig {
|
|
780
|
+
/** Whether `defineNotify` wired the FCM channel. */
|
|
781
|
+
hasFcm: boolean;
|
|
782
|
+
/** Whether `defineNotify` wired the Web Push channel. */
|
|
783
|
+
hasWebPush: boolean;
|
|
784
|
+
/** Whether any handler sends a push (`ctx.push.send` / `ctx.push.broadcast`). */
|
|
785
|
+
usesPush: boolean;
|
|
786
|
+
}
|
|
691
787
|
/**
|
|
692
788
|
* One `ctx.db` write (`insert` / `replace` / `patch` / `insertManyUnsafe`) that
|
|
693
789
|
* sets an ownership / identity column — `userId`, `ownerId`, `tenantId`, and the
|
|
@@ -786,6 +882,8 @@ interface AdvisorProcedureProtection {
|
|
|
786
882
|
unboundedAiGeneration: boolean;
|
|
787
883
|
/** `true` when the chain carries `.use(verifyTurnstile(...))` or a `protectPublic({ captcha })` bundle. */
|
|
788
884
|
usesCaptcha: boolean;
|
|
885
|
+
/** `true` when the chain carries `.use(emailGateMiddleware(...))` from `@lunora/auth`. Read by the `signup_mutation_without_disposable_gating` lint (paired with public visibility + a user-table write). */
|
|
886
|
+
usesEmailGate: boolean;
|
|
789
887
|
/** `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
888
|
usesInsertManyUnsafe: boolean;
|
|
791
889
|
/** `true` when the chain carries `.use(mask(...))`. */
|
|
@@ -1000,6 +1098,14 @@ interface AdvisorSchema {
|
|
|
1000
1098
|
}
|
|
1001
1099
|
/** A table plus the column/index/relation metadata lints inspect. */
|
|
1002
1100
|
interface AdvisorTable {
|
|
1101
|
+
/**
|
|
1102
|
+
* Effective validator kind per declared column (a `v.optional(...)` is
|
|
1103
|
+
* unwrapped to its inner kind). Read by the schema-type lints
|
|
1104
|
+
* (`ttl_field_not_timestamp`, `geo_index_field_not_geopoint`) to check a
|
|
1105
|
+
* referenced column's type. Optional — a feeder that doesn't track column
|
|
1106
|
+
* kinds omits it, and the type lints then skip the check.
|
|
1107
|
+
*/
|
|
1108
|
+
columnKinds?: Record<string, string>;
|
|
1003
1109
|
/**
|
|
1004
1110
|
* `true` when the table is written outside Lunora's discoverable insert path
|
|
1005
1111
|
* — declared via `.externallyManaged()` (e.g. `@lunora/auth`'s better-auth
|
|
@@ -1067,6 +1173,15 @@ interface AdvisorTable {
|
|
|
1067
1173
|
softDelete?: {
|
|
1068
1174
|
field: string;
|
|
1069
1175
|
};
|
|
1176
|
+
/**
|
|
1177
|
+
* Set when the table declared `.ttl(field, { after? })`. Read by the
|
|
1178
|
+
* `ttl_field_not_timestamp` lint to confirm the expiry column is time-typed.
|
|
1179
|
+
* Optional — a feeder that doesn't track TTL omits it.
|
|
1180
|
+
*/
|
|
1181
|
+
ttl?: {
|
|
1182
|
+
after?: number;
|
|
1183
|
+
field: string;
|
|
1184
|
+
};
|
|
1070
1185
|
}
|
|
1071
1186
|
/**
|
|
1072
1187
|
* One declared index, flattened across Lunora's index kinds so a single lint can
|
|
@@ -1079,7 +1194,7 @@ interface AdvisorTable {
|
|
|
1079
1194
|
*/
|
|
1080
1195
|
interface AdvisorIndex {
|
|
1081
1196
|
fields: ReadonlyArray<string>;
|
|
1082
|
-
kind: "index" | "rank" | "search" | "vector";
|
|
1197
|
+
kind: "geo" | "index" | "rank" | "search" | "vector";
|
|
1083
1198
|
name: string;
|
|
1084
1199
|
unique?: boolean;
|
|
1085
1200
|
}
|
|
@@ -1558,6 +1673,16 @@ interface LintContext {
|
|
|
1558
1673
|
* the container lints find nothing.
|
|
1559
1674
|
*/
|
|
1560
1675
|
containers?: ReadonlyArray<AdvisorContainer>;
|
|
1676
|
+
/**
|
|
1677
|
+
* CDC export-sink constructions (`defineExportSink` / `webhookExportSink` /
|
|
1678
|
+
* `r2Sink`) discovered in function bodies — the `export_sink_misconfigured`
|
|
1679
|
+
* input. Each carries which config keys were present (and which were an empty
|
|
1680
|
+
* string), so the lint can flag a sink missing a required field (a webhook
|
|
1681
|
+
* with no `url`, an R2 sink with no `bucket`, a sink with no `name`/`deliver`).
|
|
1682
|
+
* Supplied by the codegen feeder; absent for runtime callers, where the lint
|
|
1683
|
+
* finds nothing.
|
|
1684
|
+
*/
|
|
1685
|
+
exportSinks?: ReadonlyArray<AdvisorExportSink>;
|
|
1561
1686
|
/**
|
|
1562
1687
|
* `rateLimit`/`dbRateLimit` (`@lunora/ratelimit`) and `verifyTurnstileMiddleware`
|
|
1563
1688
|
* (`@lunora/auth`) middleware calls, each with whether its options literal set
|
|
@@ -1579,6 +1704,15 @@ interface LintContext {
|
|
|
1579
1704
|
* codegen feeder; absent for runtime callers, where the lint finds nothing.
|
|
1580
1705
|
*/
|
|
1581
1706
|
flagSecurityDefaults?: ReadonlyArray<AdvisorFlagSecurityDefault>;
|
|
1707
|
+
/**
|
|
1708
|
+
* `withGeoIndex("name", …)` reads discovered in function bodies — the use-side
|
|
1709
|
+
* input the `geo_index_unused` lint cross-references against the declared geo
|
|
1710
|
+
* indexes in {@link LintContext.schema}. A declared `.geoIndex(name, …)` with
|
|
1711
|
+
* no matching read is dead overhead (its geohash companion is maintained on
|
|
1712
|
+
* every write and read by nothing). Supplied by the codegen feeder; absent for
|
|
1713
|
+
* runtime callers, where the lint finds nothing.
|
|
1714
|
+
*/
|
|
1715
|
+
geoIndexUsages?: ReadonlyArray<AdvisorGeoIndexUsage>;
|
|
1582
1716
|
/**
|
|
1583
1717
|
* `httpAction`/`httpRoute` handlers that perform a side effect
|
|
1584
1718
|
* (`ctx.runMutation` / `ctx.runAction` / a `ctx.db` write) from the HTTP edge,
|
|
@@ -1706,6 +1840,21 @@ interface LintContext {
|
|
|
1706
1840
|
* for runtime callers, where the lint finds nothing.
|
|
1707
1841
|
*/
|
|
1708
1842
|
normalizeIdAuthorizations?: ReadonlyArray<AdvisorNormalizeIdAuthorization>;
|
|
1843
|
+
/**
|
|
1844
|
+
* `ctx.notify` / `ctx.push` sends discovered lexically inside `query`/`mutation`
|
|
1845
|
+
* handler bodies — the `notify_send_outside_action` input. Supplied by the
|
|
1846
|
+
* codegen feeder, which omits `action` handlers (where these facades are the
|
|
1847
|
+
* typed, intended surface); absent for runtime callers, where the lint finds
|
|
1848
|
+
* nothing.
|
|
1849
|
+
*/
|
|
1850
|
+
notifyCalls?: ReadonlyArray<AdvisorNotifyCall>;
|
|
1851
|
+
/**
|
|
1852
|
+
* Whether the app uses `ctx.push` and which push channels `defineNotify(...)`
|
|
1853
|
+
* wires — the `notify_missing_push_config` input. Supplied by the codegen
|
|
1854
|
+
* feeder from the resolved `lunora/notify.ts` definition; absent for runtime
|
|
1855
|
+
* callers, where the lint finds nothing.
|
|
1856
|
+
*/
|
|
1857
|
+
notifyConfig?: AdvisorNotifyConfig;
|
|
1709
1858
|
/**
|
|
1710
1859
|
* `ctx.db` writes (`insert` / `replace` / `patch` / `insertManyUnsafe`) that set
|
|
1711
1860
|
* an ownership / identity column (`userId`, `ownerId`, `tenantId`, …) from the
|
|
@@ -2469,6 +2618,24 @@ declare const duplicateIndex: Lint;
|
|
|
2469
2618
|
* least one field by construction, so only `kind: "index"` is checked.)
|
|
2470
2619
|
*/
|
|
2471
2620
|
declare const emptyIndex: Lint;
|
|
2621
|
+
/**
|
|
2622
|
+
* Flags a misconfigured CDC export sink (plan 170) — a `defineExportSink` /
|
|
2623
|
+
* `webhookExportSink` / `r2Sink` construction with a required config field
|
|
2624
|
+
* missing or set to an empty string.
|
|
2625
|
+
*
|
|
2626
|
+
* The runtime `defineExportSink` guard throws for a missing `name`/`deliver`, but
|
|
2627
|
+
* the built-in `webhookExportSink` / `r2Sink` never validate their `url` /
|
|
2628
|
+
* `bucket`, so a webhook sink with no URL or an R2 sink with no bucket binding
|
|
2629
|
+
* ships and silently fails to drain — the export tap advances no cursor and the
|
|
2630
|
+
* warehouse never sees a row. Catching it at codegen time beats a dead tap in
|
|
2631
|
+
* production.
|
|
2632
|
+
*
|
|
2633
|
+
* A non-literal config (a variable, a spread that could supply the key) is not
|
|
2634
|
+
* statically decidable, so those constructions are skipped rather than flagged.
|
|
2635
|
+
* Only runs when the codegen feeder supplied evidence (`context.exportSinks`
|
|
2636
|
+
* present); a runtime caller flags nothing.
|
|
2637
|
+
*/
|
|
2638
|
+
declare const exportSinkMisconfigured: Lint;
|
|
2472
2639
|
/**
|
|
2473
2640
|
* Flags a `.source({ mode: "incremental" })` table that declares neither a
|
|
2474
2641
|
* `reconcileEveryMs` sweep nor a `softDeleteColumn` (plan 136).
|
|
@@ -2559,6 +2726,33 @@ declare const filterWithoutIndex: Lint;
|
|
|
2559
2726
|
* low. One finding per read.
|
|
2560
2727
|
*/
|
|
2561
2728
|
declare const flagGatesSecurityWithUnsafeDefault: Lint;
|
|
2729
|
+
/**
|
|
2730
|
+
* A correctness lint exploiting Lunora's static edge: a `.geoIndex(name, { field })`
|
|
2731
|
+
* maintains a geohash companion over a `v.geoPoint()` column, and
|
|
2732
|
+
* `withGeoIndex(...).near()/.within()` reads its `lat`/`lng`. If `field` is any
|
|
2733
|
+
* other type, the companion can't be built from `{ lat, lng }` and the index is
|
|
2734
|
+
* dead — it can never answer a geo query. Catching it at codegen time beats a
|
|
2735
|
+
* silently-empty proximity result at runtime.
|
|
2736
|
+
*/
|
|
2737
|
+
declare const geoIndexFieldNotGeopoint: Lint;
|
|
2738
|
+
/**
|
|
2739
|
+
* Flags a declared `.geoIndex(name, { field })` that no handler queries via
|
|
2740
|
+
* `withGeoIndex(name, …)`.
|
|
2741
|
+
*
|
|
2742
|
+
* A geo index maintains a geohash companion column on the row: every write stamps
|
|
2743
|
+
* it, every byte of storage holds it. If nothing ever reads it through
|
|
2744
|
+
* `withGeoIndex(name, q => q.near(…) | q.within(…))` the companion is pure dead
|
|
2745
|
+
* overhead — the geo analogue of a dead regular index. Cross-references every geo
|
|
2746
|
+
* index in the schema against the set of index names some handler references via
|
|
2747
|
+
* `withGeoIndex("<name>")`.
|
|
2748
|
+
*
|
|
2749
|
+
* Suppressed entirely when any usage passes a non-literal name
|
|
2750
|
+
* (`withGeoIndex(someVariable, …)`), because a dynamic reference could target any
|
|
2751
|
+
* declared geo index — flagging "unused" then would be a false positive. Only
|
|
2752
|
+
* runs when the usage feeder supplied evidence (`context.geoIndexUsages`
|
|
2753
|
+
* present); a runtime caller with no evidence flags nothing.
|
|
2754
|
+
*/
|
|
2755
|
+
declare const geoIndexUnused: Lint;
|
|
2562
2756
|
/**
|
|
2563
2757
|
* Flags a secret-shaped string literal checked into the lunora source.
|
|
2564
2758
|
*
|
|
@@ -2911,6 +3105,38 @@ declare const nondeterministicQueryMutation: Lint;
|
|
|
2911
3105
|
* runtime caller flags nothing.
|
|
2912
3106
|
*/
|
|
2913
3107
|
declare const normalizeIdUsedAsAuthorization: Lint;
|
|
3108
|
+
/**
|
|
3109
|
+
* Flags an app that sends push notifications (`ctx.push.send` / `ctx.push.broadcast`)
|
|
3110
|
+
* but whose `defineNotify(...)` wires **neither** the Web Push nor the FCM channel.
|
|
3111
|
+
*
|
|
3112
|
+
* With no push channel configured, every `ctx.push` send fails at runtime (the
|
|
3113
|
+
* routing push provider has nothing to dispatch to) — a silent, deploy-time-only
|
|
3114
|
+
* misconfiguration. Web Push additionally needs the `VAPID_*` secrets and FCM the
|
|
3115
|
+
* `FCM_*` secrets; those are scaffolded into `.dev.vars` from `@lunora/config`'s
|
|
3116
|
+
* package-secrets registry, but the channel must still be wired in `defineNotify`.
|
|
3117
|
+
*
|
|
3118
|
+
* This lint runs when the codegen feeder has supplied config evidence
|
|
3119
|
+
* (`context.notifyConfig` present); a runtime caller with no evidence flags
|
|
3120
|
+
* nothing.
|
|
3121
|
+
*/
|
|
3122
|
+
declare const notifyMissingPushConfig: Lint;
|
|
3123
|
+
/**
|
|
3124
|
+
* Flags a `@lunora/notify` send (`ctx.notify.*` / `ctx.push.*`) inside a
|
|
3125
|
+
* `query(...)` or `mutation(...)` handler body.
|
|
3126
|
+
*
|
|
3127
|
+
* A notification send is external I/O — a `fetch` to a Web Push service or FCM.
|
|
3128
|
+
* Like `fetch`/`ctx.sql`, it is non-deterministic, so it breaks the determinism
|
|
3129
|
+
* the coordinator relies on when it re-runs a query on subscription re-evaluation
|
|
3130
|
+
* or a mutation on OCC retry — a retried mutation would fire the notification
|
|
3131
|
+
* again (duplicate pushes). `ctx.notify` / `ctx.push` are therefore wired onto
|
|
3132
|
+
* `ActionCtx` only and belong exclusively in `action(...)` handlers.
|
|
3133
|
+
*
|
|
3134
|
+
* This lint runs when the codegen feeder has supplied send evidence
|
|
3135
|
+
* (`context.notifyCalls` present); a runtime caller with no evidence flags
|
|
3136
|
+
* nothing rather than raising false alarms. The feeder records sends only inside
|
|
3137
|
+
* `query`/`mutation` handlers, so `action(...)` bodies never reach here.
|
|
3138
|
+
*/
|
|
3139
|
+
declare const notifySendOutsideAction: Lint;
|
|
2914
3140
|
/**
|
|
2915
3141
|
* Nudges a `.public()` `query` whose handler returns raw table rows — with no
|
|
2916
3142
|
* `.output(...)` projection and no `.use(mask(...))` — when that table carries
|
|
@@ -3328,6 +3554,24 @@ declare const shapeTargetsGlobalTable: Lint;
|
|
|
3328
3554
|
* rather than raising false alarms.
|
|
3329
3555
|
*/
|
|
3330
3556
|
declare const shapeUnknownTable: Lint;
|
|
3557
|
+
/**
|
|
3558
|
+
* Flags a public `mutation`/`action` that creates a user/session/account row but
|
|
3559
|
+
* installs no email-domain gate.
|
|
3560
|
+
*
|
|
3561
|
+
* Account-minting endpoints are the classic throwaway-signup surface: disposable
|
|
3562
|
+
* mailboxes farm free trials, evade bans, and pollute the user table. Lunora
|
|
3563
|
+
* ships `emailGateMiddleware` (`@lunora/auth/email-guard`) — pure-data on the
|
|
3564
|
+
* default (edge-safe) path — that rejects disposable domains at signup. This lint
|
|
3565
|
+
* fires when a public procedure writes a user/session/account-shaped table with
|
|
3566
|
+
* no email gate, pairing with the existing `user_creating_mutation_without_captcha`
|
|
3567
|
+
* lint (a CAPTCHA stops bots; the email gate stops throwaway domains — both are
|
|
3568
|
+
* worth having).
|
|
3569
|
+
*
|
|
3570
|
+
* Runs only when the codegen feeder supplies protection evidence
|
|
3571
|
+
* (`context.procedureProtections`); a runtime caller with no evidence flags
|
|
3572
|
+
* nothing.
|
|
3573
|
+
*/
|
|
3574
|
+
declare const signupMutationWithoutDisposableGating: Lint;
|
|
3331
3575
|
/**
|
|
3332
3576
|
* Flags a public read that resurfaces soft-deleted rows via `includeDeleted` —
|
|
3333
3577
|
* either hardcoded `true` or wired from the handler's `args`.
|
|
@@ -3487,6 +3731,15 @@ declare const storageUploadWithoutMaxSize: Lint;
|
|
|
3487
3731
|
* a runtime caller with no insert signal flags nothing rather than every table.
|
|
3488
3732
|
*/
|
|
3489
3733
|
declare const tableWithoutInsert: Lint;
|
|
3734
|
+
/**
|
|
3735
|
+
* A correctness lint with no splinter analogue — Lunora's static edge again: the
|
|
3736
|
+
* `.ttl(field)` policy names an epoch-millisecond expiry column, and the DO alarm
|
|
3737
|
+
* sweep compares `field < now`. If `field` is a non-time column (a string token, a
|
|
3738
|
+
* boolean flag, an object), the comparison is meaningless and rows either never
|
|
3739
|
+
* expire or expire unpredictably — a bug the fully-declared schema catches at
|
|
3740
|
+
* codegen time rather than at 3am when the sweep silently misbehaves.
|
|
3741
|
+
*/
|
|
3742
|
+
declare const ttlFieldNotTimestamp: Lint;
|
|
3490
3743
|
/**
|
|
3491
3744
|
* Flags a public `v.string()` argument with no length bound.
|
|
3492
3745
|
*
|
|
@@ -3653,4 +3906,4 @@ interface RunAdvisorOptions {
|
|
|
3653
3906
|
* `static` lints at build time and defer `runtime` lints to a live shard.
|
|
3654
3907
|
*/
|
|
3655
3908
|
declare const runAdvisor: (context: LintContext, options?: RunAdvisorOptions) => Finding[];
|
|
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 };
|
|
3909
|
+
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 AdvisorExportSink, type AdvisorFailOpenGuard, type AdvisorFlagSecurityDefault, type AdvisorGeoIndexUsage, 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 AdvisorNotifyCall, type AdvisorNotifyConfig, 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, exportSinkMisconfigured, externalSourceIncrementalNoDeletePath, externalSourceOnGlobal, externalSourceUnscoped, filterWithoutIndex, flagGatesSecurityWithUnsafeDefault, fromServerSchema, geoIndexFieldNotGeopoint, geoIndexUnused, hardcodedSecret, hotShard, httpActionMissingAuthGuard, httpActionResponseHeaderInjection, hyperdriveOutsideAction, identityUndeclaredClaimTrusted, imagesUrlSourceFromUserInput, indexReferencesUnknownField, indexUtilization, insertManyUnsafeUserData, kvUnscopedUserKeyIdor, loadAnalyticsRuntimeMetrics, mailInboundDispatchWithoutVerify, mailRecipientFromRequestInput, maskUncoveredPiiColumn, maskWeakHashStrategyOnPii, maskedRelationLeakViaWith, mutatorFullRowReplace, nondeterministicQueryMutation, normalizeIdUsedAsAuthorization, notifyMissingPushConfig, notifySendOutsideAction, outputProjectionMissingOnPublicRead, ownerFieldFromArgsNotAuth, paymentCreateWithoutAuthorize, paymentWebhookWideTolerance, plaintextSecretInWranglerVariables, policyReferencesUnknownTable, privilegedDispatchUnvalidatedPayload, privilegedFanoutFromPublicProcedure, publicArgumentUsesAny, publicMutationWithoutRatelimit, publicTableRlsOptoutConfusion, queueWithoutDlq, r2sqlOutsideAction, ratelimitDefaultMemoryStore, ratelimitKeySpoofableOrGlobal, ratelimitMiddlewareFailOpen, relationReferencesUnknownField, relationReferencesUnknownTable, rlsUncoveredTable, runAdvisor, shapeTargetsGlobalTable, shapeUnknownTable, signupMutationWithoutDisposableGating, softDeleteIncludeDeletedFromArgs, sqlInjectionRisk, storageGenerateUploadUrlNoContentTypePin, storageKeyFromUserArgs, storagePresignedUrlForPrivateContent, storageUploadWithoutContentTypeAllowlist, storageUploadWithoutMaxSize, tableWithoutInsert, ttlFieldNotTimestamp, unboundedStringArgument, unindexedForeignKey, unindexedRelationTarget, userCreatingMutationWithoutCaptcha, vectorsNamespaceFromUserInput, workflowDuplicateStepName, workflowUnknownTarget, workflowUnused };
|
package/dist/index.mjs
CHANGED
|
@@ -24,11 +24,14 @@ import containerRuntimeEgressRelaxation from './packem_shared/containerRuntimeEg
|
|
|
24
24
|
import containerStartEnableInternetOverride from './packem_shared/containerStartEnableInternetOverride-DDaHZQ1L.mjs';
|
|
25
25
|
import duplicateIndex from './packem_shared/duplicateIndex-BOublMSt.mjs';
|
|
26
26
|
import emptyIndex from './packem_shared/emptyIndex-BX8EuEY7.mjs';
|
|
27
|
+
import exportSinkMisconfigured from './packem_shared/exportSinkMisconfigured-897sVMUa.mjs';
|
|
27
28
|
import externalSourceIncrementalNoDeletePath from './packem_shared/externalSourceIncrementalNoDeletePath-DvX9cRBD.mjs';
|
|
28
29
|
import externalSourceOnGlobal from './packem_shared/externalSourceOnGlobal-Bg-NfCX9.mjs';
|
|
29
30
|
import externalSourceUnscoped from './packem_shared/externalSourceUnscoped-5vT-Bup3.mjs';
|
|
30
31
|
import filterWithoutIndex from './packem_shared/filterWithoutIndex-BYVeJaSs.mjs';
|
|
31
32
|
import flagGatesSecurityWithUnsafeDefault from './packem_shared/flagGatesSecurityWithUnsafeDefault-BhIs0shr.mjs';
|
|
33
|
+
import geoIndexFieldNotGeopoint from './packem_shared/geoIndexFieldNotGeopoint-CcaInLhY.mjs';
|
|
34
|
+
import geoIndexUnused from './packem_shared/geoIndexUnused-D7I0UyND.mjs';
|
|
32
35
|
import hardcodedSecret from './packem_shared/hardcodedSecret-Be-pKVdn.mjs';
|
|
33
36
|
import httpActionMissingAuthGuard from './packem_shared/httpActionMissingAuthGuard-CxipddNx.mjs';
|
|
34
37
|
import httpActionResponseHeaderInjection from './packem_shared/httpActionResponseHeaderInjection-DOFS7pFT.mjs';
|
|
@@ -46,6 +49,8 @@ import maskedRelationLeakViaWith from './packem_shared/maskedRelationLeakViaWith
|
|
|
46
49
|
import mutatorFullRowReplace from './packem_shared/mutatorFullRowReplace-BJnNDaIV.mjs';
|
|
47
50
|
import nondeterministicQueryMutation from './packem_shared/nondeterministicQueryMutation-GXES1fLp.mjs';
|
|
48
51
|
import normalizeIdUsedAsAuthorization from './packem_shared/normalizeIdUsedAsAuthorization-BVPtCpzT.mjs';
|
|
52
|
+
import notifyMissingPushConfig from './packem_shared/notifyMissingPushConfig-Ca9EzRzy.mjs';
|
|
53
|
+
import notifySendOutsideAction from './packem_shared/notifySendOutsideAction-BUZSIZa4.mjs';
|
|
49
54
|
import outputProjectionMissingOnPublicRead from './packem_shared/outputProjectionMissingOnPublicRead-Bl5IMx0k.mjs';
|
|
50
55
|
import ownerFieldFromArgsNotAuth from './packem_shared/ownerFieldFromArgsNotAuth-mOw3hE5z.mjs';
|
|
51
56
|
import paymentCreateWithoutAuthorize from './packem_shared/paymentCreateWithoutAuthorize-BYm4JLxo.mjs';
|
|
@@ -67,6 +72,7 @@ import relationReferencesUnknownTable from './packem_shared/relationReferencesUn
|
|
|
67
72
|
import rlsUncoveredTable from './packem_shared/rlsUncoveredTable-CxEfZ5eZ.mjs';
|
|
68
73
|
import shapeTargetsGlobalTable from './packem_shared/shapeTargetsGlobalTable-DHrf4Koi.mjs';
|
|
69
74
|
import shapeUnknownTable from './packem_shared/shapeUnknownTable-C8aDWFoe.mjs';
|
|
75
|
+
import signupMutationWithoutDisposableGating from './packem_shared/signupMutationWithoutDisposableGating-Dnbol2CS.mjs';
|
|
70
76
|
import softDeleteIncludeDeletedFromArgs from './packem_shared/softDeleteIncludeDeletedFromArgs-BLqDKrkM.mjs';
|
|
71
77
|
import sqlInjectionRisk from './packem_shared/sqlInjectionRisk-zwytYGLt.mjs';
|
|
72
78
|
import storageGenerateUploadUrlNoContentTypePin from './packem_shared/storageGenerateUploadUrlNoContentTypePin-Da4L9Ge8.mjs';
|
|
@@ -75,6 +81,7 @@ import storagePresignedUrlForPrivateContent from './packem_shared/storagePresign
|
|
|
75
81
|
import storageUploadWithoutContentTypeAllowlist from './packem_shared/storageUploadWithoutContentTypeAllowlist-BV-gF1lT.mjs';
|
|
76
82
|
import storageUploadWithoutMaxSize from './packem_shared/storageUploadWithoutMaxSize-DpxO59wU.mjs';
|
|
77
83
|
import tableWithoutInsert from './packem_shared/tableWithoutInsert-CbbaYIP4.mjs';
|
|
84
|
+
import ttlFieldNotTimestamp from './packem_shared/ttlFieldNotTimestamp-Dpaj63Z6.mjs';
|
|
78
85
|
import unboundedStringArgument from './packem_shared/unboundedStringArgument-DThg2-wt.mjs';
|
|
79
86
|
import unindexedForeignKey from './packem_shared/unindexedForeignKey-BgJbKyqK.mjs';
|
|
80
87
|
import unindexedRelationTarget from './packem_shared/unindexedRelationTarget-D6eyj6Xx.mjs';
|
|
@@ -84,7 +91,7 @@ import workflowDuplicateStepName from './packem_shared/workflowDuplicateStepName
|
|
|
84
91
|
import workflowUnknownTarget from './packem_shared/workflowUnknownTarget-Cdd7WhKQ.mjs';
|
|
85
92
|
import workflowUnused from './packem_shared/workflowUnused-D0jHxdz9.mjs';
|
|
86
93
|
export { AE_METRIC_EVENTS, loadAnalyticsRuntimeMetrics } from './packem_shared/AE_METRIC_EVENTS-BM14d0lm.mjs';
|
|
87
|
-
export { fromServerSchema } from './packem_shared/fromServerSchema-
|
|
94
|
+
export { fromServerSchema } from './packem_shared/fromServerSchema-CVnbSf8j.mjs';
|
|
88
95
|
|
|
89
96
|
const STATIC_LINTS = [
|
|
90
97
|
indexReferencesUnknownField,
|
|
@@ -97,6 +104,10 @@ const STATIC_LINTS = [
|
|
|
97
104
|
externalSourceOnGlobal,
|
|
98
105
|
externalSourceUnscoped,
|
|
99
106
|
emptyIndex,
|
|
107
|
+
geoIndexFieldNotGeopoint,
|
|
108
|
+
geoIndexUnused,
|
|
109
|
+
exportSinkMisconfigured,
|
|
110
|
+
ttlFieldNotTimestamp,
|
|
100
111
|
circularFk,
|
|
101
112
|
unindexedForeignKey,
|
|
102
113
|
unindexedRelationTarget,
|
|
@@ -119,6 +130,7 @@ const STATIC_LINTS = [
|
|
|
119
130
|
containerPublicInternet,
|
|
120
131
|
publicMutationWithoutRatelimit,
|
|
121
132
|
userCreatingMutationWithoutCaptcha,
|
|
133
|
+
signupMutationWithoutDisposableGating,
|
|
122
134
|
publicArgumentUsesAny,
|
|
123
135
|
unboundedStringArgument,
|
|
124
136
|
hardcodedSecret,
|
|
@@ -167,6 +179,8 @@ const STATIC_LINTS = [
|
|
|
167
179
|
maskedRelationLeakViaWith,
|
|
168
180
|
outputProjectionMissingOnPublicRead,
|
|
169
181
|
normalizeIdUsedAsAuthorization,
|
|
182
|
+
notifySendOutsideAction,
|
|
183
|
+
notifyMissingPushConfig,
|
|
170
184
|
plaintextSecretInWranglerVariables
|
|
171
185
|
];
|
|
172
186
|
const RUNTIME_LINTS = [hotShard, indexUtilization, constraintValidator];
|
|
@@ -183,4 +197,4 @@ const runAdvisor = (context, options = {}) => {
|
|
|
183
197
|
return dedupeCacheKeys(findings);
|
|
184
198
|
};
|
|
185
199
|
|
|
186
|
-
export { ALL_LINTS, RUNTIME_LINTS, 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, hardcodedSecret, hotShard, httpActionMissingAuthGuard, httpActionResponseHeaderInjection, hyperdriveOutsideAction, identityUndeclaredClaimTrusted, imagesUrlSourceFromUserInput, indexReferencesUnknownField, indexUtilization, insertManyUnsafeUserData, kvUnscopedUserKeyIdor, 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 };
|
|
200
|
+
export { ALL_LINTS, RUNTIME_LINTS, 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, exportSinkMisconfigured, externalSourceIncrementalNoDeletePath, externalSourceOnGlobal, externalSourceUnscoped, filterWithoutIndex, flagGatesSecurityWithUnsafeDefault, geoIndexFieldNotGeopoint, geoIndexUnused, hardcodedSecret, hotShard, httpActionMissingAuthGuard, httpActionResponseHeaderInjection, hyperdriveOutsideAction, identityUndeclaredClaimTrusted, imagesUrlSourceFromUserInput, indexReferencesUnknownField, indexUtilization, insertManyUnsafeUserData, kvUnscopedUserKeyIdor, mailInboundDispatchWithoutVerify, mailRecipientFromRequestInput, maskUncoveredPiiColumn, maskWeakHashStrategyOnPii, maskedRelationLeakViaWith, mutatorFullRowReplace, nondeterministicQueryMutation, normalizeIdUsedAsAuthorization, notifyMissingPushConfig, notifySendOutsideAction, outputProjectionMissingOnPublicRead, ownerFieldFromArgsNotAuth, paymentCreateWithoutAuthorize, paymentWebhookWideTolerance, plaintextSecretInWranglerVariables, policyReferencesUnknownTable, privilegedDispatchUnvalidatedPayload, privilegedFanoutFromPublicProcedure, publicArgumentUsesAny, publicMutationWithoutRatelimit, publicTableRlsOptoutConfusion, queueWithoutDlq, r2sqlOutsideAction, ratelimitDefaultMemoryStore, ratelimitKeySpoofableOrGlobal, ratelimitMiddlewareFailOpen, relationReferencesUnknownField, relationReferencesUnknownTable, rlsUncoveredTable, runAdvisor, shapeTargetsGlobalTable, shapeUnknownTable, signupMutationWithoutDisposableGating, softDeleteIncludeDeletedFromArgs, sqlInjectionRisk, storageGenerateUploadUrlNoContentTypePin, storageKeyFromUserArgs, storagePresignedUrlForPrivateContent, storageUploadWithoutContentTypeAllowlist, storageUploadWithoutMaxSize, tableWithoutInsert, ttlFieldNotTimestamp, unboundedStringArgument, unindexedForeignKey, unindexedRelationTarget, userCreatingMutationWithoutCaptcha, vectorsNamespaceFromUserInput, workflowDuplicateStepName, workflowUnknownTarget, workflowUnused };
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { e as emit } from './finding-Dm_zvzS1.mjs';
|
|
2
|
+
|
|
3
|
+
const REQUIRED_KEYS = {
|
|
4
|
+
defineExportSink: ["name", "deliver"],
|
|
5
|
+
r2Sink: ["bucket", "name"],
|
|
6
|
+
webhookExportSink: ["name", "url"]
|
|
7
|
+
};
|
|
8
|
+
const exportSinkMisconfigured = {
|
|
9
|
+
categories: ["SCHEMA"],
|
|
10
|
+
description: "A CDC export sink (`defineExportSink` / `webhookExportSink` / `r2Sink`) is missing a required config field (a webhook with no `url`, an R2 sink with no `bucket`, or a sink with no `name`/`deliver`). The sink can never deliver a change batch, so the export tap silently drains nothing.",
|
|
11
|
+
facing: "INTERNAL",
|
|
12
|
+
level: "ERROR",
|
|
13
|
+
name: "export_sink_misconfigured",
|
|
14
|
+
remediation: "Supply the missing field: a non-empty `url` for `webhookExportSink`, a `bucket` R2 binding for `r2Sink`, or `name` + `deliver` for `defineExportSink`.",
|
|
15
|
+
run: (context) => {
|
|
16
|
+
if (context.exportSinks === void 0) {
|
|
17
|
+
return [];
|
|
18
|
+
}
|
|
19
|
+
const findings = [];
|
|
20
|
+
for (const sink of context.exportSinks) {
|
|
21
|
+
if (!sink.analyzable) {
|
|
22
|
+
continue;
|
|
23
|
+
}
|
|
24
|
+
const present = new Set(sink.presentKeys);
|
|
25
|
+
const empty = new Set(sink.emptyKeys);
|
|
26
|
+
for (const key of REQUIRED_KEYS[sink.factory] ?? []) {
|
|
27
|
+
if (present.has(key) && !empty.has(key)) {
|
|
28
|
+
continue;
|
|
29
|
+
}
|
|
30
|
+
const reason = empty.has(key) ? "is set to an empty string" : "is missing";
|
|
31
|
+
findings.push(
|
|
32
|
+
emit(exportSinkMisconfigured, {
|
|
33
|
+
cacheKey: `export_sink_misconfigured:${sink.file}:${sink.line.toString()}:${key}`,
|
|
34
|
+
detail: `\`${sink.factory}({ … })\` in ${sink.file}:${sink.line.toString()} ${reason} its required \`${key}\` field — the sink can never deliver a change batch, so the export tap drains nothing.`,
|
|
35
|
+
metadata: { factory: sink.factory, field: key, file: sink.file, line: sink.line }
|
|
36
|
+
})
|
|
37
|
+
);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
return findings;
|
|
41
|
+
},
|
|
42
|
+
source: "static",
|
|
43
|
+
title: "Export sink is misconfigured"
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
export { exportSinkMisconfigured as default };
|
|
@@ -14,8 +14,16 @@ const fromServerSchema = (schema) => {
|
|
|
14
14
|
}),
|
|
15
15
|
...table.vectorIndexes.map((index) => {
|
|
16
16
|
return { fields: [index.field], kind: "vector", name: index.name };
|
|
17
|
+
}),
|
|
18
|
+
...table.geoIndexes.map((index) => {
|
|
19
|
+
return { fields: [index.field], kind: "geo", name: index.name };
|
|
17
20
|
})
|
|
18
21
|
];
|
|
22
|
+
const columnKinds = {};
|
|
23
|
+
for (const [fieldName, validator] of Object.entries(table.shape)) {
|
|
24
|
+
const inner = validator.kind === "optional" ? validator._meta?.inner : validator;
|
|
25
|
+
columnKinds[fieldName] = inner?.kind ?? validator.kind;
|
|
26
|
+
}
|
|
19
27
|
const optionalFields = /* @__PURE__ */ new Set();
|
|
20
28
|
for (const [fieldName, validator] of Object.entries(table.shape)) {
|
|
21
29
|
if (validator.kind === "optional") {
|
|
@@ -35,6 +43,7 @@ const fromServerSchema = (schema) => {
|
|
|
35
43
|
hasTenantBy: table.externalSource.tenantBy !== void 0,
|
|
36
44
|
mode: table.externalSource.mode
|
|
37
45
|
} : void 0,
|
|
46
|
+
columnKinds,
|
|
38
47
|
fields: Object.keys(table.shape),
|
|
39
48
|
indexes,
|
|
40
49
|
isPublic: table.isPublic ?? false,
|
|
@@ -42,6 +51,7 @@ const fromServerSchema = (schema) => {
|
|
|
42
51
|
optionalFields,
|
|
43
52
|
shardKind: table.shardMode.kind,
|
|
44
53
|
softDelete: table.softDeleteMode,
|
|
54
|
+
ttl: table.ttlPolicy,
|
|
45
55
|
relations: Object.entries(table.relationMap).map(([accessor, relation]) => {
|
|
46
56
|
return {
|
|
47
57
|
field: relation.field,
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { e as emit } from './finding-Dm_zvzS1.mjs';
|
|
2
|
+
|
|
3
|
+
const geoIndexFieldNotGeopoint = {
|
|
4
|
+
categories: ["SCHEMA"],
|
|
5
|
+
description: "A `.geoIndex(name, { field })` points at a column that is not a `v.geoPoint()`, so its geohash companion can't be built and `withGeoIndex(...)` can never match.",
|
|
6
|
+
facing: "INTERNAL",
|
|
7
|
+
level: "ERROR",
|
|
8
|
+
name: "geo_index_field_not_geopoint",
|
|
9
|
+
remediation: "Point the geo index at a `v.geoPoint()` column, or change the column's type to `v.geoPoint()`.",
|
|
10
|
+
run: (context) => {
|
|
11
|
+
const findings = [];
|
|
12
|
+
for (const table of context.schema.tables) {
|
|
13
|
+
for (const index of table.indexes) {
|
|
14
|
+
if (index.kind !== "geo") {
|
|
15
|
+
continue;
|
|
16
|
+
}
|
|
17
|
+
const field = index.fields[0];
|
|
18
|
+
if (field === void 0) {
|
|
19
|
+
continue;
|
|
20
|
+
}
|
|
21
|
+
const kind = table.columnKinds?.[field];
|
|
22
|
+
if (kind === void 0 || kind === "geoPoint") {
|
|
23
|
+
continue;
|
|
24
|
+
}
|
|
25
|
+
findings.push(
|
|
26
|
+
emit(geoIndexFieldNotGeopoint, {
|
|
27
|
+
cacheKey: `geo_index_field_not_geopoint:${table.name}:${index.name}:${field}`,
|
|
28
|
+
detail: `Geo index "${index.name}" on table "${table.name}" indexes column "${field}", which is a ${kind}, not a v.geoPoint().`,
|
|
29
|
+
metadata: { field, index: index.name, kind, table: table.name }
|
|
30
|
+
})
|
|
31
|
+
);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
return findings;
|
|
35
|
+
},
|
|
36
|
+
source: "static",
|
|
37
|
+
title: "Geo index field is not a geoPoint"
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
export { geoIndexFieldNotGeopoint as default };
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { e as emit } from './finding-Dm_zvzS1.mjs';
|
|
2
|
+
|
|
3
|
+
const geoIndexUnused = {
|
|
4
|
+
categories: ["SCHEMA"],
|
|
5
|
+
description: "A `.geoIndex(name, { field })` is declared but no handler reads it via `withGeoIndex(name, …)`. The geohash companion column is maintained on every write and read by nothing — dead overhead, the geo analogue of a dead index.",
|
|
6
|
+
facing: "INTERNAL",
|
|
7
|
+
level: "INFO",
|
|
8
|
+
name: "geo_index_unused",
|
|
9
|
+
remediation: 'Query the index from a handler with `ctx.db.query("<table>").withGeoIndex("<name>", q => q.near(point, radius))`, or drop the `.geoIndex(...)` declaration so writes stop maintaining its geohash companion.',
|
|
10
|
+
run: (context) => {
|
|
11
|
+
if (context.geoIndexUsages === void 0) {
|
|
12
|
+
return [];
|
|
13
|
+
}
|
|
14
|
+
const usages = context.geoIndexUsages;
|
|
15
|
+
if (usages.some((usage) => usage.indexName === "")) {
|
|
16
|
+
return [];
|
|
17
|
+
}
|
|
18
|
+
const used = new Set(usages.map((usage) => usage.indexName));
|
|
19
|
+
const findings = [];
|
|
20
|
+
for (const table of context.schema.tables) {
|
|
21
|
+
for (const index of table.indexes) {
|
|
22
|
+
if (index.kind !== "geo" || used.has(index.name)) {
|
|
23
|
+
continue;
|
|
24
|
+
}
|
|
25
|
+
findings.push(
|
|
26
|
+
emit(geoIndexUnused, {
|
|
27
|
+
cacheKey: `geo_index_unused:${table.name}:${index.name}`,
|
|
28
|
+
detail: `No handler reads geo index "${index.name}" on table "${table.name}" via \`withGeoIndex("${index.name}", …)\` — it's declared but never queried, so its geohash companion is maintained on every write for nothing.`,
|
|
29
|
+
metadata: { index: index.name, table: table.name }
|
|
30
|
+
})
|
|
31
|
+
);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
return findings;
|
|
35
|
+
},
|
|
36
|
+
source: "static",
|
|
37
|
+
title: "Geo index is declared but never queried"
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
export { geoIndexUnused as default };
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { e as emit } from './finding-Dm_zvzS1.mjs';
|
|
2
|
+
|
|
3
|
+
const notifyMissingPushConfig = {
|
|
4
|
+
categories: ["SCHEMA"],
|
|
5
|
+
description: "The app sends push notifications via `ctx.push` but `defineNotify(...)` configures neither a Web Push nor an FCM channel, so every send fails at runtime. Wire at least one push channel (`webPush` and/or `fcm`) and provide its secrets (`VAPID_*` / `FCM_*`).",
|
|
6
|
+
facing: "INTERNAL",
|
|
7
|
+
level: "WARN",
|
|
8
|
+
name: "notify_missing_push_config",
|
|
9
|
+
remediation: "In `lunora/notify.ts`, wire a push channel: `defineNotify({ webPush: (env) => webPushFromEnv(env) })` and set `VAPID_PUBLIC_KEY` / `VAPID_PRIVATE_KEY` / `VAPID_SUBJECT` in `.dev.vars` (or configure `fcm` with `FCM_PROJECT_ID`). Generate a VAPID keypair with `npx web-push generate-vapid-keys`.",
|
|
10
|
+
run: (context) => {
|
|
11
|
+
const config = context.notifyConfig;
|
|
12
|
+
if (config === void 0 || !config.usesPush || config.hasWebPush || config.hasFcm) {
|
|
13
|
+
return [];
|
|
14
|
+
}
|
|
15
|
+
return [
|
|
16
|
+
emit(notifyMissingPushConfig, {
|
|
17
|
+
cacheKey: "notify_missing_push_config",
|
|
18
|
+
detail: "`ctx.push` is used to send notifications, but `defineNotify(...)` wires neither `webPush` nor `fcm` — every push send will fail at runtime. Configure at least one push channel and its secrets.",
|
|
19
|
+
metadata: { hasFcm: config.hasFcm, hasWebPush: config.hasWebPush, usesPush: config.usesPush }
|
|
20
|
+
})
|
|
21
|
+
];
|
|
22
|
+
},
|
|
23
|
+
source: "static",
|
|
24
|
+
title: "Push used with no Web Push / FCM channel configured"
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
export { notifyMissingPushConfig as default };
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { e as emit } from './finding-Dm_zvzS1.mjs';
|
|
2
|
+
|
|
3
|
+
const notifySendOutsideAction = {
|
|
4
|
+
categories: ["SCHEMA"],
|
|
5
|
+
description: "A `query`/`mutation` handler sends a notification via `ctx.notify`/`ctx.push`. A send is external I/O (a `fetch` to a push service / FCM): it is non-deterministic like `fetch`, and a mutation re-run on OCC retry would re-send it (duplicate pushes). These facades are available on `ActionCtx` only and must be confined to `action` handlers.",
|
|
6
|
+
facing: "EXTERNAL",
|
|
7
|
+
level: "WARN",
|
|
8
|
+
name: "notify_send_outside_action",
|
|
9
|
+
remediation: "Move the `ctx.notify`/`ctx.push` send into an `action(...)`, where external I/O is allowed. If a query/mutation must trigger a notification, have it enqueue the send (`enqueuePushBroadcast` via `@lunora/queue`) or schedule an action — the queue/scheduler runs the send exactly once, off the transactional path.",
|
|
10
|
+
run: (context) => {
|
|
11
|
+
if (context.notifyCalls === void 0) {
|
|
12
|
+
return [];
|
|
13
|
+
}
|
|
14
|
+
const findings = [];
|
|
15
|
+
for (const call of context.notifyCalls) {
|
|
16
|
+
findings.push(
|
|
17
|
+
emit(notifySendOutsideAction, {
|
|
18
|
+
cacheKey: `notify_send_outside_action:${call.file}:${call.line.toString()}:${call.callee}`,
|
|
19
|
+
detail: `\`${call.callee}(…)\` in ${call.exportName} (${call.file}:${call.line.toString()}) runs inside a ${call.kind} handler — a notification send is non-deterministic external I/O and a retried ${call.kind} would re-send it. Move it into an \`action\`, or enqueue/schedule the send.`,
|
|
20
|
+
metadata: { callee: call.callee, exportName: call.exportName, file: call.file, kind: call.kind, line: call.line }
|
|
21
|
+
})
|
|
22
|
+
);
|
|
23
|
+
}
|
|
24
|
+
return findings;
|
|
25
|
+
},
|
|
26
|
+
source: "static",
|
|
27
|
+
title: "Notification send used outside an action"
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
export { notifySendOutsideAction as default };
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { e as emit } from './finding-Dm_zvzS1.mjs';
|
|
2
|
+
import { i as isPublicWrite } from './helpers-BySnKhVB.mjs';
|
|
3
|
+
|
|
4
|
+
const signupMutationWithoutDisposableGating = {
|
|
5
|
+
categories: ["SECURITY"],
|
|
6
|
+
description: "A public `mutation`/`action` that creates a user/session/account row has no disposable-email gate. Throwaway mailboxes farm free trials, evade bans, and pollute the user table.",
|
|
7
|
+
facing: "EXTERNAL",
|
|
8
|
+
level: "WARN",
|
|
9
|
+
name: "signup_mutation_without_disposable_gating",
|
|
10
|
+
remediation: "Add `.use(emailGateMiddleware({ email: (ctx) => ctx.args.email }))` from `@lunora/auth/email-guard` to reject disposable domains at signup, or gate better-auth's native signup with `withEmailGate(...)` / `emailGateDatabaseHooks(...)`.",
|
|
11
|
+
run: (context) => {
|
|
12
|
+
if (context.procedureProtections === void 0) {
|
|
13
|
+
return [];
|
|
14
|
+
}
|
|
15
|
+
const findings = [];
|
|
16
|
+
for (const procedure of context.procedureProtections) {
|
|
17
|
+
if (!isPublicWrite(procedure) || !procedure.writesUserTable || procedure.usesEmailGate) {
|
|
18
|
+
continue;
|
|
19
|
+
}
|
|
20
|
+
findings.push(
|
|
21
|
+
emit(signupMutationWithoutDisposableGating, {
|
|
22
|
+
cacheKey: `signup_mutation_without_disposable_gating:${procedure.file}:${procedure.exportName}`,
|
|
23
|
+
detail: `Public ${procedure.kind} \`${procedure.exportName}\` (${procedure.file}) writes a user/session/account table but has no disposable-email gate. Add \`.use(emailGateMiddleware(...))\` from \`@lunora/auth/email-guard\`.`,
|
|
24
|
+
metadata: {
|
|
25
|
+
exportName: procedure.exportName,
|
|
26
|
+
file: procedure.file,
|
|
27
|
+
kind: procedure.kind,
|
|
28
|
+
writesUserTable: procedure.writesUserTable
|
|
29
|
+
}
|
|
30
|
+
})
|
|
31
|
+
);
|
|
32
|
+
}
|
|
33
|
+
return findings;
|
|
34
|
+
},
|
|
35
|
+
source: "static",
|
|
36
|
+
title: "Account-creating write without a disposable-email gate"
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
export { signupMutationWithoutDisposableGating as default };
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { e as emit } from './finding-Dm_zvzS1.mjs';
|
|
2
|
+
|
|
3
|
+
const TIME_KINDS = /* @__PURE__ */ new Set(["date", "number", "timestamp"]);
|
|
4
|
+
const ttlFieldNotTimestamp = {
|
|
5
|
+
categories: ["SCHEMA"],
|
|
6
|
+
description: "A `.ttl(field)` policy points at a column that is not an epoch-millisecond timestamp, so the alarm sweep's `field < now` comparison can't decide expiry correctly.",
|
|
7
|
+
facing: "INTERNAL",
|
|
8
|
+
level: "ERROR",
|
|
9
|
+
name: "ttl_field_not_timestamp",
|
|
10
|
+
remediation: "Point `.ttl(field)` at a `v.timestamp()` / `v.date()` / `v.number()` (epoch-ms) column, or add such a column.",
|
|
11
|
+
run: (context) => {
|
|
12
|
+
const findings = [];
|
|
13
|
+
for (const table of context.schema.tables) {
|
|
14
|
+
const { ttl } = table;
|
|
15
|
+
if (!ttl) {
|
|
16
|
+
continue;
|
|
17
|
+
}
|
|
18
|
+
const kind = table.columnKinds?.[ttl.field];
|
|
19
|
+
if (kind === void 0 || TIME_KINDS.has(kind)) {
|
|
20
|
+
continue;
|
|
21
|
+
}
|
|
22
|
+
findings.push(
|
|
23
|
+
emit(ttlFieldNotTimestamp, {
|
|
24
|
+
cacheKey: `ttl_field_not_timestamp:${table.name}:${ttl.field}`,
|
|
25
|
+
detail: `Table "${table.name}" declares \`.ttl("${ttl.field}")\`, but "${ttl.field}" is a ${kind} column, not an epoch-millisecond timestamp.`,
|
|
26
|
+
metadata: { field: ttl.field, kind, table: table.name }
|
|
27
|
+
})
|
|
28
|
+
);
|
|
29
|
+
}
|
|
30
|
+
return findings;
|
|
31
|
+
},
|
|
32
|
+
source: "static",
|
|
33
|
+
title: "TTL field is not a timestamp"
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
export { ttlFieldNotTimestamp as default };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lunora/advisor",
|
|
3
|
-
"version": "1.0.0-alpha.
|
|
3
|
+
"version": "1.0.0-alpha.33",
|
|
4
4
|
"description": "Schema & query lints (splinter-style advisors) for Lunora, feeding the Studio Advisors view",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"advisor",
|
|
@@ -46,8 +46,8 @@
|
|
|
46
46
|
"access": "public"
|
|
47
47
|
},
|
|
48
48
|
"dependencies": {
|
|
49
|
-
"@lunora/errors": "1.0.0-alpha.
|
|
50
|
-
"@lunora/server": "1.0.0-alpha.
|
|
49
|
+
"@lunora/errors": "1.0.0-alpha.7",
|
|
50
|
+
"@lunora/server": "1.0.0-alpha.30"
|
|
51
51
|
},
|
|
52
52
|
"engines": {
|
|
53
53
|
"node": "^22.15.0 || >=24.11.0"
|