@lunora/advisor 1.0.0-alpha.60 → 1.0.0-alpha.61
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 +7 -5
- package/dist/index.d.mts +155 -59
- package/dist/index.d.ts +155 -59
- package/dist/index.mjs +1 -1
- package/dist/packem_shared/{MAP_VERSION-BS08jBkS.mjs → MAP_VERSION-DxkFCiLX.mjs} +1 -1
- package/dist/packem_shared/aiUnboundedGenerationPublic-iu7crnSa.mjs +1 -0
- package/dist/packem_shared/classifySensitivity-B1kkm44P.mjs +1 -0
- package/dist/packem_shared/{compareToBaseline-T1_kl8VY.mjs → compareToBaseline-BwXFusQG.mjs} +1 -1
- package/dist/packem_shared/errorRateOutlier-CgK42sq0.mjs +1 -0
- package/dist/packem_shared/insertManyUnsafeUserData-xLYAkcKj.mjs +1 -0
- package/dist/packem_shared/nondeterministicQueryMutation-BZV_biUV.mjs +1 -0
- package/dist/packem_shared/privilegedFanoutFromPublicProcedure-sMsZN4uP.mjs +1 -0
- package/dist/packem_shared/procedure-protections-DRQotu9I.mjs +1 -0
- package/dist/packem_shared/signupMutationWithoutDisposableGating-Tgv9SbsY.mjs +1 -0
- package/dist/packem_shared/storageKeyFromUserArgs-CI-6kyte.mjs +1 -0
- package/dist/packem_shared/userCreatingMutationWithoutCaptcha-DGiZ3_W7.mjs +1 -0
- package/package.json +3 -3
- package/dist/packem_shared/AE_METRIC_EVENTS-LP_0D7Uw.mjs +0 -1
- package/dist/packem_shared/aiUnboundedGenerationPublic-D3HZrh5_.mjs +0 -1
- package/dist/packem_shared/classifySensitivity-DjswRYWd.mjs +0 -1
- package/dist/packem_shared/insertManyUnsafeUserData-DrH7PdRs.mjs +0 -1
- package/dist/packem_shared/nondeterministicQueryMutation-BaMfbAk_.mjs +0 -1
- package/dist/packem_shared/privilegedFanoutFromPublicProcedure-C2j2GSUN.mjs +0 -1
- package/dist/packem_shared/signupMutationWithoutDisposableGating-BNzlsE0F.mjs +0 -1
- package/dist/packem_shared/storageKeyFromUserArgs-BeS5cp56.mjs +0 -1
- package/dist/packem_shared/userCreatingMutationWithoutCaptcha-iDjRkUFO.mjs +0 -1
package/README.md
CHANGED
|
@@ -79,18 +79,20 @@ for (const finding of findings) {
|
|
|
79
79
|
|
|
80
80
|
### Runtime lints
|
|
81
81
|
|
|
82
|
-
The runtime tier (`hot_shard`, `index_utilization`, `constraint_validator`) reads observed signal off the `LintContext` (`shardTraffic`, `tableScans`, `indexHits`, `tableSamples`). The Studio backend fills those from each shard's durable counters
|
|
82
|
+
The runtime tier (`hot_shard`, `index_utilization`, `constraint_validator`) reads observed signal off the `LintContext` (`shardTraffic`, `tableScans`, `indexHits`, `tableSamples`). The Studio backend fills those from each shard's durable counters:
|
|
83
83
|
|
|
84
84
|
```ts
|
|
85
|
-
import { fromServerSchema,
|
|
85
|
+
import { fromServerSchema, runAdvisor } from "@lunora/advisor";
|
|
86
86
|
|
|
87
87
|
import schema from "./lunora/schema";
|
|
88
88
|
|
|
89
|
-
// `
|
|
90
|
-
|
|
91
|
-
const findings = runAdvisor({ schema: fromServerSchema(schema),
|
|
89
|
+
// `shardTraffic` / `tableScans` / `indexHits` / `tableSamples` come from wherever
|
|
90
|
+
// you read your shards' durable counters — the Studio backend reads its own.
|
|
91
|
+
const findings = runAdvisor({ schema: fromServerSchema(schema), shardTraffic, tableScans, indexHits, tableSamples }, { source: "runtime" });
|
|
92
92
|
```
|
|
93
93
|
|
|
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.
|
|
95
|
+
|
|
94
96
|
A missing metric degrades to an empty array rather than throwing, so a partially configured read path still returns what it can.
|
|
95
97
|
|
|
96
98
|
### Observability map (score, coverage, baseline)
|
package/dist/index.d.mts
CHANGED
|
@@ -367,6 +367,36 @@ interface AdvisorFlagSecurityDefault {
|
|
|
367
367
|
/** 1-based line of the `ctx.flags.boolean` call, or `0` when unknown. */
|
|
368
368
|
line: number;
|
|
369
369
|
}
|
|
370
|
+
/**
|
|
371
|
+
* One function's observed call/error/latency volume over the window — the input
|
|
372
|
+
* a runtime reliability lint (`error_rate_outlier`, and a future latency
|
|
373
|
+
* counterpart) reads.
|
|
374
|
+
*
|
|
375
|
+
* Not a new collection path: this is the same `FunctionCallStat` row shape
|
|
376
|
+
* `__lunora_admin__:getFunctionStats` already returns and the studio's Advisors
|
|
377
|
+
* panel already fetches (today only its `scannedTables` field feeds
|
|
378
|
+
* `AdvisorTableScan`/`context.tableScans`); this type carries the `calls`/
|
|
379
|
+
* `errors`/`maxDurationMs` columns of that same row instead. Prototype (plan
|
|
380
|
+
* 248) — see `plans/248-runtime-lints-design.md` for the reachability finding
|
|
381
|
+
* and the open scope/threshold questions before productizing further.
|
|
382
|
+
*
|
|
383
|
+
* Scope caveat: `getFunctionStats` reads one shard's durable `__lunora_metrics`
|
|
384
|
+
* table with no cross-shard fan-out (unlike `AdvisorShardTraffic`, which
|
|
385
|
+
* `orchestrateShardTraffic` sums across every shard). A caller feeding this from
|
|
386
|
+
* a sharded function's single selected shard is reporting that shard's rate, not
|
|
387
|
+
* the function's deployment-wide rate — same scope `lunora insights` itself
|
|
388
|
+
* already operates at.
|
|
389
|
+
*/
|
|
390
|
+
interface AdvisorFunctionMetrics {
|
|
391
|
+
/** Total dispatches of `path` over the observed window. */
|
|
392
|
+
calls: number;
|
|
393
|
+
/** Dispatches that threw, over the same window. */
|
|
394
|
+
errors: number;
|
|
395
|
+
/** Slowest single dispatch, ms, over the same window. */
|
|
396
|
+
maxDurationMs: number;
|
|
397
|
+
/** The function's registered path (`<file>:<exportName>`). */
|
|
398
|
+
path: string;
|
|
399
|
+
}
|
|
370
400
|
/**
|
|
371
401
|
* One `withGeoIndex("name", …)` read discovered in a function body — the use-side
|
|
372
402
|
* input the `geo_index_unused` lint cross-references against the declared geo
|
|
@@ -881,8 +911,23 @@ interface AdvisorPrivilegedDispatch {
|
|
|
881
911
|
* feeder; runtime callers don't supply it, so the lints find nothing there.
|
|
882
912
|
*/
|
|
883
913
|
interface AdvisorProcedureProtection {
|
|
884
|
-
/**
|
|
885
|
-
|
|
914
|
+
/**
|
|
915
|
+
* `true` when the feeder could read the handler body statically (an inline
|
|
916
|
+
* function expression/arrow, or a same-file identifier resolved to one);
|
|
917
|
+
* `false` for a genuinely cross-file handler, in which case every
|
|
918
|
+
* behavioural fact below is `undefined` rather than a proven "not observed".
|
|
919
|
+
* Optional so a feeder predating this field (or a runtime caller) is treated
|
|
920
|
+
* as "unknown" rather than asserting the handler was readable.
|
|
921
|
+
*/
|
|
922
|
+
analyzableBody?: boolean;
|
|
923
|
+
/**
|
|
924
|
+
* `true` when the handler references `ctx.mail` / `ctx.email` (sends mail),
|
|
925
|
+
* `undefined` when the feeder couldn't read the handler body (a genuinely
|
|
926
|
+
* cross-file handler). `user_creating_mutation_without_captcha` treats
|
|
927
|
+
* `undefined` the same as `true` — fail-closed, since an unreadable handler
|
|
928
|
+
* might well send mail.
|
|
929
|
+
*/
|
|
930
|
+
callsMail?: boolean;
|
|
886
931
|
/** `true` when the handler emits a structured observability event (`ctx.log` / `ctx.span` / `ctx.trace`). */
|
|
887
932
|
emitsEvent?: boolean;
|
|
888
933
|
/**
|
|
@@ -895,8 +940,14 @@ interface AdvisorProcedureProtection {
|
|
|
895
940
|
exemptReason?: string;
|
|
896
941
|
/** The exported binding name of the procedure (e.g. `signUp`). */
|
|
897
942
|
exportName: string;
|
|
898
|
-
/**
|
|
899
|
-
|
|
943
|
+
/**
|
|
944
|
+
* `true` when the handler fans work out to a privileged, cost-bearing dispatch
|
|
945
|
+
* surface (scheduler `runAfter`/`runAt`, a queue producer send, or a workflow
|
|
946
|
+
* create). Read by the privileged-fanout lint, paired with public visibility
|
|
947
|
+
* and no rate limit. `undefined` when the feeder couldn't read the handler
|
|
948
|
+
* body — the lint treats that as `true` (fail-closed).
|
|
949
|
+
*/
|
|
950
|
+
fanOut?: boolean;
|
|
900
951
|
/** Source file relative to the lunora dir, no extension. */
|
|
901
952
|
file: string;
|
|
902
953
|
/** `true` when the handler wraps work in `try`/`catch`. */
|
|
@@ -913,20 +964,33 @@ interface AdvisorProcedureProtection {
|
|
|
913
964
|
hasEmailArg?: boolean;
|
|
914
965
|
/** Registration kind — `query` is read-only; `mutation`/`action` are write-shaped. */
|
|
915
966
|
kind: "action" | "mutation" | "query";
|
|
916
|
-
/** `true` when the handler runs any AI generation, bounded or not. */
|
|
917
|
-
runsAiGeneration?: boolean;
|
|
918
967
|
/** `true` when the handler reaches an outbound surface (`ctx.fetch`, mail, queues, storage, sql, ai, …) that can fail. */
|
|
919
968
|
reachesOutbound?: boolean;
|
|
969
|
+
/** `true` when the handler runs any AI generation, bounded or not. */
|
|
970
|
+
runsAiGeneration?: boolean;
|
|
920
971
|
/** `true` when the handler throws a bare `new Error(...)` rather than a coded `LunoraError`. */
|
|
921
972
|
throwsBareError?: boolean;
|
|
922
|
-
/**
|
|
923
|
-
|
|
973
|
+
/**
|
|
974
|
+
* `true` when the handler runs an AI generation (`generateText`/`streamText`/
|
|
975
|
+
* `generateObject`/`streamObject`) with no `maxOutputTokens` bound. Read by
|
|
976
|
+
* the `ai_unbounded_generation_public` lint (paired with public visibility).
|
|
977
|
+
* `undefined` when the feeder couldn't read the handler body — the lint
|
|
978
|
+
* treats that as `true` (fail-closed), distinct from the call-level opaque-
|
|
979
|
+
* config case, which the feeder always reports as `false` (fail-open by
|
|
980
|
+
* design — see `isUnboundedAiGeneration`).
|
|
981
|
+
*/
|
|
982
|
+
unboundedAiGeneration?: boolean;
|
|
924
983
|
/** `true` when the chain carries `.use(verifyTurnstile(...))` or a `protectPublic({ captcha })` bundle. */
|
|
925
984
|
usesCaptcha: boolean;
|
|
926
985
|
/** `true` when the chain carries `.use(emailGateMiddleware(...))` from `@lunora/auth`. Read by the `signup_mutation_without_disposable_gating` lint (paired with public visibility + a user-table write). */
|
|
927
986
|
usesEmailGate: boolean;
|
|
928
|
-
/**
|
|
929
|
-
|
|
987
|
+
/**
|
|
988
|
+
* `true` when the handler calls `ctx.db.insertManyUnsafe(...)`, which bypasses
|
|
989
|
+
* validators and triggers. Read by the `insert_many_unsafe_user_data` lint
|
|
990
|
+
* (paired with public visibility). `undefined` when the feeder couldn't read
|
|
991
|
+
* the handler body — the lint treats that as `true` (fail-closed).
|
|
992
|
+
*/
|
|
993
|
+
usesInsertManyUnsafe?: boolean;
|
|
930
994
|
/** `true` when the chain carries `.use(mask(...))`. */
|
|
931
995
|
usesMask: boolean;
|
|
932
996
|
/** `true` when the chain carries `.use(rateLimit(...))` or a `protectPublic({ rateLimit })` bundle. */
|
|
@@ -935,8 +999,14 @@ interface AdvisorProcedureProtection {
|
|
|
935
999
|
usesRls: boolean;
|
|
936
1000
|
/** `"internal"` when the procedure uses `internalQuery` / `internalMutation` / `internalAction`. */
|
|
937
1001
|
visibility: "internal" | "public";
|
|
938
|
-
/**
|
|
939
|
-
|
|
1002
|
+
/**
|
|
1003
|
+
* `true` when the handler inserts into a user/session/account-shaped table,
|
|
1004
|
+
* `undefined` when the feeder couldn't read the handler body (a genuinely
|
|
1005
|
+
* cross-file handler). `signup_mutation_without_disposable_gating` and
|
|
1006
|
+
* `user_creating_mutation_without_captcha` treat `undefined` the same as
|
|
1007
|
+
* `true` — fail-closed, since an unreadable handler might well write one.
|
|
1008
|
+
*/
|
|
1009
|
+
writesUserTable?: boolean;
|
|
940
1010
|
}
|
|
941
1011
|
/**
|
|
942
1012
|
* One query read discovered in a function body — the input the
|
|
@@ -1425,6 +1495,14 @@ interface AdvisorStorageKeyAccess {
|
|
|
1425
1495
|
line: number;
|
|
1426
1496
|
/** The bucket method invoked with the arg-derived key, e.g. `get` / `put` / `delete` / `download`. */
|
|
1427
1497
|
method: string;
|
|
1498
|
+
/**
|
|
1499
|
+
* Visibility of the enclosing procedure. `internal` procedures are not
|
|
1500
|
+
* reachable by a caller, so the "any caller can read/overwrite/delete
|
|
1501
|
+
* another user's object" premise does not hold there and the finding drops
|
|
1502
|
+
* to `INFO` (mirrors `AdvisorOwnerFieldWrite.visibility`). `undefined` when
|
|
1503
|
+
* the feeder could not attribute the access to a registered procedure.
|
|
1504
|
+
*/
|
|
1505
|
+
visibility?: "internal" | "public";
|
|
1428
1506
|
}
|
|
1429
1507
|
/**
|
|
1430
1508
|
* One tracked `ctx.storage.<bucket>.<method>(...)` upload/signing call — the
|
|
@@ -1792,6 +1870,16 @@ interface LintContext {
|
|
|
1792
1870
|
* codegen feeder; absent for runtime callers, where the lint finds nothing.
|
|
1793
1871
|
*/
|
|
1794
1872
|
flagSecurityDefaults?: ReadonlyArray<AdvisorFlagSecurityDefault>;
|
|
1873
|
+
/**
|
|
1874
|
+
* Per-function call/error/latency volume observed over the window — the
|
|
1875
|
+
* `error_rate_outlier` runtime lint's input (prototype, plan 248). Sourced
|
|
1876
|
+
* from the same `FunctionCallStat` rows `context.tableScans` already draws
|
|
1877
|
+
* `scannedTables` from (`__lunora_admin__:getFunctionStats`), scoped to
|
|
1878
|
+
* whichever shard the caller queried — see {@link AdvisorFunctionMetrics}.
|
|
1879
|
+
* Supplied by the studio backend; absent for static callers, where the lint
|
|
1880
|
+
* finds nothing.
|
|
1881
|
+
*/
|
|
1882
|
+
functionMetrics?: ReadonlyArray<AdvisorFunctionMetrics>;
|
|
1795
1883
|
/**
|
|
1796
1884
|
* `withGeoIndex("name", …)` reads discovered in function bodies — the use-side
|
|
1797
1885
|
* input the `geo_index_unused` lint cross-references against the declared geo
|
|
@@ -2202,29 +2290,6 @@ interface AnalyticsMetricsSource {
|
|
|
2202
2290
|
rows: ReadonlyArray<Record<string, unknown>>;
|
|
2203
2291
|
}>;
|
|
2204
2292
|
}
|
|
2205
|
-
/**
|
|
2206
|
-
* The AE event-name + dimension-column contract the runtime writes and this
|
|
2207
|
-
* reader reads. `blob1` is the event name; dimensions start at `blob2`.
|
|
2208
|
-
*/
|
|
2209
|
-
declare const AE_METRIC_EVENTS: {
|
|
2210
|
-
/** `lunora.index.hit` — one row per `(table, index)` use. `blob2`=table, `blob3`=index. */
|
|
2211
|
-
readonly indexHit: {
|
|
2212
|
-
readonly event: "lunora.index.hit";
|
|
2213
|
-
readonly index: "blob3";
|
|
2214
|
-
readonly table: "blob2";
|
|
2215
|
-
};
|
|
2216
|
-
/** `lunora.shard.request` — one row per shard dispatch. `blob2`=shardKey, `blob3`=group. */
|
|
2217
|
-
readonly shardRequest: {
|
|
2218
|
-
readonly event: "lunora.shard.request";
|
|
2219
|
-
readonly group: "blob3";
|
|
2220
|
-
readonly shardKey: "blob2";
|
|
2221
|
-
};
|
|
2222
|
-
/** `lunora.table.scan` — one row per full-scan. `blob2`=table. */
|
|
2223
|
-
readonly tableScan: {
|
|
2224
|
-
readonly event: "lunora.table.scan";
|
|
2225
|
-
readonly table: "blob2";
|
|
2226
|
-
};
|
|
2227
|
-
};
|
|
2228
2293
|
/** Options for the AE-backed runtime-metrics feeder. */
|
|
2229
2294
|
interface AnalyticsMetricsOptions {
|
|
2230
2295
|
/** The AE dataset (the wrangler `analytics_engine_datasets[].dataset`) to read from. */
|
|
@@ -2253,20 +2318,6 @@ interface AnalyticsRuntimeMetrics {
|
|
|
2253
2318
|
shardTraffic: AdvisorShardTraffic[];
|
|
2254
2319
|
tableScans: AdvisorTableScan[];
|
|
2255
2320
|
}
|
|
2256
|
-
/**
|
|
2257
|
-
* Reconstruct the runtime-lint input arrays (`shardTraffic` / `tableScans` /
|
|
2258
|
-
* `indexHits`) from the Analytics Engine SQL API. The three reads run
|
|
2259
|
-
* concurrently; each degrades to an empty array on a query failure, so a
|
|
2260
|
-
* partially-misconfigured read path still returns what it can.
|
|
2261
|
-
*
|
|
2262
|
-
* Feed the result into a {@link LintContext} alongside the declared schema:
|
|
2263
|
-
*
|
|
2264
|
-
* ```ts
|
|
2265
|
-
* const metrics = await loadAnalyticsRuntimeMetrics(client, { dataset: "ANALYTICS" });
|
|
2266
|
-
* runAdvisor({ schema, ...metrics }, { source: "runtime" });
|
|
2267
|
-
* ```
|
|
2268
|
-
*/
|
|
2269
|
-
declare const loadAnalyticsRuntimeMetrics: (source: AnalyticsMetricsSource, options: AnalyticsMetricsOptions) => Promise<AnalyticsRuntimeMetrics>;
|
|
2270
2321
|
/**
|
|
2271
2322
|
* Disambiguate {@link Finding}s that share a `cacheKey`.
|
|
2272
2323
|
*
|
|
@@ -2305,6 +2356,29 @@ declare const dedupeCacheKeys: (findings: ReadonlyArray<Finding>) => Finding[];
|
|
|
2305
2356
|
* additional read. When a sample is truncated, findings note the caveat.
|
|
2306
2357
|
*/
|
|
2307
2358
|
declare const constraintValidator: Lint;
|
|
2359
|
+
/**
|
|
2360
|
+
* `error_rate_outlier` — flag a function whose observed error rate is high
|
|
2361
|
+
* enough to be a genuine reliability signal, not noise.
|
|
2362
|
+
*
|
|
2363
|
+
* PROTOTYPE (plan 248): the read side of the runtime-lint category is thin (3
|
|
2364
|
+
* lints, all traffic/conflict/index shaped) next to the static category's 96.
|
|
2365
|
+
* `lunora insights` already collects and ranks this exact signal
|
|
2366
|
+
* (`errorHotspots`) for a human report; this lint closes the loop by feeding
|
|
2367
|
+
* the same per-function `errors`/`calls` counts into the `lunora advisor` gate
|
|
2368
|
+
* so a CI run can flag a genuinely failing function, not just a report a human
|
|
2369
|
+
* has to remember to read.
|
|
2370
|
+
*
|
|
2371
|
+
* The per-function volume comes from the runtime feeder (`context.
|
|
2372
|
+
* functionMetrics`): the studio backend reads `__lunora_admin__:getFunctionStats`
|
|
2373
|
+
* for the shard in scope — the SAME rows `context.tableScans` already draws
|
|
2374
|
+
* `scannedTables` from, just projecting `errors`/`calls` instead. See
|
|
2375
|
+
* `AdvisorFunctionMetrics` for the scope caveat (one shard, not fanned out
|
|
2376
|
+
* cross-shard like `context.shardTraffic`) and `plans/248-runtime-lints-design.md`
|
|
2377
|
+
* for the reachability finding and open questions this prototype does not
|
|
2378
|
+
* resolve (threshold model, single- vs. cross-shard scope, whether this ships
|
|
2379
|
+
* as its own lint or merges with a future latency counterpart).
|
|
2380
|
+
*/
|
|
2381
|
+
declare const errorRateOutlier: Lint;
|
|
2308
2382
|
/**
|
|
2309
2383
|
* `hot_shard` — flag a shard whose request share is disproportionately high.
|
|
2310
2384
|
*
|
|
@@ -3242,15 +3316,29 @@ declare const mutatorFullRowReplace: Lint;
|
|
|
3242
3316
|
* Flags a non-deterministic API call inside a `query(...)` or `mutation(...)`
|
|
3243
3317
|
* handler body.
|
|
3244
3318
|
*
|
|
3245
|
-
*
|
|
3246
|
-
*
|
|
3247
|
-
*
|
|
3248
|
-
*
|
|
3249
|
-
*
|
|
3250
|
-
*
|
|
3251
|
-
*
|
|
3252
|
-
* `
|
|
3253
|
-
*
|
|
3319
|
+
* The two procedure kinds do NOT share the same hazard (issue #286).
|
|
3320
|
+
*
|
|
3321
|
+
* A `query` handler genuinely CAN run more than once for the same logical
|
|
3322
|
+
* read: a live subscription re-runs its query whenever a table it reads
|
|
3323
|
+
* changes, so `Date.now()`/`Math.random()`/etc. there can flicker between
|
|
3324
|
+
* re-evaluations — this half stays at WARN.
|
|
3325
|
+
*
|
|
3326
|
+
* A `mutation` handler does NOT replay under ordinary dispatch on this
|
|
3327
|
+
* (DO-backed) runtime: a client-issued idempotency key dedups a replayed call
|
|
3328
|
+
* to the **cached** result without re-running the handler, and an
|
|
3329
|
+
* optimistic-concurrency conflict throws back to the caller as an error
|
|
3330
|
+
* rather than triggering an internal retry of the handler. So a mutation
|
|
3331
|
+
* handler body runs at most once per logical write, and `Date.now()` inside
|
|
3332
|
+
* one is stable — the premise this lint used to apply uniformly (borrowed
|
|
3333
|
+
* from Convex's OCC-retries-the-handler model, which does not hold here) was
|
|
3334
|
+
* responsible for 193 of 385 non-INFO findings on one real codebase, all from
|
|
3335
|
+
* the ordinary "stamp `createdAt`" pattern. Dropped to INFO rather than
|
|
3336
|
+
* silenced: a mutation invoked from inside a workflow step or queue consumer
|
|
3337
|
+
* CAN be re-dispatched if the surrounding step/consumer itself replays (a
|
|
3338
|
+
* fresh `ctx.runMutation` call from the step's perspective, not a replay this
|
|
3339
|
+
* lint can distinguish from any other mutation call), so the finding stays as
|
|
3340
|
+
* a breadcrumb rather than disappearing outright. See the "Determinism: what
|
|
3341
|
+
* actually replays" section of the `@lunora/server` docs.
|
|
3254
3342
|
*
|
|
3255
3343
|
* This lint runs when the codegen feeder has supplied call evidence
|
|
3256
3344
|
* (`context.nondeterministicCalls` present); a runtime caller with no evidence
|
|
@@ -4350,6 +4438,14 @@ declare const scoreAdvisor: (procedures: ReadonlyArray<AdvisorProcedureProtectio
|
|
|
4350
4438
|
* Runs before any rule so a lint can gate on it, and feeds the global weighting:
|
|
4351
4439
|
* a handler touching identity, mail, or tenant-scoped rows pulls harder on the
|
|
4352
4440
|
* grade than a plain read. `none` means no signal fired — not "safe".
|
|
4441
|
+
*
|
|
4442
|
+
* A fact reads `undefined` only when the feeder couldn't read the handler body
|
|
4443
|
+
* (a cross-file handler, or — degenerately — a partial payload), and that stays
|
|
4444
|
+
* fail-closed: `level` goes `"high"` the same as a proven `true` would. The
|
|
4445
|
+
* reason text does not, though — asserting "writes an identity table" for a
|
|
4446
|
+
* fact that was never observed would misattribute an unproven claim. A proven
|
|
4447
|
+
* `true` still gets its specific reason; every `undefined` fact collapses into
|
|
4448
|
+
* one shared "could not be read" reason instead of one unproven claim apiece.
|
|
4353
4449
|
*/
|
|
4354
4450
|
declare const classifySensitivity: (procedure: AdvisorProcedureProtection) => Sensitivity;
|
|
4355
4451
|
/**
|
|
@@ -4383,4 +4479,4 @@ interface RunAdvisorOptions {
|
|
|
4383
4479
|
* `static` lints at build time and defer `runtime` lints to a live shard.
|
|
4384
4480
|
*/
|
|
4385
4481
|
declare const runAdvisor: (context: LintContext, options?: RunAdvisorOptions) => Finding[];
|
|
4386
|
-
export {
|
|
4482
|
+
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 AdvisorFlagSecurityDefault, type AdvisorFunctionMetrics, 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 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, compareToBaseline, constraintValidator, containerInstanceKeyFromUserInput, containerOversizedInstance, containerPublicInternet, containerRuntimeEgressRelaxation, containerStartEnableInternetOverride, dedupeCacheKeys, duplicateIndex, emptyIndex, errorRateOutlier, errorWithoutCatalog, exportSinkMisconfigured, externalSourceIncrementalNoDeletePath, externalSourceOnGlobal, externalSourceUnscoped, filterOnPrimaryKey, filterWithoutIndex, flagGatesSecurityWithUnsafeDefault, fromServerSchema, geoIndexFieldNotGeopoint, geoIndexUnused, 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
|
@@ -367,6 +367,36 @@ interface AdvisorFlagSecurityDefault {
|
|
|
367
367
|
/** 1-based line of the `ctx.flags.boolean` call, or `0` when unknown. */
|
|
368
368
|
line: number;
|
|
369
369
|
}
|
|
370
|
+
/**
|
|
371
|
+
* One function's observed call/error/latency volume over the window — the input
|
|
372
|
+
* a runtime reliability lint (`error_rate_outlier`, and a future latency
|
|
373
|
+
* counterpart) reads.
|
|
374
|
+
*
|
|
375
|
+
* Not a new collection path: this is the same `FunctionCallStat` row shape
|
|
376
|
+
* `__lunora_admin__:getFunctionStats` already returns and the studio's Advisors
|
|
377
|
+
* panel already fetches (today only its `scannedTables` field feeds
|
|
378
|
+
* `AdvisorTableScan`/`context.tableScans`); this type carries the `calls`/
|
|
379
|
+
* `errors`/`maxDurationMs` columns of that same row instead. Prototype (plan
|
|
380
|
+
* 248) — see `plans/248-runtime-lints-design.md` for the reachability finding
|
|
381
|
+
* and the open scope/threshold questions before productizing further.
|
|
382
|
+
*
|
|
383
|
+
* Scope caveat: `getFunctionStats` reads one shard's durable `__lunora_metrics`
|
|
384
|
+
* table with no cross-shard fan-out (unlike `AdvisorShardTraffic`, which
|
|
385
|
+
* `orchestrateShardTraffic` sums across every shard). A caller feeding this from
|
|
386
|
+
* a sharded function's single selected shard is reporting that shard's rate, not
|
|
387
|
+
* the function's deployment-wide rate — same scope `lunora insights` itself
|
|
388
|
+
* already operates at.
|
|
389
|
+
*/
|
|
390
|
+
interface AdvisorFunctionMetrics {
|
|
391
|
+
/** Total dispatches of `path` over the observed window. */
|
|
392
|
+
calls: number;
|
|
393
|
+
/** Dispatches that threw, over the same window. */
|
|
394
|
+
errors: number;
|
|
395
|
+
/** Slowest single dispatch, ms, over the same window. */
|
|
396
|
+
maxDurationMs: number;
|
|
397
|
+
/** The function's registered path (`<file>:<exportName>`). */
|
|
398
|
+
path: string;
|
|
399
|
+
}
|
|
370
400
|
/**
|
|
371
401
|
* One `withGeoIndex("name", …)` read discovered in a function body — the use-side
|
|
372
402
|
* input the `geo_index_unused` lint cross-references against the declared geo
|
|
@@ -881,8 +911,23 @@ interface AdvisorPrivilegedDispatch {
|
|
|
881
911
|
* feeder; runtime callers don't supply it, so the lints find nothing there.
|
|
882
912
|
*/
|
|
883
913
|
interface AdvisorProcedureProtection {
|
|
884
|
-
/**
|
|
885
|
-
|
|
914
|
+
/**
|
|
915
|
+
* `true` when the feeder could read the handler body statically (an inline
|
|
916
|
+
* function expression/arrow, or a same-file identifier resolved to one);
|
|
917
|
+
* `false` for a genuinely cross-file handler, in which case every
|
|
918
|
+
* behavioural fact below is `undefined` rather than a proven "not observed".
|
|
919
|
+
* Optional so a feeder predating this field (or a runtime caller) is treated
|
|
920
|
+
* as "unknown" rather than asserting the handler was readable.
|
|
921
|
+
*/
|
|
922
|
+
analyzableBody?: boolean;
|
|
923
|
+
/**
|
|
924
|
+
* `true` when the handler references `ctx.mail` / `ctx.email` (sends mail),
|
|
925
|
+
* `undefined` when the feeder couldn't read the handler body (a genuinely
|
|
926
|
+
* cross-file handler). `user_creating_mutation_without_captcha` treats
|
|
927
|
+
* `undefined` the same as `true` — fail-closed, since an unreadable handler
|
|
928
|
+
* might well send mail.
|
|
929
|
+
*/
|
|
930
|
+
callsMail?: boolean;
|
|
886
931
|
/** `true` when the handler emits a structured observability event (`ctx.log` / `ctx.span` / `ctx.trace`). */
|
|
887
932
|
emitsEvent?: boolean;
|
|
888
933
|
/**
|
|
@@ -895,8 +940,14 @@ interface AdvisorProcedureProtection {
|
|
|
895
940
|
exemptReason?: string;
|
|
896
941
|
/** The exported binding name of the procedure (e.g. `signUp`). */
|
|
897
942
|
exportName: string;
|
|
898
|
-
/**
|
|
899
|
-
|
|
943
|
+
/**
|
|
944
|
+
* `true` when the handler fans work out to a privileged, cost-bearing dispatch
|
|
945
|
+
* surface (scheduler `runAfter`/`runAt`, a queue producer send, or a workflow
|
|
946
|
+
* create). Read by the privileged-fanout lint, paired with public visibility
|
|
947
|
+
* and no rate limit. `undefined` when the feeder couldn't read the handler
|
|
948
|
+
* body — the lint treats that as `true` (fail-closed).
|
|
949
|
+
*/
|
|
950
|
+
fanOut?: boolean;
|
|
900
951
|
/** Source file relative to the lunora dir, no extension. */
|
|
901
952
|
file: string;
|
|
902
953
|
/** `true` when the handler wraps work in `try`/`catch`. */
|
|
@@ -913,20 +964,33 @@ interface AdvisorProcedureProtection {
|
|
|
913
964
|
hasEmailArg?: boolean;
|
|
914
965
|
/** Registration kind — `query` is read-only; `mutation`/`action` are write-shaped. */
|
|
915
966
|
kind: "action" | "mutation" | "query";
|
|
916
|
-
/** `true` when the handler runs any AI generation, bounded or not. */
|
|
917
|
-
runsAiGeneration?: boolean;
|
|
918
967
|
/** `true` when the handler reaches an outbound surface (`ctx.fetch`, mail, queues, storage, sql, ai, …) that can fail. */
|
|
919
968
|
reachesOutbound?: boolean;
|
|
969
|
+
/** `true` when the handler runs any AI generation, bounded or not. */
|
|
970
|
+
runsAiGeneration?: boolean;
|
|
920
971
|
/** `true` when the handler throws a bare `new Error(...)` rather than a coded `LunoraError`. */
|
|
921
972
|
throwsBareError?: boolean;
|
|
922
|
-
/**
|
|
923
|
-
|
|
973
|
+
/**
|
|
974
|
+
* `true` when the handler runs an AI generation (`generateText`/`streamText`/
|
|
975
|
+
* `generateObject`/`streamObject`) with no `maxOutputTokens` bound. Read by
|
|
976
|
+
* the `ai_unbounded_generation_public` lint (paired with public visibility).
|
|
977
|
+
* `undefined` when the feeder couldn't read the handler body — the lint
|
|
978
|
+
* treats that as `true` (fail-closed), distinct from the call-level opaque-
|
|
979
|
+
* config case, which the feeder always reports as `false` (fail-open by
|
|
980
|
+
* design — see `isUnboundedAiGeneration`).
|
|
981
|
+
*/
|
|
982
|
+
unboundedAiGeneration?: boolean;
|
|
924
983
|
/** `true` when the chain carries `.use(verifyTurnstile(...))` or a `protectPublic({ captcha })` bundle. */
|
|
925
984
|
usesCaptcha: boolean;
|
|
926
985
|
/** `true` when the chain carries `.use(emailGateMiddleware(...))` from `@lunora/auth`. Read by the `signup_mutation_without_disposable_gating` lint (paired with public visibility + a user-table write). */
|
|
927
986
|
usesEmailGate: boolean;
|
|
928
|
-
/**
|
|
929
|
-
|
|
987
|
+
/**
|
|
988
|
+
* `true` when the handler calls `ctx.db.insertManyUnsafe(...)`, which bypasses
|
|
989
|
+
* validators and triggers. Read by the `insert_many_unsafe_user_data` lint
|
|
990
|
+
* (paired with public visibility). `undefined` when the feeder couldn't read
|
|
991
|
+
* the handler body — the lint treats that as `true` (fail-closed).
|
|
992
|
+
*/
|
|
993
|
+
usesInsertManyUnsafe?: boolean;
|
|
930
994
|
/** `true` when the chain carries `.use(mask(...))`. */
|
|
931
995
|
usesMask: boolean;
|
|
932
996
|
/** `true` when the chain carries `.use(rateLimit(...))` or a `protectPublic({ rateLimit })` bundle. */
|
|
@@ -935,8 +999,14 @@ interface AdvisorProcedureProtection {
|
|
|
935
999
|
usesRls: boolean;
|
|
936
1000
|
/** `"internal"` when the procedure uses `internalQuery` / `internalMutation` / `internalAction`. */
|
|
937
1001
|
visibility: "internal" | "public";
|
|
938
|
-
/**
|
|
939
|
-
|
|
1002
|
+
/**
|
|
1003
|
+
* `true` when the handler inserts into a user/session/account-shaped table,
|
|
1004
|
+
* `undefined` when the feeder couldn't read the handler body (a genuinely
|
|
1005
|
+
* cross-file handler). `signup_mutation_without_disposable_gating` and
|
|
1006
|
+
* `user_creating_mutation_without_captcha` treat `undefined` the same as
|
|
1007
|
+
* `true` — fail-closed, since an unreadable handler might well write one.
|
|
1008
|
+
*/
|
|
1009
|
+
writesUserTable?: boolean;
|
|
940
1010
|
}
|
|
941
1011
|
/**
|
|
942
1012
|
* One query read discovered in a function body — the input the
|
|
@@ -1425,6 +1495,14 @@ interface AdvisorStorageKeyAccess {
|
|
|
1425
1495
|
line: number;
|
|
1426
1496
|
/** The bucket method invoked with the arg-derived key, e.g. `get` / `put` / `delete` / `download`. */
|
|
1427
1497
|
method: string;
|
|
1498
|
+
/**
|
|
1499
|
+
* Visibility of the enclosing procedure. `internal` procedures are not
|
|
1500
|
+
* reachable by a caller, so the "any caller can read/overwrite/delete
|
|
1501
|
+
* another user's object" premise does not hold there and the finding drops
|
|
1502
|
+
* to `INFO` (mirrors `AdvisorOwnerFieldWrite.visibility`). `undefined` when
|
|
1503
|
+
* the feeder could not attribute the access to a registered procedure.
|
|
1504
|
+
*/
|
|
1505
|
+
visibility?: "internal" | "public";
|
|
1428
1506
|
}
|
|
1429
1507
|
/**
|
|
1430
1508
|
* One tracked `ctx.storage.<bucket>.<method>(...)` upload/signing call — the
|
|
@@ -1792,6 +1870,16 @@ interface LintContext {
|
|
|
1792
1870
|
* codegen feeder; absent for runtime callers, where the lint finds nothing.
|
|
1793
1871
|
*/
|
|
1794
1872
|
flagSecurityDefaults?: ReadonlyArray<AdvisorFlagSecurityDefault>;
|
|
1873
|
+
/**
|
|
1874
|
+
* Per-function call/error/latency volume observed over the window — the
|
|
1875
|
+
* `error_rate_outlier` runtime lint's input (prototype, plan 248). Sourced
|
|
1876
|
+
* from the same `FunctionCallStat` rows `context.tableScans` already draws
|
|
1877
|
+
* `scannedTables` from (`__lunora_admin__:getFunctionStats`), scoped to
|
|
1878
|
+
* whichever shard the caller queried — see {@link AdvisorFunctionMetrics}.
|
|
1879
|
+
* Supplied by the studio backend; absent for static callers, where the lint
|
|
1880
|
+
* finds nothing.
|
|
1881
|
+
*/
|
|
1882
|
+
functionMetrics?: ReadonlyArray<AdvisorFunctionMetrics>;
|
|
1795
1883
|
/**
|
|
1796
1884
|
* `withGeoIndex("name", …)` reads discovered in function bodies — the use-side
|
|
1797
1885
|
* input the `geo_index_unused` lint cross-references against the declared geo
|
|
@@ -2202,29 +2290,6 @@ interface AnalyticsMetricsSource {
|
|
|
2202
2290
|
rows: ReadonlyArray<Record<string, unknown>>;
|
|
2203
2291
|
}>;
|
|
2204
2292
|
}
|
|
2205
|
-
/**
|
|
2206
|
-
* The AE event-name + dimension-column contract the runtime writes and this
|
|
2207
|
-
* reader reads. `blob1` is the event name; dimensions start at `blob2`.
|
|
2208
|
-
*/
|
|
2209
|
-
declare const AE_METRIC_EVENTS: {
|
|
2210
|
-
/** `lunora.index.hit` — one row per `(table, index)` use. `blob2`=table, `blob3`=index. */
|
|
2211
|
-
readonly indexHit: {
|
|
2212
|
-
readonly event: "lunora.index.hit";
|
|
2213
|
-
readonly index: "blob3";
|
|
2214
|
-
readonly table: "blob2";
|
|
2215
|
-
};
|
|
2216
|
-
/** `lunora.shard.request` — one row per shard dispatch. `blob2`=shardKey, `blob3`=group. */
|
|
2217
|
-
readonly shardRequest: {
|
|
2218
|
-
readonly event: "lunora.shard.request";
|
|
2219
|
-
readonly group: "blob3";
|
|
2220
|
-
readonly shardKey: "blob2";
|
|
2221
|
-
};
|
|
2222
|
-
/** `lunora.table.scan` — one row per full-scan. `blob2`=table. */
|
|
2223
|
-
readonly tableScan: {
|
|
2224
|
-
readonly event: "lunora.table.scan";
|
|
2225
|
-
readonly table: "blob2";
|
|
2226
|
-
};
|
|
2227
|
-
};
|
|
2228
2293
|
/** Options for the AE-backed runtime-metrics feeder. */
|
|
2229
2294
|
interface AnalyticsMetricsOptions {
|
|
2230
2295
|
/** The AE dataset (the wrangler `analytics_engine_datasets[].dataset`) to read from. */
|
|
@@ -2253,20 +2318,6 @@ interface AnalyticsRuntimeMetrics {
|
|
|
2253
2318
|
shardTraffic: AdvisorShardTraffic[];
|
|
2254
2319
|
tableScans: AdvisorTableScan[];
|
|
2255
2320
|
}
|
|
2256
|
-
/**
|
|
2257
|
-
* Reconstruct the runtime-lint input arrays (`shardTraffic` / `tableScans` /
|
|
2258
|
-
* `indexHits`) from the Analytics Engine SQL API. The three reads run
|
|
2259
|
-
* concurrently; each degrades to an empty array on a query failure, so a
|
|
2260
|
-
* partially-misconfigured read path still returns what it can.
|
|
2261
|
-
*
|
|
2262
|
-
* Feed the result into a {@link LintContext} alongside the declared schema:
|
|
2263
|
-
*
|
|
2264
|
-
* ```ts
|
|
2265
|
-
* const metrics = await loadAnalyticsRuntimeMetrics(client, { dataset: "ANALYTICS" });
|
|
2266
|
-
* runAdvisor({ schema, ...metrics }, { source: "runtime" });
|
|
2267
|
-
* ```
|
|
2268
|
-
*/
|
|
2269
|
-
declare const loadAnalyticsRuntimeMetrics: (source: AnalyticsMetricsSource, options: AnalyticsMetricsOptions) => Promise<AnalyticsRuntimeMetrics>;
|
|
2270
2321
|
/**
|
|
2271
2322
|
* Disambiguate {@link Finding}s that share a `cacheKey`.
|
|
2272
2323
|
*
|
|
@@ -2305,6 +2356,29 @@ declare const dedupeCacheKeys: (findings: ReadonlyArray<Finding>) => Finding[];
|
|
|
2305
2356
|
* additional read. When a sample is truncated, findings note the caveat.
|
|
2306
2357
|
*/
|
|
2307
2358
|
declare const constraintValidator: Lint;
|
|
2359
|
+
/**
|
|
2360
|
+
* `error_rate_outlier` — flag a function whose observed error rate is high
|
|
2361
|
+
* enough to be a genuine reliability signal, not noise.
|
|
2362
|
+
*
|
|
2363
|
+
* PROTOTYPE (plan 248): the read side of the runtime-lint category is thin (3
|
|
2364
|
+
* lints, all traffic/conflict/index shaped) next to the static category's 96.
|
|
2365
|
+
* `lunora insights` already collects and ranks this exact signal
|
|
2366
|
+
* (`errorHotspots`) for a human report; this lint closes the loop by feeding
|
|
2367
|
+
* the same per-function `errors`/`calls` counts into the `lunora advisor` gate
|
|
2368
|
+
* so a CI run can flag a genuinely failing function, not just a report a human
|
|
2369
|
+
* has to remember to read.
|
|
2370
|
+
*
|
|
2371
|
+
* The per-function volume comes from the runtime feeder (`context.
|
|
2372
|
+
* functionMetrics`): the studio backend reads `__lunora_admin__:getFunctionStats`
|
|
2373
|
+
* for the shard in scope — the SAME rows `context.tableScans` already draws
|
|
2374
|
+
* `scannedTables` from, just projecting `errors`/`calls` instead. See
|
|
2375
|
+
* `AdvisorFunctionMetrics` for the scope caveat (one shard, not fanned out
|
|
2376
|
+
* cross-shard like `context.shardTraffic`) and `plans/248-runtime-lints-design.md`
|
|
2377
|
+
* for the reachability finding and open questions this prototype does not
|
|
2378
|
+
* resolve (threshold model, single- vs. cross-shard scope, whether this ships
|
|
2379
|
+
* as its own lint or merges with a future latency counterpart).
|
|
2380
|
+
*/
|
|
2381
|
+
declare const errorRateOutlier: Lint;
|
|
2308
2382
|
/**
|
|
2309
2383
|
* `hot_shard` — flag a shard whose request share is disproportionately high.
|
|
2310
2384
|
*
|
|
@@ -3242,15 +3316,29 @@ declare const mutatorFullRowReplace: Lint;
|
|
|
3242
3316
|
* Flags a non-deterministic API call inside a `query(...)` or `mutation(...)`
|
|
3243
3317
|
* handler body.
|
|
3244
3318
|
*
|
|
3245
|
-
*
|
|
3246
|
-
*
|
|
3247
|
-
*
|
|
3248
|
-
*
|
|
3249
|
-
*
|
|
3250
|
-
*
|
|
3251
|
-
*
|
|
3252
|
-
* `
|
|
3253
|
-
*
|
|
3319
|
+
* The two procedure kinds do NOT share the same hazard (issue #286).
|
|
3320
|
+
*
|
|
3321
|
+
* A `query` handler genuinely CAN run more than once for the same logical
|
|
3322
|
+
* read: a live subscription re-runs its query whenever a table it reads
|
|
3323
|
+
* changes, so `Date.now()`/`Math.random()`/etc. there can flicker between
|
|
3324
|
+
* re-evaluations — this half stays at WARN.
|
|
3325
|
+
*
|
|
3326
|
+
* A `mutation` handler does NOT replay under ordinary dispatch on this
|
|
3327
|
+
* (DO-backed) runtime: a client-issued idempotency key dedups a replayed call
|
|
3328
|
+
* to the **cached** result without re-running the handler, and an
|
|
3329
|
+
* optimistic-concurrency conflict throws back to the caller as an error
|
|
3330
|
+
* rather than triggering an internal retry of the handler. So a mutation
|
|
3331
|
+
* handler body runs at most once per logical write, and `Date.now()` inside
|
|
3332
|
+
* one is stable — the premise this lint used to apply uniformly (borrowed
|
|
3333
|
+
* from Convex's OCC-retries-the-handler model, which does not hold here) was
|
|
3334
|
+
* responsible for 193 of 385 non-INFO findings on one real codebase, all from
|
|
3335
|
+
* the ordinary "stamp `createdAt`" pattern. Dropped to INFO rather than
|
|
3336
|
+
* silenced: a mutation invoked from inside a workflow step or queue consumer
|
|
3337
|
+
* CAN be re-dispatched if the surrounding step/consumer itself replays (a
|
|
3338
|
+
* fresh `ctx.runMutation` call from the step's perspective, not a replay this
|
|
3339
|
+
* lint can distinguish from any other mutation call), so the finding stays as
|
|
3340
|
+
* a breadcrumb rather than disappearing outright. See the "Determinism: what
|
|
3341
|
+
* actually replays" section of the `@lunora/server` docs.
|
|
3254
3342
|
*
|
|
3255
3343
|
* This lint runs when the codegen feeder has supplied call evidence
|
|
3256
3344
|
* (`context.nondeterministicCalls` present); a runtime caller with no evidence
|
|
@@ -4350,6 +4438,14 @@ declare const scoreAdvisor: (procedures: ReadonlyArray<AdvisorProcedureProtectio
|
|
|
4350
4438
|
* Runs before any rule so a lint can gate on it, and feeds the global weighting:
|
|
4351
4439
|
* a handler touching identity, mail, or tenant-scoped rows pulls harder on the
|
|
4352
4440
|
* grade than a plain read. `none` means no signal fired — not "safe".
|
|
4441
|
+
*
|
|
4442
|
+
* A fact reads `undefined` only when the feeder couldn't read the handler body
|
|
4443
|
+
* (a cross-file handler, or — degenerately — a partial payload), and that stays
|
|
4444
|
+
* fail-closed: `level` goes `"high"` the same as a proven `true` would. The
|
|
4445
|
+
* reason text does not, though — asserting "writes an identity table" for a
|
|
4446
|
+
* fact that was never observed would misattribute an unproven claim. A proven
|
|
4447
|
+
* `true` still gets its specific reason; every `undefined` fact collapses into
|
|
4448
|
+
* one shared "could not be read" reason instead of one unproven claim apiece.
|
|
4353
4449
|
*/
|
|
4354
4450
|
declare const classifySensitivity: (procedure: AdvisorProcedureProtection) => Sensitivity;
|
|
4355
4451
|
/**
|
|
@@ -4383,4 +4479,4 @@ interface RunAdvisorOptions {
|
|
|
4383
4479
|
* `static` lints at build time and defer `runtime` lints to a live shard.
|
|
4384
4480
|
*/
|
|
4385
4481
|
declare const runAdvisor: (context: LintContext, options?: RunAdvisorOptions) => Finding[];
|
|
4386
|
-
export {
|
|
4482
|
+
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 AdvisorFlagSecurityDefault, type AdvisorFunctionMetrics, 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 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, compareToBaseline, constraintValidator, containerInstanceKeyFromUserInput, containerOversizedInstance, containerPublicInternet, containerRuntimeEgressRelaxation, containerStartEnableInternetOverride, dedupeCacheKeys, duplicateIndex, emptyIndex, errorRateOutlier, errorWithoutCatalog, exportSinkMisconfigured, externalSourceIncrementalNoDeletePath, externalSourceOnGlobal, externalSourceUnscoped, filterOnPrimaryKey, filterWithoutIndex, flagGatesSecurityWithUnsafeDefault, fromServerSchema, geoIndexFieldNotGeopoint, geoIndexUnused, 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 a}from"./packem_shared/dedupeCacheKeys-7JgxaQWV.mjs";import m from"./packem_shared/constraintValidator-N339mfyx.mjs";import s from"./packem_shared/hotShard-DVs_U17R.mjs";import n from"./packem_shared/indexUtilization-QfbIH_w4.mjs";import p from"./packem_shared/actionFetchSsrf-BbWxy1Yf.mjs";import f from"./packem_shared/actionWithoutErrorHandling-D1DqSPpg.mjs";import l from"./packem_shared/adminRouteWithoutGuard-DSo4t9j4.mjs";import u from"./packem_shared/aiRawRunEscapeHatch-DfhjYm43.mjs";import d from"./packem_shared/aiRunWithoutLogging-CMvQPNBr.mjs";import c from"./packem_shared/aiToolSideEffectPromptInjection-DnWQOoKi.mjs";import $ from"./packem_shared/aiUnboundedGenerationPublic-D3HZrh5_.mjs";import h from"./packem_shared/allowUnauthenticatedShardAccessEnabled-D4aMNBb0.mjs";import g from"./packem_shared/authApiCallWithoutHeaders-BItMMdZP.mjs";import b from"./packem_shared/authCsrfCheckDisabled-De9ammtV.mjs";import U from"./packem_shared/authEmailVerificationDisabled-BFKdJcx4.mjs";import S from"./packem_shared/authScimWithoutTransactions-DCtLXzks.mjs";import y from"./packem_shared/authSecureCookiesDisabled-B4WIzvMm.mjs";import I from"./packem_shared/authSessionFreshageZero-Bpcaf_Pe.mjs";import A from"./packem_shared/authTrustedOriginsWildcard-DjU9NXEm.mjs";import T from"./packem_shared/browserAllowPrivateTargets-Dxyeh6lg.mjs";import W from"./packem_shared/browserUserUrlWithoutAllowlist-BHBS6b0N.mjs";import R from"./packem_shared/circularFk-BMuJKmuZ.mjs";import w from"./packem_shared/containerInstanceKeyFromUserInput-C8S4T3ty.mjs";import x from"./packem_shared/containerOversizedInstance-B9wW_sU5.mjs";import k from"./packem_shared/containerPublicInternet-Bx2J7tha.mjs";import F from"./packem_shared/containerRuntimeEgressRelaxation-BQ2ZD1aV.mjs";import v from"./packem_shared/containerStartEnableInternetOverride-B62WUura.mjs";import C from"./packem_shared/duplicateIndex-DFT7qLQD.mjs";import P from"./packem_shared/emptyIndex-BiXEfyfN.mjs";import M from"./packem_shared/errorWithoutCatalog-DTDVKU4b.mjs";import D from"./packem_shared/exportSinkMisconfigured-DvpAaa6j.mjs";import E from"./packem_shared/externalSourceIncrementalNoDeletePath-DnoL64YY.mjs";import O from"./packem_shared/externalSourceOnGlobal-DNPLoNUg.mjs";import N from"./packem_shared/externalSourceUnscoped-DqBp5jYr.mjs";import G from"./packem_shared/filterOnPrimaryKey-CPPa7fog.mjs";import K from"./packem_shared/filterWithoutIndex-DYLs-Jf2.mjs";import L from"./packem_shared/flagGatesSecurityWithUnsafeDefault-DPGFEHTt.mjs";import V from"./packem_shared/geoIndexFieldNotGeopoint-DAGLZtaH.mjs";import z from"./packem_shared/geoIndexUnused-BgDE6_AR.mjs";import _ from"./packem_shared/hardcodedSecret-rIRc3aNk.mjs";import j from"./packem_shared/httpActionMissingAuthGuard-BZDdeW_D.mjs";import q from"./packem_shared/httpActionResponseHeaderInjection-D063rB7U.mjs";import H from"./packem_shared/hyperdriveOutsideAction-CRmOQz67.mjs";import B from"./packem_shared/identityUndeclaredClaimTrusted-BYRYGQ74.mjs";import Q from"./packem_shared/imagesUrlSourceFromUserInput-Cy5pc_dA.mjs";import Z from"./packem_shared/indexReferencesUnknownField-r3QHlaz6.mjs";import J from"./packem_shared/insertManyUnsafeUserData-DrH7PdRs.mjs";import X from"./packem_shared/kvUnscopedUserKeyIdor-B4lGxRxh.mjs";import Y from"./packem_shared/mailInboundDispatchWithoutVerify-BivYJ5F9.mjs";import rr from"./packem_shared/mailRecipientFromRequestInput-BR_oSh65.mjs";import or from"./packem_shared/maskUncoveredPiiColumn-DqPfZM2O.mjs";import tr from"./packem_shared/maskWeakHashStrategyOnPii-CEiiGgT7.mjs";import ir from"./packem_shared/maskedRelationLeakViaWith-CxYk0MDW.mjs";import er from"./packem_shared/mutatorFullRowReplace-CEgecSGm.mjs";import ar from"./packem_shared/nondeterministicQueryMutation-BaMfbAk_.mjs";import mr from"./packem_shared/normalizeIdUsedAsAuthorization-FjISSXJm.mjs";import sr from"./packem_shared/notifyMissingPushConfig-C_GbVw3S.mjs";import nr from"./packem_shared/notifySendOutsideAction-B0jZuUP3.mjs";import pr from"./packem_shared/outputProjectionMissingOnPublicRead-9Pm9766s.mjs";import fr from"./packem_shared/ownerFieldFromArgsNotAuth-D4K-_0aD.mjs";import lr from"./packem_shared/paymentCreateWithoutAuthorize-CurtqN8K.mjs";import ur from"./packem_shared/paymentWebhookWideTolerance-I7ingfR8.mjs";import dr from"./packem_shared/plaintextSecretInWranglerVariables-DXjKIm8H.mjs";import cr from"./packem_shared/policyReferencesUnknownTable-D3WelAQa.mjs";import $r from"./packem_shared/privilegedDispatchUnvalidatedPayload-74GuGye1.mjs";import hr from"./packem_shared/privilegedFanoutFromPublicProcedure-C2j2GSUN.mjs";import gr from"./packem_shared/procedureWithoutStructuredEvent-D4Qi2vH7.mjs";import br from"./packem_shared/publicArgumentUsesAny-BJb4cTqL.mjs";import Ur from"./packem_shared/publicMutationWithoutRatelimit-D9phluHW.mjs";import Sr from"./packem_shared/publicTableRlsOptoutConfusion-CaWtPGAn.mjs";import yr from"./packem_shared/queueWithoutDlq-BLY3iy9Q.mjs";import Ir from"./packem_shared/r2sqlOutsideAction-CKK_I1Ms.mjs";import Ar from"./packem_shared/ratelimitDefaultMemoryStore-mOdI3Jmb.mjs";import Tr from"./packem_shared/ratelimitKeySpoofableOrGlobal-DaGId_Fh.mjs";import Wr from"./packem_shared/ratelimitMiddlewareFailOpen-CTWVosEc.mjs";import Rr from"./packem_shared/relationReferencesUnknownField-clqyB69Y.mjs";import wr from"./packem_shared/relationReferencesUnknownTable-CLdrhkmS.mjs";import xr from"./packem_shared/rlsUncoveredTable-DYHgnoos.mjs";import kr from"./packem_shared/shapeTargetsGlobalTable-Blz-aSkg.mjs";import Fr from"./packem_shared/shapeUnknownTable-DFxtyFio.mjs";import vr from"./packem_shared/signupMutationWithoutDisposableGating-BNzlsE0F.mjs";import Cr from"./packem_shared/softDeleteIncludeDeletedFromArgs-DgxpQHRA.mjs";import Pr from"./packem_shared/sqlInjectionRisk-D2NaT-5c.mjs";import Mr from"./packem_shared/storageGenerateUploadUrlNoContentTypePin-C9cjl6Jh.mjs";import Dr from"./packem_shared/storageKeyFromUserArgs-BeS5cp56.mjs";import Er from"./packem_shared/storagePresignedUrlForPrivateContent-j7VhwIdu.mjs";import Or from"./packem_shared/storageUploadWithoutContentTypeAllowlist-oxKLecuf.mjs";import Nr from"./packem_shared/storageUploadWithoutMaxSize-DUDxPutk.mjs";import Gr from"./packem_shared/tableWithoutInsert-D3AhaNzR.mjs";import Kr from"./packem_shared/ttlFieldNotTimestamp-BFiRkSZD.mjs";import Lr from"./packem_shared/unboundedStringArgument-AkkIcr-S.mjs";import Vr from"./packem_shared/unindexedForeignKey-DK4eWXkb.mjs";import zr from"./packem_shared/unindexedRelationTarget-B5fFq1E8.mjs";import _r from"./packem_shared/unrestrictedWhereBranch-YKiydie4.mjs";import jr from"./packem_shared/userCreatingMutationWithoutCaptcha-iDjRkUFO.mjs";import qr from"./packem_shared/vectorsNamespaceFromUserInput-Cyq5B2kF.mjs";import Hr from"./packem_shared/workflowDuplicateStepName-cXF6Rpvg.mjs";import Br from"./packem_shared/workflowUnknownTarget-JF8rITLP.mjs";import Qr from"./packem_shared/workflowUnused-Dx_FQwGE.mjs";import{AE_METRIC_EVENTS as Bt,loadAnalyticsRuntimeMetrics as Qt}from"./packem_shared/AE_METRIC_EVENTS-LP_0D7Uw.mjs";import{compareToBaseline as Jt,parseAdvisorMap as Xt}from"./packem_shared/compareToBaseline-T1_kl8VY.mjs";import{gradeFromScore as ri}from"./packem_shared/gradeFromScore-DW82YDVY.mjs";import{MAP_VERSION as ti,byCodepoint as ii,scoreAdvisor as ei}from"./packem_shared/MAP_VERSION-BS08jBkS.mjs";import{default as mi}from"./packem_shared/classifySensitivity-DjswRYWd.mjs";import{fromServerSchema as ni}from"./packem_shared/fromServerSchema-CJ-AQ2kw.mjs";const Zr=[Z,wr,Rr,Br,Hr,Fr,E,O,N,P,M,V,z,D,Kr,R,Vr,zr,C,Gr,Qr,yr,G,K,kr,er,ar,H,Ir,g,cr,xr,or,tr,x,k,Ur,_r,jr,vr,br,Lr,_,Pr,l,lr,Y,Ar,T,hr,gr,J,$,p,f,fr,Dr,X,w,u,d,qr,rr,W,$r,v,F,S,A,b,y,U,I,Q,Tr,Sr,h,Or,Nr,Mr,Er,j,q,Wr,L,c,B,ur,Cr,ir,pr,mr,nr,sr,dr],Jr=[s,n,m],Xr=[...Zr,...Jr],jt=(i,r={})=>{const e=r.lints??Xr,o=[];for(const t of e)r.source!==void 0&&t.source!==r.source||o.push(...t.run(i));return a(o)};export{Bt as AE_METRIC_EVENTS,Xr as ALL_LINTS,ti as MAP_VERSION,Jr as RUNTIME_LINTS,Zr as STATIC_LINTS,p as actionFetchSsrf,f as actionWithoutErrorHandling,l as adminRouteWithoutGuard,u as aiRawRunEscapeHatch,d as aiRunWithoutLogging,c as aiToolSideEffectPromptInjection,$ as aiUnboundedGenerationPublic,h as allowUnauthenticatedShardAccessEnabled,g as authApiCallWithoutHeaders,b as authCsrfCheckDisabled,U as authEmailVerificationDisabled,S as authScimWithoutTransactions,y as authSecureCookiesDisabled,I as authSessionFreshageZero,A as authTrustedOriginsWildcard,T as browserAllowPrivateTargets,W as browserUserUrlWithoutAllowlist,ii as byCodepoint,R as circularFk,mi as classifySensitivity,Jt as compareToBaseline,m as constraintValidator,w as containerInstanceKeyFromUserInput,x as containerOversizedInstance,k as containerPublicInternet,F as containerRuntimeEgressRelaxation,v as containerStartEnableInternetOverride,a as dedupeCacheKeys,C as duplicateIndex,P as emptyIndex,M as errorWithoutCatalog,D as exportSinkMisconfigured,E as externalSourceIncrementalNoDeletePath,O as externalSourceOnGlobal,N as externalSourceUnscoped,G as filterOnPrimaryKey,K as filterWithoutIndex,L as flagGatesSecurityWithUnsafeDefault,ni as fromServerSchema,V as geoIndexFieldNotGeopoint,z as geoIndexUnused,ri as gradeFromScore,_ as hardcodedSecret,s as hotShard,j as httpActionMissingAuthGuard,q as httpActionResponseHeaderInjection,H as hyperdriveOutsideAction,B as identityUndeclaredClaimTrusted,Q as imagesUrlSourceFromUserInput,Z as indexReferencesUnknownField,n as indexUtilization,J as insertManyUnsafeUserData,X as kvUnscopedUserKeyIdor,Qt as loadAnalyticsRuntimeMetrics,Y as mailInboundDispatchWithoutVerify,rr as mailRecipientFromRequestInput,or as maskUncoveredPiiColumn,tr as maskWeakHashStrategyOnPii,ir as maskedRelationLeakViaWith,er as mutatorFullRowReplace,ar as nondeterministicQueryMutation,mr as normalizeIdUsedAsAuthorization,sr as notifyMissingPushConfig,nr as notifySendOutsideAction,pr as outputProjectionMissingOnPublicRead,fr as ownerFieldFromArgsNotAuth,Xt as parseAdvisorMap,lr as paymentCreateWithoutAuthorize,ur as paymentWebhookWideTolerance,dr as plaintextSecretInWranglerVariables,cr as policyReferencesUnknownTable,$r as privilegedDispatchUnvalidatedPayload,hr as privilegedFanoutFromPublicProcedure,gr as procedureWithoutStructuredEvent,br as publicArgumentUsesAny,Ur as publicMutationWithoutRatelimit,Sr as publicTableRlsOptoutConfusion,yr as queueWithoutDlq,Ir as r2sqlOutsideAction,Ar as ratelimitDefaultMemoryStore,Tr as ratelimitKeySpoofableOrGlobal,Wr as ratelimitMiddlewareFailOpen,Rr as relationReferencesUnknownField,wr as relationReferencesUnknownTable,xr as rlsUncoveredTable,jt as runAdvisor,ei as scoreAdvisor,kr as shapeTargetsGlobalTable,Fr as shapeUnknownTable,vr as signupMutationWithoutDisposableGating,Cr as softDeleteIncludeDeletedFromArgs,Pr as sqlInjectionRisk,Mr as storageGenerateUploadUrlNoContentTypePin,Dr as storageKeyFromUserArgs,Er as storagePresignedUrlForPrivateContent,Or as storageUploadWithoutContentTypeAllowlist,Nr as storageUploadWithoutMaxSize,Gr as tableWithoutInsert,Kr as ttlFieldNotTimestamp,Lr as unboundedStringArgument,Vr as unindexedForeignKey,zr as unindexedRelationTarget,_r as unrestrictedWhereBranch,jr as userCreatingMutationWithoutCaptcha,qr as vectorsNamespaceFromUserInput,Hr as workflowDuplicateStepName,Br as workflowUnknownTarget,Qr as workflowUnused};
|
|
1
|
+
import{dedupeCacheKeys as a}from"./packem_shared/dedupeCacheKeys-7JgxaQWV.mjs";import m from"./packem_shared/constraintValidator-N339mfyx.mjs";import s from"./packem_shared/errorRateOutlier-CgK42sq0.mjs";import n from"./packem_shared/hotShard-DVs_U17R.mjs";import p from"./packem_shared/indexUtilization-QfbIH_w4.mjs";import f from"./packem_shared/actionFetchSsrf-BbWxy1Yf.mjs";import l from"./packem_shared/actionWithoutErrorHandling-D1DqSPpg.mjs";import u from"./packem_shared/adminRouteWithoutGuard-DSo4t9j4.mjs";import d from"./packem_shared/aiRawRunEscapeHatch-DfhjYm43.mjs";import c from"./packem_shared/aiRunWithoutLogging-CMvQPNBr.mjs";import $ from"./packem_shared/aiToolSideEffectPromptInjection-DnWQOoKi.mjs";import h from"./packem_shared/aiUnboundedGenerationPublic-iu7crnSa.mjs";import g from"./packem_shared/allowUnauthenticatedShardAccessEnabled-D4aMNBb0.mjs";import b from"./packem_shared/authApiCallWithoutHeaders-BItMMdZP.mjs";import U from"./packem_shared/authCsrfCheckDisabled-De9ammtV.mjs";import S from"./packem_shared/authEmailVerificationDisabled-BFKdJcx4.mjs";import y from"./packem_shared/authScimWithoutTransactions-DCtLXzks.mjs";import I from"./packem_shared/authSecureCookiesDisabled-B4WIzvMm.mjs";import A from"./packem_shared/authSessionFreshageZero-Bpcaf_Pe.mjs";import W from"./packem_shared/authTrustedOriginsWildcard-DjU9NXEm.mjs";import T from"./packem_shared/browserAllowPrivateTargets-Dxyeh6lg.mjs";import R from"./packem_shared/browserUserUrlWithoutAllowlist-BHBS6b0N.mjs";import w from"./packem_shared/circularFk-BMuJKmuZ.mjs";import k from"./packem_shared/containerInstanceKeyFromUserInput-C8S4T3ty.mjs";import x from"./packem_shared/containerOversizedInstance-B9wW_sU5.mjs";import F from"./packem_shared/containerPublicInternet-Bx2J7tha.mjs";import v from"./packem_shared/containerRuntimeEgressRelaxation-BQ2ZD1aV.mjs";import C from"./packem_shared/containerStartEnableInternetOverride-B62WUura.mjs";import P from"./packem_shared/duplicateIndex-DFT7qLQD.mjs";import M from"./packem_shared/emptyIndex-BiXEfyfN.mjs";import O from"./packem_shared/errorWithoutCatalog-DTDVKU4b.mjs";import D from"./packem_shared/exportSinkMisconfigured-DvpAaa6j.mjs";import N from"./packem_shared/externalSourceIncrementalNoDeletePath-DnoL64YY.mjs";import E from"./packem_shared/externalSourceOnGlobal-DNPLoNUg.mjs";import G from"./packem_shared/externalSourceUnscoped-DqBp5jYr.mjs";import K from"./packem_shared/filterOnPrimaryKey-CPPa7fog.mjs";import L from"./packem_shared/filterWithoutIndex-DYLs-Jf2.mjs";import z from"./packem_shared/flagGatesSecurityWithUnsafeDefault-DPGFEHTt.mjs";import V from"./packem_shared/geoIndexFieldNotGeopoint-DAGLZtaH.mjs";import j from"./packem_shared/geoIndexUnused-BgDE6_AR.mjs";import q from"./packem_shared/hardcodedSecret-rIRc3aNk.mjs";import H from"./packem_shared/httpActionMissingAuthGuard-BZDdeW_D.mjs";import _ from"./packem_shared/httpActionResponseHeaderInjection-D063rB7U.mjs";import B from"./packem_shared/hyperdriveOutsideAction-CRmOQz67.mjs";import Q from"./packem_shared/identityUndeclaredClaimTrusted-BYRYGQ74.mjs";import Z from"./packem_shared/imagesUrlSourceFromUserInput-Cy5pc_dA.mjs";import J from"./packem_shared/indexReferencesUnknownField-r3QHlaz6.mjs";import X from"./packem_shared/insertManyUnsafeUserData-xLYAkcKj.mjs";import Y from"./packem_shared/kvUnscopedUserKeyIdor-B4lGxRxh.mjs";import rr from"./packem_shared/mailInboundDispatchWithoutVerify-BivYJ5F9.mjs";import or from"./packem_shared/mailRecipientFromRequestInput-BR_oSh65.mjs";import tr from"./packem_shared/maskUncoveredPiiColumn-DqPfZM2O.mjs";import ir from"./packem_shared/maskWeakHashStrategyOnPii-CEiiGgT7.mjs";import er from"./packem_shared/maskedRelationLeakViaWith-CxYk0MDW.mjs";import ar from"./packem_shared/mutatorFullRowReplace-CEgecSGm.mjs";import mr from"./packem_shared/nondeterministicQueryMutation-BZV_biUV.mjs";import sr from"./packem_shared/normalizeIdUsedAsAuthorization-FjISSXJm.mjs";import nr from"./packem_shared/notifyMissingPushConfig-C_GbVw3S.mjs";import pr from"./packem_shared/notifySendOutsideAction-B0jZuUP3.mjs";import fr from"./packem_shared/outputProjectionMissingOnPublicRead-9Pm9766s.mjs";import lr from"./packem_shared/ownerFieldFromArgsNotAuth-D4K-_0aD.mjs";import ur from"./packem_shared/paymentCreateWithoutAuthorize-CurtqN8K.mjs";import dr from"./packem_shared/paymentWebhookWideTolerance-I7ingfR8.mjs";import cr from"./packem_shared/plaintextSecretInWranglerVariables-DXjKIm8H.mjs";import $r from"./packem_shared/policyReferencesUnknownTable-D3WelAQa.mjs";import hr from"./packem_shared/privilegedDispatchUnvalidatedPayload-74GuGye1.mjs";import gr from"./packem_shared/privilegedFanoutFromPublicProcedure-sMsZN4uP.mjs";import br from"./packem_shared/procedureWithoutStructuredEvent-D4Qi2vH7.mjs";import Ur from"./packem_shared/publicArgumentUsesAny-BJb4cTqL.mjs";import Sr from"./packem_shared/publicMutationWithoutRatelimit-D9phluHW.mjs";import yr from"./packem_shared/publicTableRlsOptoutConfusion-CaWtPGAn.mjs";import Ir from"./packem_shared/queueWithoutDlq-BLY3iy9Q.mjs";import Ar from"./packem_shared/r2sqlOutsideAction-CKK_I1Ms.mjs";import Wr from"./packem_shared/ratelimitDefaultMemoryStore-mOdI3Jmb.mjs";import Tr from"./packem_shared/ratelimitKeySpoofableOrGlobal-DaGId_Fh.mjs";import Rr from"./packem_shared/ratelimitMiddlewareFailOpen-CTWVosEc.mjs";import wr from"./packem_shared/relationReferencesUnknownField-clqyB69Y.mjs";import kr from"./packem_shared/relationReferencesUnknownTable-CLdrhkmS.mjs";import xr from"./packem_shared/rlsUncoveredTable-DYHgnoos.mjs";import Fr from"./packem_shared/shapeTargetsGlobalTable-Blz-aSkg.mjs";import vr from"./packem_shared/shapeUnknownTable-DFxtyFio.mjs";import Cr from"./packem_shared/signupMutationWithoutDisposableGating-Tgv9SbsY.mjs";import Pr from"./packem_shared/softDeleteIncludeDeletedFromArgs-DgxpQHRA.mjs";import Mr from"./packem_shared/sqlInjectionRisk-D2NaT-5c.mjs";import Or from"./packem_shared/storageGenerateUploadUrlNoContentTypePin-C9cjl6Jh.mjs";import Dr from"./packem_shared/storageKeyFromUserArgs-CI-6kyte.mjs";import Nr from"./packem_shared/storagePresignedUrlForPrivateContent-j7VhwIdu.mjs";import Er from"./packem_shared/storageUploadWithoutContentTypeAllowlist-oxKLecuf.mjs";import Gr from"./packem_shared/storageUploadWithoutMaxSize-DUDxPutk.mjs";import Kr from"./packem_shared/tableWithoutInsert-D3AhaNzR.mjs";import Lr from"./packem_shared/ttlFieldNotTimestamp-BFiRkSZD.mjs";import zr from"./packem_shared/unboundedStringArgument-AkkIcr-S.mjs";import Vr from"./packem_shared/unindexedForeignKey-DK4eWXkb.mjs";import jr from"./packem_shared/unindexedRelationTarget-B5fFq1E8.mjs";import qr from"./packem_shared/unrestrictedWhereBranch-YKiydie4.mjs";import Hr from"./packem_shared/userCreatingMutationWithoutCaptcha-DGiZ3_W7.mjs";import _r from"./packem_shared/vectorsNamespaceFromUserInput-Cyq5B2kF.mjs";import Br from"./packem_shared/workflowDuplicateStepName-cXF6Rpvg.mjs";import Qr from"./packem_shared/workflowUnknownTarget-JF8rITLP.mjs";import Zr from"./packem_shared/workflowUnused-Dx_FQwGE.mjs";import{compareToBaseline as Zt,parseAdvisorMap as Jt}from"./packem_shared/compareToBaseline-BwXFusQG.mjs";import{gradeFromScore as Yt}from"./packem_shared/gradeFromScore-DW82YDVY.mjs";import{MAP_VERSION as oi,byCodepoint as ti,scoreAdvisor as ii}from"./packem_shared/MAP_VERSION-DxkFCiLX.mjs";import{default as ai}from"./packem_shared/classifySensitivity-B1kkm44P.mjs";import{fromServerSchema as si}from"./packem_shared/fromServerSchema-CJ-AQ2kw.mjs";const Jr=[J,kr,wr,Qr,Br,vr,N,E,G,M,O,V,j,D,Lr,w,Vr,jr,P,Kr,Zr,Ir,K,L,Fr,ar,mr,B,Ar,b,$r,xr,tr,ir,x,F,Sr,qr,Hr,Cr,Ur,zr,q,Mr,u,ur,rr,Wr,T,gr,br,X,h,f,l,lr,Dr,Y,k,d,c,_r,or,R,hr,C,v,y,W,U,I,S,A,Z,Tr,yr,g,Er,Gr,Or,Nr,H,_,Rr,z,$,Q,dr,Pr,er,fr,sr,pr,nr,cr],Xr=[n,p,m,s],Yr=[...Jr,...Xr],_t=(i,r={})=>{const e=r.lints??Yr,o=[];for(const t of e)r.source!==void 0&&t.source!==r.source||o.push(...t.run(i));return a(o)};export{Yr as ALL_LINTS,oi as MAP_VERSION,Xr as RUNTIME_LINTS,Jr as STATIC_LINTS,f as actionFetchSsrf,l as actionWithoutErrorHandling,u as adminRouteWithoutGuard,d as aiRawRunEscapeHatch,c as aiRunWithoutLogging,$ as aiToolSideEffectPromptInjection,h as aiUnboundedGenerationPublic,g as allowUnauthenticatedShardAccessEnabled,b as authApiCallWithoutHeaders,U as authCsrfCheckDisabled,S as authEmailVerificationDisabled,y as authScimWithoutTransactions,I as authSecureCookiesDisabled,A as authSessionFreshageZero,W as authTrustedOriginsWildcard,T as browserAllowPrivateTargets,R as browserUserUrlWithoutAllowlist,ti as byCodepoint,w as circularFk,ai as classifySensitivity,Zt as compareToBaseline,m as constraintValidator,k as containerInstanceKeyFromUserInput,x as containerOversizedInstance,F as containerPublicInternet,v as containerRuntimeEgressRelaxation,C as containerStartEnableInternetOverride,a as dedupeCacheKeys,P as duplicateIndex,M as emptyIndex,s as errorRateOutlier,O as errorWithoutCatalog,D as exportSinkMisconfigured,N as externalSourceIncrementalNoDeletePath,E as externalSourceOnGlobal,G as externalSourceUnscoped,K as filterOnPrimaryKey,L as filterWithoutIndex,z as flagGatesSecurityWithUnsafeDefault,si as fromServerSchema,V as geoIndexFieldNotGeopoint,j as geoIndexUnused,Yt as gradeFromScore,q as hardcodedSecret,n as hotShard,H as httpActionMissingAuthGuard,_ as httpActionResponseHeaderInjection,B as hyperdriveOutsideAction,Q as identityUndeclaredClaimTrusted,Z as imagesUrlSourceFromUserInput,J as indexReferencesUnknownField,p as indexUtilization,X as insertManyUnsafeUserData,Y as kvUnscopedUserKeyIdor,rr as mailInboundDispatchWithoutVerify,or as mailRecipientFromRequestInput,tr as maskUncoveredPiiColumn,ir as maskWeakHashStrategyOnPii,er as maskedRelationLeakViaWith,ar as mutatorFullRowReplace,mr as nondeterministicQueryMutation,sr as normalizeIdUsedAsAuthorization,nr as notifyMissingPushConfig,pr as notifySendOutsideAction,fr as outputProjectionMissingOnPublicRead,lr as ownerFieldFromArgsNotAuth,Jt as parseAdvisorMap,ur as paymentCreateWithoutAuthorize,dr as paymentWebhookWideTolerance,cr as plaintextSecretInWranglerVariables,$r as policyReferencesUnknownTable,hr as privilegedDispatchUnvalidatedPayload,gr as privilegedFanoutFromPublicProcedure,br as procedureWithoutStructuredEvent,Ur as publicArgumentUsesAny,Sr as publicMutationWithoutRatelimit,yr as publicTableRlsOptoutConfusion,Ir as queueWithoutDlq,Ar as r2sqlOutsideAction,Wr as ratelimitDefaultMemoryStore,Tr as ratelimitKeySpoofableOrGlobal,Rr as ratelimitMiddlewareFailOpen,wr as relationReferencesUnknownField,kr as relationReferencesUnknownTable,xr as rlsUncoveredTable,_t as runAdvisor,ii as scoreAdvisor,Fr as shapeTargetsGlobalTable,vr as shapeUnknownTable,Cr as signupMutationWithoutDisposableGating,Pr as softDeleteIncludeDeletedFromArgs,Mr as sqlInjectionRisk,Or as storageGenerateUploadUrlNoContentTypePin,Dr as storageKeyFromUserArgs,Nr as storagePresignedUrlForPrivateContent,Er as storageUploadWithoutContentTypeAllowlist,Gr as storageUploadWithoutMaxSize,Kr as tableWithoutInsert,Lr as ttlFieldNotTimestamp,zr as unboundedStringArgument,Vr as unindexedForeignKey,jr as unindexedRelationTarget,qr as unrestrictedWhereBranch,Hr as userCreatingMutationWithoutCaptcha,_r as vectorsNamespaceFromUserInput,Br as workflowDuplicateStepName,Qr as workflowUnknownTarget,Zr as workflowUnused};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{scoreProcedure as h,procedureWeight as y,coverageFromScore as k,scoreGlobal as N,gradeFromScore as
|
|
1
|
+
import{scoreProcedure as h,procedureWeight as y,coverageFromScore as k,scoreGlobal as N,gradeFromScore as $,projectWeight as b,worstLevel as M,weightFor as j}from"./gradeFromScore-DW82YDVY.mjs";import F from"./classifySensitivity-B1kkm44P.mjs";const S=(e,o)=>e===o?0:e<o?-1:1,w=(e,o)=>S(e.name,o.name),x=(e,o)=>{const t=e[o];return typeof t=="string"?t:void 0},p=(e,o)=>`${e}#${o}`,R=(e,o,t,s)=>e===void 0?{level:o,name:t,occurrences:1,weight:s}:{level:M(e.level,o),name:t,occurrences:e.occurrences+1,weight:Math.max(e.weight,s)},A=(e,o,t)=>{const s=new Map;for(const a of e)s.set(p(a.file,a.exportName),new Map);const c=new Map;for(const a of o){const n=x(a.metadata,"file"),l=x(a.metadata,"exportName"),m=(n!==void 0&&l!==void 0?s.get(p(n,l)):void 0)??c;m.set(a.name,R(m.get(a.name),a.level,a.name,t(a)))}return{byProcedure:new Map([...s].map(([a,n])=>[a,[...n.values()].toSorted(w)])),project:[...c.values()].toSorted(w)}},I=(e,o)=>{const t={clean:0,exempt:0,failing:0,warned:0};let s=o.checks.length;for(const c of e)t[c.coverage]+=1,s+=c.checks.length;return{clean:t.clean,exempt:t.exempt,failing:t.failing,procedures:e.length,rulesFired:s,warned:t.warned}},O=1,W=(e,o,t={})=>{const s=new Set(t.exempt),c=A(e,o,r=>j(r.level)),a=e.map(r=>{const i=p(r.file,r.exportName),v=c.byProcedure.get(i)??[],u=h(v),d=s.has(i)||r.exempt===!0,f=F(r);return{checks:v,coverage:d?"exempt":k(u),...d?{exemptReason:r.exemptReason??""}:{},exportName:r.exportName,file:r.file,id:i,kind:r.kind,score:u,sensitivity:f,visibility:r.visibility,weight:d?0:y(r,f.level)}}).toSorted((r,i)=>S(r.id,i.id)),n={checks:c.project,score:h(c.project)},l=a.filter(r=>r.coverage!=="exempt"),m=l.reduce((r,i)=>r+i.weight,0),g=N([...l,{score:n.score,weight:b(m)}]);return{generatedAt:t.generatedAt??new Date().toISOString(),grade:$(g),procedures:a,project:n,score:g,summary:I(a,n),version:O}};export{O as MAP_VERSION,S as byCodepoint,W as scoreAdvisor};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{i as n}from"./finding-BAVgqnCF.mjs";import{m as i}from"./procedure-protections-DRQotu9I.mjs";const o={categories:["SECURITY"],description:"A public procedure runs an AI generation (`generateText`/`streamText`/`generateObject`/`streamObject`) with no `maxOutputTokens` bound. Generation bills per output token, so an anonymous caller can request arbitrarily long completions in a loop — a denial-of-wallet vector that also ties up worker time on long streams.",facing:"EXTERNAL",level:"WARN",name:"ai_unbounded_generation_public",remediation:"Cap the completion with `maxOutputTokens` in the generation config, and rate-limit the public entry point (`.use(rateLimit(...))`). For expensive models, prefer running generation from an internal, authenticated path rather than exposing it directly to anonymous callers.",run:t=>t.procedureProtections===void 0?[]:t.procedureProtections.filter(e=>i(e.unboundedAiGeneration)&&e.visibility==="public").map(e=>n(o,{cacheKey:`ai_unbounded_generation_public:${e.file}:${e.exportName}`,detail:`\`${e.exportName}\` (${e.file}) is public and runs an AI generation with no \`maxOutputTokens\` bound — an anonymous caller can drive unbounded, billable completions in a loop. Add a \`maxOutputTokens\` cap and rate-limit the entry point.`,metadata:{exportName:e.exportName,file:e.file,kind:e.kind}})),source:"static",title:"Public procedure runs unbounded AI generation (no maxOutputTokens)"};export{o as default};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
const o=[{fact:e=>e.writesUserTable,reason:"writes an identity table"},{fact:e=>e.callsMail,reason:"sends mail"},{fact:e=>e.fanOut,reason:"fans out to a privileged dispatch surface"},{fact:e=>e.usesInsertManyUnsafe,reason:"bypasses validators with insertManyUnsafe"},{fact:e=>e.unboundedAiGeneration,reason:"runs an unbounded AI generation"}],r="may exhibit sensitive behaviour — its handler body could not be read",i=e=>{const a=[];let n=!1;for(const s of o){const t=s.fact(e);t===!0?a.push(s.reason):t===void 0&&(n=!0)}return n&&a.push(r),{level:a.length===0?"none":"high",reasons:a}};export{i as default};
|
package/dist/packem_shared/{compareToBaseline-T1_kl8VY.mjs → compareToBaseline-BwXFusQG.mjs}
RENAMED
|
@@ -1 +1 @@
|
|
|
1
|
-
import{byCodepoint as p}from"./MAP_VERSION-
|
|
1
|
+
import{byCodepoint as p}from"./MAP_VERSION-DxkFCiLX.mjs";const l=new Set(["clean","exempt","failing","warned"]),d=(e,r)=>{const t=new Map(e.map(s=>[s.name,s.occurrences]));return r.some(s=>s.occurrences>(t.get(s.name)??0))},i=e=>typeof e=="number"&&Number.isFinite(e),g=e=>{if(typeof e!="object"||e===null)return!1;const r=e;return typeof r.id=="string"&&i(r.score)&&typeof r.coverage=="string"&&l.has(r.coverage)},m=e=>{if(typeof e!="object"||e===null)return!1;const r=e;return Array.isArray(r.checks)&&i(r.score)},v=(e,r)=>{if(r.version!==e.version)return{comparable:!1,reason:"version-mismatch"};const t=new Map(r.procedures.map(o=>[o.id,o])),s=[],n=[],a=[];for(const o of e.procedures){if(o.coverage==="exempt")continue;const c=t.get(o.id);c!==void 0&&c.coverage!=="exempt"&&o.score<c.score&&s.push({after:o.score,before:c.score,id:o.id}),o.coverage==="failing"&&c?.coverage!=="failing"&&n.push(o.id),d(c?.checks??[],o.checks)&&a.push(o.id)}s.sort((o,c)=>p(o.id,c.id)),n.sort(p),a.sort(p);const u=e.score-r.score,f=d(r.project.checks,e.project.checks);return{comparable:!0,dropped:s,newFailing:n,projectRegressed:f,regressed:u<0||s.length>0||n.length>0||a.length>0||f,scoreDelta:u,worsened:a}},y=e=>{if(typeof e!="object"||e===null)return;const r=e;if(!(!i(r.version)||!i(r.score))&&!(!Array.isArray(r.procedures)||!r.procedures.every(t=>g(t)))&&m(r.project))return e};export{v as compareToBaseline,y as parseAdvisorMap};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{i as n}from"./finding-BAVgqnCF.mjs";const i=20,s=.1,l={categories:["PERFORMANCE"],description:"A function's observed error rate has cleared a threshold high enough to be a genuine reliability signal — the same errorHotspots signal `lunora insights` reports, now gated in `lunora advisor`.",facing:"EXTERNAL",level:"WARN",name:"error_rate_outlier",remediation:"Read the function's `lastErrorMessage` / traces to find the failing dependency or input shape, then fix or guard it; `lunora insights` ranks every function by error rate for the same window.",run:o=>{const t=[];for(const r of o.functionMetrics??[]){if(r.calls<i)continue;const e=r.errors/r.calls;if(e<s)continue;const a=Math.round(e*100);t.push(n(l,{cacheKey:`error_rate_outlier:${r.path}`,detail:`"${r.path}" errored on ${r.errors.toString()} of ${r.calls.toString()} calls (${a.toString()}%) over the observed window.`,metadata:{calls:r.calls,errors:r.errors,path:r.path,rate:e}}))}return t},source:"runtime",title:"Error-rate outlier"};export{l as default};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{i as r}from"./finding-BAVgqnCF.mjs";import{m as i}from"./procedure-protections-DRQotu9I.mjs";const s={categories:["SECURITY"],description:"A public procedure calls `ctx.db.insertManyUnsafe(...)`, which writes rows straight to storage and bypasses both the per-row validators and the schema's insert triggers (server-trusted columns, ownership stamping, RLS write checks). On a public entry point the rows are request-shaped, so a caller can write columns they shouldn't control and skip every trigger-enforced invariant.",facing:"EXTERNAL",level:"WARN",name:"insert_many_unsafe_user_data",remediation:"Use the validated `ctx.db.insert(...)` path in public procedures so validators and insert triggers run. If you need the unsafe bulk write, move it into an internal function (`internalMutation`/`internalAction`) that is fed only server-constructed rows, never request input.",run:t=>t.procedureProtections===void 0?[]:t.procedureProtections.filter(e=>i(e.usesInsertManyUnsafe)&&e.visibility==="public").map(e=>r(s,{cacheKey:`insert_many_unsafe_user_data:${e.file}:${e.exportName}`,detail:`\`${e.exportName}\` (${e.file}) is public and calls \`ctx.db.insertManyUnsafe(...)\`, bypassing validators and insert triggers on request-shaped rows. Switch to \`ctx.db.insert(...)\`, or move the bulk write into an internal function fed server-built rows.`,metadata:{exportName:e.exportName,file:e.file,kind:e.kind}})),source:"static",title:"Public procedure uses insertManyUnsafe (bypasses validators and triggers)"};export{s as default};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{i as l}from"./finding-BAVgqnCF.mjs";const u={categories:["SCHEMA"],description:"A `query`/`mutation` handler calls a non-deterministic API (`Date.now`, `Math.random`, `crypto.randomUUID`, `crypto.getRandomValues`, or `fetch`). A `query` may be re-run by a live subscription, so non-determinism there can flicker between evaluations (WARN). An ordinary `mutation` handler does not replay on this runtime — it runs at most once per logical write — so this is informational there (INFO) unless the mutation is itself invoked from a workflow step or queue consumer that can replay.",facing:"EXTERNAL",level:"WARN",name:"nondeterministic_query_mutation",remediation:"For a `query`: move the non-deterministic call into an `action(...)` (which runs once and may use ambient APIs), then pass the computed value into the mutation as an argument, or accept that the value may differ across re-evaluations. For an ordinary `mutation`: no action needed — the handler runs at most once per logical write on this runtime. If the mutation is dispatched from inside a workflow step or queue consumer, treat it like an action value instead, since the surrounding step/consumer can replay.",run:a=>{if(a.nondeterministicCalls===void 0)return[];const n=[],o=new Map;for(const e of a.nondeterministicCalls){const t=`${e.file}:${e.line.toString()}:${e.callee}`,i=(o.get(t)??0)+1;o.set(t,i);const r=i>1?`:${i.toString()}`:"",s={callee:e.callee,exportName:e.exportName,file:e.file,kind:e.kind,line:e.line};if(e.kind==="mutation"){n.push(l(u,{cacheKey:`nondeterministic_query_mutation:${t}${r}`,detail:`\`${e.callee}(…)\` in ${e.exportName} (${e.file}:${e.line.toString()}) runs inside a mutation handler. Ordinary mutations don't replay on this runtime (idempotency dedup returns a cached result rather than re-running the handler, and an OCC conflict throws to the caller instead of retrying internally), so this is informational — no action needed unless \`${e.exportName}\` is invoked from a workflow step or queue consumer that can itself replay.`,facing:"INTERNAL",level:"INFO",metadata:s}));continue}n.push(l(u,{cacheKey:`nondeterministic_query_mutation:${t}${r}`,detail:`\`${e.callee}(…)\` in ${e.exportName} (${e.file}:${e.line.toString()}) runs inside a query handler — a live subscription may re-run this query, so the result can differ between evaluations. Compute it in an \`action\` and pass the value into the mutation as an argument.`,metadata:s}))}return n},source:"static",title:"Non-deterministic call in query/mutation handler"};export{u as default};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{i as a}from"./finding-BAVgqnCF.mjs";import{m as i}from"./procedure-protections-DRQotu9I.mjs";const r={categories:["SECURITY"],description:"A public procedure fans work out to a privileged dispatch surface (`ctx.scheduler.runAfter`/`runAt`, a `ctx.queues.<name>` send, or `ctx.workflows.<name>.create`) with no rate-limit guard. Each dispatch runs later under the system identity and bills the account, so an anonymous caller can amplify cost and flood the async surface in a loop.",facing:"EXTERNAL",level:"WARN",name:"privileged_fanout_from_public_procedure",remediation:"Gate the public entry point with a rate limit (`.use(rateLimit(...))` or a `protectPublic({ rateLimit })` bundle), or make the fan-out procedure internal and trigger it only from an authenticated, guarded path. Never expose an unbounded scheduler/queue/workflow dispatch to anonymous callers.",run:t=>t.procedureProtections===void 0?[]:t.procedureProtections.filter(e=>i(e.fanOut)&&e.visibility==="public"&&!e.usesRateLimit).map(e=>a(r,{cacheKey:`privileged_fanout_from_public_procedure:${e.file}:${e.exportName}`,detail:`\`${e.exportName}\` (${e.file}) is public and fans work out to a privileged dispatch surface (scheduler/queue/workflow) with no rate limit — an anonymous caller can drive that billable, system-identity dispatch in a loop. Add a rate-limit guard, or make it internal and trigger it from an authenticated path.`,metadata:{exportName:e.exportName,file:e.file,kind:e.kind}})),source:"static",title:"Public procedure fans out to a privileged dispatch surface"};export{r as default};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
const i=t=>t!==!1;export{i as m};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{i as s}from"./finding-BAVgqnCF.mjs";import{m as i}from"./procedure-protections-DRQotu9I.mjs";import{i as o}from"./helpers-DwlvEUCh.mjs";const r={categories:["SECURITY"],description:"A public `mutation`/`action` that creates a user/session/account row has no disposable-email gate. Throwaway mailboxes farm free trials, evade bans, and pollute the user table.",facing:"EXTERNAL",level:"WARN",name:"signup_mutation_without_disposable_gating",remediation:"Add `.use(emailGateMiddleware({ email: (ctx) => ctx.args.email }))` from `@lunora/auth/email-guard` to reject disposable domains at signup, or gate better-auth's native signup with `withEmailGate(...)` / `emailGateDatabaseHooks(...)`.",run:a=>{if(a.procedureProtections===void 0)return[];const t=[];for(const e of a.procedureProtections)!o(e)||!i(e.writesUserTable)||e.usesEmailGate||i(e.hasEmailArg)&&t.push(s(r,{cacheKey:`signup_mutation_without_disposable_gating:${e.file}:${e.exportName}`,detail:`Public ${e.kind} \`${e.exportName}\` (${e.file}) writes a user/session/account table but has no disposable-email gate. Add \`.use(emailGateMiddleware(...))\` from \`@lunora/auth/email-guard\`.`,metadata:{exportName:e.exportName,file:e.file,kind:e.kind,writesUserTable:e.writesUserTable}}));return t},source:"static",title:"Account-creating write without a disposable-email gate"};export{r as default};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{i}from"./finding-BAVgqnCF.mjs";const o={categories:["SECURITY"],description:"A `ctx.storage.*` call uses an R2 object key taken directly from the handler's `args` with no server-side scoping. The bucket methods key by the caller-supplied string, so any caller can read, overwrite, or delete another user's object — object-level IDOR.",facing:"EXTERNAL",level:"ERROR",name:"storage_key_from_user_args",remediation:"Prefix the object key with a server-trusted identity (e.g. `${ctx.auth.userId}/…`) or resolve the object through a record the caller is known to own. Never pass request input straight through as an R2 object key.",run:t=>t.storageKeyAccesses===void 0?[]:t.storageKeyAccesses.map(e=>{const r=`\`ctx.storage.*.${e.method}\` in \`${e.exportName}\` (${e.file}:${e.line.toString()})`,s={exportName:e.exportName,file:e.file,line:e.line,method:e.method,visibility:e.visibility??"unknown"};return e.visibility==="internal"?i(o,{cacheKey:`storage_key_from_user_args:${e.file}:${e.line.toString()}`,detail:`${r} uses an object key derived from \`args\` with no server-side scoping. This is expected for an \`internal\` procedure — no caller can reach it directly, so the key is only ever supplied by trusted server code. Audit the PUBLIC procedures that dispatch to it: if one forwards \`args\` straight through, the IDOR is there.`,facing:"INTERNAL",level:"INFO",metadata:s}):i(o,{cacheKey:`storage_key_from_user_args:${e.file}:${e.line.toString()}`,detail:`${r} uses an object key derived from \`args\` with no server-side scoping — any caller can read/overwrite/delete another user's object (IDOR). Prefix the key with a server-trusted identity (e.g. \`\${ctx.auth.userId}/…\`) or resolve the object through an owned record.`,metadata:s})}),source:"static",title:"R2 object key taken directly from user args (IDOR)"};export{o as default};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{i as o}from"./finding-BAVgqnCF.mjs";import{m as i}from"./procedure-protections-DRQotu9I.mjs";import{i as n}from"./helpers-DwlvEUCh.mjs";const c=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",l={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:t=>{if(t.procedureProtections===void 0)return[];const a=[];for(const e of t.procedureProtections){const r=i(e.writesUserTable)||i(e.callsMail);if(!n(e)||!r||e.usesCaptcha)continue;const s=c(e);a.push(o(l,{cacheKey:`user_creating_mutation_without_captcha:${e.file}:${e.exportName}`,detail:`Public ${e.kind} \`${e.exportName}\` (${e.file}) ${s} but has no CAPTCHA check. Add \`.use(verifyTurnstile(...))\` or \`.use(protectPublic({ captcha }))\`.`,metadata:{callsMail:e.callsMail,exportName:e.exportName,file:e.file,kind:e.kind,writesUserTable:e.writesUserTable}}))}return a},source:"static",title:"Account-creating / mail-sending write without a CAPTCHA"};export{l as default};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lunora/advisor",
|
|
3
|
-
"version": "1.0.0-alpha.
|
|
3
|
+
"version": "1.0.0-alpha.61",
|
|
4
4
|
"description": "Schema & query lints (splinter-style advisors) for Lunora, feeding the Studio Advisors view",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"advisor",
|
|
@@ -46,8 +46,8 @@
|
|
|
46
46
|
"access": "public"
|
|
47
47
|
},
|
|
48
48
|
"dependencies": {
|
|
49
|
-
"@lunora/errors": "1.0.0-alpha.
|
|
50
|
-
"@lunora/server": "1.0.0-alpha.
|
|
49
|
+
"@lunora/errors": "1.0.0-alpha.12",
|
|
50
|
+
"@lunora/server": "1.0.0-alpha.57"
|
|
51
51
|
},
|
|
52
52
|
"engines": {
|
|
53
53
|
"node": "^22.15.0 || >=24.11.0"
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
import{LunoraError as m}from"@lunora/errors";const y=/^[\w.-]+$/u,c={indexHit:{event:"lunora.index.hit",index:"blob3",table:"blob2"},shardRequest:{event:"lunora.shard.request",group:"blob3",shardKey:"blob2"},tableScan:{event:"lunora.table.scan",table:"blob2"}},p=e=>{const a=typeof e=="number"?e:Number(e);return Number.isFinite(a)?a:0},o=e=>typeof e=="string"?e:typeof e=="number"||typeof e=="boolean"||typeof e=="bigint"?String(e):"",$=e=>{if(!y.test(e))throw new m("INTERNAL",`@lunora/advisor: invalid Analytics Engine dataset name "${e}" — expected a bare table identifier.`)},u=e=>`'${e.replaceAll("\\","\\\\").replaceAll("'","''")}'`,h=async(e,a)=>{try{return(await e.query(a)).rows}catch{return[]}},x=async(e,a)=>{const{event:s,group:n,shardKey:r}=c.shardRequest,l=a.group===void 0?"":` AND ${n} = ${u(a.group)}`,i=`SELECT ${r} AS shardKey, ${n} AS shardGroup, sum(_sample_interval) AS requests FROM ${a.dataset} WHERE blob1 = ${u(s)}${l} GROUP BY shardKey, shardGroup`;return(await h(e,i)).map(d=>{const b=o(d.shardGroup);return{...b===""?{}:{group:b},requests:p(d.requests),shardKey:o(d.shardKey)}})},E=async(e,a)=>{const{event:s,table:n}=c.tableScan,r=`SELECT ${n} AS scanTable, sum(_sample_interval) AS scans FROM ${a.dataset} WHERE blob1 = ${u(s)} GROUP BY scanTable`;return(await h(e,r)).map(l=>({scans:p(l.scans),table:o(l.scanTable)}))},S=async(e,a)=>{const{event:s,index:n,table:r}=c.indexHit,l=`SELECT ${r} AS hitTable, ${n} AS hitIndex, sum(_sample_interval) AS reads FROM ${a.dataset} WHERE blob1 = ${u(s)} GROUP BY hitTable, hitIndex`,i=(await h(e,l)).map(t=>({index:o(t.hitIndex),reads:p(t.reads),table:o(t.hitTable)}));if(a.declaredIndexes===void 0)return i;const d=new Set(i.map(t=>`${t.table}\0${t.index}`)),b=a.declaredIndexes.filter(t=>!d.has(`${t.table}\0${t.index}`)).map(t=>({index:t.index,reads:0,table:t.table}));return[...i,...b]},R=async(e,a)=>{$(a.dataset);const[s,n,r]=await Promise.all([x(e,a),E(e,a),S(e,a)]);return{indexHits:r,shardTraffic:s,tableScans:n}};export{c as AE_METRIC_EVENTS,R as loadAnalyticsRuntimeMetrics};
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
import{i as n}from"./finding-BAVgqnCF.mjs";const o={categories:["SECURITY"],description:"A public procedure runs an AI generation (`generateText`/`streamText`/`generateObject`/`streamObject`) with no `maxOutputTokens` bound. Generation bills per output token, so an anonymous caller can request arbitrarily long completions in a loop — a denial-of-wallet vector that also ties up worker time on long streams.",facing:"EXTERNAL",level:"WARN",name:"ai_unbounded_generation_public",remediation:"Cap the completion with `maxOutputTokens` in the generation config, and rate-limit the public entry point (`.use(rateLimit(...))`). For expensive models, prefer running generation from an internal, authenticated path rather than exposing it directly to anonymous callers.",run:t=>t.procedureProtections===void 0?[]:t.procedureProtections.filter(e=>e.unboundedAiGeneration&&e.visibility==="public").map(e=>n(o,{cacheKey:`ai_unbounded_generation_public:${e.file}:${e.exportName}`,detail:`\`${e.exportName}\` (${e.file}) is public and runs an AI generation with no \`maxOutputTokens\` bound — an anonymous caller can drive unbounded, billable completions in a loop. Add a \`maxOutputTokens\` cap and rate-limit the entry point.`,metadata:{exportName:e.exportName,file:e.file,kind:e.kind}})),source:"static",title:"Public procedure runs unbounded AI generation (no maxOutputTokens)"};export{o as default};
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
const t=[{reason:"writes an identity table",test:e=>e.writesUserTable},{reason:"sends mail",test:e=>e.callsMail},{reason:"fans out to a privileged dispatch surface",test:e=>e.fanOut},{reason:"bypasses validators with insertManyUnsafe",test:e=>e.usesInsertManyUnsafe},{reason:"runs an unbounded AI generation",test:e=>e.unboundedAiGeneration}],a=e=>{const s=[];for(const n of t)n.test(e)&&s.push(n.reason);return{level:s.length===0?"none":"high",reasons:s}};export{a as default};
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
import{i as r}from"./finding-BAVgqnCF.mjs";const s={categories:["SECURITY"],description:"A public procedure calls `ctx.db.insertManyUnsafe(...)`, which writes rows straight to storage and bypasses both the per-row validators and the schema's insert triggers (server-trusted columns, ownership stamping, RLS write checks). On a public entry point the rows are request-shaped, so a caller can write columns they shouldn't control and skip every trigger-enforced invariant.",facing:"EXTERNAL",level:"WARN",name:"insert_many_unsafe_user_data",remediation:"Use the validated `ctx.db.insert(...)` path in public procedures so validators and insert triggers run. If you need the unsafe bulk write, move it into an internal function (`internalMutation`/`internalAction`) that is fed only server-constructed rows, never request input.",run:t=>t.procedureProtections===void 0?[]:t.procedureProtections.filter(e=>e.usesInsertManyUnsafe&&e.visibility==="public").map(e=>r(s,{cacheKey:`insert_many_unsafe_user_data:${e.file}:${e.exportName}`,detail:`\`${e.exportName}\` (${e.file}) is public and calls \`ctx.db.insertManyUnsafe(...)\`, bypassing validators and insert triggers on request-shaped rows. Switch to \`ctx.db.insert(...)\`, or move the bulk write into an internal function fed server-built rows.`,metadata:{exportName:e.exportName,file:e.file,kind:e.kind}})),source:"static",title:"Public procedure uses insertManyUnsafe (bypasses validators and triggers)"};export{s as default};
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
import{i as s}from"./finding-BAVgqnCF.mjs";const l={categories:["SCHEMA"],description:"A `query`/`mutation` handler calls a non-deterministic API (`Date.now`, `Math.random`, `crypto.randomUUID`, `crypto.getRandomValues`, or `fetch`). These handlers may be re-run on OCC retry / subscription re-evaluation, so they must be deterministic — time, randomness, and network belong in an `action`.",facing:"EXTERNAL",level:"WARN",name:"nondeterministic_query_mutation",remediation:"Move the non-deterministic call into an `action(...)` (which runs once and may use ambient APIs), then pass the computed value into the mutation as an argument — e.g. compute `Date.now()` in the action and call `ctx.runMutation(api.…, { now })`. Take generated ids/timestamps as args instead of producing them in the handler.",run:i=>{if(i.nondeterministicCalls===void 0)return[];const a=[],o=new Map;for(const e of i.nondeterministicCalls){const t=`${e.file}:${e.line.toString()}:${e.callee}`,n=(o.get(t)??0)+1;o.set(t,n);const r=n>1?`:${n.toString()}`:"";a.push(s(l,{cacheKey:`nondeterministic_query_mutation:${t}${r}`,detail:`\`${e.callee}(…)\` in ${e.exportName} (${e.file}:${e.line.toString()}) runs inside a ${e.kind} handler — query/mutation handlers must be deterministic. Compute it in an \`action\` and pass the value into the mutation as an argument.`,metadata:{callee:e.callee,exportName:e.exportName,file:e.file,kind:e.kind,line:e.line}}))}return a},source:"static",title:"Non-deterministic call in query/mutation handler"};export{l as default};
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
import{i as a}from"./finding-BAVgqnCF.mjs";const i={categories:["SECURITY"],description:"A public procedure fans work out to a privileged dispatch surface (`ctx.scheduler.runAfter`/`runAt`, a `ctx.queues.<name>` send, or `ctx.workflows.<name>.create`) with no rate-limit guard. Each dispatch runs later under the system identity and bills the account, so an anonymous caller can amplify cost and flood the async surface in a loop.",facing:"EXTERNAL",level:"WARN",name:"privileged_fanout_from_public_procedure",remediation:"Gate the public entry point with a rate limit (`.use(rateLimit(...))` or a `protectPublic({ rateLimit })` bundle), or make the fan-out procedure internal and trigger it only from an authenticated, guarded path. Never expose an unbounded scheduler/queue/workflow dispatch to anonymous callers.",run:t=>t.procedureProtections===void 0?[]:t.procedureProtections.filter(e=>e.fanOut&&e.visibility==="public"&&!e.usesRateLimit).map(e=>a(i,{cacheKey:`privileged_fanout_from_public_procedure:${e.file}:${e.exportName}`,detail:`\`${e.exportName}\` (${e.file}) is public and fans work out to a privileged dispatch surface (scheduler/queue/workflow) with no rate limit — an anonymous caller can drive that billable, system-identity dispatch in a loop. Add a rate-limit guard, or make it internal and trigger it from an authenticated path.`,metadata:{exportName:e.exportName,file:e.file,kind:e.kind}})),source:"static",title:"Public procedure fans out to a privileged dispatch surface"};export{i as default};
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
import{i}from"./finding-BAVgqnCF.mjs";import{i as s}from"./helpers-DwlvEUCh.mjs";const o={categories:["SECURITY"],description:"A public `mutation`/`action` that creates a user/session/account row has no disposable-email gate. Throwaway mailboxes farm free trials, evade bans, and pollute the user table.",facing:"EXTERNAL",level:"WARN",name:"signup_mutation_without_disposable_gating",remediation:"Add `.use(emailGateMiddleware({ email: (ctx) => ctx.args.email }))` from `@lunora/auth/email-guard` to reject disposable domains at signup, or gate better-auth's native signup with `withEmailGate(...)` / `emailGateDatabaseHooks(...)`.",run:a=>{if(a.procedureProtections===void 0)return[];const t=[];for(const e of a.procedureProtections)!s(e)||!e.writesUserTable||e.usesEmailGate||e.hasEmailArg!==!1&&t.push(i(o,{cacheKey:`signup_mutation_without_disposable_gating:${e.file}:${e.exportName}`,detail:`Public ${e.kind} \`${e.exportName}\` (${e.file}) writes a user/session/account table but has no disposable-email gate. Add \`.use(emailGateMiddleware(...))\` from \`@lunora/auth/email-guard\`.`,metadata:{exportName:e.exportName,file:e.file,kind:e.kind,writesUserTable:e.writesUserTable}}));return t},source:"static",title:"Account-creating write without a disposable-email gate"};export{o as default};
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
import{m as t}from"./argument-derived-sink-BpO3NPPM.mjs";const o=t({cacheKey:e=>`storage_key_from_user_args:${e.file}:${e.line.toString()}`,categories:["SECURITY"],description:"A `ctx.storage.*` call uses an R2 object key taken directly from the handler's `args` with no server-side scoping. The bucket methods key by the caller-supplied string, so any caller can read, overwrite, or delete another user's object — object-level IDOR.",detail:e=>`\`ctx.storage.*.${e.method}\` in \`${e.exportName}\` (${e.file}:${e.line.toString()}) uses an object key derived from \`args\` with no server-side scoping — any caller can read/overwrite/delete another user's object (IDOR). Prefix the key with a server-trusted identity (e.g. \`\${ctx.auth.userId}/…\`) or resolve the object through an owned record.`,facing:"EXTERNAL",getAccesses:e=>e.storageKeyAccesses,level:"ERROR",metadata:e=>({exportName:e.exportName,file:e.file,line:e.line,method:e.method}),name:"storage_key_from_user_args",remediation:"Prefix the object key with a server-trusted identity (e.g. `${ctx.auth.userId}/…`) or resolve the object through a record the caller is known to own. Never pass request input straight through as an R2 object key.",title:"R2 object key taken directly from user args (IDOR)"});export{o as default};
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
import{i as s}from"./finding-BAVgqnCF.mjs";import{i as o}from"./helpers-DwlvEUCh.mjs";const c={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:t=>{if(t.procedureProtections===void 0)return[];const i=[];for(const e of t.procedureProtections){const a=e.writesUserTable||e.callsMail;if(!o(e)||!a||e.usesCaptcha)continue;const r=e.writesUserTable?"writes a user/session table":"sends mail";i.push(s(c,{cacheKey:`user_creating_mutation_without_captcha:${e.file}:${e.exportName}`,detail:`Public ${e.kind} \`${e.exportName}\` (${e.file}) ${r} but has no CAPTCHA check. Add \`.use(verifyTurnstile(...))\` or \`.use(protectPublic({ captcha }))\`.`,metadata:{callsMail:e.callsMail,exportName:e.exportName,file:e.file,kind:e.kind,writesUserTable:e.writesUserTable}}))}return i},source:"static",title:"Account-creating / mail-sending write without a CAPTCHA"};export{c as default};
|