@lunora/advisor 1.0.0-alpha.104 → 1.0.0-alpha.106

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 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`, `constraint_validator`) reads observed signal from a running deployment.
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`, `constraint_validator`) reads observed signal off the `LintContext` (`shardTraffic`, `tableScans`, `indexHits`, `tableSamples`). The Studio backend fills `shardTraffic` / `tableScans` / `indexHits` from the shards' admin signal.
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` / `tableSamples` come from wherever
92
- // you read your shards' durable counters — the Studio backend reads its own.
93
- const findings = runAdvisor({ schema: fromServerSchema(schema), shardTraffic, tableScans, indexHits, tableSamples }, { source: "runtime" });
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 and coverage gap**: this reads `context.configCalls`, fed by the
2598
- * codegen `discover/config-calls.ts` feeder's `.extend(fn)` callback-shape
2599
- * support it only sees the setting when a `lunora/`-local file calls the
2600
- * generated `defineApp()...extend(() => ({ allowUnauthenticatedShardAccess:
2601
- * true }))` escape hatch (the pattern the `nuxt` / `analog` templates use in
2602
- * `lunora/server.ts`). An app that sets the same field via `@lunora/vite`'s
2603
- * `LunoraPluginOptions` (`vite.config.ts`) or a hand-authored worker entry
2604
- * outside `lunora/` (the `sveltekit` / `astro` / `react-router` /
2605
- * `tanstack-start` template style) is invisible to this lint — a coverage gap,
2606
- * not a false negative this lint claims to catch.
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 `.extend(...)` call site.
2533
+ * caller flags nothing. One finding per opted-in call site.
2610
2534
  */
2611
2535
  declare const allowUnauthenticatedShardAccessEnabled: Lint;
2612
2536
  /**
@@ -3391,18 +3315,29 @@ declare const maskWeakHashStrategyOnPii: Lint;
3391
3315
  * Flags a public read that hydrates a masked table's rows in the clear through a
3392
3316
  * `with` relation.
3393
3317
  *
3394
- * Column masking (`.use(mask(...))`) is applied per-procedure to the *top-level*
3395
- * rows of the table named in a read. It does **not** descend into relations
3396
- * hydrated via `with` `ctx.db.posts.findMany({ with: { author: true } })`
3397
- * returns each `author` fully unmasked even when the `users` table is masked
3398
- * elsewhere. So a table whose columns you carefully mask on its own reads is
3399
- * still served in the clear whenever an unprotected parent read pulls it in as a
3400
- * relation.
3318
+ * Column masking is **per-procedure**, and that — not the `with` boundary — is
3319
+ * what this catches. `.use(mask(policies))` installs a `relationMask` hook on
3320
+ * the read's args (`@lunora/server`'s `mask/middleware`), and the relation
3321
+ * loader calls it for the TARGET table of every hop, at every nesting depth
3322
+ * (`@lunora/shard-engine`'s `relations`); the one hop it cannot reach, a
3323
+ * cross-shard child, fails closed with `MASK_UNSUPPORTED` rather than returning
3324
+ * cleartext. So a procedure that masks `users` gets masked `users` through
3325
+ * `with` too.
3326
+ *
3327
+ * What is still real is a read whose OWN procedure declares no policy for the
3328
+ * related table. `ctx.db.posts.findMany({ with: { author: true } })` in a
3329
+ * procedure with no `.use(mask(...))` — or one whose policy names only `posts` —
3330
+ * hands back every `author` in the clear, including the columns another
3331
+ * procedure carefully masks on `users`' own reads. Nothing about the mask on
3332
+ * `users` reaches this read.
3401
3333
  *
3402
3334
  * INFO, near-zero false positives by construction: it fires only when all of
3403
3335
  * (1) the enclosing read is public, (2) the read declares `with: { <rel> }`,
3404
- * (3) `<rel>` resolves through the schema to a real target table, and (4) that
3405
- * target table actually has masked columns (per the discovered mask evidence).
3336
+ * (3) `<rel>` resolves through the schema to a real target table, (4) that
3337
+ * target table actually has masked columns (per the discovered mask evidence),
3338
+ * and (5) the reading procedure's own mask policy does not cover that target
3339
+ * table. A policy this feeder could not read statically counts as covering
3340
+ * everything, so an opaque `mask(policies)` never produces a finding.
3406
3341
  * Absent any mask usage the lint is a no-op. Runs only when the codegen feeder
3407
3342
  * supplies `context.relationLoads`; a runtime caller flags nothing. One finding
3408
3343
  * per `(read, masked relation)` pair.
@@ -4028,13 +3963,20 @@ declare const sqlInjectionRisk: Lint;
4028
3963
  * Flags a `ctx.storage.generateUploadUrl(key, …)` call whose options argument
4029
3964
  * omits `contentType`.
4030
3965
  *
4031
- * `generateUploadUrl` mints a signed `PUT` URL the *client* uploads directly
4032
- * to R2, bypassing `upload()`/`store()` entirely — including their
4033
- * `allowedContentTypes`/`maxSize` guards, which this alias never sees. The one
4034
- * guard `generateUploadUrl` itself offers is `contentType`: passing it pins
4035
- * the `Content-Type` into the signature, so the signed URL only authorizes a
4036
- * PUT with exactly that content-type. Omit it and the minted URL accepts any
4037
- * content-type/size the client chooses, entirely unchecked server-side.
3966
+ * `generateUploadUrl` is `getSignedUrl(key, { method: "PUT" })`: it mints a
3967
+ * signed PUT URL the *client* uploads with, bypassing `upload()`/`store()`
3968
+ * entirely — including their `allowedContentTypes`/`maxSize` guards, which this
3969
+ * alias never sees. The one guard it offers is `contentType`, which
3970
+ * `buildSignedUrl` binds into the HMAC canonical (PUT only) and
3971
+ * `verifySignedUrl` returns as `contentType` on a valid verdict.
3972
+ *
3973
+ * Note what that does and does not buy. Binding it means the pin cannot be
3974
+ * swapped without breaking the signature, so the URL is scoped to one declared
3975
+ * content-type. It does NOT by itself reject a mismatched upload: nothing in
3976
+ * `@lunora/storage` compares the pin against the request's actual
3977
+ * `Content-Type` — the PUT route you mount at `publicBaseUrl` does, by checking
3978
+ * `verifySignedUrl`'s `contentType` against the inbound header. Omit the pin
3979
+ * and there is nothing to check against at all, and no size bound either way.
4038
3980
  *
4039
3981
  * Runs only when the codegen feeder supplies storage-upload evidence
4040
3982
  * (`context.storageUploads`); a runtime caller flags nothing. Skips calls
@@ -4068,15 +4010,23 @@ declare const storageKeyFromUserArgs: Lint;
4068
4010
  *
4069
4011
  * `getPresignedUrl` mints a native S3 SigV4 URL that resolves directly
4070
4012
  * against R2's S3 endpoint — the holder reaches the object straight off R2,
4071
- * **bypassing the Worker entirely**, so any auth/RLS/rate-limit gate the app
4072
- * enforces in its own handlers never runs for that request. That's the right
4073
- * trade for genuinely public or bulk content where the app has no per-request
4074
- * gating to apply; it's the wrong choice for private, per-user, or
4075
- * policy-gated content, where `getSignedUrl` (worker-signed, resolves back
4076
- * through the app) is the fit. Separately, either signer minting a long TTL
4077
- * near the shared 7-day ceiling hands out a bearer credential that stays
4078
- * valid almost as long as the platform allows a leaked link (referrer,
4079
- * logs, browser history) then grants access for nearly a week.
4013
+ * **bypassing the Worker entirely**, so no request-time gating is even
4014
+ * possible. That's the right trade for genuinely public or bulk content; it's
4015
+ * the wrong choice for private, per-user, or policy-gated content, where
4016
+ * `getSignedUrl` is the fit.
4017
+ *
4018
+ * What `getSignedUrl` actually gives you is a URL at your own
4019
+ * `publicBaseUrl` origin, HMAC-bound to (method, host, bucket, key, expiry).
4020
+ * That is an OPPORTUNITY to gate, not a gate: `@lunora/storage` ships no
4021
+ * serving route, and nothing in the framework calls `verifySignedUrl`. The
4022
+ * route you mount there is what validates the signature and runs whatever
4023
+ * auth/policy/rate-limit checks the app needs. The lint's point is that
4024
+ * `getPresignedUrl` removes that seam entirely.
4025
+ *
4026
+ * Separately, either signer minting a long TTL near the shared 7-day ceiling
4027
+ * hands out a bearer credential that stays valid almost as long as the
4028
+ * platform allows — a leaked link (referrer, logs, browser history) then
4029
+ * grants access for nearly a week.
4080
4030
  *
4081
4031
  * Runs only when the codegen feeder supplies storage-upload evidence
4082
4032
  * (`context.storageUploads`); a runtime caller flags nothing. The
@@ -4159,9 +4109,12 @@ declare const ttlFieldNotTimestamp: Lint;
4159
4109
  *
4160
4110
  * A string field that accepts an unbounded value lets a client send megabytes of
4161
4111
  * text per request — inflating storage, blowing the row/document size budget, and
4162
- * driving CPU/memory on every handler that processes it. A `.check()`/`.meta()`
4163
- * max-length bound caps the blast radius. Advisory (INFO): a deliberately-open
4164
- * free-text field is sometimes legitimate, so this nudges rather than blocks.
4112
+ * driving CPU/memory on every handler that processes it. A `.max(n)` (or
4113
+ * `.length(n)`) bound caps the blast radius. Only those two count: `.meta({
4114
+ * maxLength })` publishes a cap the parser never enforces, and a bare `.check()`
4115
+ * may predicate anything — neither is evidence the length is bounded. Advisory
4116
+ * (INFO): a deliberately-open free-text field is sometimes legitimate, so this
4117
+ * nudges rather than blocks.
4165
4118
  *
4166
4119
  * Runs only when the codegen feeder supplies arg evidence
4167
4120
  * (`context.argValidators`, public procedures only); a runtime caller flags
@@ -4234,10 +4187,15 @@ declare const unrestrictedWhereBranch: Lint;
4234
4187
  * Endpoints that mint accounts or trigger emails are the classic automated-abuse
4235
4188
  * surface: credential-stuffing sign-ups, mailbox-flooding "forgot password" loops,
4236
4189
  * and disposable-account farming. A server-verified human check (Turnstile) in
4237
- * front of them is the defense. Lunora ships `verifyTurnstile()` (`@lunora/auth`)
4238
- * and the `protectPublic({ captcha })` bundle; this lint fires when a public
4239
- * procedure writes a user/session/account-shaped table (or references `ctx.mail`)
4240
- * with no captcha middleware.
4190
+ * front of them is the defense. Lunora ships `verifyTurnstileMiddleware()`
4191
+ * (`@lunora/auth`) and the `protectPublic({ captcha })` bundle; this lint fires
4192
+ * when a public procedure writes a user/session/account-shaped table (or
4193
+ * references `ctx.mail`) with no captcha middleware.
4194
+ *
4195
+ * The middleware, NOT `verifyTurnstile`: that one is the async verdict function
4196
+ * the middleware calls, so `.use(verifyTurnstile({...}))` installs a Promise in
4197
+ * the chain and checks nothing. The feeder counts only the middleware, so the
4198
+ * remediation below has to name the same thing the feeder will accept.
4241
4199
  *
4242
4200
  * Runs only when the codegen feeder supplies protection evidence
4243
4201
  * (`context.procedureProtections`); a runtime caller with no evidence flags
@@ -4599,4 +4557,4 @@ interface RunAdvisorOptions {
4599
4557
  * `static` lints at build time and defer `runtime` lints to a live shard.
4600
4558
  */
4601
4559
  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 AdvisorTableSample, 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, constraintValidator, 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 };
4560
+ 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 and coverage gap**: this reads `context.configCalls`, fed by the
2598
- * codegen `discover/config-calls.ts` feeder's `.extend(fn)` callback-shape
2599
- * support it only sees the setting when a `lunora/`-local file calls the
2600
- * generated `defineApp()...extend(() => ({ allowUnauthenticatedShardAccess:
2601
- * true }))` escape hatch (the pattern the `nuxt` / `analog` templates use in
2602
- * `lunora/server.ts`). An app that sets the same field via `@lunora/vite`'s
2603
- * `LunoraPluginOptions` (`vite.config.ts`) or a hand-authored worker entry
2604
- * outside `lunora/` (the `sveltekit` / `astro` / `react-router` /
2605
- * `tanstack-start` template style) is invisible to this lint — a coverage gap,
2606
- * not a false negative this lint claims to catch.
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 `.extend(...)` call site.
2533
+ * caller flags nothing. One finding per opted-in call site.
2610
2534
  */
2611
2535
  declare const allowUnauthenticatedShardAccessEnabled: Lint;
2612
2536
  /**
@@ -3391,18 +3315,29 @@ declare const maskWeakHashStrategyOnPii: Lint;
3391
3315
  * Flags a public read that hydrates a masked table's rows in the clear through a
3392
3316
  * `with` relation.
3393
3317
  *
3394
- * Column masking (`.use(mask(...))`) is applied per-procedure to the *top-level*
3395
- * rows of the table named in a read. It does **not** descend into relations
3396
- * hydrated via `with` `ctx.db.posts.findMany({ with: { author: true } })`
3397
- * returns each `author` fully unmasked even when the `users` table is masked
3398
- * elsewhere. So a table whose columns you carefully mask on its own reads is
3399
- * still served in the clear whenever an unprotected parent read pulls it in as a
3400
- * relation.
3318
+ * Column masking is **per-procedure**, and that — not the `with` boundary — is
3319
+ * what this catches. `.use(mask(policies))` installs a `relationMask` hook on
3320
+ * the read's args (`@lunora/server`'s `mask/middleware`), and the relation
3321
+ * loader calls it for the TARGET table of every hop, at every nesting depth
3322
+ * (`@lunora/shard-engine`'s `relations`); the one hop it cannot reach, a
3323
+ * cross-shard child, fails closed with `MASK_UNSUPPORTED` rather than returning
3324
+ * cleartext. So a procedure that masks `users` gets masked `users` through
3325
+ * `with` too.
3326
+ *
3327
+ * What is still real is a read whose OWN procedure declares no policy for the
3328
+ * related table. `ctx.db.posts.findMany({ with: { author: true } })` in a
3329
+ * procedure with no `.use(mask(...))` — or one whose policy names only `posts` —
3330
+ * hands back every `author` in the clear, including the columns another
3331
+ * procedure carefully masks on `users`' own reads. Nothing about the mask on
3332
+ * `users` reaches this read.
3401
3333
  *
3402
3334
  * INFO, near-zero false positives by construction: it fires only when all of
3403
3335
  * (1) the enclosing read is public, (2) the read declares `with: { <rel> }`,
3404
- * (3) `<rel>` resolves through the schema to a real target table, and (4) that
3405
- * target table actually has masked columns (per the discovered mask evidence).
3336
+ * (3) `<rel>` resolves through the schema to a real target table, (4) that
3337
+ * target table actually has masked columns (per the discovered mask evidence),
3338
+ * and (5) the reading procedure's own mask policy does not cover that target
3339
+ * table. A policy this feeder could not read statically counts as covering
3340
+ * everything, so an opaque `mask(policies)` never produces a finding.
3406
3341
  * Absent any mask usage the lint is a no-op. Runs only when the codegen feeder
3407
3342
  * supplies `context.relationLoads`; a runtime caller flags nothing. One finding
3408
3343
  * per `(read, masked relation)` pair.
@@ -4028,13 +3963,20 @@ declare const sqlInjectionRisk: Lint;
4028
3963
  * Flags a `ctx.storage.generateUploadUrl(key, …)` call whose options argument
4029
3964
  * omits `contentType`.
4030
3965
  *
4031
- * `generateUploadUrl` mints a signed `PUT` URL the *client* uploads directly
4032
- * to R2, bypassing `upload()`/`store()` entirely — including their
4033
- * `allowedContentTypes`/`maxSize` guards, which this alias never sees. The one
4034
- * guard `generateUploadUrl` itself offers is `contentType`: passing it pins
4035
- * the `Content-Type` into the signature, so the signed URL only authorizes a
4036
- * PUT with exactly that content-type. Omit it and the minted URL accepts any
4037
- * content-type/size the client chooses, entirely unchecked server-side.
3966
+ * `generateUploadUrl` is `getSignedUrl(key, { method: "PUT" })`: it mints a
3967
+ * signed PUT URL the *client* uploads with, bypassing `upload()`/`store()`
3968
+ * entirely — including their `allowedContentTypes`/`maxSize` guards, which this
3969
+ * alias never sees. The one guard it offers is `contentType`, which
3970
+ * `buildSignedUrl` binds into the HMAC canonical (PUT only) and
3971
+ * `verifySignedUrl` returns as `contentType` on a valid verdict.
3972
+ *
3973
+ * Note what that does and does not buy. Binding it means the pin cannot be
3974
+ * swapped without breaking the signature, so the URL is scoped to one declared
3975
+ * content-type. It does NOT by itself reject a mismatched upload: nothing in
3976
+ * `@lunora/storage` compares the pin against the request's actual
3977
+ * `Content-Type` — the PUT route you mount at `publicBaseUrl` does, by checking
3978
+ * `verifySignedUrl`'s `contentType` against the inbound header. Omit the pin
3979
+ * and there is nothing to check against at all, and no size bound either way.
4038
3980
  *
4039
3981
  * Runs only when the codegen feeder supplies storage-upload evidence
4040
3982
  * (`context.storageUploads`); a runtime caller flags nothing. Skips calls
@@ -4068,15 +4010,23 @@ declare const storageKeyFromUserArgs: Lint;
4068
4010
  *
4069
4011
  * `getPresignedUrl` mints a native S3 SigV4 URL that resolves directly
4070
4012
  * against R2's S3 endpoint — the holder reaches the object straight off R2,
4071
- * **bypassing the Worker entirely**, so any auth/RLS/rate-limit gate the app
4072
- * enforces in its own handlers never runs for that request. That's the right
4073
- * trade for genuinely public or bulk content where the app has no per-request
4074
- * gating to apply; it's the wrong choice for private, per-user, or
4075
- * policy-gated content, where `getSignedUrl` (worker-signed, resolves back
4076
- * through the app) is the fit. Separately, either signer minting a long TTL
4077
- * near the shared 7-day ceiling hands out a bearer credential that stays
4078
- * valid almost as long as the platform allows a leaked link (referrer,
4079
- * logs, browser history) then grants access for nearly a week.
4013
+ * **bypassing the Worker entirely**, so no request-time gating is even
4014
+ * possible. That's the right trade for genuinely public or bulk content; it's
4015
+ * the wrong choice for private, per-user, or policy-gated content, where
4016
+ * `getSignedUrl` is the fit.
4017
+ *
4018
+ * What `getSignedUrl` actually gives you is a URL at your own
4019
+ * `publicBaseUrl` origin, HMAC-bound to (method, host, bucket, key, expiry).
4020
+ * That is an OPPORTUNITY to gate, not a gate: `@lunora/storage` ships no
4021
+ * serving route, and nothing in the framework calls `verifySignedUrl`. The
4022
+ * route you mount there is what validates the signature and runs whatever
4023
+ * auth/policy/rate-limit checks the app needs. The lint's point is that
4024
+ * `getPresignedUrl` removes that seam entirely.
4025
+ *
4026
+ * Separately, either signer minting a long TTL near the shared 7-day ceiling
4027
+ * hands out a bearer credential that stays valid almost as long as the
4028
+ * platform allows — a leaked link (referrer, logs, browser history) then
4029
+ * grants access for nearly a week.
4080
4030
  *
4081
4031
  * Runs only when the codegen feeder supplies storage-upload evidence
4082
4032
  * (`context.storageUploads`); a runtime caller flags nothing. The
@@ -4159,9 +4109,12 @@ declare const ttlFieldNotTimestamp: Lint;
4159
4109
  *
4160
4110
  * A string field that accepts an unbounded value lets a client send megabytes of
4161
4111
  * text per request — inflating storage, blowing the row/document size budget, and
4162
- * driving CPU/memory on every handler that processes it. A `.check()`/`.meta()`
4163
- * max-length bound caps the blast radius. Advisory (INFO): a deliberately-open
4164
- * free-text field is sometimes legitimate, so this nudges rather than blocks.
4112
+ * driving CPU/memory on every handler that processes it. A `.max(n)` (or
4113
+ * `.length(n)`) bound caps the blast radius. Only those two count: `.meta({
4114
+ * maxLength })` publishes a cap the parser never enforces, and a bare `.check()`
4115
+ * may predicate anything — neither is evidence the length is bounded. Advisory
4116
+ * (INFO): a deliberately-open free-text field is sometimes legitimate, so this
4117
+ * nudges rather than blocks.
4165
4118
  *
4166
4119
  * Runs only when the codegen feeder supplies arg evidence
4167
4120
  * (`context.argValidators`, public procedures only); a runtime caller flags
@@ -4234,10 +4187,15 @@ declare const unrestrictedWhereBranch: Lint;
4234
4187
  * Endpoints that mint accounts or trigger emails are the classic automated-abuse
4235
4188
  * surface: credential-stuffing sign-ups, mailbox-flooding "forgot password" loops,
4236
4189
  * and disposable-account farming. A server-verified human check (Turnstile) in
4237
- * front of them is the defense. Lunora ships `verifyTurnstile()` (`@lunora/auth`)
4238
- * and the `protectPublic({ captcha })` bundle; this lint fires when a public
4239
- * procedure writes a user/session/account-shaped table (or references `ctx.mail`)
4240
- * with no captcha middleware.
4190
+ * front of them is the defense. Lunora ships `verifyTurnstileMiddleware()`
4191
+ * (`@lunora/auth`) and the `protectPublic({ captcha })` bundle; this lint fires
4192
+ * when a public procedure writes a user/session/account-shaped table (or
4193
+ * references `ctx.mail`) with no captcha middleware.
4194
+ *
4195
+ * The middleware, NOT `verifyTurnstile`: that one is the async verdict function
4196
+ * the middleware calls, so `.use(verifyTurnstile({...}))` installs a Promise in
4197
+ * the chain and checks nothing. The feeder counts only the middleware, so the
4198
+ * remediation below has to name the same thing the feeder will accept.
4241
4199
  *
4242
4200
  * Runs only when the codegen feeder supplies protection evidence
4243
4201
  * (`context.procedureProtections`); a runtime caller with no evidence flags
@@ -4599,4 +4557,4 @@ interface RunAdvisorOptions {
4599
4557
  * `static` lints at build time and defer `runtime` lints to a live shard.
4600
4558
  */
4601
4559
  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 AdvisorTableSample, 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, constraintValidator, 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 };
4560
+ 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-COQRpmnu.mjs";import V from"./packem_shared/filterWithoutIndex-Cf-tXY_A.mjs";import Q from"./packem_shared/flagGatesSecurityWithUnsafeDefault-i-Befg8b.mjs";import X from"./packem_shared/flagReadInSubscription-DabIhYGD.mjs";import Z from"./packem_shared/geoIndexFieldNotGeopoint-D0lOTm-_.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-B_c3o9QR.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-BC1n0F4-.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-DhiA3mpG.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-Dsd4cSPD.mjs";import Fo from"./packem_shared/publicTableRlsOptoutConfusion-Dz7lbKKe.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-iDJq68qx.mjs";import Eo from"./packem_shared/relationReferencesUnknownField-D8Qyth_P.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-BIsSLyhJ.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-E14I83wY.mjs";import{s as Jo,q as Yo}from"./packem_shared/helpers-CwSEZdku.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-BYzcTLE2.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};
@@ -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 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};
@@ -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 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-CwSEZdku.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.104",
3
+ "version": "1.0.0-alpha.106",
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.28",
50
- "@lunora/server": "1.0.0-alpha.98"
49
+ "@lunora/errors": "1.0.0-alpha.30",
50
+ "@lunora/server": "1.0.0-alpha.100"
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
- 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 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 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 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};