@lunora/advisor 1.0.0-alpha.105 → 1.0.0-alpha.107
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/README.md +5 -7
- package/dist/index.d.mts +85 -125
- package/dist/index.d.ts +85 -125
- package/dist/index.mjs +1 -1
- package/dist/packem_shared/allowUnauthenticatedShardAccessEnabled-BQufF5Kb.mjs +1 -0
- package/dist/packem_shared/{filterOnPrimaryKey-COQRpmnu.mjs → filterOnPrimaryKey-jmz_ApDa.mjs} +1 -1
- package/dist/packem_shared/{filterWithoutIndex-Cf-tXY_A.mjs → filterWithoutIndex-9kAguEVb.mjs} +1 -1
- package/dist/packem_shared/flagGatesSecurityWithUnsafeDefault-20ccteHn.mjs +1 -0
- package/dist/packem_shared/fromServerSchema-fAymN8o2.mjs +1 -0
- package/dist/packem_shared/geoIndexFieldNotGeopoint-Cd5mywEg.mjs +1 -0
- package/dist/packem_shared/helpers-BCqZbKga.mjs +1 -0
- package/dist/packem_shared/{indexReferencesUnknownField-B_c3o9QR.mjs → indexReferencesUnknownField-DiC2WN4V.mjs} +1 -1
- package/dist/packem_shared/{maskWeakHashStrategyOnPii-BC1n0F4-.mjs → maskWeakHashStrategyOnPii-DXhCr75r.mjs} +1 -1
- package/dist/packem_shared/maskedRelationLeakViaWith-CIFij6vE.mjs +1 -0
- package/dist/packem_shared/notifySendOutsideAction-CWx7KuJS.mjs +1 -0
- package/dist/packem_shared/{outputProjectionMissingOnPublicRead-DhiA3mpG.mjs → outputProjectionMissingOnPublicRead-HwweA4NI.mjs} +1 -1
- package/dist/packem_shared/publicMutationWithoutRatelimit-cDJph2zk.mjs +1 -0
- package/dist/packem_shared/{publicTableRlsOptoutConfusion-Dz7lbKKe.mjs → publicTableRlsOptoutConfusion-B4zbtE9m.mjs} +1 -1
- package/dist/packem_shared/ratelimitMiddlewareFailOpen-eOeyAyC-.mjs +1 -0
- package/dist/packem_shared/{relationReferencesUnknownField-D8Qyth_P.mjs → relationReferencesUnknownField-GpJPLGDE.mjs} +1 -1
- package/dist/packem_shared/{signupMutationWithoutDisposableGating-BIsSLyhJ.mjs → signupMutationWithoutDisposableGating-BMM2vTNu.mjs} +1 -1
- package/dist/packem_shared/storageGenerateUploadUrlNoContentTypePin-DBq_DeHO.mjs +1 -0
- package/dist/packem_shared/storagePresignedUrlForPrivateContent-DZdUkDdW.mjs +1 -0
- package/dist/packem_shared/ttlFieldNotTimestamp-CixnZ0Ii.mjs +1 -0
- package/dist/packem_shared/unboundedStringArgument-jZnCbwB0.mjs +1 -0
- package/dist/packem_shared/userCreatingMutationWithoutCaptcha-CGpMm45R.mjs +1 -0
- package/package.json +3 -3
- package/dist/packem_shared/allowUnauthenticatedShardAccessEnabled-CjXI-kda.mjs +0 -1
- package/dist/packem_shared/constraintValidator-DoRJD9Is.mjs +0 -1
- package/dist/packem_shared/flagGatesSecurityWithUnsafeDefault-i-Befg8b.mjs +0 -1
- package/dist/packem_shared/fromServerSchema-D2nTknz0.mjs +0 -1
- package/dist/packem_shared/geoIndexFieldNotGeopoint-D0lOTm-_.mjs +0 -1
- package/dist/packem_shared/helpers-CwSEZdku.mjs +0 -1
- package/dist/packem_shared/maskedRelationLeakViaWith-C8n-Mzb1.mjs +0 -1
- package/dist/packem_shared/notifySendOutsideAction-CU4T16cR.mjs +0 -1
- package/dist/packem_shared/publicMutationWithoutRatelimit-Dsd4cSPD.mjs +0 -1
- package/dist/packem_shared/ratelimitMiddlewareFailOpen-iDJq68qx.mjs +0 -1
- package/dist/packem_shared/storageGenerateUploadUrlNoContentTypePin-CTIU_7Fj.mjs +0 -1
- package/dist/packem_shared/storagePresignedUrlForPrivateContent-yeu1s81k.mjs +0 -1
- package/dist/packem_shared/ttlFieldNotTimestamp-E14I83wY.mjs +0 -1
- package/dist/packem_shared/unboundedStringArgument-DzbFJc5q.mjs +0 -1
- package/dist/packem_shared/userCreatingMutationWithoutCaptcha-B7l-S-rV.mjs +0 -1
package/README.md
CHANGED
|
@@ -36,7 +36,7 @@
|
|
|
36
36
|
|
|
37
37
|
Schema and query lints for Lunora, modeled on Supabase's splinter. Each lint is a pure rule over a normalized `LintContext`; `runAdvisor()` runs a set and flattens their findings for the CLI, the Vite plugin, and the Studio Advisors view.
|
|
38
38
|
|
|
39
|
-
Most lints are `static`: they run against the declared schema (and the query reads / inserts the codegen feeder discovers in your function bodies), so a problem surfaces at codegen time before it ships — the edge over a live-database-only advisor. A smaller `runtime` tier (`hot_shard`, `index_utilization`, `
|
|
39
|
+
Most lints are `static`: they run against the declared schema (and the query reads / inserts the codegen feeder discovers in your function bodies), so a problem surfaces at codegen time before it ships — the edge over a live-database-only advisor. A smaller `runtime` tier (`hot_shard`, `index_utilization`, `fan_out_breadth`) reads observed signal from a running deployment.
|
|
40
40
|
|
|
41
41
|
Part of the [Lunora](https://github.com/anolilab/lunora) framework — a type-safe, real-time backend on Cloudflare Workers + Durable Objects with a Vite-first DX.
|
|
42
42
|
|
|
@@ -79,18 +79,16 @@ for (const finding of findings) {
|
|
|
79
79
|
|
|
80
80
|
### Runtime lints
|
|
81
81
|
|
|
82
|
-
The runtime tier (`hot_shard`, `index_utilization`, `fan_out_breadth
|
|
83
|
-
|
|
84
|
-
`tableSamples` has **no shipped feeder**: nothing in the runtime or the Studio reads bounded row samples out of a shard, so `constraint_validator` is a no-op unless _you_ pass samples yourself (the Studio excludes it from the lint set it runs, rather than running it against an input it cannot fill). It is exported and driveable — the sample shape is `AdvisorTableSample` — but wiring it to a deployment means adding a sampling admin read first.
|
|
82
|
+
The runtime tier (`hot_shard`, `index_utilization`, `fan_out_breadth`) reads observed signal off the `LintContext` (`shardTraffic`, `tableScans`, `indexHits`). The Studio backend fills all three from the shards' admin signal.
|
|
85
83
|
|
|
86
84
|
```ts
|
|
87
85
|
import { fromServerSchema, runAdvisor } from "@lunora/advisor";
|
|
88
86
|
|
|
89
87
|
import schema from "./lunora/schema";
|
|
90
88
|
|
|
91
|
-
// `shardTraffic` / `tableScans` / `indexHits`
|
|
92
|
-
//
|
|
93
|
-
const findings = runAdvisor({ schema: fromServerSchema(schema), shardTraffic, tableScans, indexHits
|
|
89
|
+
// `shardTraffic` / `tableScans` / `indexHits` come from wherever you read your
|
|
90
|
+
// shards' durable counters — the Studio backend reads its own.
|
|
91
|
+
const findings = runAdvisor({ schema: fromServerSchema(schema), shardTraffic, tableScans, indexHits }, { source: "runtime" });
|
|
94
92
|
```
|
|
95
93
|
|
|
96
94
|
An Analytics-Engine-backed alternative feeder (querying AE SQL for `lunora.index.hit`/`lunora.shard.request`/`lunora.table.scan` events instead of the in-DO counters) was quarantined off the package root: nothing in the runtime ever writes those AE events, so `shardTraffic` and `tableScans` always came back empty. `indexHits` came back empty too, unless the caller passed `declaredIndexes` — then it listed every declared index with `reads: 0`, a real zero-reads fact rather than a stand-in for "no data". The `AnalyticsMetricsOptions` / `AnalyticsMetricsSource` / `AnalyticsRuntimeMetrics` types it would have produced are still exported from `@lunora/advisor` — they describe the still-valid, still-optional shape of `runAdvisor`'s runtime input — but the loader function itself is not, until something actually emits those events.
|
package/dist/index.d.mts
CHANGED
|
@@ -1289,16 +1289,6 @@ interface AdvisorTable {
|
|
|
1289
1289
|
isPublic?: boolean;
|
|
1290
1290
|
/** Table name. */
|
|
1291
1291
|
name: string;
|
|
1292
|
-
/**
|
|
1293
|
-
* Column names that are optional or nullable and therefore may legally hold
|
|
1294
|
-
* `null` / `undefined` in stored rows. Populated by {@link fromServerSchema}
|
|
1295
|
-
* from the runtime validator graph (`v.optional(...)` → kind `"optional"`;
|
|
1296
|
-
* `.nullable()` → `column.notNull === false`). When absent (e.g. from the
|
|
1297
|
-
* codegen feeder, which does not supply this field), constraint lints that
|
|
1298
|
-
* check NOT NULL should skip the check entirely or treat every field as
|
|
1299
|
-
* required (the codegen feeder never runs runtime lints anyway).
|
|
1300
|
-
*/
|
|
1301
|
-
optionalFields?: ReadonlySet<string>;
|
|
1302
1292
|
/** Declared relations (`.relations((r) => …)`). */
|
|
1303
1293
|
relations: ReadonlyArray<AdvisorRelation>;
|
|
1304
1294
|
/**
|
|
@@ -1574,31 +1564,6 @@ interface AdvisorStorageUpload {
|
|
|
1574
1564
|
/** Options-object keys present at the call site (empty when not `analyzable`, or when no options argument was passed). */
|
|
1575
1565
|
presentKeys: string[];
|
|
1576
1566
|
}
|
|
1577
|
-
/**
|
|
1578
|
-
* A bounded sample of rows from one table, fed into the constraint-validator
|
|
1579
|
-
* lint by the studio backend (via `readTablePage`). The cap prevents unbounded
|
|
1580
|
-
* scans while still catching obvious violations on small-to-medium tables.
|
|
1581
|
-
*
|
|
1582
|
-
* The studio notes the cap to the operator when the row count exceeds it
|
|
1583
|
-
* (`truncated: true`), so violations on rows beyond the sample window are not
|
|
1584
|
-
* silently missed — the finding description mentions the cap.
|
|
1585
|
-
*/
|
|
1586
|
-
interface AdvisorTableSample {
|
|
1587
|
-
/** The cap applied; equals `rows.length` when not truncated. */
|
|
1588
|
-
readonly cap: number;
|
|
1589
|
-
/**
|
|
1590
|
-
* The row ids of every existing row in this table (bounded to `cap`), used
|
|
1591
|
-
* for FK referential-integrity checks: if a FK value does not appear in the
|
|
1592
|
-
* target table's `existingIds`, it is a dangling reference.
|
|
1593
|
-
*/
|
|
1594
|
-
readonly existingIds: ReadonlySet<string>;
|
|
1595
|
-
/** Sampled rows (up to `cap`). Each row includes `_id` and all declared columns. */
|
|
1596
|
-
readonly rows: ReadonlyArray<Record<string, unknown>>;
|
|
1597
|
-
/** The table's name. */
|
|
1598
|
-
readonly table: string;
|
|
1599
|
-
/** Whether more rows exist beyond the cap. */
|
|
1600
|
-
readonly truncated: boolean;
|
|
1601
|
-
}
|
|
1602
1567
|
/**
|
|
1603
1568
|
* One branching `defineShape({ where })` / `definePolicy({ when })` predicate arm
|
|
1604
1569
|
* that returns an unrestricted predicate — the `unrestricted_where_branch` lint
|
|
@@ -2239,21 +2204,6 @@ interface LintContext {
|
|
|
2239
2204
|
* nothing.
|
|
2240
2205
|
*/
|
|
2241
2206
|
storageUploads?: ReadonlyArray<AdvisorStorageUpload>;
|
|
2242
|
-
/**
|
|
2243
|
-
* Bounded row samples per table — the `constraint_validator` lint input.
|
|
2244
|
-
* There is NO shipped feeder: neither the runtime nor the studio reads row
|
|
2245
|
-
* samples out of a shard, so this is absent for every caller in-tree and the
|
|
2246
|
-
* constraint lint finds nothing. Supply it yourself (a paged read per table,
|
|
2247
|
-
* plus the existing-id set for the FK referential-integrity checks) to drive
|
|
2248
|
-
* that lint.
|
|
2249
|
-
*
|
|
2250
|
-
* Each entry carries `existingIds` (every `_id` in the sample window) so
|
|
2251
|
-
* FK columns can be cross-checked across tables in O(1) per value. When
|
|
2252
|
-
* `truncated` is `true`, violations on rows beyond the cap are not reported
|
|
2253
|
-
* — the finding description notes the sample cap so the operator understands
|
|
2254
|
-
* the bounded window.
|
|
2255
|
-
*/
|
|
2256
|
-
tableSamples?: ReadonlyArray<AdvisorTableSample>;
|
|
2257
2207
|
/**
|
|
2258
2208
|
* Per-table full-scan volume observed at runtime (the hot-scan half of the
|
|
2259
2209
|
* `index_utilization` lint input). Sourced from the per-`(function, table)`
|
|
@@ -2382,34 +2332,6 @@ interface AnalyticsRuntimeMetrics {
|
|
|
2382
2332
|
* lint-name-prefixed, so this never merges across lints.
|
|
2383
2333
|
*/
|
|
2384
2334
|
declare const dedupeCacheKeys: (findings: ReadonlyArray<Finding>) => Finding[];
|
|
2385
|
-
/**
|
|
2386
|
-
* Constraint validator — flag rows that violate declared FK / NOT NULL / UNIQUE
|
|
2387
|
-
* constraints by cross-checking sampled row data against the schema.
|
|
2388
|
-
*
|
|
2389
|
-
* No shipped feeder fills `context.tableSamples` — neither the runtime nor the
|
|
2390
|
-
* Studio reads bounded row samples out of a shard (the Studio drops this lint
|
|
2391
|
-
* from the set it runs for exactly that reason). It runs only for a caller that
|
|
2392
|
-
* gathers samples itself, which is why the guard below returns `[]` rather than
|
|
2393
|
-
* assuming the feed is present.
|
|
2394
|
-
*
|
|
2395
|
-
* This lint reads the `context.tableSamples` feed (bounded row samples supplied
|
|
2396
|
-
* by the studio backend via `readTablePage`) and the declared schema. Three
|
|
2397
|
-
* families of check run over each sample:
|
|
2398
|
-
*
|
|
2399
|
-
* FK referential integrity: for every `one` relation the holding table declares,
|
|
2400
|
-
* check that each sampled row's FK column value appears in the target table's
|
|
2401
|
-
* sampled id set. A dangling value means no target row exists for the reference.
|
|
2402
|
-
*
|
|
2403
|
-
* NOT NULL / non-optional columns: the lint surfaces rows with null/undefined in
|
|
2404
|
-
* declared fields — inserted before a column was added or via raw import.
|
|
2405
|
-
*
|
|
2406
|
-
* UNIQUE index violations: for each declared unique secondary index, check the
|
|
2407
|
-
* sampled rows for duplicate values across the index's columns.
|
|
2408
|
-
*
|
|
2409
|
-
* All checks are bounded by the cap in each sample; the lint never triggers an
|
|
2410
|
-
* additional read. When a sample is truncated, findings note the caveat.
|
|
2411
|
-
*/
|
|
2412
|
-
declare const constraintValidator: Lint;
|
|
2413
2335
|
/**
|
|
2414
2336
|
* `fan_out_breadth` — flag a shard set wide enough that a cross-shard read over
|
|
2415
2337
|
* it would approach the per-invocation subrequest ceiling.
|
|
@@ -2594,19 +2516,21 @@ declare const aiUnboundedGenerationPublic: Lint;
|
|
|
2594
2516
|
* a table `.public()`, i.e. exempt from it), an unauthenticated caller can shard-hop
|
|
2595
2517
|
* and read another tenant's rows with no row-security guard behind the door.
|
|
2596
2518
|
*
|
|
2597
|
-
* **Evidence
|
|
2598
|
-
*
|
|
2599
|
-
*
|
|
2600
|
-
*
|
|
2601
|
-
*
|
|
2602
|
-
* `
|
|
2603
|
-
* `
|
|
2604
|
-
*
|
|
2605
|
-
*
|
|
2606
|
-
*
|
|
2519
|
+
* **Evidence**: this reads `context.configCalls`, fed by the codegen
|
|
2520
|
+
* `discover/config-calls.ts` feeder, and covers BOTH places the field can be
|
|
2521
|
+
* set. `lunora({ allowUnauthenticatedShardAccess: true })` in `vite.config.*`
|
|
2522
|
+
* is the documented opt-in for the auto-composed class-A worker, and is the
|
|
2523
|
+
* only place a class-A app (the default Vite path — `sveltekit` / `astro` /
|
|
2524
|
+
* `react-router` / `tanstack-start`) can set it at all, since it has no worker
|
|
2525
|
+
* entry. `defineApp()...extend(() => ({ allowUnauthenticatedShardAccess: true }))`
|
|
2526
|
+
* is the class-B escape hatch (the `nuxt` / `analog` templates' `lunora/server.ts`),
|
|
2527
|
+
* read from `lunora/` and the worker entry alike.
|
|
2528
|
+
*
|
|
2529
|
+
* Still out of view: a hand-written entry passing the field straight to
|
|
2530
|
+
* `createWorker({...})` — that callee is not one the feeder reads.
|
|
2607
2531
|
*
|
|
2608
2532
|
* Runs only when the codegen feeder supplies config-call evidence; a runtime
|
|
2609
|
-
* caller flags nothing. One finding per opted-in
|
|
2533
|
+
* caller flags nothing. One finding per opted-in call site.
|
|
2610
2534
|
*/
|
|
2611
2535
|
declare const allowUnauthenticatedShardAccessEnabled: Lint;
|
|
2612
2536
|
/**
|
|
@@ -3031,7 +2955,9 @@ declare const filterWithoutIndex: Lint;
|
|
|
3031
2955
|
* a protection (`enforce*`/`rls*`/`gate*`/`lockdown*`) and defaults `false`, or
|
|
3032
2956
|
* names a permission/bypass (`allow*`/`permit*`/`bypass*`) and defaults `true`,
|
|
3033
2957
|
* a flag-backend outage silently disables the protection or grants the
|
|
3034
|
-
* permission for every request.
|
|
2958
|
+
* permission for every request. A negating token in the key
|
|
2959
|
+
* (`disable*`/`*Disabled`/`skip*`/`no*`) inverts that — see
|
|
2960
|
+
* {@link safeDefaultFor}.
|
|
3035
2961
|
*
|
|
3036
2962
|
* Runs only when the codegen feeder supplies flag-default evidence
|
|
3037
2963
|
* (`context.flagSecurityDefaults`); a runtime caller flags nothing. Deliberately
|
|
@@ -3391,18 +3317,29 @@ declare const maskWeakHashStrategyOnPii: Lint;
|
|
|
3391
3317
|
* Flags a public read that hydrates a masked table's rows in the clear through a
|
|
3392
3318
|
* `with` relation.
|
|
3393
3319
|
*
|
|
3394
|
-
* Column masking
|
|
3395
|
-
*
|
|
3396
|
-
*
|
|
3397
|
-
*
|
|
3398
|
-
*
|
|
3399
|
-
*
|
|
3400
|
-
*
|
|
3320
|
+
* Column masking is **per-procedure**, and that — not the `with` boundary — is
|
|
3321
|
+
* what this catches. `.use(mask(policies))` installs a `relationMask` hook on
|
|
3322
|
+
* the read's args (`@lunora/server`'s `mask/middleware`), and the relation
|
|
3323
|
+
* loader calls it for the TARGET table of every hop, at every nesting depth
|
|
3324
|
+
* (`@lunora/shard-engine`'s `relations`); the one hop it cannot reach, a
|
|
3325
|
+
* cross-shard child, fails closed with `MASK_UNSUPPORTED` rather than returning
|
|
3326
|
+
* cleartext. So a procedure that masks `users` gets masked `users` through
|
|
3327
|
+
* `with` too.
|
|
3328
|
+
*
|
|
3329
|
+
* What is still real is a read whose OWN procedure declares no policy for the
|
|
3330
|
+
* related table. `ctx.db.posts.findMany({ with: { author: true } })` in a
|
|
3331
|
+
* procedure with no `.use(mask(...))` — or one whose policy names only `posts` —
|
|
3332
|
+
* hands back every `author` in the clear, including the columns another
|
|
3333
|
+
* procedure carefully masks on `users`' own reads. Nothing about the mask on
|
|
3334
|
+
* `users` reaches this read.
|
|
3401
3335
|
*
|
|
3402
3336
|
* INFO, near-zero false positives by construction: it fires only when all of
|
|
3403
3337
|
* (1) the enclosing read is public, (2) the read declares `with: { <rel> }`,
|
|
3404
|
-
* (3) `<rel>` resolves through the schema to a real target table,
|
|
3405
|
-
* target table actually has masked columns (per the discovered mask evidence)
|
|
3338
|
+
* (3) `<rel>` resolves through the schema to a real target table, (4) that
|
|
3339
|
+
* target table actually has masked columns (per the discovered mask evidence),
|
|
3340
|
+
* and (5) the reading procedure's own mask policy does not cover that target
|
|
3341
|
+
* table. A policy this feeder could not read statically counts as covering
|
|
3342
|
+
* everything, so an opaque `mask(policies)` never produces a finding.
|
|
3406
3343
|
* Absent any mask usage the lint is a no-op. Runs only when the codegen feeder
|
|
3407
3344
|
* supplies `context.relationLoads`; a runtime caller flags nothing. One finding
|
|
3408
3345
|
* per `(read, masked relation)` pair.
|
|
@@ -3861,8 +3798,8 @@ declare const ratelimitKeySpoofableOrGlobal: Lint;
|
|
|
3861
3798
|
* Runs only when the codegen feeder supplies fail-open-guard evidence
|
|
3862
3799
|
* (`context.failOpenGuards`); a runtime caller flags nothing. Deliberately narrow
|
|
3863
3800
|
* — fires only when the options literal provably set `failOpen: true` AND the
|
|
3864
|
-
* guarded procedure's export name or rate-limit `name`
|
|
3865
|
-
*
|
|
3801
|
+
* guarded procedure's export name or rate-limit `name` carries an auth/payment
|
|
3802
|
+
* WORD, keeping the false-positive rate low. One finding per guard.
|
|
3866
3803
|
*/
|
|
3867
3804
|
declare const ratelimitMiddlewareFailOpen: Lint;
|
|
3868
3805
|
/**
|
|
@@ -4028,13 +3965,20 @@ declare const sqlInjectionRisk: Lint;
|
|
|
4028
3965
|
* Flags a `ctx.storage.generateUploadUrl(key, …)` call whose options argument
|
|
4029
3966
|
* omits `contentType`.
|
|
4030
3967
|
*
|
|
4031
|
-
* `generateUploadUrl`
|
|
4032
|
-
*
|
|
4033
|
-
* `allowedContentTypes`/`maxSize` guards, which this
|
|
4034
|
-
* guard
|
|
4035
|
-
*
|
|
4036
|
-
*
|
|
4037
|
-
*
|
|
3968
|
+
* `generateUploadUrl` is `getSignedUrl(key, { method: "PUT" })`: it mints a
|
|
3969
|
+
* signed PUT URL the *client* uploads with, bypassing `upload()`/`store()`
|
|
3970
|
+
* entirely — including their `allowedContentTypes`/`maxSize` guards, which this
|
|
3971
|
+
* alias never sees. The one guard it offers is `contentType`, which
|
|
3972
|
+
* `buildSignedUrl` binds into the HMAC canonical (PUT only) and
|
|
3973
|
+
* `verifySignedUrl` returns as `contentType` on a valid verdict.
|
|
3974
|
+
*
|
|
3975
|
+
* Note what that does and does not buy. Binding it means the pin cannot be
|
|
3976
|
+
* swapped without breaking the signature, so the URL is scoped to one declared
|
|
3977
|
+
* content-type. It does NOT by itself reject a mismatched upload: nothing in
|
|
3978
|
+
* `@lunora/storage` compares the pin against the request's actual
|
|
3979
|
+
* `Content-Type` — the PUT route you mount at `publicBaseUrl` does, by checking
|
|
3980
|
+
* `verifySignedUrl`'s `contentType` against the inbound header. Omit the pin
|
|
3981
|
+
* and there is nothing to check against at all, and no size bound either way.
|
|
4038
3982
|
*
|
|
4039
3983
|
* Runs only when the codegen feeder supplies storage-upload evidence
|
|
4040
3984
|
* (`context.storageUploads`); a runtime caller flags nothing. Skips calls
|
|
@@ -4068,15 +4012,23 @@ declare const storageKeyFromUserArgs: Lint;
|
|
|
4068
4012
|
*
|
|
4069
4013
|
* `getPresignedUrl` mints a native S3 SigV4 URL that resolves directly
|
|
4070
4014
|
* against R2's S3 endpoint — the holder reaches the object straight off R2,
|
|
4071
|
-
* **bypassing the Worker entirely**, so
|
|
4072
|
-
*
|
|
4073
|
-
*
|
|
4074
|
-
*
|
|
4075
|
-
*
|
|
4076
|
-
*
|
|
4077
|
-
*
|
|
4078
|
-
*
|
|
4079
|
-
*
|
|
4015
|
+
* **bypassing the Worker entirely**, so no request-time gating is even
|
|
4016
|
+
* possible. That's the right trade for genuinely public or bulk content; it's
|
|
4017
|
+
* the wrong choice for private, per-user, or policy-gated content, where
|
|
4018
|
+
* `getSignedUrl` is the fit.
|
|
4019
|
+
*
|
|
4020
|
+
* What `getSignedUrl` actually gives you is a URL at your own
|
|
4021
|
+
* `publicBaseUrl` origin, HMAC-bound to (method, host, bucket, key, expiry).
|
|
4022
|
+
* That is an OPPORTUNITY to gate, not a gate: `@lunora/storage` ships no
|
|
4023
|
+
* serving route, and nothing in the framework calls `verifySignedUrl`. The
|
|
4024
|
+
* route you mount there is what validates the signature and runs whatever
|
|
4025
|
+
* auth/policy/rate-limit checks the app needs. The lint's point is that
|
|
4026
|
+
* `getPresignedUrl` removes that seam entirely.
|
|
4027
|
+
*
|
|
4028
|
+
* Separately, either signer minting a long TTL near the shared 7-day ceiling
|
|
4029
|
+
* hands out a bearer credential that stays valid almost as long as the
|
|
4030
|
+
* platform allows — a leaked link (referrer, logs, browser history) then
|
|
4031
|
+
* grants access for nearly a week.
|
|
4080
4032
|
*
|
|
4081
4033
|
* Runs only when the codegen feeder supplies storage-upload evidence
|
|
4082
4034
|
* (`context.storageUploads`); a runtime caller flags nothing. The
|
|
@@ -4159,9 +4111,12 @@ declare const ttlFieldNotTimestamp: Lint;
|
|
|
4159
4111
|
*
|
|
4160
4112
|
* A string field that accepts an unbounded value lets a client send megabytes of
|
|
4161
4113
|
* text per request — inflating storage, blowing the row/document size budget, and
|
|
4162
|
-
* driving CPU/memory on every handler that processes it. A `.
|
|
4163
|
-
*
|
|
4164
|
-
*
|
|
4114
|
+
* driving CPU/memory on every handler that processes it. A `.max(n)` (or
|
|
4115
|
+
* `.length(n)`) bound caps the blast radius. Only those two count: `.meta({
|
|
4116
|
+
* maxLength })` publishes a cap the parser never enforces, and a bare `.check()`
|
|
4117
|
+
* may predicate anything — neither is evidence the length is bounded. Advisory
|
|
4118
|
+
* (INFO): a deliberately-open free-text field is sometimes legitimate, so this
|
|
4119
|
+
* nudges rather than blocks.
|
|
4165
4120
|
*
|
|
4166
4121
|
* Runs only when the codegen feeder supplies arg evidence
|
|
4167
4122
|
* (`context.argValidators`, public procedures only); a runtime caller flags
|
|
@@ -4234,10 +4189,15 @@ declare const unrestrictedWhereBranch: Lint;
|
|
|
4234
4189
|
* Endpoints that mint accounts or trigger emails are the classic automated-abuse
|
|
4235
4190
|
* surface: credential-stuffing sign-ups, mailbox-flooding "forgot password" loops,
|
|
4236
4191
|
* and disposable-account farming. A server-verified human check (Turnstile) in
|
|
4237
|
-
* front of them is the defense. Lunora ships `
|
|
4238
|
-
* and the `protectPublic({ captcha })` bundle; this lint fires
|
|
4239
|
-
* procedure writes a user/session/account-shaped table (or
|
|
4240
|
-
* with no captcha middleware.
|
|
4192
|
+
* front of them is the defense. Lunora ships `verifyTurnstileMiddleware()`
|
|
4193
|
+
* (`@lunora/auth`) and the `protectPublic({ captcha })` bundle; this lint fires
|
|
4194
|
+
* when a public procedure writes a user/session/account-shaped table (or
|
|
4195
|
+
* references `ctx.mail`) with no captcha middleware.
|
|
4196
|
+
*
|
|
4197
|
+
* The middleware, NOT `verifyTurnstile`: that one is the async verdict function
|
|
4198
|
+
* the middleware calls, so `.use(verifyTurnstile({...}))` installs a Promise in
|
|
4199
|
+
* the chain and checks nothing. The feeder counts only the middleware, so the
|
|
4200
|
+
* remediation below has to name the same thing the feeder will accept.
|
|
4241
4201
|
*
|
|
4242
4202
|
* Runs only when the codegen feeder supplies protection evidence
|
|
4243
4203
|
* (`context.procedureProtections`); a runtime caller with no evidence flags
|
|
@@ -4599,4 +4559,4 @@ interface RunAdvisorOptions {
|
|
|
4599
4559
|
* `static` lints at build time and defer `runtime` lints to a live shard.
|
|
4600
4560
|
*/
|
|
4601
4561
|
declare const runAdvisor: (context: LintContext, options?: RunAdvisorOptions) => Finding[];
|
|
4602
|
-
export { 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 AdvisorFlagRead, 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 AdvisorMap, 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 AdvisorStaleMigrationImport, type AdvisorStorageKeyAccess, type AdvisorStorageUpload, type AdvisorTable, type
|
|
4562
|
+
export { 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 AdvisorFlagRead, 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 AdvisorMap, 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 AdvisorStaleMigrationImport, type AdvisorStorageKeyAccess, type AdvisorStorageUpload, type AdvisorTable, type AdvisorTableScan, type AdvisorVectorNamespaceAccess, type AdvisorWorkflow, type AdvisorWorkflowCall, type AdvisorWranglerVariable, type AnalyticsMetricsOptions, type AnalyticsMetricsSource, type AnalyticsRuntimeMetrics, type BaselineComparison, type Category, type CheckResult, type Coverage, type Facing, type Finding, type Grade, type Level, type Lint, type LintContext, type LintSource, MAP_VERSION, type MapSummary, type ProcedureDelta, type ProcedureScore, type ProjectScore, RUNTIME_LINTS, RunAdvisorOptions, STATIC_LINTS, type ScoreAdvisorOptions, type Sensitivity, type SensitivityLevel, actionFetchSsrf, actionWithoutErrorHandling, adminRouteWithoutGuard, aiRawRunEscapeHatch, aiRunWithoutLogging, aiToolSideEffectPromptInjection, aiUnboundedGenerationPublic, allowUnauthenticatedShardAccessEnabled, authApiCallWithoutHeaders, authCsrfCheckDisabled, authEmailVerificationDisabled, authScimWithoutTransactions, authSecureCookiesDisabled, authSessionFreshageZero, authTrustedOriginsWildcard, browserAllowPrivateTargets, browserUserUrlWithoutAllowlist, byCodepoint, circularFk, classifySensitivity, commitOrderedHardDelete, compareToBaseline, containerInstanceKeyFromUserInput, containerOversizedInstance, containerPublicInternet, containerRuntimeEgressRelaxation, containerStartEnableInternetOverride, dedupeCacheKeys, duplicateIndex, emptyIndex, errorWithoutCatalog, exportSinkMisconfigured, externalSourceIncrementalNoDeletePath, externalSourceOnGlobal, externalSourceUnscoped, fanOutBreadth, filterOnPrimaryKey, filterWithoutIndex, flagGatesSecurityWithUnsafeDefault, flagReadInSubscription, fromServerSchema, geoIndexFieldNotGeopoint, geoIndexUnused, globalTableNearColumnLimit, gradeFromScore, hardcodedSecret, hotShard, httpActionMissingAuthGuard, httpActionResponseHeaderInjection, hyperdriveOutsideAction, identityUndeclaredClaimTrusted, imagesUrlSourceFromUserInput, indexReferencesUnknownField, indexUtilization, insertManyUnsafeUserData, kvUnscopedUserKeyIdor, mailInboundDispatchWithoutVerify, mailRecipientFromRequestInput, maskUncoveredPiiColumn, maskWeakHashStrategyOnPii, maskedRelationLeakViaWith, mutatorFullRowReplace, nondeterministicQueryMutation, normalizeIdUsedAsAuthorization, notifyMissingPushConfig, notifySendOutsideAction, outputProjectionMissingOnPublicRead, ownerFieldFromArgsNotAuth, parseAdvisorMap, paymentCreateWithoutAuthorize, paymentWebhookWideTolerance, plaintextSecretInWranglerVariables, policyReferencesUnknownTable, privilegedDispatchUnvalidatedPayload, privilegedFanoutFromPublicProcedure, procedureWithoutStructuredEvent, publicArgumentUsesAny, publicMutationWithoutRatelimit, publicTableRlsOptoutConfusion, queueWithoutDlq, r2sqlOutsideAction, ratelimitDefaultMemoryStore, ratelimitKeySpoofableOrGlobal, ratelimitMiddlewareFailOpen, relationReferencesUnknownField, relationReferencesUnknownTable, rlsUncoveredTable, runAdvisor, scoreAdvisor, shapeTargetsGlobalTable, shapeUnknownTable, signupMutationWithoutDisposableGating, softDeleteIncludeDeletedFromArgs, sqlInjectionRisk, storageGenerateUploadUrlNoContentTypePin, storageKeyFromUserArgs, storagePresignedUrlForPrivateContent, storageUploadWithoutContentTypeAllowlist, storageUploadWithoutMaxSize, tableWithoutInsert, ttlFieldNotTimestamp, unboundedStringArgument, unindexedForeignKey, unindexedRelationTarget, unrestrictedWhereBranch, userCreatingMutationWithoutCaptcha, vectorsNamespaceFromUserInput, workflowDuplicateStepName, workflowUnknownTarget, workflowUnused };
|
package/dist/index.d.ts
CHANGED
|
@@ -1289,16 +1289,6 @@ interface AdvisorTable {
|
|
|
1289
1289
|
isPublic?: boolean;
|
|
1290
1290
|
/** Table name. */
|
|
1291
1291
|
name: string;
|
|
1292
|
-
/**
|
|
1293
|
-
* Column names that are optional or nullable and therefore may legally hold
|
|
1294
|
-
* `null` / `undefined` in stored rows. Populated by {@link fromServerSchema}
|
|
1295
|
-
* from the runtime validator graph (`v.optional(...)` → kind `"optional"`;
|
|
1296
|
-
* `.nullable()` → `column.notNull === false`). When absent (e.g. from the
|
|
1297
|
-
* codegen feeder, which does not supply this field), constraint lints that
|
|
1298
|
-
* check NOT NULL should skip the check entirely or treat every field as
|
|
1299
|
-
* required (the codegen feeder never runs runtime lints anyway).
|
|
1300
|
-
*/
|
|
1301
|
-
optionalFields?: ReadonlySet<string>;
|
|
1302
1292
|
/** Declared relations (`.relations((r) => …)`). */
|
|
1303
1293
|
relations: ReadonlyArray<AdvisorRelation>;
|
|
1304
1294
|
/**
|
|
@@ -1574,31 +1564,6 @@ interface AdvisorStorageUpload {
|
|
|
1574
1564
|
/** Options-object keys present at the call site (empty when not `analyzable`, or when no options argument was passed). */
|
|
1575
1565
|
presentKeys: string[];
|
|
1576
1566
|
}
|
|
1577
|
-
/**
|
|
1578
|
-
* A bounded sample of rows from one table, fed into the constraint-validator
|
|
1579
|
-
* lint by the studio backend (via `readTablePage`). The cap prevents unbounded
|
|
1580
|
-
* scans while still catching obvious violations on small-to-medium tables.
|
|
1581
|
-
*
|
|
1582
|
-
* The studio notes the cap to the operator when the row count exceeds it
|
|
1583
|
-
* (`truncated: true`), so violations on rows beyond the sample window are not
|
|
1584
|
-
* silently missed — the finding description mentions the cap.
|
|
1585
|
-
*/
|
|
1586
|
-
interface AdvisorTableSample {
|
|
1587
|
-
/** The cap applied; equals `rows.length` when not truncated. */
|
|
1588
|
-
readonly cap: number;
|
|
1589
|
-
/**
|
|
1590
|
-
* The row ids of every existing row in this table (bounded to `cap`), used
|
|
1591
|
-
* for FK referential-integrity checks: if a FK value does not appear in the
|
|
1592
|
-
* target table's `existingIds`, it is a dangling reference.
|
|
1593
|
-
*/
|
|
1594
|
-
readonly existingIds: ReadonlySet<string>;
|
|
1595
|
-
/** Sampled rows (up to `cap`). Each row includes `_id` and all declared columns. */
|
|
1596
|
-
readonly rows: ReadonlyArray<Record<string, unknown>>;
|
|
1597
|
-
/** The table's name. */
|
|
1598
|
-
readonly table: string;
|
|
1599
|
-
/** Whether more rows exist beyond the cap. */
|
|
1600
|
-
readonly truncated: boolean;
|
|
1601
|
-
}
|
|
1602
1567
|
/**
|
|
1603
1568
|
* One branching `defineShape({ where })` / `definePolicy({ when })` predicate arm
|
|
1604
1569
|
* that returns an unrestricted predicate — the `unrestricted_where_branch` lint
|
|
@@ -2239,21 +2204,6 @@ interface LintContext {
|
|
|
2239
2204
|
* nothing.
|
|
2240
2205
|
*/
|
|
2241
2206
|
storageUploads?: ReadonlyArray<AdvisorStorageUpload>;
|
|
2242
|
-
/**
|
|
2243
|
-
* Bounded row samples per table — the `constraint_validator` lint input.
|
|
2244
|
-
* There is NO shipped feeder: neither the runtime nor the studio reads row
|
|
2245
|
-
* samples out of a shard, so this is absent for every caller in-tree and the
|
|
2246
|
-
* constraint lint finds nothing. Supply it yourself (a paged read per table,
|
|
2247
|
-
* plus the existing-id set for the FK referential-integrity checks) to drive
|
|
2248
|
-
* that lint.
|
|
2249
|
-
*
|
|
2250
|
-
* Each entry carries `existingIds` (every `_id` in the sample window) so
|
|
2251
|
-
* FK columns can be cross-checked across tables in O(1) per value. When
|
|
2252
|
-
* `truncated` is `true`, violations on rows beyond the cap are not reported
|
|
2253
|
-
* — the finding description notes the sample cap so the operator understands
|
|
2254
|
-
* the bounded window.
|
|
2255
|
-
*/
|
|
2256
|
-
tableSamples?: ReadonlyArray<AdvisorTableSample>;
|
|
2257
2207
|
/**
|
|
2258
2208
|
* Per-table full-scan volume observed at runtime (the hot-scan half of the
|
|
2259
2209
|
* `index_utilization` lint input). Sourced from the per-`(function, table)`
|
|
@@ -2382,34 +2332,6 @@ interface AnalyticsRuntimeMetrics {
|
|
|
2382
2332
|
* lint-name-prefixed, so this never merges across lints.
|
|
2383
2333
|
*/
|
|
2384
2334
|
declare const dedupeCacheKeys: (findings: ReadonlyArray<Finding>) => Finding[];
|
|
2385
|
-
/**
|
|
2386
|
-
* Constraint validator — flag rows that violate declared FK / NOT NULL / UNIQUE
|
|
2387
|
-
* constraints by cross-checking sampled row data against the schema.
|
|
2388
|
-
*
|
|
2389
|
-
* No shipped feeder fills `context.tableSamples` — neither the runtime nor the
|
|
2390
|
-
* Studio reads bounded row samples out of a shard (the Studio drops this lint
|
|
2391
|
-
* from the set it runs for exactly that reason). It runs only for a caller that
|
|
2392
|
-
* gathers samples itself, which is why the guard below returns `[]` rather than
|
|
2393
|
-
* assuming the feed is present.
|
|
2394
|
-
*
|
|
2395
|
-
* This lint reads the `context.tableSamples` feed (bounded row samples supplied
|
|
2396
|
-
* by the studio backend via `readTablePage`) and the declared schema. Three
|
|
2397
|
-
* families of check run over each sample:
|
|
2398
|
-
*
|
|
2399
|
-
* FK referential integrity: for every `one` relation the holding table declares,
|
|
2400
|
-
* check that each sampled row's FK column value appears in the target table's
|
|
2401
|
-
* sampled id set. A dangling value means no target row exists for the reference.
|
|
2402
|
-
*
|
|
2403
|
-
* NOT NULL / non-optional columns: the lint surfaces rows with null/undefined in
|
|
2404
|
-
* declared fields — inserted before a column was added or via raw import.
|
|
2405
|
-
*
|
|
2406
|
-
* UNIQUE index violations: for each declared unique secondary index, check the
|
|
2407
|
-
* sampled rows for duplicate values across the index's columns.
|
|
2408
|
-
*
|
|
2409
|
-
* All checks are bounded by the cap in each sample; the lint never triggers an
|
|
2410
|
-
* additional read. When a sample is truncated, findings note the caveat.
|
|
2411
|
-
*/
|
|
2412
|
-
declare const constraintValidator: Lint;
|
|
2413
2335
|
/**
|
|
2414
2336
|
* `fan_out_breadth` — flag a shard set wide enough that a cross-shard read over
|
|
2415
2337
|
* it would approach the per-invocation subrequest ceiling.
|
|
@@ -2594,19 +2516,21 @@ declare const aiUnboundedGenerationPublic: Lint;
|
|
|
2594
2516
|
* a table `.public()`, i.e. exempt from it), an unauthenticated caller can shard-hop
|
|
2595
2517
|
* and read another tenant's rows with no row-security guard behind the door.
|
|
2596
2518
|
*
|
|
2597
|
-
* **Evidence
|
|
2598
|
-
*
|
|
2599
|
-
*
|
|
2600
|
-
*
|
|
2601
|
-
*
|
|
2602
|
-
* `
|
|
2603
|
-
* `
|
|
2604
|
-
*
|
|
2605
|
-
*
|
|
2606
|
-
*
|
|
2519
|
+
* **Evidence**: this reads `context.configCalls`, fed by the codegen
|
|
2520
|
+
* `discover/config-calls.ts` feeder, and covers BOTH places the field can be
|
|
2521
|
+
* set. `lunora({ allowUnauthenticatedShardAccess: true })` in `vite.config.*`
|
|
2522
|
+
* is the documented opt-in for the auto-composed class-A worker, and is the
|
|
2523
|
+
* only place a class-A app (the default Vite path — `sveltekit` / `astro` /
|
|
2524
|
+
* `react-router` / `tanstack-start`) can set it at all, since it has no worker
|
|
2525
|
+
* entry. `defineApp()...extend(() => ({ allowUnauthenticatedShardAccess: true }))`
|
|
2526
|
+
* is the class-B escape hatch (the `nuxt` / `analog` templates' `lunora/server.ts`),
|
|
2527
|
+
* read from `lunora/` and the worker entry alike.
|
|
2528
|
+
*
|
|
2529
|
+
* Still out of view: a hand-written entry passing the field straight to
|
|
2530
|
+
* `createWorker({...})` — that callee is not one the feeder reads.
|
|
2607
2531
|
*
|
|
2608
2532
|
* Runs only when the codegen feeder supplies config-call evidence; a runtime
|
|
2609
|
-
* caller flags nothing. One finding per opted-in
|
|
2533
|
+
* caller flags nothing. One finding per opted-in call site.
|
|
2610
2534
|
*/
|
|
2611
2535
|
declare const allowUnauthenticatedShardAccessEnabled: Lint;
|
|
2612
2536
|
/**
|
|
@@ -3031,7 +2955,9 @@ declare const filterWithoutIndex: Lint;
|
|
|
3031
2955
|
* a protection (`enforce*`/`rls*`/`gate*`/`lockdown*`) and defaults `false`, or
|
|
3032
2956
|
* names a permission/bypass (`allow*`/`permit*`/`bypass*`) and defaults `true`,
|
|
3033
2957
|
* a flag-backend outage silently disables the protection or grants the
|
|
3034
|
-
* permission for every request.
|
|
2958
|
+
* permission for every request. A negating token in the key
|
|
2959
|
+
* (`disable*`/`*Disabled`/`skip*`/`no*`) inverts that — see
|
|
2960
|
+
* {@link safeDefaultFor}.
|
|
3035
2961
|
*
|
|
3036
2962
|
* Runs only when the codegen feeder supplies flag-default evidence
|
|
3037
2963
|
* (`context.flagSecurityDefaults`); a runtime caller flags nothing. Deliberately
|
|
@@ -3391,18 +3317,29 @@ declare const maskWeakHashStrategyOnPii: Lint;
|
|
|
3391
3317
|
* Flags a public read that hydrates a masked table's rows in the clear through a
|
|
3392
3318
|
* `with` relation.
|
|
3393
3319
|
*
|
|
3394
|
-
* Column masking
|
|
3395
|
-
*
|
|
3396
|
-
*
|
|
3397
|
-
*
|
|
3398
|
-
*
|
|
3399
|
-
*
|
|
3400
|
-
*
|
|
3320
|
+
* Column masking is **per-procedure**, and that — not the `with` boundary — is
|
|
3321
|
+
* what this catches. `.use(mask(policies))` installs a `relationMask` hook on
|
|
3322
|
+
* the read's args (`@lunora/server`'s `mask/middleware`), and the relation
|
|
3323
|
+
* loader calls it for the TARGET table of every hop, at every nesting depth
|
|
3324
|
+
* (`@lunora/shard-engine`'s `relations`); the one hop it cannot reach, a
|
|
3325
|
+
* cross-shard child, fails closed with `MASK_UNSUPPORTED` rather than returning
|
|
3326
|
+
* cleartext. So a procedure that masks `users` gets masked `users` through
|
|
3327
|
+
* `with` too.
|
|
3328
|
+
*
|
|
3329
|
+
* What is still real is a read whose OWN procedure declares no policy for the
|
|
3330
|
+
* related table. `ctx.db.posts.findMany({ with: { author: true } })` in a
|
|
3331
|
+
* procedure with no `.use(mask(...))` — or one whose policy names only `posts` —
|
|
3332
|
+
* hands back every `author` in the clear, including the columns another
|
|
3333
|
+
* procedure carefully masks on `users`' own reads. Nothing about the mask on
|
|
3334
|
+
* `users` reaches this read.
|
|
3401
3335
|
*
|
|
3402
3336
|
* INFO, near-zero false positives by construction: it fires only when all of
|
|
3403
3337
|
* (1) the enclosing read is public, (2) the read declares `with: { <rel> }`,
|
|
3404
|
-
* (3) `<rel>` resolves through the schema to a real target table,
|
|
3405
|
-
* target table actually has masked columns (per the discovered mask evidence)
|
|
3338
|
+
* (3) `<rel>` resolves through the schema to a real target table, (4) that
|
|
3339
|
+
* target table actually has masked columns (per the discovered mask evidence),
|
|
3340
|
+
* and (5) the reading procedure's own mask policy does not cover that target
|
|
3341
|
+
* table. A policy this feeder could not read statically counts as covering
|
|
3342
|
+
* everything, so an opaque `mask(policies)` never produces a finding.
|
|
3406
3343
|
* Absent any mask usage the lint is a no-op. Runs only when the codegen feeder
|
|
3407
3344
|
* supplies `context.relationLoads`; a runtime caller flags nothing. One finding
|
|
3408
3345
|
* per `(read, masked relation)` pair.
|
|
@@ -3861,8 +3798,8 @@ declare const ratelimitKeySpoofableOrGlobal: Lint;
|
|
|
3861
3798
|
* Runs only when the codegen feeder supplies fail-open-guard evidence
|
|
3862
3799
|
* (`context.failOpenGuards`); a runtime caller flags nothing. Deliberately narrow
|
|
3863
3800
|
* — fires only when the options literal provably set `failOpen: true` AND the
|
|
3864
|
-
* guarded procedure's export name or rate-limit `name`
|
|
3865
|
-
*
|
|
3801
|
+
* guarded procedure's export name or rate-limit `name` carries an auth/payment
|
|
3802
|
+
* WORD, keeping the false-positive rate low. One finding per guard.
|
|
3866
3803
|
*/
|
|
3867
3804
|
declare const ratelimitMiddlewareFailOpen: Lint;
|
|
3868
3805
|
/**
|
|
@@ -4028,13 +3965,20 @@ declare const sqlInjectionRisk: Lint;
|
|
|
4028
3965
|
* Flags a `ctx.storage.generateUploadUrl(key, …)` call whose options argument
|
|
4029
3966
|
* omits `contentType`.
|
|
4030
3967
|
*
|
|
4031
|
-
* `generateUploadUrl`
|
|
4032
|
-
*
|
|
4033
|
-
* `allowedContentTypes`/`maxSize` guards, which this
|
|
4034
|
-
* guard
|
|
4035
|
-
*
|
|
4036
|
-
*
|
|
4037
|
-
*
|
|
3968
|
+
* `generateUploadUrl` is `getSignedUrl(key, { method: "PUT" })`: it mints a
|
|
3969
|
+
* signed PUT URL the *client* uploads with, bypassing `upload()`/`store()`
|
|
3970
|
+
* entirely — including their `allowedContentTypes`/`maxSize` guards, which this
|
|
3971
|
+
* alias never sees. The one guard it offers is `contentType`, which
|
|
3972
|
+
* `buildSignedUrl` binds into the HMAC canonical (PUT only) and
|
|
3973
|
+
* `verifySignedUrl` returns as `contentType` on a valid verdict.
|
|
3974
|
+
*
|
|
3975
|
+
* Note what that does and does not buy. Binding it means the pin cannot be
|
|
3976
|
+
* swapped without breaking the signature, so the URL is scoped to one declared
|
|
3977
|
+
* content-type. It does NOT by itself reject a mismatched upload: nothing in
|
|
3978
|
+
* `@lunora/storage` compares the pin against the request's actual
|
|
3979
|
+
* `Content-Type` — the PUT route you mount at `publicBaseUrl` does, by checking
|
|
3980
|
+
* `verifySignedUrl`'s `contentType` against the inbound header. Omit the pin
|
|
3981
|
+
* and there is nothing to check against at all, and no size bound either way.
|
|
4038
3982
|
*
|
|
4039
3983
|
* Runs only when the codegen feeder supplies storage-upload evidence
|
|
4040
3984
|
* (`context.storageUploads`); a runtime caller flags nothing. Skips calls
|
|
@@ -4068,15 +4012,23 @@ declare const storageKeyFromUserArgs: Lint;
|
|
|
4068
4012
|
*
|
|
4069
4013
|
* `getPresignedUrl` mints a native S3 SigV4 URL that resolves directly
|
|
4070
4014
|
* against R2's S3 endpoint — the holder reaches the object straight off R2,
|
|
4071
|
-
* **bypassing the Worker entirely**, so
|
|
4072
|
-
*
|
|
4073
|
-
*
|
|
4074
|
-
*
|
|
4075
|
-
*
|
|
4076
|
-
*
|
|
4077
|
-
*
|
|
4078
|
-
*
|
|
4079
|
-
*
|
|
4015
|
+
* **bypassing the Worker entirely**, so no request-time gating is even
|
|
4016
|
+
* possible. That's the right trade for genuinely public or bulk content; it's
|
|
4017
|
+
* the wrong choice for private, per-user, or policy-gated content, where
|
|
4018
|
+
* `getSignedUrl` is the fit.
|
|
4019
|
+
*
|
|
4020
|
+
* What `getSignedUrl` actually gives you is a URL at your own
|
|
4021
|
+
* `publicBaseUrl` origin, HMAC-bound to (method, host, bucket, key, expiry).
|
|
4022
|
+
* That is an OPPORTUNITY to gate, not a gate: `@lunora/storage` ships no
|
|
4023
|
+
* serving route, and nothing in the framework calls `verifySignedUrl`. The
|
|
4024
|
+
* route you mount there is what validates the signature and runs whatever
|
|
4025
|
+
* auth/policy/rate-limit checks the app needs. The lint's point is that
|
|
4026
|
+
* `getPresignedUrl` removes that seam entirely.
|
|
4027
|
+
*
|
|
4028
|
+
* Separately, either signer minting a long TTL near the shared 7-day ceiling
|
|
4029
|
+
* hands out a bearer credential that stays valid almost as long as the
|
|
4030
|
+
* platform allows — a leaked link (referrer, logs, browser history) then
|
|
4031
|
+
* grants access for nearly a week.
|
|
4080
4032
|
*
|
|
4081
4033
|
* Runs only when the codegen feeder supplies storage-upload evidence
|
|
4082
4034
|
* (`context.storageUploads`); a runtime caller flags nothing. The
|
|
@@ -4159,9 +4111,12 @@ declare const ttlFieldNotTimestamp: Lint;
|
|
|
4159
4111
|
*
|
|
4160
4112
|
* A string field that accepts an unbounded value lets a client send megabytes of
|
|
4161
4113
|
* text per request — inflating storage, blowing the row/document size budget, and
|
|
4162
|
-
* driving CPU/memory on every handler that processes it. A `.
|
|
4163
|
-
*
|
|
4164
|
-
*
|
|
4114
|
+
* driving CPU/memory on every handler that processes it. A `.max(n)` (or
|
|
4115
|
+
* `.length(n)`) bound caps the blast radius. Only those two count: `.meta({
|
|
4116
|
+
* maxLength })` publishes a cap the parser never enforces, and a bare `.check()`
|
|
4117
|
+
* may predicate anything — neither is evidence the length is bounded. Advisory
|
|
4118
|
+
* (INFO): a deliberately-open free-text field is sometimes legitimate, so this
|
|
4119
|
+
* nudges rather than blocks.
|
|
4165
4120
|
*
|
|
4166
4121
|
* Runs only when the codegen feeder supplies arg evidence
|
|
4167
4122
|
* (`context.argValidators`, public procedures only); a runtime caller flags
|
|
@@ -4234,10 +4189,15 @@ declare const unrestrictedWhereBranch: Lint;
|
|
|
4234
4189
|
* Endpoints that mint accounts or trigger emails are the classic automated-abuse
|
|
4235
4190
|
* surface: credential-stuffing sign-ups, mailbox-flooding "forgot password" loops,
|
|
4236
4191
|
* and disposable-account farming. A server-verified human check (Turnstile) in
|
|
4237
|
-
* front of them is the defense. Lunora ships `
|
|
4238
|
-
* and the `protectPublic({ captcha })` bundle; this lint fires
|
|
4239
|
-
* procedure writes a user/session/account-shaped table (or
|
|
4240
|
-
* with no captcha middleware.
|
|
4192
|
+
* front of them is the defense. Lunora ships `verifyTurnstileMiddleware()`
|
|
4193
|
+
* (`@lunora/auth`) and the `protectPublic({ captcha })` bundle; this lint fires
|
|
4194
|
+
* when a public procedure writes a user/session/account-shaped table (or
|
|
4195
|
+
* references `ctx.mail`) with no captcha middleware.
|
|
4196
|
+
*
|
|
4197
|
+
* The middleware, NOT `verifyTurnstile`: that one is the async verdict function
|
|
4198
|
+
* the middleware calls, so `.use(verifyTurnstile({...}))` installs a Promise in
|
|
4199
|
+
* the chain and checks nothing. The feeder counts only the middleware, so the
|
|
4200
|
+
* remediation below has to name the same thing the feeder will accept.
|
|
4241
4201
|
*
|
|
4242
4202
|
* Runs only when the codegen feeder supplies protection evidence
|
|
4243
4203
|
* (`context.procedureProtections`); a runtime caller with no evidence flags
|
|
@@ -4599,4 +4559,4 @@ interface RunAdvisorOptions {
|
|
|
4599
4559
|
* `static` lints at build time and defer `runtime` lints to a live shard.
|
|
4600
4560
|
*/
|
|
4601
4561
|
declare const runAdvisor: (context: LintContext, options?: RunAdvisorOptions) => Finding[];
|
|
4602
|
-
export { 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 AdvisorFlagRead, 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 AdvisorMap, 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 AdvisorStaleMigrationImport, type AdvisorStorageKeyAccess, type AdvisorStorageUpload, type AdvisorTable, type
|
|
4562
|
+
export { 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 AdvisorFlagRead, 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 AdvisorMap, 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 AdvisorStaleMigrationImport, type AdvisorStorageKeyAccess, type AdvisorStorageUpload, type AdvisorTable, type AdvisorTableScan, type AdvisorVectorNamespaceAccess, type AdvisorWorkflow, type AdvisorWorkflowCall, type AdvisorWranglerVariable, type AnalyticsMetricsOptions, type AnalyticsMetricsSource, type AnalyticsRuntimeMetrics, type BaselineComparison, type Category, type CheckResult, type Coverage, type Facing, type Finding, type Grade, type Level, type Lint, type LintContext, type LintSource, MAP_VERSION, type MapSummary, type ProcedureDelta, type ProcedureScore, type ProjectScore, RUNTIME_LINTS, RunAdvisorOptions, STATIC_LINTS, type ScoreAdvisorOptions, type Sensitivity, type SensitivityLevel, actionFetchSsrf, actionWithoutErrorHandling, adminRouteWithoutGuard, aiRawRunEscapeHatch, aiRunWithoutLogging, aiToolSideEffectPromptInjection, aiUnboundedGenerationPublic, allowUnauthenticatedShardAccessEnabled, authApiCallWithoutHeaders, authCsrfCheckDisabled, authEmailVerificationDisabled, authScimWithoutTransactions, authSecureCookiesDisabled, authSessionFreshageZero, authTrustedOriginsWildcard, browserAllowPrivateTargets, browserUserUrlWithoutAllowlist, byCodepoint, circularFk, classifySensitivity, commitOrderedHardDelete, compareToBaseline, containerInstanceKeyFromUserInput, containerOversizedInstance, containerPublicInternet, containerRuntimeEgressRelaxation, containerStartEnableInternetOverride, dedupeCacheKeys, duplicateIndex, emptyIndex, errorWithoutCatalog, exportSinkMisconfigured, externalSourceIncrementalNoDeletePath, externalSourceOnGlobal, externalSourceUnscoped, fanOutBreadth, filterOnPrimaryKey, filterWithoutIndex, flagGatesSecurityWithUnsafeDefault, flagReadInSubscription, fromServerSchema, geoIndexFieldNotGeopoint, geoIndexUnused, globalTableNearColumnLimit, gradeFromScore, hardcodedSecret, hotShard, httpActionMissingAuthGuard, httpActionResponseHeaderInjection, hyperdriveOutsideAction, identityUndeclaredClaimTrusted, imagesUrlSourceFromUserInput, indexReferencesUnknownField, indexUtilization, insertManyUnsafeUserData, kvUnscopedUserKeyIdor, mailInboundDispatchWithoutVerify, mailRecipientFromRequestInput, maskUncoveredPiiColumn, maskWeakHashStrategyOnPii, maskedRelationLeakViaWith, mutatorFullRowReplace, nondeterministicQueryMutation, normalizeIdUsedAsAuthorization, notifyMissingPushConfig, notifySendOutsideAction, outputProjectionMissingOnPublicRead, ownerFieldFromArgsNotAuth, parseAdvisorMap, paymentCreateWithoutAuthorize, paymentWebhookWideTolerance, plaintextSecretInWranglerVariables, policyReferencesUnknownTable, privilegedDispatchUnvalidatedPayload, privilegedFanoutFromPublicProcedure, procedureWithoutStructuredEvent, publicArgumentUsesAny, publicMutationWithoutRatelimit, publicTableRlsOptoutConfusion, queueWithoutDlq, r2sqlOutsideAction, ratelimitDefaultMemoryStore, ratelimitKeySpoofableOrGlobal, ratelimitMiddlewareFailOpen, relationReferencesUnknownField, relationReferencesUnknownTable, rlsUncoveredTable, runAdvisor, scoreAdvisor, shapeTargetsGlobalTable, shapeUnknownTable, signupMutationWithoutDisposableGating, softDeleteIncludeDeletedFromArgs, sqlInjectionRisk, storageGenerateUploadUrlNoContentTypePin, storageKeyFromUserArgs, storagePresignedUrlForPrivateContent, storageUploadWithoutContentTypeAllowlist, storageUploadWithoutMaxSize, tableWithoutInsert, ttlFieldNotTimestamp, unboundedStringArgument, unindexedForeignKey, unindexedRelationTarget, unrestrictedWhereBranch, userCreatingMutationWithoutCaptcha, vectorsNamespaceFromUserInput, workflowDuplicateStepName, workflowUnknownTarget, workflowUnused };
|
package/dist/index.mjs
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import{dedupeCacheKeys as c}from"./packem_shared/dedupeCacheKeys-DtBOHffV.mjs";import d from"./packem_shared/constraintValidator-DoRJD9Is.mjs";import u from"./packem_shared/fanOutBreadth-CBtmZnoh.mjs";import h from"./packem_shared/hotShard-BwGYZ3Tq.mjs";import g from"./packem_shared/indexUtilization-CkVPZcVe.mjs";import b from"./packem_shared/actionFetchSsrf-Z61P0o8U.mjs";import y from"./packem_shared/actionWithoutErrorHandling-4GCBT0_z.mjs";import w from"./packem_shared/adminRouteWithoutGuard-DPE7LuNh.mjs";import S from"./packem_shared/aiRawRunEscapeHatch-Dq43DVD9.mjs";import v from"./packem_shared/aiRunWithoutLogging-CvHHtEN9.mjs";import A from"./packem_shared/aiToolSideEffectPromptInjection-K42X3QzJ.mjs";import U from"./packem_shared/aiUnboundedGenerationPublic-C17h6sMV.mjs";import R from"./packem_shared/allowUnauthenticatedShardAccessEnabled-CjXI-kda.mjs";import W from"./packem_shared/authApiCallWithoutHeaders-C1OOWML5.mjs";import T from"./packem_shared/authCsrfCheckDisabled-Dz27bDzp.mjs";import I from"./packem_shared/authEmailVerificationDisabled-QTDD7TAU.mjs";import N from"./packem_shared/authScimWithoutTransactions-FqdTmUJs.mjs";import k from"./packem_shared/authSecureCookiesDisabled-CrGYulfJ.mjs";import x from"./packem_shared/authSessionFreshageZero-yWE6CGYP.mjs";import F from"./packem_shared/authTrustedOriginsWildcard-xpeRXfGF.mjs";import C from"./packem_shared/browserAllowPrivateTargets-Cj5sizhv.mjs";import O from"./packem_shared/browserUserUrlWithoutAllowlist-Wl3xHr7v.mjs";import M from"./packem_shared/circularFk-DtcWFJxK.mjs";import $ from"./packem_shared/commitOrderedHardDelete-BPdwOKA7.mjs";import D from"./packem_shared/containerInstanceKeyFromUserInput-uEQQVsEz.mjs";import E from"./packem_shared/containerOversizedInstance-Bx89uR7E.mjs";import P from"./packem_shared/containerPublicInternet-BFfZf_P4.mjs";import K from"./packem_shared/containerRuntimeEgressRelaxation-pkpyXNou.mjs";import q from"./packem_shared/containerStartEnableInternetOverride-BBY4PMG1.mjs";import L from"./packem_shared/duplicateIndex-Cip6-Rpu.mjs";import G from"./packem_shared/emptyIndex-BnHDcXza.mjs";import _ from"./packem_shared/errorWithoutCatalog-BTfvaXHR.mjs";import z from"./packem_shared/exportSinkMisconfigured-JfbAx9AI.mjs";import H from"./packem_shared/externalSourceIncrementalNoDeletePath-BCzm3HzF.mjs";import B from"./packem_shared/externalSourceOnGlobal-CH7xbJ49.mjs";import V from"./packem_shared/externalSourceUnscoped-BxU2uSXk.mjs";import j from"./packem_shared/filterOnPrimaryKey-COQRpmnu.mjs";import Q from"./packem_shared/filterWithoutIndex-Cf-tXY_A.mjs";import X from"./packem_shared/flagGatesSecurityWithUnsafeDefault-i-Befg8b.mjs";import Z from"./packem_shared/flagReadInSubscription-DabIhYGD.mjs";import J from"./packem_shared/geoIndexFieldNotGeopoint-D0lOTm-_.mjs";import Y from"./packem_shared/geoIndexUnused-D7C9Qr4U.mjs";import oo from"./packem_shared/globalTableNearColumnLimit-BFbBBd6A.mjs";import eo from"./packem_shared/hardcodedSecret-Bw_4FYrs.mjs";import ro from"./packem_shared/httpActionMissingAuthGuard-BS3JZgaz.mjs";import to from"./packem_shared/httpActionResponseHeaderInjection-DHnc8c9f.mjs";import io from"./packem_shared/hyperdriveOutsideAction-CPDdGP2g.mjs";import no from"./packem_shared/identityUndeclaredClaimTrusted-BDFqB7Dw.mjs";import ao from"./packem_shared/imagesUrlSourceFromUserInput-DizsRh3M.mjs";import mo from"./packem_shared/indexReferencesUnknownField-B_c3o9QR.mjs";import so from"./packem_shared/insertManyUnsafeUserData-DWE8d_DF.mjs";import lo from"./packem_shared/kvUnscopedUserKeyIdor-CIgMPzFj.mjs";import po from"./packem_shared/mailInboundDispatchWithoutVerify-CWwqXyPX.mjs";import fo from"./packem_shared/mailRecipientFromRequestInput-SDCchLP7.mjs";import co from"./packem_shared/maskUncoveredPiiColumn-axEka4nd.mjs";import uo from"./packem_shared/maskWeakHashStrategyOnPii-BC1n0F4-.mjs";import ho from"./packem_shared/maskedRelationLeakViaWith-C8n-Mzb1.mjs";import{e as a}from"./packem_shared/finding-NrKO8idM.mjs";import go from"./packem_shared/mutatorFullRowReplace-BzRpZ47r.mjs";import bo from"./packem_shared/nondeterministicQueryMutation-sxmixc0l.mjs";import yo from"./packem_shared/normalizeIdUsedAsAuthorization-BXN-Can6.mjs";import wo from"./packem_shared/notifyMissingPushConfig-DeQMoNwm.mjs";import So from"./packem_shared/notifySendOutsideAction-CU4T16cR.mjs";import vo from"./packem_shared/outputProjectionMissingOnPublicRead-DhiA3mpG.mjs";import Ao from"./packem_shared/ownerFieldFromArgsNotAuth-CjOSUDKi.mjs";import Uo from"./packem_shared/paymentCreateWithoutAuthorize-CICGmRFR.mjs";import Ro from"./packem_shared/paymentWebhookWideTolerance-B-F5jOeA.mjs";import Wo from"./packem_shared/plaintextSecretInWranglerVariables-bNkqmNqV.mjs";import To from"./packem_shared/policyReferencesUnknownTable-CIKuRZ5Y.mjs";import Io from"./packem_shared/privilegedDispatchUnvalidatedPayload-C6Qtc8aT.mjs";import No from"./packem_shared/privilegedFanoutFromPublicProcedure-Coqt23CQ.mjs";import ko from"./packem_shared/procedureWithoutStructuredEvent-CZ_B23nM.mjs";import xo from"./packem_shared/publicArgumentUsesAny-BDEvMA8Q.mjs";import Fo from"./packem_shared/publicMutationWithoutRatelimit-Dsd4cSPD.mjs";import Co from"./packem_shared/publicTableRlsOptoutConfusion-Dz7lbKKe.mjs";import Oo from"./packem_shared/queueWithoutDlq-BvHz3Opg.mjs";import Mo from"./packem_shared/r2sqlOutsideAction-BdLiLOAX.mjs";import $o from"./packem_shared/ratelimitDefaultMemoryStore-Dj0Kk7t9.mjs";import Do from"./packem_shared/ratelimitKeySpoofableOrGlobal-BzDzxnXm.mjs";import Eo from"./packem_shared/ratelimitMiddlewareFailOpen-iDJq68qx.mjs";import Po from"./packem_shared/relationReferencesUnknownField-D8Qyth_P.mjs";import Ko from"./packem_shared/relationReferencesUnknownTable-CP4aWtAJ.mjs";import qo from"./packem_shared/rlsUncoveredTable-CZ4ie4gX.mjs";import Lo from"./packem_shared/shapeTargetsGlobalTable-Bu3eEDic.mjs";import Go from"./packem_shared/shapeUnknownTable-CREfNnWi.mjs";import _o from"./packem_shared/signupMutationWithoutDisposableGating-BIsSLyhJ.mjs";import zo from"./packem_shared/softDeleteIncludeDeletedFromArgs-C7Ugg4zb.mjs";import Ho from"./packem_shared/sqlInjectionRisk-CslaRBxz.mjs";import Bo from"./packem_shared/storageGenerateUploadUrlNoContentTypePin-CTIU_7Fj.mjs";import Vo from"./packem_shared/storageKeyFromUserArgs-C-QUcdJ3.mjs";import jo from"./packem_shared/storagePresignedUrlForPrivateContent-yeu1s81k.mjs";import Qo from"./packem_shared/storageUploadWithoutContentTypeAllowlist-KSRG81Iu.mjs";import Xo from"./packem_shared/storageUploadWithoutMaxSize-BTiM9Q3A.mjs";import Zo from"./packem_shared/tableWithoutInsert-DQ-GxjFF.mjs";import Jo from"./packem_shared/ttlFieldNotTimestamp-E14I83wY.mjs";import{s as Yo,q as oe}from"./packem_shared/helpers-CwSEZdku.mjs";import ee from"./packem_shared/unboundedStringArgument-DzbFJc5q.mjs";import re from"./packem_shared/unindexedForeignKey-Dypgn8uH.mjs";import te from"./packem_shared/unindexedRelationTarget-CSqRJWYZ.mjs";import ie from"./packem_shared/unrestrictedWhereBranch-CcIBmHik.mjs";import ne from"./packem_shared/userCreatingMutationWithoutCaptcha-B7l-S-rV.mjs";import ae from"./packem_shared/vectorsNamespaceFromUserInput-CDSOrkyX.mjs";import me from"./packem_shared/workflowDuplicateStepName-BU4rg5So.mjs";import se from"./packem_shared/workflowUnknownTarget-B8H7jwnH.mjs";import le from"./packem_shared/workflowUnused-BUSOPdHq.mjs";import{compareToBaseline as gt,parseAdvisorMap as bt}from"./packem_shared/compareToBaseline-DBgN5YqX.mjs";import{gradeFromScore as wt}from"./packem_shared/gradeFromScore-KSt58rj1.mjs";import{MAP_VERSION as vt,byCodepoint as At,scoreAdvisor as Ut}from"./packem_shared/MAP_VERSION-DEJGm0wI.mjs";import{default as Wt}from"./packem_shared/classifySensitivity-JnjaTGYi.mjs";import{fromServerSchema as It}from"./packem_shared/fromServerSchema-D2nTknz0.mjs";const pe={convex:"migrating/from-convex",firebase:"migrating/from-firebase",supabase:"migrating/from-supabase"},m={categories:["SCHEMA"],description:"A migrated-away platform's SDK is still imported from `lunora/` source. The code compiles and typechecks, so a half-finished port keeps reading from the old platform at runtime and the two stores silently drift apart.",facing:"INTERNAL",level:"WARN",name:"migration_stale_import",remediation:"Replace the call with its Lunora equivalent and drop the dependency. If the import is deliberate — a second data source you still read from — move it out of `lunora/` so it is not mistaken for an unfinished migration.",run:r=>r.staleMigrationImports===void 0?[]:r.staleMigrationImports.map(o=>a(m,{cacheKey:`migration_stale_import:${o.file}:${o.line.toString()}:${o.moduleSpecifier}`,detail:`\`${o.file}\` (line ${o.line.toString()}) still imports \`${o.moduleSpecifier}\`. Finish the port — see \`${pe[o.platform]}\` — or move the import out of \`lunora/\` if you genuinely still read from ${o.platform}.`,metadata:{file:o.file,line:o.line,moduleSpecifier:o.moduleSpecifier,platform:o.platform}})),source:"static",title:"Stale migration import"},fe=new Map([["global",{level:"WARN",scope:r=>`it reads the whole D1 table "${r}" over a cross-region round trip`}],["root",{level:"WARN",scope:r=>`it loads every row of "${r}" from the root Durable Object's SQLite into memory`}],["shardBy",{level:"INFO",scope:r=>`"${r}" is \`.shardBy()\`, so this collects one shard's rows rather than the whole table — bounded by a single tenant's row count`}]]),ce={level:"WARN",scope:r=>`it loads every row of "${r}"`},s={categories:["PERFORMANCE"],description:"A query calls `.collect()` with no `.withIndex()` and no `.filter()`, so it materializes every row of the table. Any live subscription over it also re-sends that whole result to every subscribed client on every write to the table.",facing:"EXTERNAL",level:"WARN",name:"unbounded_collect",remediation:'Narrow the read with `.withIndex("name", (q) => q.eq(...))`, cap it with `.take(n)`, or page it with `.paginate(args.paginationOpts)` so neither the scan nor the subscription payload grows with the table.',run:r=>{const o=[],i=Yo(r.schema);for(const e of r.queries??[]){if(e.terminal!=="collect"||e.hasIndex||e.table===""||e.hasFilter)continue;const t=i.get(e.table),{level:n,scope:l}=fe.get(t??"")??ce,p=oe(e),f=n==="WARN"?` A live subscription over this query records a whole-table dependency, so every write to "${e.table}" re-runs it and re-sends the full result to each subscribed socket.`:" Cap it with `.take(n)` if that count can grow.";o.push(a(s,{cacheKey:`unbounded_collect:${e.file}:${e.line.toString()}:${e.table}`,detail:`Query on "${e.table}" at ${p} calls .collect() with no index and no filter — ${l(e.table)}.${f}`,level:n,metadata:{exportName:e.exportName,file:e.file,line:e.line,shardKind:t??"unknown",table:e.table}}))}return o},source:"static",title:"Unbounded collect"},de=[mo,Ko,Po,se,me,Go,H,B,V,G,oo,_,J,Y,z,Jo,$,M,re,te,L,Zo,le,Oo,j,Q,s,Lo,go,bo,io,Mo,W,To,qo,co,uo,E,P,Fo,ie,ne,_o,xo,ee,eo,Ho,w,Uo,po,$o,C,No,ko,so,U,b,y,Ao,Vo,lo,D,S,v,ae,fo,O,Io,q,K,N,F,T,k,I,x,ao,Do,Co,R,Qo,Xo,Bo,jo,ro,to,Eo,X,Z,A,no,Ro,zo,ho,vo,yo,So,wo,Wo,m],ue=[h,g,d,u],he=[...de,...ue],dt=(r,o={})=>{const i=o.lints??he,e=[];for(const t of i)o.source!==void 0&&t.source!==o.source||e.push(...t.run(r));return c(e)};export{he as ALL_LINTS,vt as MAP_VERSION,ue as RUNTIME_LINTS,de as STATIC_LINTS,b as actionFetchSsrf,y as actionWithoutErrorHandling,w as adminRouteWithoutGuard,S as aiRawRunEscapeHatch,v as aiRunWithoutLogging,A as aiToolSideEffectPromptInjection,U as aiUnboundedGenerationPublic,R as allowUnauthenticatedShardAccessEnabled,W as authApiCallWithoutHeaders,T as authCsrfCheckDisabled,I as authEmailVerificationDisabled,N as authScimWithoutTransactions,k as authSecureCookiesDisabled,x as authSessionFreshageZero,F as authTrustedOriginsWildcard,C as browserAllowPrivateTargets,O as browserUserUrlWithoutAllowlist,At as byCodepoint,M as circularFk,Wt as classifySensitivity,$ as commitOrderedHardDelete,gt as compareToBaseline,d as constraintValidator,D as containerInstanceKeyFromUserInput,E as containerOversizedInstance,P as containerPublicInternet,K as containerRuntimeEgressRelaxation,q as containerStartEnableInternetOverride,c as dedupeCacheKeys,L as duplicateIndex,G as emptyIndex,_ as errorWithoutCatalog,z as exportSinkMisconfigured,H as externalSourceIncrementalNoDeletePath,B as externalSourceOnGlobal,V as externalSourceUnscoped,u as fanOutBreadth,j as filterOnPrimaryKey,Q as filterWithoutIndex,X as flagGatesSecurityWithUnsafeDefault,Z as flagReadInSubscription,It as fromServerSchema,J as geoIndexFieldNotGeopoint,Y as geoIndexUnused,oo as globalTableNearColumnLimit,wt as gradeFromScore,eo as hardcodedSecret,h as hotShard,ro as httpActionMissingAuthGuard,to as httpActionResponseHeaderInjection,io as hyperdriveOutsideAction,no as identityUndeclaredClaimTrusted,ao as imagesUrlSourceFromUserInput,mo as indexReferencesUnknownField,g as indexUtilization,so as insertManyUnsafeUserData,lo as kvUnscopedUserKeyIdor,po as mailInboundDispatchWithoutVerify,fo as mailRecipientFromRequestInput,co as maskUncoveredPiiColumn,uo as maskWeakHashStrategyOnPii,ho as maskedRelationLeakViaWith,go as mutatorFullRowReplace,bo as nondeterministicQueryMutation,yo as normalizeIdUsedAsAuthorization,wo as notifyMissingPushConfig,So as notifySendOutsideAction,vo as outputProjectionMissingOnPublicRead,Ao as ownerFieldFromArgsNotAuth,bt as parseAdvisorMap,Uo as paymentCreateWithoutAuthorize,Ro as paymentWebhookWideTolerance,Wo as plaintextSecretInWranglerVariables,To as policyReferencesUnknownTable,Io as privilegedDispatchUnvalidatedPayload,No as privilegedFanoutFromPublicProcedure,ko as procedureWithoutStructuredEvent,xo as publicArgumentUsesAny,Fo as publicMutationWithoutRatelimit,Co as publicTableRlsOptoutConfusion,Oo as queueWithoutDlq,Mo as r2sqlOutsideAction,$o as ratelimitDefaultMemoryStore,Do as ratelimitKeySpoofableOrGlobal,Eo as ratelimitMiddlewareFailOpen,Po as relationReferencesUnknownField,Ko as relationReferencesUnknownTable,qo as rlsUncoveredTable,dt as runAdvisor,Ut as scoreAdvisor,Lo as shapeTargetsGlobalTable,Go as shapeUnknownTable,_o as signupMutationWithoutDisposableGating,zo as softDeleteIncludeDeletedFromArgs,Ho as sqlInjectionRisk,Bo as storageGenerateUploadUrlNoContentTypePin,Vo as storageKeyFromUserArgs,jo as storagePresignedUrlForPrivateContent,Qo as storageUploadWithoutContentTypeAllowlist,Xo as storageUploadWithoutMaxSize,Zo as tableWithoutInsert,Jo as ttlFieldNotTimestamp,ee as unboundedStringArgument,re as unindexedForeignKey,te as unindexedRelationTarget,ie as unrestrictedWhereBranch,ne as userCreatingMutationWithoutCaptcha,ae as vectorsNamespaceFromUserInput,me as workflowDuplicateStepName,se as workflowUnknownTarget,le as workflowUnused};
|
|
1
|
+
import{dedupeCacheKeys as c}from"./packem_shared/dedupeCacheKeys-DtBOHffV.mjs";import d from"./packem_shared/fanOutBreadth-CBtmZnoh.mjs";import u from"./packem_shared/hotShard-BwGYZ3Tq.mjs";import h from"./packem_shared/indexUtilization-CkVPZcVe.mjs";import g from"./packem_shared/actionFetchSsrf-Z61P0o8U.mjs";import b from"./packem_shared/actionWithoutErrorHandling-4GCBT0_z.mjs";import y from"./packem_shared/adminRouteWithoutGuard-DPE7LuNh.mjs";import w from"./packem_shared/aiRawRunEscapeHatch-Dq43DVD9.mjs";import S from"./packem_shared/aiRunWithoutLogging-CvHHtEN9.mjs";import v from"./packem_shared/aiToolSideEffectPromptInjection-K42X3QzJ.mjs";import A from"./packem_shared/aiUnboundedGenerationPublic-C17h6sMV.mjs";import U from"./packem_shared/allowUnauthenticatedShardAccessEnabled-BQufF5Kb.mjs";import R from"./packem_shared/authApiCallWithoutHeaders-C1OOWML5.mjs";import W from"./packem_shared/authCsrfCheckDisabled-Dz27bDzp.mjs";import T from"./packem_shared/authEmailVerificationDisabled-QTDD7TAU.mjs";import I from"./packem_shared/authScimWithoutTransactions-FqdTmUJs.mjs";import N from"./packem_shared/authSecureCookiesDisabled-CrGYulfJ.mjs";import k from"./packem_shared/authSessionFreshageZero-yWE6CGYP.mjs";import x from"./packem_shared/authTrustedOriginsWildcard-xpeRXfGF.mjs";import F from"./packem_shared/browserAllowPrivateTargets-Cj5sizhv.mjs";import C from"./packem_shared/browserUserUrlWithoutAllowlist-Wl3xHr7v.mjs";import O from"./packem_shared/circularFk-DtcWFJxK.mjs";import M from"./packem_shared/commitOrderedHardDelete-BPdwOKA7.mjs";import $ from"./packem_shared/containerInstanceKeyFromUserInput-uEQQVsEz.mjs";import D from"./packem_shared/containerOversizedInstance-Bx89uR7E.mjs";import E from"./packem_shared/containerPublicInternet-BFfZf_P4.mjs";import P from"./packem_shared/containerRuntimeEgressRelaxation-pkpyXNou.mjs";import K from"./packem_shared/containerStartEnableInternetOverride-BBY4PMG1.mjs";import q from"./packem_shared/duplicateIndex-Cip6-Rpu.mjs";import L from"./packem_shared/emptyIndex-BnHDcXza.mjs";import G from"./packem_shared/errorWithoutCatalog-BTfvaXHR.mjs";import _ from"./packem_shared/exportSinkMisconfigured-JfbAx9AI.mjs";import z from"./packem_shared/externalSourceIncrementalNoDeletePath-BCzm3HzF.mjs";import H from"./packem_shared/externalSourceOnGlobal-CH7xbJ49.mjs";import B from"./packem_shared/externalSourceUnscoped-BxU2uSXk.mjs";import j from"./packem_shared/filterOnPrimaryKey-jmz_ApDa.mjs";import V from"./packem_shared/filterWithoutIndex-9kAguEVb.mjs";import Q from"./packem_shared/flagGatesSecurityWithUnsafeDefault-20ccteHn.mjs";import X from"./packem_shared/flagReadInSubscription-DabIhYGD.mjs";import Z from"./packem_shared/geoIndexFieldNotGeopoint-Cd5mywEg.mjs";import J from"./packem_shared/geoIndexUnused-D7C9Qr4U.mjs";import Y from"./packem_shared/globalTableNearColumnLimit-BFbBBd6A.mjs";import oo from"./packem_shared/hardcodedSecret-Bw_4FYrs.mjs";import eo from"./packem_shared/httpActionMissingAuthGuard-BS3JZgaz.mjs";import ro from"./packem_shared/httpActionResponseHeaderInjection-DHnc8c9f.mjs";import to from"./packem_shared/hyperdriveOutsideAction-CPDdGP2g.mjs";import io from"./packem_shared/identityUndeclaredClaimTrusted-BDFqB7Dw.mjs";import no from"./packem_shared/imagesUrlSourceFromUserInput-DizsRh3M.mjs";import ao from"./packem_shared/indexReferencesUnknownField-DiC2WN4V.mjs";import mo from"./packem_shared/insertManyUnsafeUserData-DWE8d_DF.mjs";import so from"./packem_shared/kvUnscopedUserKeyIdor-CIgMPzFj.mjs";import lo from"./packem_shared/mailInboundDispatchWithoutVerify-CWwqXyPX.mjs";import po from"./packem_shared/mailRecipientFromRequestInput-SDCchLP7.mjs";import fo from"./packem_shared/maskUncoveredPiiColumn-axEka4nd.mjs";import co from"./packem_shared/maskWeakHashStrategyOnPii-DXhCr75r.mjs";import uo from"./packem_shared/maskedRelationLeakViaWith-CIFij6vE.mjs";import{e as a}from"./packem_shared/finding-NrKO8idM.mjs";import ho from"./packem_shared/mutatorFullRowReplace-BzRpZ47r.mjs";import go from"./packem_shared/nondeterministicQueryMutation-sxmixc0l.mjs";import bo from"./packem_shared/normalizeIdUsedAsAuthorization-BXN-Can6.mjs";import yo from"./packem_shared/notifyMissingPushConfig-DeQMoNwm.mjs";import wo from"./packem_shared/notifySendOutsideAction-CWx7KuJS.mjs";import So from"./packem_shared/outputProjectionMissingOnPublicRead-HwweA4NI.mjs";import vo from"./packem_shared/ownerFieldFromArgsNotAuth-CjOSUDKi.mjs";import Ao from"./packem_shared/paymentCreateWithoutAuthorize-CICGmRFR.mjs";import Uo from"./packem_shared/paymentWebhookWideTolerance-B-F5jOeA.mjs";import Ro from"./packem_shared/plaintextSecretInWranglerVariables-bNkqmNqV.mjs";import Wo from"./packem_shared/policyReferencesUnknownTable-CIKuRZ5Y.mjs";import To from"./packem_shared/privilegedDispatchUnvalidatedPayload-C6Qtc8aT.mjs";import Io from"./packem_shared/privilegedFanoutFromPublicProcedure-Coqt23CQ.mjs";import No from"./packem_shared/procedureWithoutStructuredEvent-CZ_B23nM.mjs";import ko from"./packem_shared/publicArgumentUsesAny-BDEvMA8Q.mjs";import xo from"./packem_shared/publicMutationWithoutRatelimit-cDJph2zk.mjs";import Fo from"./packem_shared/publicTableRlsOptoutConfusion-B4zbtE9m.mjs";import Co from"./packem_shared/queueWithoutDlq-BvHz3Opg.mjs";import Oo from"./packem_shared/r2sqlOutsideAction-BdLiLOAX.mjs";import Mo from"./packem_shared/ratelimitDefaultMemoryStore-Dj0Kk7t9.mjs";import $o from"./packem_shared/ratelimitKeySpoofableOrGlobal-BzDzxnXm.mjs";import Do from"./packem_shared/ratelimitMiddlewareFailOpen-eOeyAyC-.mjs";import Eo from"./packem_shared/relationReferencesUnknownField-GpJPLGDE.mjs";import Po from"./packem_shared/relationReferencesUnknownTable-CP4aWtAJ.mjs";import Ko from"./packem_shared/rlsUncoveredTable-CZ4ie4gX.mjs";import qo from"./packem_shared/shapeTargetsGlobalTable-Bu3eEDic.mjs";import Lo from"./packem_shared/shapeUnknownTable-CREfNnWi.mjs";import Go from"./packem_shared/signupMutationWithoutDisposableGating-BMM2vTNu.mjs";import _o from"./packem_shared/softDeleteIncludeDeletedFromArgs-C7Ugg4zb.mjs";import zo from"./packem_shared/sqlInjectionRisk-CslaRBxz.mjs";import Ho from"./packem_shared/storageGenerateUploadUrlNoContentTypePin-DBq_DeHO.mjs";import Bo from"./packem_shared/storageKeyFromUserArgs-C-QUcdJ3.mjs";import jo from"./packem_shared/storagePresignedUrlForPrivateContent-DZdUkDdW.mjs";import Vo from"./packem_shared/storageUploadWithoutContentTypeAllowlist-KSRG81Iu.mjs";import Qo from"./packem_shared/storageUploadWithoutMaxSize-BTiM9Q3A.mjs";import Xo from"./packem_shared/tableWithoutInsert-DQ-GxjFF.mjs";import Zo from"./packem_shared/ttlFieldNotTimestamp-CixnZ0Ii.mjs";import{s as Jo,q as Yo}from"./packem_shared/helpers-BCqZbKga.mjs";import oe from"./packem_shared/unboundedStringArgument-jZnCbwB0.mjs";import ee from"./packem_shared/unindexedForeignKey-Dypgn8uH.mjs";import re from"./packem_shared/unindexedRelationTarget-CSqRJWYZ.mjs";import te from"./packem_shared/unrestrictedWhereBranch-CcIBmHik.mjs";import ie from"./packem_shared/userCreatingMutationWithoutCaptcha-CGpMm45R.mjs";import ne from"./packem_shared/vectorsNamespaceFromUserInput-CDSOrkyX.mjs";import ae from"./packem_shared/workflowDuplicateStepName-BU4rg5So.mjs";import me from"./packem_shared/workflowUnknownTarget-B8H7jwnH.mjs";import se from"./packem_shared/workflowUnused-BUSOPdHq.mjs";import{compareToBaseline as ut,parseAdvisorMap as ht}from"./packem_shared/compareToBaseline-DBgN5YqX.mjs";import{gradeFromScore as bt}from"./packem_shared/gradeFromScore-KSt58rj1.mjs";import{MAP_VERSION as wt,byCodepoint as St,scoreAdvisor as vt}from"./packem_shared/MAP_VERSION-DEJGm0wI.mjs";import{default as Ut}from"./packem_shared/classifySensitivity-JnjaTGYi.mjs";import{fromServerSchema as Wt}from"./packem_shared/fromServerSchema-fAymN8o2.mjs";const le={convex:"migrating/from-convex",firebase:"migrating/from-firebase",supabase:"migrating/from-supabase"},m={categories:["SCHEMA"],description:"A migrated-away platform's SDK is still imported from `lunora/` source. The code compiles and typechecks, so a half-finished port keeps reading from the old platform at runtime and the two stores silently drift apart.",facing:"INTERNAL",level:"WARN",name:"migration_stale_import",remediation:"Replace the call with its Lunora equivalent and drop the dependency. If the import is deliberate — a second data source you still read from — move it out of `lunora/` so it is not mistaken for an unfinished migration.",run:r=>r.staleMigrationImports===void 0?[]:r.staleMigrationImports.map(o=>a(m,{cacheKey:`migration_stale_import:${o.file}:${o.line.toString()}:${o.moduleSpecifier}`,detail:`\`${o.file}\` (line ${o.line.toString()}) still imports \`${o.moduleSpecifier}\`. Finish the port — see \`${le[o.platform]}\` — or move the import out of \`lunora/\` if you genuinely still read from ${o.platform}.`,metadata:{file:o.file,line:o.line,moduleSpecifier:o.moduleSpecifier,platform:o.platform}})),source:"static",title:"Stale migration import"},pe=new Map([["global",{level:"WARN",scope:r=>`it reads the whole D1 table "${r}" over a cross-region round trip`}],["root",{level:"WARN",scope:r=>`it loads every row of "${r}" from the root Durable Object's SQLite into memory`}],["shardBy",{level:"INFO",scope:r=>`"${r}" is \`.shardBy()\`, so this collects one shard's rows rather than the whole table — bounded by a single tenant's row count`}]]),fe={level:"WARN",scope:r=>`it loads every row of "${r}"`},s={categories:["PERFORMANCE"],description:"A query calls `.collect()` with no `.withIndex()` and no `.filter()`, so it materializes every row of the table. Any live subscription over it also re-sends that whole result to every subscribed client on every write to the table.",facing:"EXTERNAL",level:"WARN",name:"unbounded_collect",remediation:'Narrow the read with `.withIndex("name", (q) => q.eq(...))`, cap it with `.take(n)`, or page it with `.paginate(args.paginationOpts)` so neither the scan nor the subscription payload grows with the table.',run:r=>{const o=[],i=Jo(r.schema);for(const e of r.queries??[]){if(e.terminal!=="collect"||e.hasIndex||e.table===""||e.hasFilter)continue;const t=i.get(e.table),{level:n,scope:l}=pe.get(t??"")??fe,p=Yo(e),f=n==="WARN"?` A live subscription over this query records a whole-table dependency, so every write to "${e.table}" re-runs it and re-sends the full result to each subscribed socket.`:" Cap it with `.take(n)` if that count can grow.";o.push(a(s,{cacheKey:`unbounded_collect:${e.file}:${e.line.toString()}:${e.table}`,detail:`Query on "${e.table}" at ${p} calls .collect() with no index and no filter — ${l(e.table)}.${f}`,level:n,metadata:{exportName:e.exportName,file:e.file,line:e.line,shardKind:t??"unknown",table:e.table}}))}return o},source:"static",title:"Unbounded collect"},ce=[ao,Po,Eo,me,ae,Lo,z,H,B,L,Y,G,Z,J,_,Zo,M,O,ee,re,q,Xo,se,Co,j,V,s,qo,ho,go,to,Oo,R,Wo,Ko,fo,co,D,E,xo,te,ie,Go,ko,oe,oo,zo,y,Ao,lo,Mo,F,Io,No,mo,A,g,b,vo,Bo,so,$,w,S,ne,po,C,To,K,P,I,x,W,N,T,k,no,$o,Fo,U,Vo,Qo,Ho,jo,eo,ro,Do,Q,X,v,io,Uo,_o,uo,So,bo,wo,yo,Ro,m],de=[u,h,d],ue=[...ce,...de],ft=(r,o={})=>{const i=o.lints??ue,e=[];for(const t of i)o.source!==void 0&&t.source!==o.source||e.push(...t.run(r));return c(e)};export{ue as ALL_LINTS,wt as MAP_VERSION,de as RUNTIME_LINTS,ce as STATIC_LINTS,g as actionFetchSsrf,b as actionWithoutErrorHandling,y as adminRouteWithoutGuard,w as aiRawRunEscapeHatch,S as aiRunWithoutLogging,v as aiToolSideEffectPromptInjection,A as aiUnboundedGenerationPublic,U as allowUnauthenticatedShardAccessEnabled,R as authApiCallWithoutHeaders,W as authCsrfCheckDisabled,T as authEmailVerificationDisabled,I as authScimWithoutTransactions,N as authSecureCookiesDisabled,k as authSessionFreshageZero,x as authTrustedOriginsWildcard,F as browserAllowPrivateTargets,C as browserUserUrlWithoutAllowlist,St as byCodepoint,O as circularFk,Ut as classifySensitivity,M as commitOrderedHardDelete,ut as compareToBaseline,$ as containerInstanceKeyFromUserInput,D as containerOversizedInstance,E as containerPublicInternet,P as containerRuntimeEgressRelaxation,K as containerStartEnableInternetOverride,c as dedupeCacheKeys,q as duplicateIndex,L as emptyIndex,G as errorWithoutCatalog,_ as exportSinkMisconfigured,z as externalSourceIncrementalNoDeletePath,H as externalSourceOnGlobal,B as externalSourceUnscoped,d as fanOutBreadth,j as filterOnPrimaryKey,V as filterWithoutIndex,Q as flagGatesSecurityWithUnsafeDefault,X as flagReadInSubscription,Wt as fromServerSchema,Z as geoIndexFieldNotGeopoint,J as geoIndexUnused,Y as globalTableNearColumnLimit,bt as gradeFromScore,oo as hardcodedSecret,u as hotShard,eo as httpActionMissingAuthGuard,ro as httpActionResponseHeaderInjection,to as hyperdriveOutsideAction,io as identityUndeclaredClaimTrusted,no as imagesUrlSourceFromUserInput,ao as indexReferencesUnknownField,h as indexUtilization,mo as insertManyUnsafeUserData,so as kvUnscopedUserKeyIdor,lo as mailInboundDispatchWithoutVerify,po as mailRecipientFromRequestInput,fo as maskUncoveredPiiColumn,co as maskWeakHashStrategyOnPii,uo as maskedRelationLeakViaWith,ho as mutatorFullRowReplace,go as nondeterministicQueryMutation,bo as normalizeIdUsedAsAuthorization,yo as notifyMissingPushConfig,wo as notifySendOutsideAction,So as outputProjectionMissingOnPublicRead,vo as ownerFieldFromArgsNotAuth,ht as parseAdvisorMap,Ao as paymentCreateWithoutAuthorize,Uo as paymentWebhookWideTolerance,Ro as plaintextSecretInWranglerVariables,Wo as policyReferencesUnknownTable,To as privilegedDispatchUnvalidatedPayload,Io as privilegedFanoutFromPublicProcedure,No as procedureWithoutStructuredEvent,ko as publicArgumentUsesAny,xo as publicMutationWithoutRatelimit,Fo as publicTableRlsOptoutConfusion,Co as queueWithoutDlq,Oo as r2sqlOutsideAction,Mo as ratelimitDefaultMemoryStore,$o as ratelimitKeySpoofableOrGlobal,Do as ratelimitMiddlewareFailOpen,Eo as relationReferencesUnknownField,Po as relationReferencesUnknownTable,Ko as rlsUncoveredTable,ft as runAdvisor,vt as scoreAdvisor,qo as shapeTargetsGlobalTable,Lo as shapeUnknownTable,Go as signupMutationWithoutDisposableGating,_o as softDeleteIncludeDeletedFromArgs,zo as sqlInjectionRisk,Ho as storageGenerateUploadUrlNoContentTypePin,Bo as storageKeyFromUserArgs,jo as storagePresignedUrlForPrivateContent,Vo as storageUploadWithoutContentTypeAllowlist,Qo as storageUploadWithoutMaxSize,Xo as tableWithoutInsert,Zo as ttlFieldNotTimestamp,oe as unboundedStringArgument,ee as unindexedForeignKey,re as unindexedRelationTarget,te as unrestrictedWhereBranch,ie as userCreatingMutationWithoutCaptcha,ne as vectorsNamespaceFromUserInput,ae as workflowDuplicateStepName,me as workflowUnknownTarget,se as workflowUnused};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{e as t}from"./finding-NrKO8idM.mjs";const n=new Set(["extend","lunora"]),s={categories:["SECURITY"],description:"`allowUnauthenticatedShardAccess: true` — set on the `lunora()` Vite plugin, or through `.extend(() => ({ … }))` — disables the fail-closed default that denies an unauthenticated shard lookup. Combined with a schema that doesn't enforce `.rls(\"required\")` everywhere (or that leaves a table `.public()`), an unauthenticated caller can shard-hop into another tenant's rows with no row-security guard behind the door.",facing:"EXTERNAL",level:"WARN",name:"allow_unauthenticated_shard_access_enabled",remediation:"Prefer `authorizeShard` / `authorizeFanOut` (which take precedence over `allowUnauthenticatedShardAccess` and let you allow specific unauthenticated cases) over the blanket opt-out. If the app genuinely needs open shard access, enforce `.rls(\"required\")` on the schema with no `.public()` tables so a missing identity still can't read another tenant's rows.",run:a=>a.configCalls===void 0?[]:a.schema.rlsMode!=="required"||a.schema.tables.some(e=>e.isPublic)?a.configCalls.filter(e=>n.has(e.callee)&&e.trueKeys.includes("allowUnauthenticatedShardAccess")).map(e=>t(s,{cacheKey:`allow_unauthenticated_shard_access_enabled:${e.file}:${e.line.toString()}`,detail:`\`${e.callee==="lunora"?"lunora(...)":".extend(...)"}\` in ${e.file}:${e.line.toString()} sets \`allowUnauthenticatedShardAccess: true\`, and the schema has an RLS gap (no \`.rls("required")\`, or a \`.public()\` table) — an unauthenticated caller can shard-hop into another tenant's rows with no row-security guard behind the door.`,metadata:{callee:e.callee,file:e.file,line:e.line}})):[],source:"static",title:"Unauthenticated shard access enabled on an RLS-gapped schema"};export{s as default};
|
package/dist/packem_shared/{filterOnPrimaryKey-COQRpmnu.mjs → filterOnPrimaryKey-jmz_ApDa.mjs}
RENAMED
|
@@ -1 +1 @@
|
|
|
1
|
-
import{e as r}from"./finding-NrKO8idM.mjs";import{q as a}from"./helpers-
|
|
1
|
+
import{e as r}from"./finding-NrKO8idM.mjs";import{q as a}from"./helpers-BCqZbKga.mjs";const l={categories:["PERFORMANCE"],description:"A query filters on `_id`, the primary key. That walks the table comparing every row's id, when `ctx.db.get(id)` fetches the row directly. There is no case where the scan is preferable.",facing:"EXTERNAL",level:"WARN",name:"filter_on_primary_key",remediation:'Replace `ctx.db.query("table").filter((d) => d._id === id).first()` with `ctx.db.get(id)`. Passing a typed `Id<"table">` also narrows the result to that table\'s `Doc`, where the scan form returns the shared row type.',run:t=>(t.queries??[]).filter(e=>e.filtersPrimaryKey===!0&&e.table!=="").map(e=>{const i=a(e);return r(l,{cacheKey:`filter_on_primary_key:${e.file}:${e.line.toString()}:${e.table}`,detail:`Query on "${e.table}" at ${i} filters on \`_id\` — it scans "${e.table}" to find a row \`ctx.db.get(id)\` addresses directly.`,metadata:{exportName:e.exportName,file:e.file,line:e.line,table:e.table}})}),source:"static",title:"Filter on primary key instead of ctx.db.get"};export{l as default};
|
package/dist/packem_shared/{filterWithoutIndex-Cf-tXY_A.mjs → filterWithoutIndex-9kAguEVb.mjs}
RENAMED
|
@@ -1 +1 @@
|
|
|
1
|
-
import{e as s}from"./finding-NrKO8idM.mjs";import{s as c,q as f}from"./helpers-
|
|
1
|
+
import{e as s}from"./finding-NrKO8idM.mjs";import{s as c,q as f}from"./helpers-BCqZbKga.mjs";const l={categories:["PERFORMANCE"],description:"A query calls `.filter()` without a `.withIndex()` / `.withSearchIndex()`, so it loads every row in the table and filters in memory — a full table scan that gets linearly slower as the table grows.",facing:"EXTERNAL",level:"WARN",name:"filter_without_index",remediation:'Narrow the read with `.withIndex("name", (q) => q.eq(...))` first, then `.filter()` only for what the index cannot express.',run:i=>{const t=[],d=c(i.schema);for(const e of i.queries??[]){if(!e.hasFilter||e.hasIndex||e.table===""||e.filtersPrimaryKey===!0)continue;const n=f(e),a=d.get(e.table),o={exportName:e.exportName,file:e.file,line:e.line,shardKind:a??"unknown",table:e.table},r=`filter_without_index:${e.file}:${e.line.toString()}:${e.table}`;if(a==="shardBy"){t.push(s(l,{cacheKey:r,detail:`Query on "${e.table}" at ${n} calls .filter() without an index. "${e.table}" is \`.shardBy()\`, so the read is already scoped to one shard rather than the whole table — this is bounded by one tenant's row count, not the dataset. Add an index if that per-shard count grows large.`,level:"INFO",metadata:o}));continue}const h=new Map([["global",`it scans the whole D1 table "${e.table}" — unbounded, and the cost is a cross-region round trip`],["root",`it loads every row of "${e.table}" from the root Durable Object's SQLite and filters in memory`]]).get(a??"")??`it loads every row of "${e.table}" and filters in memory`;t.push(s(l,{cacheKey:r,detail:`Query on "${e.table}" at ${n} calls .filter() without an index — ${h}.`,metadata:o}))}return t},source:"static",title:"Filter without index"};export{l as default};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{e as i}from"./finding-NrKO8idM.mjs";const n=new Set(["disallow","disallowed","enforce","enforced","enforcement","enforcing","gate","gated","gating","lockdown","rls"]),o=new Set(["allow","allowed","bypass","bypassed","permit","permitted"]),r=new Set(["disable","disabled","no","off","skip","skipped","without"]),f=/(?<=[A-Z])(?=[A-Z][a-z])/gu,d=/(?<=[a-z0-9])(?=[A-Z])/gu,u=/[^a-z0-9]+/u,c=t=>t.replaceAll(f," ").replaceAll(d," ").toLowerCase().split(u).filter(e=>e.length>0),g=t=>{const e=c(t),a=e.some(s=>n.has(s)),l=e.some(s=>o.has(s));return a===l?void 0:e.filter(s=>r.has(s)).length%2===1?!a:a},p={categories:["SECURITY"],description:"A `ctx.flags.boolean(key, default)` read on a security-shaped key has a fail-open default that selects the permissive branch. OpenFeature returns the default when the provider errors, so an outage silently disables a protection (a `false` default on an `enforce`/`rls`/`gate`/`lockdown` key) or grants a permission (a `true` default on an `allow`/`permit`/`bypass` key). A negating token in the key (`disableRls`, `rlsDisabled`, `skipEnforcement`) inverts which default is the safe one.",facing:"EXTERNAL",level:"WARN",name:"flag_gates_security_with_unsafe_default",remediation:"Flip the default so a provider outage fails closed — the finding's own detail names the value to write. The safe default is always the RESTRICTIVE branch, which is not a fixed value: a protection flag (`enforce*`/`rls*`/`gate*`/`lockdown*`/`disallow*`) defaults `true` and a permission/bypass flag (`allow*`/`permit*`/`bypass*`) defaults `false`, but a negated key (`disableRls`, `rlsDisabled`, `skipEnforcement`) inverts both. Never let an unreachable flag backend open access.",run:t=>t.flagSecurityDefaults===void 0?[]:t.flagSecurityDefaults.filter(e=>{const a=g(e.key);return a!==void 0&&a!==e.defaultValue}).map(e=>i(p,{cacheKey:`flag_gates_security_with_unsafe_default:${e.file}:${e.line.toString()}`,detail:`\`ctx.flags.boolean("${e.key}", ${String(e.defaultValue)})\` in \`${e.exportName}\` (${e.file}:${e.line.toString()}) fails open to the permissive branch — a provider outage returns \`${String(e.defaultValue)}\`, ${e.defaultValue?"granting the guarded permission":"disabling the guarded protection"}. Fail closed: default it to \`${String(!e.defaultValue)}\`.`,metadata:{defaultValue:e.defaultValue,exportName:e.exportName,file:e.file,key:e.key,line:e.line}})),source:"static",title:"Security flag fails open to the permissive branch"};export{p as default};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
const a=i=>({rlsMode:i.rlsMode,tables:Object.entries(i.tables).map(([d,n])=>{const o=[...n.indexes.map(e=>({fields:e.fields,kind:"index",name:e.name,unique:e.unique})),...n.searchIndexes.map(e=>({fields:[e.field,...e.filterFields??[]],kind:"search",name:e.name})),...n.rankIndexes.map(e=>({fields:[...e.sortBy.map(r=>r.field),...e.partitionBy??[]],kind:"rank",name:e.name})),...n.vectorIndexes.map(e=>({fields:[e.field],kind:"vector",name:e.name})),...n.geoIndexes.map(e=>({fields:[e.field],kind:"geo",name:e.name}))],s={};for(const[e,r]of Object.entries(n.shape))if(r.kind==="optional"){const t=r._meta?.inner;s[e]=t?.kind??r.kind}else s[e]=r.kind;return{externallyManaged:n.isExternallyManaged??!1,externalSource:n.externalSource?{hasReconcile:n.externalSource.reconcileEveryMs!==void 0,hasSoftDelete:n.externalSource.softDeleteColumn!==void 0,hasTenantBy:n.externalSource.tenantBy!==void 0,mode:n.externalSource.mode}:void 0,columnKinds:s,commitOrdered:n.commitOrderedMode,fields:Object.keys(n.shape),indexes:o,isPublic:n.isPublic??!1,name:d,shardKind:n.shardMode.kind,softDelete:n.softDeleteMode,ttl:n.ttlPolicy,relations:Object.entries(n.relationMap).map(([e,r])=>({field:r.field,kind:r.kind,name:e,onDelete:r.onDelete,references:r.references,table:r.table}))}})});export{a as fromServerSchema};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{e as d}from"./finding-NrKO8idM.mjs";import{c}from"./helpers-BCqZbKga.mjs";const s={categories:["SCHEMA"],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.",facing:"INTERNAL",level:"ERROR",name:"geo_index_field_not_geopoint",remediation:"Point the geo index at a `v.geoPoint()` column, or change the column's type to `v.geoPoint()`.",run:a=>{const t=[];for(const e of a.schema.tables)for(const n of e.indexes){if(n.kind!=="geo")continue;const o=n.fields[0];if(o===void 0)continue;const i=c(e,o);i===void 0||i==="geoPoint"||t.push(d(s,{cacheKey:`geo_index_field_not_geopoint:${e.name}:${n.name}:${o}`,detail:`Geo index "${n.name}" on table "${e.name}" indexes column "${o}", which is a ${i}, not a v.geoPoint().`,metadata:{field:o,index:n.name,kind:i,table:e.name}}))}return t},source:"static",title:"Geo index field is not a geoPoint"};export{s as default};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
const o=["address","birthdate","creditCard","dateOfBirth","dob","driversLicense","email","firstName","fullName","lastName","nationalId","passport","phone","phoneNumber","socialSecurity","socialSecurityNumber","ssn","taxId"],I=/[^a-z0-9]/giu,u=/([a-z0-9])([A-Z])/gu,S=/[^a-zA-Z0-9]+/u,n=e=>e.replaceAll(I,"").toLowerCase(),s=e=>e.replaceAll(u,"$1_$2").split(S).filter(t=>t.length>0).map(t=>t.toLowerCase()),h=new Set(o.map(e=>n(e))),N=new Set(o.filter(e=>s(e).length===1).map(e=>e.toLowerCase())),_=new Set(["accountId","authorId","createdBy","createdById","organizationId","orgId","ownerId","tenantId","updatedBy","userId","workspaceId"]),w=e=>h.has(n(e))||s(e).some(t=>N.has(t));new Set(o);const E=(e,t)=>{const a=s(e),r=n(e);return t.some(i=>{if(r===n(i))return!0;const c=s(i);return c.length>0&&a.some((A,l)=>c.every((d,m)=>a[l+m]===d))})},L=(e,t)=>e.columnKinds!==void 0&&Object.hasOwn(e.columnKinds,t)?e.columnKinds[t]:void 0,p=new Set(["_creationTime","_id"]),O=e=>e.fields.filter(t=>_.has(t)||w(t)),P=e=>new Set([...p,...e.fields]),f=e=>e.visibility==="public"&&(e.kind==="mutation"||e.kind==="action"),y=e=>e.line>0?`${e.file}:${e.line.toString()}`:e.file,C=e=>new Map(e.tables.map(t=>[t.name,t.shardKind]));export{f as a,L as c,w as i,E as m,O as o,y as q,C as s,P as t};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{e as i}from"./finding-NrKO8idM.mjs";import{t as r}from"./helpers-
|
|
1
|
+
import{e as i}from"./finding-NrKO8idM.mjs";import{t as r}from"./helpers-BCqZbKga.mjs";const c={categories:["SCHEMA"],description:"An index references a column that is not declared on its table. The index can never match, and the typo would otherwise surface only at runtime.",facing:"INTERNAL",level:"ERROR",name:"index_references_unknown_field",remediation:"Fix the column name in the index declaration, or add the column to the table.",run:a=>a.schema.tables.flatMap(e=>{const o=r(e);return e.indexes.flatMap(n=>n.fields.filter(t=>!o.has(t)).map(t=>i(c,{cacheKey:`index_references_unknown_field:${e.name}:${n.name}:${t}`,detail:`Index "${n.name}" on table "${e.name}" references column "${t}", which is not declared on the table.`,metadata:{field:t,index:n.name,indexKind:n.kind,table:e.name}})))}),source:"static",title:"Index references unknown field"};export{c as default};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{e as i}from"./finding-NrKO8idM.mjs";import{i as s}from"./helpers-
|
|
1
|
+
import{e as i}from"./finding-NrKO8idM.mjs";import{i as s}from"./helpers-BCqZbKga.mjs";const o={categories:["SECURITY"],description:'A `mask(policies)` column named like PII (email, SSN, phone, …) uses the `"hash"` strategy. `"hash"` is an unsalted 32-bit FNV-1a digest — brute-force-recoverable and cross-row-correlatable — not a confidentiality control, so it does not actually hide the value.',facing:"EXTERNAL",level:"WARN",name:"mask_weak_hash_strategy_on_pii",remediation:'Use `"redact"` instead of `"hash"` for this column — e.g. `mask({ <table>: { <column>: "redact" } })`. Reserve `"hash"` for columns where a stable, joinable pseudonym is the goal and the value itself is not sensitive.',run:a=>a.maskStrategies===void 0?[]:a.maskStrategies.filter(e=>e.strategy==="hash"&&s(e.column)).map(e=>i(o,{cacheKey:`mask_weak_hash_strategy_on_pii:${e.file}:${e.line.toString()}`,detail:`\`${e.exportName}\` in ${e.file} masks \`${e.table===""?e.column:`${e.table}.${e.column}`}\` with \`"hash"\` — its name suggests PII, and \`"hash"\` is brute-force-recoverable and leaks cross-row equality. Use \`"redact"\` instead.`,metadata:{column:e.column,exportName:e.exportName,file:e.file,table:e.table}})),source:"static",title:'Weak "hash" mask strategy on a PII column'};export{o as default};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{e as c}from"./finding-NrKO8idM.mjs";const d=t=>{const o=new Set;for(const a of t.maskProcedures??[])for(const{table:r}of a.maskColumns)r!==""&&o.add(r);for(const a of t.maskStrategies??[])a.table!==""&&o.add(a.table);return o},p=t=>{const o=new Map;for(const a of t.maskProcedures??[]){if(!a.usesMask)continue;const r=new Set(a.maskColumns.map(({table:n})=>n));o.set(`${a.file} ${a.exportName}`,r.size===0?"all":r)}return o},m=t=>{const o=new Map;for(const a of t.schema.tables)for(const r of a.relations)o.set(`${a.name} ${r.name}`,r.table);return o},u={categories:["SECURITY"],description:"A public read hydrates a masked table through a `with` relation, from a procedure whose own mask policy does not cover that table. Masking is per-procedure — the relation loader applies the READING procedure's policy to every hop — so a table masked only on its own reads comes back in the clear here.",facing:"EXTERNAL",level:"INFO",name:"masked_relation_leak_via_with",remediation:"Add the related table to this read's own `.use(mask({ … }))` policy — the relation loader applies it to every `with` hop, so one policy covers the parent and its children. Alternatively drop the relation or project only non-sensitive columns. A mask declared on the related table's own procedures does not carry over: masking is per-procedure.",run:t=>{if(t.relationLoads===void 0)return[];const o=d(t);if(o.size===0)return[];const a=m(t),r=p(t),n=[];for(const e of t.relationLoads){if(e.visibility!=="public"||e.parentTable==="")continue;const l=r.get(`${e.file} ${e.exportName}`);for(const i of e.relations){const s=a.get(`${e.parentTable} ${i}`);s===void 0||!o.has(s)||l==="all"||l?.has(s)===!0||n.push(c(u,{cacheKey:`masked_relation_leak_via_with:${e.file}:${e.line.toString()}:${e.parentTable}:${i}`,detail:`The public read in \`${e.exportName}\` (${e.file}:${e.line.toString()}) hydrates relation \`${i}\` (masked table \`${s}\`) via \`with\` on \`${e.parentTable}\`, and its own mask policy does not cover \`${s}\`. Masking is per-procedure, so \`${s}\`'s masked columns are returned in the clear here.`,metadata:{exportName:e.exportName,file:e.file,line:e.line,parentTable:e.parentTable,relation:i,relationTable:s}}))}}return n},source:"static",title:"Masked table surfaced unmasked through a with-relation on a public read"};export{u as default};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{e as n}from"./finding-NrKO8idM.mjs";const i={categories:["SCHEMA"],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.",facing:"EXTERNAL",level:"WARN",name:"notify_send_outside_action",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` from `@lunora/notify`, onto a `ctx.queues.*` producer) or schedule an action — the queue/scheduler runs the send exactly once, off the transactional path.",run:t=>t.notifyCalls===void 0?[]:t.notifyCalls.map(e=>n(i,{cacheKey:`notify_send_outside_action:${e.file}:${e.line.toString()}:${e.callee}`,detail:`\`${e.callee}(…)\` in ${e.exportName} (${e.file}:${e.line.toString()}) runs inside a ${e.kind} handler — a notification send is non-deterministic external I/O and a retried ${e.kind} would re-send it. Move it into an \`action\`, or enqueue/schedule the send.`,metadata:{callee:e.callee,exportName:e.exportName,file:e.file,kind:e.kind,line:e.line}})),source:"static",title:"Notification send used outside an action"};export{i as default};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{e as r}from"./finding-NrKO8idM.mjs";import{i as s}from"./helpers-
|
|
1
|
+
import{e as r}from"./finding-NrKO8idM.mjs";import{i as s}from"./helpers-BCqZbKga.mjs";const l={categories:["SECURITY"],description:"A `.public()` `query` returns raw table rows (no `.output(...)` projection, no `.use(mask(...))`) from a table carrying PII-named columns (`email`, `phone`, `ssn`, …). Every column ships to the caller, and a column added to the table later leaks by default.",facing:"EXTERNAL",level:"INFO",name:"output_projection_missing_on_public_read",remediation:"Add an explicit return projection to the public query — `.output(v.object({ … }).strip())` listing only the fields a client needs — or apply a `.use(mask(...))` policy to the PII columns. This makes the exposed shape intentional and stops a newly-added column from leaking through this query by default.",run:o=>{if(o.rawRowReturns===void 0)return[];const i=new Map;for(const e of o.schema.tables){const t=e.fields.filter(a=>s(a));t.length>0&&i.set(e.name,t)}const n=[];for(const e of o.rawRowReturns){if(e.visibility!=="public"||e.usesOutput||e.usesMask||e.table==="")continue;const t=i.get(e.table);t!==void 0&&n.push(r(l,{cacheKey:`output_projection_missing_on_public_read:${e.file}:${e.line.toString()}`,detail:`Public query \`${e.exportName}\` (${e.file}:${e.line.toString()}) returns raw \`${e.table}\` rows with no \`.output(...)\` projection — shipping PII column(s) ${t.join(", ")} to every caller, and any column added to \`${e.table}\` later leaks by default. Project the return with \`.output(v.object({ … }).strip())\` or mask the PII columns.`,metadata:{columns:t,exportName:e.exportName,file:e.file,line:e.line,table:e.table}}))}return n},source:"static",title:"Public query returns raw rows with PII and no output projection"};export{l as default};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{e as a}from"./finding-NrKO8idM.mjs";import{a as r,m as s}from"./helpers-BCqZbKga.mjs";const n=["contact","forgot","login","logIn","magic","otp","register","reset","signin","signIn","signup","signUp","subscribe","verify"],o={categories:["SECURITY"],description:"A public `mutation`/`action` has no `rateLimit` middleware. Publicly-callable writes are flood and brute-force targets — an attacker can exhaust writes, mail quota, or credits, or guess credentials on auth-shaped endpoints.",facing:"EXTERNAL",level:"WARN",name:"public_mutation_without_ratelimit",remediation:'Attach a rate limit: `.use(rateLimit(limiter, "<bucket>"))` from `@lunora/ratelimit`, or wrap the recommended public-procedure guards with `.use(protectPublic({ rateLimit, captcha }))` from `@lunora/server`. Genuinely-open writes can be acknowledged by adding a permissive limiter.',run:i=>i.procedureProtections===void 0?[]:i.procedureProtections.filter(t=>r(t)&&!t.usesRateLimit).map(t=>{const e=s(t.exportName,n);return a(o,{cacheKey:`public_mutation_without_ratelimit:${t.file}:${t.exportName}`,detail:`Public ${t.kind} \`${t.exportName}\` (${t.file}) has no rate limit${e?" — its name suggests an auth/abuse-sensitive endpoint, so this is high-risk":""}. Add \`.use(rateLimit(...))\` or \`.use(protectPublic({ rateLimit }))\`.`,metadata:{exportName:t.exportName,file:t.file,kind:t.kind,sensitive:e}})}),source:"static",title:"Public write without a rate limit"};export{o as default};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{e as i}from"./finding-NrKO8idM.mjs";import{o as a}from"./helpers-
|
|
1
|
+
import{e as i}from"./finding-NrKO8idM.mjs";import{o as a}from"./helpers-BCqZbKga.mjs";const n={categories:["SECURITY"],description:'`.public()` opts a table OUT of the schema\'s `.rls("required")` enforcement — it does not mean the table holds public data. A `.public()` table whose columns look ownership- or PII-shaped (`userId`, `email`, `ssn`, …) is exempt from the row-security guard, not "safe to expose".',facing:"EXTERNAL",level:"WARN",name:"public_table_rls_optout_confusion",remediation:'Double-check this table actually needs the RLS opt-out. If it does not, remove `.public()` so `.rls("required")` covers it like every other table. If it genuinely must stay open (a shared lookup table, for instance), rename or comment it so the exemption is intentional rather than a name-implies-safety mistake, and make sure no procedure trusts `.public()` as an authorization boundary.',run:o=>{if(o.schema.rlsMode!=="required")return[];const s=[];for(const e of o.schema.tables){if(!e.isPublic)continue;const t=a(e);t.length!==0&&s.push(i(n,{cacheKey:`public_table_rls_optout_confusion:${e.name}`,detail:`Table "${e.name}" is \`.public()\` (opted OUT of the schema's \`.rls("required")\` enforcement) but carries ownership/PII-shaped column(s): ${t.join(", ")}. \`.public()\` means "exempt from RLS", not "safe to expose" — confirm this table really needs the opt-out.`,metadata:{columns:t,table:e.name}}))}return s},source:"static",title:"Public table opts out of RLS but carries sensitive columns"};export{n as default};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{e as r}from"./finding-NrKO8idM.mjs";import{m as t}from"./helpers-BCqZbKga.mjs";const a=["signin","signIn","signup","signUp","login","logIn","register","reset","password","otp","verify","payment","checkout"],n=i=>t(i.exportName,a)||t(i.limitName,a),l={categories:["SECURITY"],description:"A rate-limit or Turnstile middleware guarding a sensitive auth/payment procedure is configured `failOpen: true`, so a limiter store outage or a failed Turnstile siteverify silently admits every request — turning a transient failure into an unthrottled brute-force / abuse window.",facing:"EXTERNAL",level:"WARN",name:"ratelimit_middleware_fail_open",remediation:"Drop `failOpen: true` (the default is fail-closed) on the rate-limit / Turnstile guard for auth and payment flows, or move the escape hatch to a non-sensitive procedure. Prefer rejecting traffic with a 503 over admitting unlimited unauthenticated attempts when the limiter or siteverify is unavailable.",run:i=>i.failOpenGuards===void 0?[]:i.failOpenGuards.filter(e=>e.failOpen&&n(e)).map(e=>r(l,{cacheKey:`ratelimit_middleware_fail_open:${e.file}:${e.line.toString()}`,detail:`\`${e.callee}(...)\` in \`${e.exportName}\` (${e.file}:${e.line.toString()}) sets \`failOpen: true\` while guarding a sensitive flow — a limiter/siteverify outage then admits every request instead of rejecting it.`,metadata:{callee:e.callee,exportName:e.exportName,file:e.file,limitName:e.limitName,line:e.line}})),source:"static",title:"Fail-open rate-limit / CAPTCHA guard on a sensitive procedure"};export{l as default};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{e as c}from"./finding-NrKO8idM.mjs";import{t as m}from"./helpers-
|
|
1
|
+
import{e as c}from"./finding-NrKO8idM.mjs";import{t as m}from"./helpers-BCqZbKga.mjs";const f=(o,i,t,a)=>{const e=o.get(a.table);if(!e)return[];const n=a.kind==="one"?t:e,s=a.kind==="one"?e:t,r=[];return i(n).has(a.field)||r.push({column:a.field,owner:n.name,side:"field"}),i(s).has(a.references)||r.push({column:a.references,owner:s.name,side:"references"}),r},d={categories:["SCHEMA"],description:"A relation references a foreign-key or referenced column that is not declared on its table, so the join can never resolve.",facing:"INTERNAL",level:"ERROR",name:"relation_references_unknown_field",remediation:"Fix the `field` / `references` column name in the relation, or add the missing column.",run:o=>{const i=new Map(o.schema.tables.map(e=>[e.name,e])),t=new Map,a=e=>{let n=t.get(e);return n||(n=m(e),t.set(e,n)),n};return o.schema.tables.flatMap(e=>e.relations.flatMap(n=>f(i,a,e,n).map(s=>c(d,{cacheKey:`relation_references_unknown_field:${e.name}:${n.name}:${s.side}`,detail:`Relation "${n.name}" on table "${e.name}" uses ${s.side} "${s.column}", which is not declared on table "${s.owner}".`,metadata:{column:s.column,owner:s.owner,relation:n.name,side:s.side,table:e.name}}))))},source:"static",title:"Relation references unknown field"};export{d as default};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{e as s}from"./finding-NrKO8idM.mjs";import{m as i}from"./procedure-protections-DRQotu9I.mjs";import{a as o}from"./helpers-
|
|
1
|
+
import{e as s}from"./finding-NrKO8idM.mjs";import{m as i}from"./procedure-protections-DRQotu9I.mjs";import{a as o}from"./helpers-BCqZbKga.mjs";const r={categories:["SECURITY"],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.",facing:"EXTERNAL",level:"WARN",name:"signup_mutation_without_disposable_gating",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(...)`.",run:a=>{if(a.procedureProtections===void 0)return[];const t=[];for(const e of a.procedureProtections)!o(e)||!i(e.writesUserTable)||e.usesEmailGate||i(e.hasEmailArg)&&t.push(s(r,{cacheKey:`signup_mutation_without_disposable_gating:${e.file}:${e.exportName}`,detail:`Public ${e.kind} \`${e.exportName}\` (${e.file}) writes a user/session/account table but has no disposable-email gate. Add \`.use(emailGateMiddleware(...))\` from \`@lunora/auth/email-guard\`.`,metadata:{exportName:e.exportName,file:e.file,kind:e.kind,writesUserTable:e.writesUserTable}}));return t},source:"static",title:"Account-creating write without a disposable-email gate"};export{r as default};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{e as n}from"./finding-NrKO8idM.mjs";const o={categories:["SECURITY"],description:"A `ctx.storage.generateUploadUrl(key, …)` call has no `contentType` pin, so the signed PUT URL it mints declares no expected content-type for the serving route to check — `upload()`'s `allowedContentTypes`/`maxSize` guards never run for a client-side PUT against this URL.",facing:"EXTERNAL",level:"WARN",name:"storage_generate_upload_url_no_content_type_pin",remediation:"Pass `contentType` to `ctx.storage.generateUploadUrl(key, { contentType })` so the signature is scoped to that content-type, and have your PUT route reject a request whose `Content-Type` differs from `verifySignedUrl`'s returned `contentType` — the pin is bound into the signature, but nothing in `@lunora/storage` compares it to the upload for you. If the size also needs bounding, prefer `upload()`/`store()` (with `allowedContentTypes`/`maxSize`) over a client-side signed PUT.",run:t=>t.storageUploads===void 0?[]:t.storageUploads.filter(e=>e.method==="generateUploadUrl"&&e.analyzable&&!e.presentKeys.includes("contentType")).map(e=>n(o,{cacheKey:`storage_generate_upload_url_no_content_type_pin:${e.file}:${e.line.toString()}`,detail:`\`ctx.storage.generateUploadUrl\` in \`${e.exportName}\` (${e.file}:${e.line.toString()}) has no \`contentType\` pin — the signed PUT it mints declares no expected type for the serving route to check, and bypasses \`upload()\`'s \`allowedContentTypes\`/\`maxSize\` guards.`,metadata:{exportName:e.exportName,file:e.file,line:e.line}})),source:"static",title:"generateUploadUrl signed PUT with no content-type pin"};export{o as default};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{e as l}from"./finding-NrKO8idM.mjs";const g=new Set(["getPresignedUrl","getSignedUrl"]),d=8640*60,a=t=>t.method==="getPresignedUrl",s=t=>g.has(t.method)&&t.expiresInSeconds!==void 0&&t.expiresInSeconds>=d,c={categories:["SECURITY"],description:"`ctx.storage.getPresignedUrl(...)` mints a native S3 SigV4 URL that resolves directly against R2, so the request never reaches the Worker and cannot be gated at all — the wrong choice for private or policy-gated content. Either signer minting an `expiresInSeconds` near the shared 7-day ceiling also hands out a long-lived bearer credential.",facing:"EXTERNAL",level:"WARN",name:"storage_presigned_url_for_private_content",remediation:"For private/per-user/policy-gated content, use `getSignedUrl` instead of `getPresignedUrl`: it mints the URL at your own `publicBaseUrl` origin, so the request reaches a route you control. You still have to write that route — validate the URL with `verifySignedUrl` and run your auth/policy/rate-limit checks there; the framework ships no serving handler. On either signer, keep `expiresInSeconds` as short as the use case allows — well under the 7-day ceiling — rather than minting a near-maximum-lifetime URL.",run:t=>t.storageUploads===void 0?[]:t.storageUploads.filter(e=>a(e)||s(e)).map(e=>{const r=a(e),o=s(e),i=`\`${e.exportName}\` (${e.file}:${e.line.toString()})`;let n;return r&&o?n=`\`ctx.storage.getPresignedUrl\` in ${i} mints a native S3 SigV4 URL — resolves straight off R2, so the request never reaches the Worker to be gated — with an \`expiresInSeconds\` of ${(e.expiresInSeconds??0).toString()}s, near the 7-day signing ceiling.`:r?n=`\`ctx.storage.getPresignedUrl\` in ${i} mints a native S3 SigV4 URL that resolves directly against R2, so the request never reaches the Worker and cannot be gated. Use \`getSignedUrl\` and serve it from a route that calls \`verifySignedUrl\` and runs your checks.`:n=`\`ctx.storage.${e.method}\` in ${i} requests an \`expiresInSeconds\` of ${(e.expiresInSeconds??0).toString()}s, near the 7-day signing ceiling — a leaked link stays valid for almost a week.`,l(c,{cacheKey:`storage_presigned_url_for_private_content:${e.file}:${e.line.toString()}`,detail:n,metadata:{expiresInSeconds:e.expiresInSeconds,exportName:e.exportName,file:e.file,line:e.line,method:e.method}})}),source:"static",title:"Native presigned URL or near-max-TTL signed URL for private content"};export{c as default};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{e as o}from"./finding-NrKO8idM.mjs";import{c as l}from"./helpers-BCqZbKga.mjs";const m=new Set(["date","number","timestamp"]),s={categories:["SCHEMA"],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.",facing:"INTERNAL",level:"ERROR",name:"ttl_field_not_timestamp",remediation:"Point `.ttl(field)` at a `v.timestamp()` / `v.date()` / `v.number()` (epoch-ms) column, or add such a column.",run:n=>{const a=[];for(const e of n.schema.tables){const{ttl:t}=e;if(!t)continue;const i=l(e,t.field);i===void 0||m.has(i)||a.push(o(s,{cacheKey:`ttl_field_not_timestamp:${e.name}:${t.field}`,detail:`Table "${e.name}" declares \`.ttl("${t.field}")\`, but "${t.field}" is a ${i} column, not an epoch-millisecond timestamp.`,metadata:{field:t.field,kind:i,table:e.name}}))}return a},source:"static",title:"TTL field is not a timestamp"};export{s as default};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{e as n}from"./finding-NrKO8idM.mjs";const i={categories:["SECURITY"],description:"A public `v.string()` argument has no maximum-length bound. An unbounded string lets a client submit arbitrarily large input — abusing storage and CPU on every request that processes it.",facing:"EXTERNAL",level:"INFO",name:"unbounded_string_arg",remediation:"Add an enforced max-length bound with `.max(n)` on the string validator (e.g. cap a name at 256, a body at a few KB). `.meta({ maxLength })` only documents a cap — the parser does not enforce it. Size the cap to the field's real-world maximum.",run:e=>e.argValidators===void 0?[]:e.argValidators.flatMap(a=>a.unboundedStringArgs.map(t=>n(i,{cacheKey:`unbounded_string_arg:${a.file}:${a.exportName}:${t}`,detail:`Arg \`${t}\` of public procedure \`${a.exportName}\` (${a.file}:${a.line.toString()}) is an unbounded \`v.string()\`. Add a max-length bound to cap payload size.`,metadata:{argument:t,exportName:a.exportName,file:a.file,line:a.line}}))),source:"static",title:"Public string argument has no length bound"};export{i as default};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{e as i}from"./finding-NrKO8idM.mjs";import{m as a}from"./procedure-protections-DRQotu9I.mjs";import{a as s}from"./helpers-BCqZbKga.mjs";const r=t=>t.writesUserTable===!0?"writes a user/session table":t.callsMail===!0?"sends mail":t.analyzableBody===!1?"may write a user/session table or send mail — its handler body could not be read":"may write a user/session table or send mail — at least one could not be determined",n={categories:["SECURITY"],description:"A public `mutation`/`action` that creates a user/session or sends mail has no CAPTCHA / bot check. Account-creating and mail-sending endpoints are prime automated-abuse targets (credential stuffing, mailbox flooding, disposable-account farming).",facing:"EXTERNAL",level:"WARN",name:"user_creating_mutation_without_captcha",remediation:"Add a server-verified human check: `.use(verifyTurnstileMiddleware({ secret, token: (c) => c.args.captchaToken }))` from `@lunora/auth`, or wrap it with `.use(protectPublic({ rateLimit, captcha }))` from `@lunora/server`. Pair with a rate limit for defense in depth. Note it is `verifyTurnstileMiddleware`, not the bare `verifyTurnstile` verdict function — that one returns a Promise and verifies nothing when placed in a `.use()` chain.",run:t=>t.procedureProtections===void 0?[]:t.procedureProtections.filter(e=>s(e)&&(a(e.writesUserTable)||a(e.callsMail))&&!e.usesCaptcha).map(e=>i(n,{cacheKey:`user_creating_mutation_without_captcha:${e.file}:${e.exportName}`,detail:`Public ${e.kind} \`${e.exportName}\` (${e.file}) ${r(e)} but has no CAPTCHA check. Add \`.use(verifyTurnstileMiddleware(...))\` or \`.use(protectPublic({ captcha }))\`.`,metadata:{callsMail:e.callsMail,exportName:e.exportName,file:e.file,kind:e.kind,writesUserTable:e.writesUserTable}})),source:"static",title:"Account-creating / mail-sending write without a CAPTCHA"};export{n 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.107",
|
|
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.30",
|
|
50
|
+
"@lunora/server": "1.0.0-alpha.101"
|
|
51
51
|
},
|
|
52
52
|
"engines": {
|
|
53
53
|
"node": "^22.15.0 || >=24.11.0"
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
import{e as t}from"./finding-NrKO8idM.mjs";const n={categories:["SECURITY"],description:"`.extend(() => ({ allowUnauthenticatedShardAccess: true }))` disables the fail-closed default that denies an unauthenticated shard lookup. Combined with a schema that doesn't enforce `.rls(\"required\")` everywhere (or that leaves a table `.public()`), an unauthenticated caller can shard-hop into another tenant's rows with no row-security guard behind the door.",facing:"EXTERNAL",level:"WARN",name:"allow_unauthenticated_shard_access_enabled",remediation:"Prefer `authorizeShard` / `authorizeFanOut` (which take precedence over `allowUnauthenticatedShardAccess` and let you allow specific unauthenticated cases) over the blanket opt-out. If the app genuinely needs open shard access, enforce `.rls(\"required\")` on the schema with no `.public()` tables so a missing identity still can't read another tenant's rows.",run:a=>a.configCalls===void 0?[]:a.schema.rlsMode!=="required"||a.schema.tables.some(e=>e.isPublic)?a.configCalls.filter(e=>e.callee==="extend"&&e.trueKeys.includes("allowUnauthenticatedShardAccess")).map(e=>t(n,{cacheKey:`allow_unauthenticated_shard_access_enabled:${e.file}:${e.line.toString()}`,detail:`\`.extend(...)\` in ${e.file}:${e.line.toString()} sets \`allowUnauthenticatedShardAccess: true\`, and the schema has an RLS gap (no \`.rls("required")\`, or a \`.public()\` table) — an unauthenticated caller can shard-hop into another tenant's rows with no row-security guard behind the door.`,metadata:{callee:e.callee,file:e.file,line:e.line}})):[],source:"static",title:"Unauthenticated shard access enabled on an RLS-gapped schema"};export{n as default};
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
import{e as f}from"./finding-NrKO8idM.mjs";const d=5,h=o=>typeof o._id=="string"?o._id:"?",g=(o,e,t)=>o?` (sample capped at ${e.toString()} rows — ${t})`:"",b=(o,e)=>{const t=o.slice(0,d),i=e-t.length,n=t.map(s=>`"${s}"`).join(", ");return i>0?`${n} (+${i.toString()} more)`:n},$=(o,e)=>{if(o==="_id")return e.existingIds;const t=new Set;for(const i of e.rows){const n=i[o];n!=null&&t.add(String(n))}return t},k=(o,e,t,i)=>{if(t.kind!=="one"||i.truncated)return;const n=t.references,s=$(n,i),r=t.field,a=[];for(const m of e.rows){const u=m[r];if(u==null)continue;const p=String(u);s.has(p)||a.push(h(m))}if(a.length===0)return;const c=e.truncated,{cap:l}=e,v=n==="_id"?`"${t.table}"`:`"${t.table}"."${n}"`,w=`Table "${e.table}": ${a.length.toString()} sampled row(s) have a dangling FK value in column "${r}" — the referenced ${v} row does not exist. Row ids: ${b(a,a.length)}${g(c,l,"more rows may exist beyond the window")}.`;return f(o,{cacheKey:`constraint_validator:fk:${e.table}:${r}`,detail:w,metadata:{cap:l,column:r,count:a.length,examples:a.slice(0,d),kind:"fk",references:n,referencesTable:t.table,table:e.table,truncated:c}})},S=(o,e,t,i)=>{if(!i)return;const n=[];for(const r of e.rows){const a=r[t];a==null&&n.push(h(r))}if(n.length===0)return;const s=`Table "${e.table}": ${n.length.toString()} sampled row(s) have a null/missing value in column "${t}", which is declared as non-optional. Row ids: ${b(n,n.length)}${g(e.truncated,e.cap,"more rows may exist beyond the window")}.`;return f(o,{cacheKey:`constraint_validator:null:${e.table}:${t}`,detail:s,metadata:{cap:e.cap,column:t,count:n.length,examples:n.slice(0,d),kind:"null",table:e.table,truncated:e.truncated}})},y=(o,e)=>{const t=[];for(const i of e){const n=o[i];if(n==null)return;t.push(JSON.stringify(n))}return t.join("|")},x=(o,e,t)=>{t.includes(e)||t.push(e),t.push(o)},T=(o,e,t)=>{if(t.kind!=="index"||t.unique!==!0)return;const i=new Map,n=[];for(const r of e.rows){const a=h(r),c=y(r,t.fields);if(c===void 0)continue;const l=i.get(c);l===void 0?i.set(c,a):x(a,l,n)}if(n.length===0)return;const s=`Table "${e.table}": ${n.length.toString()} sampled row(s) share duplicate values on unique index "${t.name}" (${t.fields.join(", ")}). Row ids: ${b(n,n.length)}${g(e.truncated,e.cap,"duplicates beyond the window may exist")}.`;return f(o,{cacheKey:`constraint_validator:unique:${e.table}:${t.name}`,detail:s,metadata:{cap:e.cap,count:n.length,examples:n.slice(0,d),fields:t.fields,index:t.name,kind:"unique",table:e.table,truncated:e.truncated}})},_=(o,e,t,i)=>{const n=i.get(e.table);if(!n)return[];const s=[];for(const r of n.relations){const a=t.get(r.table);if(!a)continue;const c=k(o,e,r,a);c&&s.push(c)}for(const r of n.fields){const a=!n.optionalFields?.has(r),c=S(o,e,r,a);c&&s.push(c)}for(const r of n.indexes){const a=T(o,e,r);a&&s.push(a)}return s},R={categories:["SCHEMA"],description:"Sampled rows violate one or more declared constraints: a foreign-key column references a non-existent row, a non-optional column contains null, or a unique-indexed column has duplicate values. These violations indicate data inserted before a constraint was enforced (e.g. a schema migration, a raw import, or a bug in a past data-migration transform).",facing:"INTERNAL",level:"WARN",name:"constraint_validator",remediation:"Inspect the listed rows and correct or remove the violating values. Run a data migration to backfill nulls, dedup unique violations, or re-link dangling FK references. For FK violations, confirm the target table rows exist before fixing the referencing rows.",run:o=>{if(!o.tableSamples||o.tableSamples.length===0)return[];const e=new Map;for(const i of o.tableSamples)e.set(i.table,i);const t=new Map(o.schema.tables.map(i=>[i.name,i]));return o.tableSamples.flatMap(i=>_(R,i,e,t))},source:"runtime",title:"Constraint violation"};export{R as default};
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
import{e as n}from"./finding-NrKO8idM.mjs";const i=new Set(["enforce","gate","lockdown","rls"]),r=new Set(["allow","bypass","permit"]),o=/(?<=[A-Z])(?=[A-Z][a-z])/gu,f=/(?<=[a-z0-9])(?=[A-Z])/gu,u=/[^a-z0-9]+/u,c=a=>a.replaceAll(o," ").replaceAll(f," ").toLowerCase().split(u).filter(e=>e.length>0),d=a=>{const e=c(a.key),l=e.some(t=>i.has(t)),s=e.some(t=>r.has(t));return l===s?!1:l?!a.defaultValue:a.defaultValue},p={categories:["SECURITY"],description:"A `ctx.flags.boolean(key, default)` read on a security-shaped key has a fail-open default that selects the permissive branch. OpenFeature returns the default when the provider errors, so an outage silently disables a protection (a `false` default on an `enforce`/`rls`/`gate`/`lockdown` key) or grants a permission (a `true` default on an `allow`/`permit`/`bypass` key).",facing:"EXTERNAL",level:"WARN",name:"flag_gates_security_with_unsafe_default",remediation:"Flip the default so a provider outage fails closed: default a protection flag (`enforce*`/`rls*`/`gate*`/`lockdown*`) to `true`, and a permission/bypass flag (`allow*`/`permit*`/`bypass*`) to `false`. The safe default is always the restrictive branch — never let an unreachable flag backend open access.",run:a=>a.flagSecurityDefaults===void 0?[]:a.flagSecurityDefaults.filter(e=>d(e)).map(e=>n(p,{cacheKey:`flag_gates_security_with_unsafe_default:${e.file}:${e.line.toString()}`,detail:`\`ctx.flags.boolean("${e.key}", ${String(e.defaultValue)})\` in \`${e.exportName}\` (${e.file}:${e.line.toString()}) fails open to the permissive branch — a provider outage returns \`${String(e.defaultValue)}\`, ${e.defaultValue?"granting the guarded permission":"disabling the guarded protection"}.`,metadata:{defaultValue:e.defaultValue,exportName:e.exportName,file:e.file,key:e.key,line:e.line}})),source:"static",title:"Security flag fails open to the permissive branch"};export{p as default};
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
const l=d=>({rlsMode:d.rlsMode,tables:Object.entries(d.tables).map(([t,n])=>{const a=[...n.indexes.map(e=>({fields:e.fields,kind:"index",name:e.name,unique:e.unique})),...n.searchIndexes.map(e=>({fields:[e.field,...e.filterFields??[]],kind:"search",name:e.name})),...n.rankIndexes.map(e=>({fields:[...e.sortBy.map(r=>r.field),...e.partitionBy??[]],kind:"rank",name:e.name})),...n.vectorIndexes.map(e=>({fields:[e.field],kind:"vector",name:e.name})),...n.geoIndexes.map(e=>({fields:[e.field],kind:"geo",name:e.name}))],o={},s=new Set;for(const[e,r]of Object.entries(n.shape))if(r.kind==="optional"){const i=r._meta?.inner;o[e]=i?.kind??r.kind,s.add(e)}else o[e]=r.kind,r._meta?.column?.notNull===!1&&s.add(e);return{externallyManaged:n.isExternallyManaged??!1,externalSource:n.externalSource?{hasReconcile:n.externalSource.reconcileEveryMs!==void 0,hasSoftDelete:n.externalSource.softDeleteColumn!==void 0,hasTenantBy:n.externalSource.tenantBy!==void 0,mode:n.externalSource.mode}:void 0,columnKinds:o,commitOrdered:n.commitOrderedMode,fields:Object.keys(n.shape),indexes:a,isPublic:n.isPublic??!1,name:t,optionalFields:s,shardKind:n.shardMode.kind,softDelete:n.softDeleteMode,ttl:n.ttlPolicy,relations:Object.entries(n.relationMap).map(([e,r])=>({field:r.field,kind:r.kind,name:e,onDelete:r.onDelete,references:r.references,table:r.table}))}})});export{l as fromServerSchema};
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
import{e as d}from"./finding-NrKO8idM.mjs";const c={categories:["SCHEMA"],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.",facing:"INTERNAL",level:"ERROR",name:"geo_index_field_not_geopoint",remediation:"Point the geo index at a `v.geoPoint()` column, or change the column's type to `v.geoPoint()`.",run:a=>{const t=[];for(const e of a.schema.tables)for(const n of e.indexes){if(n.kind!=="geo")continue;const o=n.fields[0];if(o===void 0)continue;const i=e.columnKinds?.[o];i===void 0||i==="geoPoint"||t.push(d(c,{cacheKey:`geo_index_field_not_geopoint:${e.name}:${n.name}:${o}`,detail:`Geo index "${n.name}" on table "${e.name}" indexes column "${o}", which is a ${i}, not a v.geoPoint().`,metadata:{field:o,index:n.name,kind:i,table:e.name}}))}return t},source:"static",title:"Geo index field is not a geoPoint"};export{c as default};
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
const s=["address","birthdate","creditCard","dateOfBirth","dob","driversLicense","email","firstName","fullName","lastName","nationalId","passport","phone","phoneNumber","socialSecurity","socialSecurityNumber","ssn","taxId"],i=/[^a-z0-9]/giu,o=/([a-z0-9])([A-Z])/gu,l=/[^a-zA-Z0-9]+/u,a=e=>e.replaceAll(i,"").toLowerCase(),n=e=>e.replaceAll(o,"$1_$2").split(l).filter(t=>t.length>0).map(t=>t.toLowerCase()),r=new Set(s.map(e=>a(e))),c=new Set(s.filter(e=>n(e).length===1).map(e=>e.toLowerCase())),d=new Set(["accountId","authorId","createdBy","createdById","organizationId","orgId","ownerId","tenantId","updatedBy","userId","workspaceId"]),I=e=>r.has(a(e))||n(e).some(t=>c.has(t));new Set(s);const S=new Set(["_creationTime","_id"]),m=e=>e.fields.filter(t=>d.has(t)||I(t)),u=e=>new Set([...S,...e.fields]),p=e=>e.visibility==="public"&&(e.kind==="mutation"||e.kind==="action"),N=e=>e.line>0?`${e.file}:${e.line.toString()}`:e.file,_=e=>new Map(e.tables.map(t=>[t.name,t.shardKind]));export{p as a,I as i,m as o,N as q,_ as s,u as t};
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
import{e as s}from"./finding-NrKO8idM.mjs";const l=a=>{const t=new Set;for(const o of a.maskProcedures??[])for(const{table:r}of o.maskColumns)r!==""&&t.add(r);for(const o of a.maskStrategies??[])o.table!==""&&t.add(o.table);return t},d=a=>{const t=new Map;for(const o of a.schema.tables)for(const r of o.relations)t.set(`${o.name} ${r.name}`,r.table);return t},c={categories:["SECURITY"],description:"A public read hydrates a masked table through a `with` relation. Column masking does not descend into `with`-hydrated relations, so the related table's masked columns are returned in the clear.",facing:"EXTERNAL",level:"INFO",name:"masked_relation_leak_via_with",remediation:"Don't hydrate a masked table through `with` on an unprotected read: drop the relation, project only non-sensitive columns, or route the parent read through a procedure that re-applies masking to the joined table. Masking is per-procedure and top-level only — it never reaches `with`-loaded relations.",run:a=>{if(a.relationLoads===void 0)return[];const t=l(a);if(t.size===0)return[];const o=d(a),r=[];for(const e of a.relationLoads)if(!(e.visibility!=="public"||e.parentTable===""))for(const i of e.relations){const n=o.get(`${e.parentTable} ${i}`);n===void 0||!t.has(n)||r.push(s(c,{cacheKey:`masked_relation_leak_via_with:${e.file}:${e.line.toString()}:${e.parentTable}:${i}`,detail:`The public read in \`${e.exportName}\` (${e.file}:${e.line.toString()}) hydrates relation \`${i}\` (masked table \`${n}\`) via \`with\` on \`${e.parentTable}\`. Masking is top-level only and does not descend into \`with\`, so \`${n}\`'s masked columns are returned in the clear.`,metadata:{exportName:e.exportName,file:e.file,line:e.line,parentTable:e.parentTable,relation:i,relationTable:n}}))}return r},source:"static",title:"Masked table surfaced unmasked through a with-relation on a public read"};export{c as default};
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
import{e as n}from"./finding-NrKO8idM.mjs";const i={categories:["SCHEMA"],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.",facing:"EXTERNAL",level:"WARN",name:"notify_send_outside_action",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.",run:t=>t.notifyCalls===void 0?[]:t.notifyCalls.map(e=>n(i,{cacheKey:`notify_send_outside_action:${e.file}:${e.line.toString()}:${e.callee}`,detail:`\`${e.callee}(…)\` in ${e.exportName} (${e.file}:${e.line.toString()}) runs inside a ${e.kind} handler — a notification send is non-deterministic external I/O and a retried ${e.kind} would re-send it. Move it into an \`action\`, or enqueue/schedule the send.`,metadata:{callee:e.callee,exportName:e.exportName,file:e.file,kind:e.kind,line:e.line}})),source:"static",title:"Notification send used outside an action"};export{i as default};
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
import{e as a}from"./finding-NrKO8idM.mjs";import{a as r}from"./helpers-CwSEZdku.mjs";const s=/contact|forgot|login|magic|otp|register|reset|signin|signup|subscribe|verify/iu,o={categories:["SECURITY"],description:"A public `mutation`/`action` has no `rateLimit` middleware. Publicly-callable writes are flood and brute-force targets — an attacker can exhaust writes, mail quota, or credits, or guess credentials on auth-shaped endpoints.",facing:"EXTERNAL",level:"WARN",name:"public_mutation_without_ratelimit",remediation:'Attach a rate limit: `.use(rateLimit(limiter, "<bucket>"))` from `@lunora/ratelimit`, or wrap the recommended public-procedure guards with `.use(protectPublic({ rateLimit, captcha }))` from `@lunora/server`. Genuinely-open writes can be acknowledged by adding a permissive limiter.',run:i=>i.procedureProtections===void 0?[]:i.procedureProtections.filter(t=>r(t)&&!t.usesRateLimit).map(t=>{const e=s.test(t.exportName);return a(o,{cacheKey:`public_mutation_without_ratelimit:${t.file}:${t.exportName}`,detail:`Public ${t.kind} \`${t.exportName}\` (${t.file}) has no rate limit${e?" — its name suggests an auth/abuse-sensitive endpoint, so this is high-risk":""}. Add \`.use(rateLimit(...))\` or \`.use(protectPublic({ rateLimit }))\`.`,metadata:{exportName:t.exportName,file:t.file,kind:t.kind,sensitive:e}})}),source:"static",title:"Public write without a rate limit"};export{o as default};
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
import{e as n}from"./finding-NrKO8idM.mjs";const r=["signin","signup","login","register","reset","password","otp","verify","payment","checkout"],l=i=>i.toLowerCase().replaceAll(/[^a-z0-9]+/gu,""),t=i=>{const e=l(i);return r.some(a=>e.includes(a))},s=i=>t(i.exportName)||t(i.limitName),o={categories:["SECURITY"],description:"A rate-limit or Turnstile middleware guarding a sensitive auth/payment procedure is configured `failOpen: true`, so a limiter store outage or a failed Turnstile siteverify silently admits every request — turning a transient failure into an unthrottled brute-force / abuse window.",facing:"EXTERNAL",level:"WARN",name:"ratelimit_middleware_fail_open",remediation:"Drop `failOpen: true` (the default is fail-closed) on the rate-limit / Turnstile guard for auth and payment flows, or move the escape hatch to a non-sensitive procedure. Prefer rejecting traffic with a 503 over admitting unlimited unauthenticated attempts when the limiter or siteverify is unavailable.",run:i=>i.failOpenGuards===void 0?[]:i.failOpenGuards.filter(e=>e.failOpen&&s(e)).map(e=>n(o,{cacheKey:`ratelimit_middleware_fail_open:${e.file}:${e.line.toString()}`,detail:`\`${e.callee}(...)\` in \`${e.exportName}\` (${e.file}:${e.line.toString()}) sets \`failOpen: true\` while guarding a sensitive flow — a limiter/siteverify outage then admits every request instead of rejecting it.`,metadata:{callee:e.callee,exportName:e.exportName,file:e.file,limitName:e.limitName,line:e.line}})),source:"static",title:"Fail-open rate-limit / CAPTCHA guard on a sensitive procedure"};export{o as default};
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
import{e as n}from"./finding-NrKO8idM.mjs";const a={categories:["SECURITY"],description:"A `ctx.storage.generateUploadUrl(key, …)` call has no `contentType` pin, so the signed PUT URL it mints authorizes any content-type/size the client chooses — `upload()`'s `allowedContentTypes`/`maxSize` guards never run for a client-side PUT against this URL.",facing:"EXTERNAL",level:"WARN",name:"storage_generate_upload_url_no_content_type_pin",remediation:"Pass `contentType` to `ctx.storage.generateUploadUrl(key, { contentType })` so the signature only authorizes a PUT with that exact content-type. If the client's upload size/type also needs bounding, prefer `upload()`/`store()` (with `allowedContentTypes`/`maxSize`) over a client-side signed PUT.",run:t=>t.storageUploads===void 0?[]:t.storageUploads.filter(e=>e.method==="generateUploadUrl"&&e.analyzable&&!e.presentKeys.includes("contentType")).map(e=>n(a,{cacheKey:`storage_generate_upload_url_no_content_type_pin:${e.file}:${e.line.toString()}`,detail:`\`ctx.storage.generateUploadUrl\` in \`${e.exportName}\` (${e.file}:${e.line.toString()}) has no \`contentType\` pin — the signed PUT it mints accepts any type/size, minted client-side and bypassing \`upload()\`'s guards.`,metadata:{exportName:e.exportName,file:e.file,line:e.line}})),source:"static",title:"generateUploadUrl signed PUT with no content-type pin"};export{a as default};
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
import{e as l}from"./finding-NrKO8idM.mjs";const g=new Set(["getPresignedUrl","getSignedUrl"]),d=8640*60,s=t=>t.method==="getPresignedUrl",a=t=>g.has(t.method)&&t.expiresInSeconds!==void 0&&t.expiresInSeconds>=d,c={categories:["SECURITY"],description:"`ctx.storage.getPresignedUrl(...)` mints a native S3 SigV4 URL that resolves directly against R2, bypassing the Worker's auth/RLS/rate-limit entirely — the wrong choice for private or policy-gated content. Either signer minting an `expiresInSeconds` near the shared 7-day ceiling also hands out a long-lived bearer credential.",facing:"EXTERNAL",level:"WARN",name:"storage_presigned_url_for_private_content",remediation:"For private/per-user/policy-gated content, use `getSignedUrl` (resolves back through the Worker, so your auth/RLS/rate-limit gates still run) instead of `getPresignedUrl`. Reserve `getPresignedUrl` for genuinely public or bulk content. On either signer, keep `expiresInSeconds` as short as the use case allows — well under the 7-day ceiling — rather than minting a near-maximum-lifetime URL.",run:t=>t.storageUploads===void 0?[]:t.storageUploads.filter(e=>s(e)||a(e)).map(e=>{const r=s(e),o=a(e),n=`\`${e.exportName}\` (${e.file}:${e.line.toString()})`;let i;return r&&o?i=`\`ctx.storage.getPresignedUrl\` in ${n} mints a native S3 SigV4 URL — bypasses the Worker's auth/RLS/rate-limit entirely — with an \`expiresInSeconds\` of ${(e.expiresInSeconds??0).toString()}s, near the 7-day signing ceiling.`:r?i=`\`ctx.storage.getPresignedUrl\` in ${n} mints a native S3 SigV4 URL that resolves directly against R2, bypassing the Worker's auth/RLS/rate-limit entirely. Use \`getSignedUrl\` for app-gated access to private content.`:i=`\`ctx.storage.${e.method}\` in ${n} requests an \`expiresInSeconds\` of ${(e.expiresInSeconds??0).toString()}s, near the 7-day signing ceiling — a leaked link stays valid for almost a week.`,l(c,{cacheKey:`storage_presigned_url_for_private_content:${e.file}:${e.line.toString()}`,detail:i,metadata:{expiresInSeconds:e.expiresInSeconds,exportName:e.exportName,file:e.file,line:e.line,method:e.method}})}),source:"static",title:"Native presigned URL or near-max-TTL signed URL for private content"};export{c as default};
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
import{e as o}from"./finding-NrKO8idM.mjs";const l=new Set(["date","number","timestamp"]),s={categories:["SCHEMA"],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.",facing:"INTERNAL",level:"ERROR",name:"ttl_field_not_timestamp",remediation:"Point `.ttl(field)` at a `v.timestamp()` / `v.date()` / `v.number()` (epoch-ms) column, or add such a column.",run:n=>{const a=[];for(const e of n.schema.tables){const{ttl:t}=e;if(!t)continue;const i=e.columnKinds?.[t.field];i===void 0||l.has(i)||a.push(o(s,{cacheKey:`ttl_field_not_timestamp:${e.name}:${t.field}`,detail:`Table "${e.name}" declares \`.ttl("${t.field}")\`, but "${t.field}" is a ${i} column, not an epoch-millisecond timestamp.`,metadata:{field:t.field,kind:i,table:e.name}}))}return a},source:"static",title:"TTL field is not a timestamp"};export{s as default};
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
import{e as n}from"./finding-NrKO8idM.mjs";const i={categories:["SECURITY"],description:"A public `v.string()` argument has no maximum-length bound. An unbounded string lets a client submit arbitrarily large input — abusing storage and CPU on every request that processes it.",facing:"EXTERNAL",level:"INFO",name:"unbounded_string_arg",remediation:"Add a max-length bound via `.check(...)` / `.meta({ maxLength })` on the string validator (e.g. cap a name at 256, a body at a few KB). Size the cap to the field's real-world maximum.",run:e=>e.argValidators===void 0?[]:e.argValidators.flatMap(a=>a.unboundedStringArgs.map(t=>n(i,{cacheKey:`unbounded_string_arg:${a.file}:${a.exportName}:${t}`,detail:`Arg \`${t}\` of public procedure \`${a.exportName}\` (${a.file}:${a.line.toString()}) is an unbounded \`v.string()\`. Add a max-length bound to cap payload size.`,metadata:{argument:t,exportName:a.exportName,file:a.file,line:a.line}}))),source:"static",title:"Public string argument has no length bound"};export{i as default};
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
import{e as i}from"./finding-NrKO8idM.mjs";import{m as a}from"./procedure-protections-DRQotu9I.mjs";import{a as s}from"./helpers-CwSEZdku.mjs";const r=e=>e.writesUserTable===!0?"writes a user/session table":e.callsMail===!0?"sends mail":e.analyzableBody===!1?"may write a user/session table or send mail — its handler body could not be read":"may write a user/session table or send mail — at least one could not be determined",n={categories:["SECURITY"],description:"A public `mutation`/`action` that creates a user/session or sends mail has no CAPTCHA / bot check. Account-creating and mail-sending endpoints are prime automated-abuse targets (credential stuffing, mailbox flooding, disposable-account farming).",facing:"EXTERNAL",level:"WARN",name:"user_creating_mutation_without_captcha",remediation:"Add a server-verified human check: `.use(verifyTurnstile({ secret, token }))` from `@lunora/auth`, or wrap it with `.use(protectPublic({ rateLimit, captcha }))` from `@lunora/server`. Pair with a rate limit for defense in depth.",run:e=>e.procedureProtections===void 0?[]:e.procedureProtections.filter(t=>s(t)&&(a(t.writesUserTable)||a(t.callsMail))&&!t.usesCaptcha).map(t=>i(n,{cacheKey:`user_creating_mutation_without_captcha:${t.file}:${t.exportName}`,detail:`Public ${t.kind} \`${t.exportName}\` (${t.file}) ${r(t)} but has no CAPTCHA check. Add \`.use(verifyTurnstile(...))\` or \`.use(protectPublic({ captcha }))\`.`,metadata:{callsMail:t.callsMail,exportName:t.exportName,file:t.file,kind:t.kind,writesUserTable:t.writesUserTable}})),source:"static",title:"Account-creating / mail-sending write without a CAPTCHA"};export{n as default};
|