@lunora/advisor 1.0.0-alpha.7 → 1.0.0-alpha.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.mts +173 -1
- package/dist/index.d.ts +173 -1
- package/dist/index.mjs +10 -2
- package/dist/packem_shared/{fromServerSchema-DinF1nph.mjs → fromServerSchema-TWb7dTHC.mjs} +1 -0
- package/dist/packem_shared/mutatorFullRowReplace-BJnNDaIV.mjs +26 -0
- package/dist/packem_shared/shapeTargetsGlobalTable-DHrf4Koi.mjs +34 -0
- package/dist/packem_shared/shapeUnknownTable-C8aDWFoe.mjs +34 -0
- package/dist/packem_shared/workflowDuplicateStepName-ioBxPBCy.mjs +48 -0
- package/package.json +2 -2
package/dist/index.d.mts
CHANGED
|
@@ -214,6 +214,28 @@ interface AdvisorMaskProcedure {
|
|
|
214
214
|
visibility: "internal" | "public";
|
|
215
215
|
}
|
|
216
216
|
/**
|
|
217
|
+
* One whole-row `ctx.db.replace(id, document)` write discovered inside a custom
|
|
218
|
+
* mutator's authoritative `server` impl (`lunora/mutators.ts`) — the input the
|
|
219
|
+
* `mutator_full_row_replace` lint consumes.
|
|
220
|
+
*
|
|
221
|
+
* In the local-first sync engine a `replace` overwrites the entire row, so a
|
|
222
|
+
* concurrent edit to a *different* column (committed between this mutator's read
|
|
223
|
+
* and its write) is silently clobbered. `patch(id, { onlyTheField })` merges at
|
|
224
|
+
* the column level instead, letting independent field edits coexist — the
|
|
225
|
+
* blessed pattern for mutators on a synced (poke-live) table. Produced by the
|
|
226
|
+
* codegen feeder, which attributes each `replace` to the mutator export
|
|
227
|
+
* performing it; runtime callers don't supply it, so the lint finds nothing
|
|
228
|
+
* there.
|
|
229
|
+
*/
|
|
230
|
+
interface AdvisorMutatorWrite {
|
|
231
|
+
/** The mutator export whose `server` impl performs the replace (e.g. `renameChannel`). */
|
|
232
|
+
exportName: string;
|
|
233
|
+
/** Openable source path the replace appears in — always `lunora/mutators.ts`. */
|
|
234
|
+
file: string;
|
|
235
|
+
/** 1-based line of the `replace(...)` call, or `0` when unknown. */
|
|
236
|
+
line: number;
|
|
237
|
+
}
|
|
238
|
+
/**
|
|
217
239
|
* One non-deterministic API call discovered lexically inside a `query(...)` or
|
|
218
240
|
* `mutation(...)` handler body — the input the `nondeterministic_query_mutation`
|
|
219
241
|
* lint consumes. Produced by the codegen feeder, which walks each exported
|
|
@@ -384,6 +406,16 @@ interface AdvisorTable {
|
|
|
384
406
|
optionalFields?: ReadonlySet<string>;
|
|
385
407
|
/** Declared relations (`.relations((r) => …)`). */
|
|
386
408
|
relations: ReadonlyArray<AdvisorRelation>;
|
|
409
|
+
/**
|
|
410
|
+
* Storage tier the table is declared in: `"global"` (a `.global()` table,
|
|
411
|
+
* lives in D1 — the cross-shard tier), `"shardBy"` (partitioned across
|
|
412
|
+
* shard DOs by a key), or `"root"` (the default single-DO table). Read by
|
|
413
|
+
* the `shape_*` lints to flag replication shapes targeting a `.global()`
|
|
414
|
+
* table (poll-refreshed/latency-tiered, not poke-live). Optional — the
|
|
415
|
+
* codegen feeder always supplies it, the runtime feeder derives it; a feeder
|
|
416
|
+
* that omits it leaves tier-sensitive lints to treat the table as local.
|
|
417
|
+
*/
|
|
418
|
+
shardKind?: "global" | "root" | "shardBy";
|
|
387
419
|
}
|
|
388
420
|
/**
|
|
389
421
|
* One declared index, flattened across Lunora's index kinds so a single lint can
|
|
@@ -440,6 +472,28 @@ interface AdvisorSecretLiteral {
|
|
|
440
472
|
preview: string;
|
|
441
473
|
}
|
|
442
474
|
/**
|
|
475
|
+
* A replication shape declared via `defineShape({ table, where, columns? })` in
|
|
476
|
+
* `lunora/shapes.ts` (the local-first sync engine's partial-replication unit).
|
|
477
|
+
* The `shape_*` lints cross-reference each shape's {@link AdvisorShape.table}
|
|
478
|
+
* against the declared schema to flag a shape targeting an unknown table or a
|
|
479
|
+
* `.global()` table (which replicates through the latency-tiered D1 poll path,
|
|
480
|
+
* not the poke-live op-log). Supplied by the codegen feeder, which lifts only
|
|
481
|
+
* the export name + the static `table` literal; absent for runtime callers,
|
|
482
|
+
* where the shape lints find nothing.
|
|
483
|
+
*/
|
|
484
|
+
interface AdvisorShape {
|
|
485
|
+
/** Export binding name — the shape's registry key (e.g. `channelMessages`). */
|
|
486
|
+
exportName: string;
|
|
487
|
+
/** File the shape is declared in (relative, for the operator to open). */
|
|
488
|
+
file: string;
|
|
489
|
+
/**
|
|
490
|
+
* The `table` string literal the shape replicates from, or `undefined` when
|
|
491
|
+
* the feeder could not read it as a plain string literal — tier-sensitive
|
|
492
|
+
* lints skip a shape with no resolvable table rather than guessing.
|
|
493
|
+
*/
|
|
494
|
+
table?: string;
|
|
495
|
+
}
|
|
496
|
+
/**
|
|
443
497
|
* One shard's observed traffic share — the input the `hot_shard` runtime lint
|
|
444
498
|
* consumes. Produced by the studio backend, which fans out over a sharded
|
|
445
499
|
* function's shards and reads each shard's recorded request volume from the
|
|
@@ -523,10 +577,27 @@ interface AdvisorTableSample {
|
|
|
523
577
|
* how `AdvisorContainer` tracks `ContainerIR` and `AdvisorInsertWrite` tracks
|
|
524
578
|
* `InsertWriteIR`).
|
|
525
579
|
*/
|
|
580
|
+
/** One durable step call lifted from a workflow handler body — the input the duplicate-step-name lint compares. Structural subset of codegen's `WorkflowStepIR`. */
|
|
581
|
+
interface AdvisorWorkflowStep {
|
|
582
|
+
/** 1-based line of the durable step call, or `0` when unknown. */
|
|
583
|
+
line: number;
|
|
584
|
+
/** The native step method invoked: `do` / `sleep` / `sleepUntil` / `waitForEvent`. */
|
|
585
|
+
method: string;
|
|
586
|
+
/** The step's static label (the first string-literal argument). */
|
|
587
|
+
name: string;
|
|
588
|
+
}
|
|
526
589
|
/** One workflow declared via a `defineWorkflow()` export in `lunora/workflows.ts`. */
|
|
527
590
|
interface AdvisorWorkflow {
|
|
528
591
|
/** The `lunora/workflows.ts` export name, e.g. `orderPipeline`. */
|
|
529
592
|
exportName: string;
|
|
593
|
+
/**
|
|
594
|
+
* The durable step labels discovered in the handler body, in source order —
|
|
595
|
+
* the duplicate-step-name input. Cloudflare memoizes a step by its name, so a
|
|
596
|
+
* name used twice makes the second call silently return the first's cached
|
|
597
|
+
* result. Supplied by the codegen feeder; `undefined` for runtime callers,
|
|
598
|
+
* where the lint finds nothing.
|
|
599
|
+
*/
|
|
600
|
+
steps?: ReadonlyArray<AdvisorWorkflowStep>;
|
|
530
601
|
}
|
|
531
602
|
/** One `ctx.workflows.get("name")` call discovered in a function body. */
|
|
532
603
|
interface AdvisorWorkflowCall {
|
|
@@ -660,6 +731,14 @@ interface LintContext {
|
|
|
660
731
|
*/
|
|
661
732
|
maskProcedures?: ReadonlyArray<AdvisorMaskProcedure>;
|
|
662
733
|
/**
|
|
734
|
+
* Whole-row `ctx.db.replace(id, document)` writes lifted from custom
|
|
735
|
+
* mutators' authoritative `server` impls (the `mutator_full_row_replace`
|
|
736
|
+
* input). Each `replace` overwrites the entire row, clobbering a concurrent
|
|
737
|
+
* edit to a different column on a synced table. Supplied by the codegen
|
|
738
|
+
* feeder; absent for runtime callers, where the lint finds nothing.
|
|
739
|
+
*/
|
|
740
|
+
mutatorWrites?: ReadonlyArray<AdvisorMutatorWrite>;
|
|
741
|
+
/**
|
|
663
742
|
* Non-deterministic API calls (`Date.now`, `Math.random`,
|
|
664
743
|
* `crypto.randomUUID`, `crypto.getRandomValues`, `fetch`) discovered lexically
|
|
665
744
|
* inside `query`/`mutation` handler bodies — the `nondeterministic_query_mutation`
|
|
@@ -708,6 +787,14 @@ interface LintContext {
|
|
|
708
787
|
*/
|
|
709
788
|
secretLiterals?: ReadonlyArray<AdvisorSecretLiteral>;
|
|
710
789
|
/**
|
|
790
|
+
* Replication shapes declared via `defineShape` in `lunora/shapes.ts` — the
|
|
791
|
+
* `shape_unknown_table` and `shape_targets_global_table` lint input. Each
|
|
792
|
+
* carries the export name and its static `table` literal, cross-referenced
|
|
793
|
+
* against {@link LintContext.schema}. Supplied by the codegen feeder; absent
|
|
794
|
+
* for runtime callers, where the shape lints find nothing.
|
|
795
|
+
*/
|
|
796
|
+
shapes?: ReadonlyArray<AdvisorShape>;
|
|
797
|
+
/**
|
|
711
798
|
* Per-shard observed traffic — the `hot_shard` lint input. Supplied by the
|
|
712
799
|
* studio backend, which fans out over a sharded function's shards and reads
|
|
713
800
|
* each shard's recorded request volume from the durable `__lunora_metrics`
|
|
@@ -1133,6 +1220,30 @@ declare const indexReferencesUnknownField: Lint;
|
|
|
1133
1220
|
*/
|
|
1134
1221
|
declare const maskUncoveredPiiColumn: Lint;
|
|
1135
1222
|
/**
|
|
1223
|
+
* Flags a custom mutator whose authoritative `server` impl writes a row with
|
|
1224
|
+
* `ctx.db.replace(id, document)` — a whole-document overwrite.
|
|
1225
|
+
*
|
|
1226
|
+
* The local-first sync engine serializes mutators in the shard DO, so two
|
|
1227
|
+
* mutators that touch the *same row* but *different columns* both run to
|
|
1228
|
+
* completion — but only if each writes its own column. A `replace` overwrites
|
|
1229
|
+
* the entire row from the document the mutator assembled, so a concurrent edit
|
|
1230
|
+
* to another column (committed between this mutator's read and its write, or
|
|
1231
|
+
* carried as a pending optimistic overlay on a client) is silently clobbered:
|
|
1232
|
+
* the kind of "two offline edits to different fields fight each other" data loss
|
|
1233
|
+
* a column-level merge avoids. `ctx.db.patch(id, { onlyTheChangedField })`
|
|
1234
|
+
* merges at the column level instead, so independent field edits coexist.
|
|
1235
|
+
*
|
|
1236
|
+
* `WARN`, not `ERROR`: `replace` is legitimate when the mutator genuinely owns
|
|
1237
|
+
* the whole row (a full-form save, a state-machine transition that rewrites
|
|
1238
|
+
* every field). The lint just surfaces the column-clobber risk so a developer
|
|
1239
|
+
* reaches for `patch` by default on a synced table.
|
|
1240
|
+
*
|
|
1241
|
+
* **Evidence supply**: runs only when the codegen feeder supplies
|
|
1242
|
+
* `context.mutatorWrites` (each a `replace` call lifted from a mutator's inline
|
|
1243
|
+
* `server` body); absent for runtime callers, where the lint finds nothing.
|
|
1244
|
+
*/
|
|
1245
|
+
declare const mutatorFullRowReplace: Lint;
|
|
1246
|
+
/**
|
|
1136
1247
|
* Flags a non-deterministic API call inside a `query(...)` or `mutation(...)`
|
|
1137
1248
|
* handler body.
|
|
1138
1249
|
*
|
|
@@ -1285,6 +1396,48 @@ declare const relationReferencesUnknownTable: Lint;
|
|
|
1285
1396
|
*/
|
|
1286
1397
|
declare const rlsUncoveredTable: Lint;
|
|
1287
1398
|
/**
|
|
1399
|
+
* Flags a replication shape whose `table` is a `.global()` table.
|
|
1400
|
+
*
|
|
1401
|
+
* Poke-live replication is a per-shard-DO property: the shard owns its SQLite
|
|
1402
|
+
* and a monotonic `__cdc_log`, so a write produces an ordered op the DO pokes to
|
|
1403
|
+
* every subscriber at the next flush. A `.global()` table lives outside the
|
|
1404
|
+
* shard DO's SQLite op-log (in a global backend — D1, or Hyperdrive-fronted
|
|
1405
|
+
* Postgres/MySQL) — so a shape over a global table cannot be poke-live. It is
|
|
1406
|
+
* served through the cross-shard tier: **coordinator/poll-refreshed, latency-
|
|
1407
|
+
* tiered**, not live. That is a real and supported tier (it is the recommended
|
|
1408
|
+
* answer for cross-shard reads — denormalize, or move the joined table to
|
|
1409
|
+
* `.global()` and read through the global backend), but its freshness semantics
|
|
1410
|
+
* differ from a sharded shape's, so the boundary is surfaced rather than hidden.
|
|
1411
|
+
*
|
|
1412
|
+
* `WARN`, not `ERROR`: a global-table shape is a legitimate design once you
|
|
1413
|
+
* accept the poll-refresh latency; the lint just makes the tier explicit so a
|
|
1414
|
+
* developer does not assume poke-live freshness.
|
|
1415
|
+
*
|
|
1416
|
+
* **Evidence supply**: runs only when the codegen feeder supplies
|
|
1417
|
+
* `context.shapes`; the table's tier comes from the schema's `shardKind`. A
|
|
1418
|
+
* shape whose table is unknown (caught by `shape_unknown_table`) or whose tier
|
|
1419
|
+
* the feeder didn't supply is skipped.
|
|
1420
|
+
*/
|
|
1421
|
+
declare const shapeTargetsGlobalTable: Lint;
|
|
1422
|
+
/**
|
|
1423
|
+
* Flags a replication shape whose `table` names a table that does not exist in
|
|
1424
|
+
* the schema.
|
|
1425
|
+
*
|
|
1426
|
+
* `defineShape({ table: "messages", … })` binds a shape to a table by a plain
|
|
1427
|
+
* string. A live `subscribeShape("…")` resolves that shape server-side and runs
|
|
1428
|
+
* its membership query against the named table — so a typo, a stale name after a
|
|
1429
|
+
* rename, or a copy-paste mistake produces a shape that can never resolve a
|
|
1430
|
+
* rowset: the subscription seeds empty and then errors at the first flush
|
|
1431
|
+
* (`no such table`). This is a definite, build-time-detectable break, so it is
|
|
1432
|
+
* an `ERROR` — surfaced before the broken shape ever ships.
|
|
1433
|
+
*
|
|
1434
|
+
* **Evidence supply**: runs only when the codegen feeder supplies
|
|
1435
|
+
* `context.shapes`. A shape whose `table` wasn't a static string literal (no
|
|
1436
|
+
* resolvable name) is skipped rather than guessed at, so the lint under-reports
|
|
1437
|
+
* rather than raising false alarms.
|
|
1438
|
+
*/
|
|
1439
|
+
declare const shapeUnknownTable: Lint;
|
|
1440
|
+
/**
|
|
1288
1441
|
* Flags a `ctx.sql` tagged-template that splices an unparameterized
|
|
1289
1442
|
* string-building expression into the query.
|
|
1290
1443
|
*
|
|
@@ -1387,6 +1540,25 @@ declare const unindexedRelationTarget: Lint;
|
|
|
1387
1540
|
*/
|
|
1388
1541
|
declare const userCreatingMutationWithoutCaptcha: Lint;
|
|
1389
1542
|
/**
|
|
1543
|
+
* Flags a durable step name reused within one workflow.
|
|
1544
|
+
*
|
|
1545
|
+
* Cloudflare Workflows memoizes every `step.do` / `step.sleep` / `step.sleepUntil`
|
|
1546
|
+
* / `step.waitForEvent` call by its name: on replay the runtime returns the cached
|
|
1547
|
+
* result for a name it has already seen. Two distinct steps that share a name are
|
|
1548
|
+
* therefore a silent bug — the second call never runs its body and instead yields
|
|
1549
|
+
* the first's result, skipping the work (a charge, a write, an external wait)
|
|
1550
|
+
* without error. Hence `ERROR`/`INTERNAL`: it is a developer-facing correctness
|
|
1551
|
+
* defect in the workflow's own code, not a runtime-data nit.
|
|
1552
|
+
*
|
|
1553
|
+
* Only the first string-literal argument of each step call is compared; a step
|
|
1554
|
+
* named dynamically (`step.do(\`load-${id}\`, …)`) is omitted by the feeder, so a
|
|
1555
|
+
* deliberately-parameterized fan-out is never flagged. `ctx.runStep(stepDef, …)`
|
|
1556
|
+
* names (which come from `defineStep` in another file) are out of scope here.
|
|
1557
|
+
* Only runs when the declaration feeder supplied step evidence
|
|
1558
|
+
* (`workflow.steps` present); a runtime caller flags nothing.
|
|
1559
|
+
*/
|
|
1560
|
+
declare const workflowDuplicateStepName: Lint;
|
|
1561
|
+
/**
|
|
1390
1562
|
* A correctness lint: every `ctx.workflows.get("name")` call must reference a
|
|
1391
1563
|
* workflow that exists — i.e. a `defineWorkflow` export in `lunora/workflows.ts`.
|
|
1392
1564
|
* A `.get("x")` whose `"x"` resolves to no declared workflow is a typo or a
|
|
@@ -1448,4 +1620,4 @@ interface RunAdvisorOptions {
|
|
|
1448
1620
|
* `static` lints at build time and defer `runtime` lints to a live shard.
|
|
1449
1621
|
*/
|
|
1450
1622
|
declare const runAdvisor: (context: LintContext, options?: RunAdvisorOptions) => Finding[];
|
|
1451
|
-
export { AE_METRIC_EVENTS, ALL_LINTS, type AdvisorAdminRoute, type AdvisorArgumentValidator, type AdvisorAuthApiCall, type AdvisorContainer, type AdvisorHyperdriveCall, type AdvisorIndex, type AdvisorIndexHit, type AdvisorInsertWrite, type AdvisorMaskProcedure, type AdvisorNondeterministicCall, type AdvisorProcedureProtection, type AdvisorQueryRead, type AdvisorR2sqlCall, type AdvisorRelation, type AdvisorRlsProcedure, type AdvisorSchema, type AdvisorSecretLiteral, type AdvisorShardTraffic, type AdvisorSqlInterpolation, type AdvisorTable, type AdvisorTableSample, type AdvisorTableScan, type AdvisorWorkflow, type AdvisorWorkflowCall, type AnalyticsMetricsOptions, type AnalyticsMetricsSource, type AnalyticsRuntimeMetrics, type Category, type Facing, type Finding, type Level, type Lint, type LintContext, type LintSource, RUNTIME_LINTS, RunAdvisorOptions, STATIC_LINTS, adminRouteWithoutGuard, authApiCallWithoutHeaders, circularFk, constraintValidator, containerOversizedInstance, containerPublicInternet, duplicateIndex, emptyIndex, filterWithoutIndex, fromServerSchema, hardcodedSecret, hotShard, hyperdriveOutsideAction, indexReferencesUnknownField, indexUtilization, loadAnalyticsRuntimeMetrics, maskUncoveredPiiColumn, nondeterministicQueryMutation, policyReferencesUnknownTable, publicArgumentUsesAny, publicMutationWithoutRatelimit, r2sqlOutsideAction, relationReferencesUnknownField, relationReferencesUnknownTable, rlsUncoveredTable, runAdvisor, sqlInjectionRisk, tableWithoutInsert, unboundedStringArgument, unindexedForeignKey, unindexedRelationTarget, userCreatingMutationWithoutCaptcha, workflowUnknownTarget, workflowUnused };
|
|
1623
|
+
export { AE_METRIC_EVENTS, ALL_LINTS, type AdvisorAdminRoute, type AdvisorArgumentValidator, type AdvisorAuthApiCall, type AdvisorContainer, type AdvisorHyperdriveCall, type AdvisorIndex, type AdvisorIndexHit, type AdvisorInsertWrite, type AdvisorMaskProcedure, type AdvisorMutatorWrite, type AdvisorNondeterministicCall, type AdvisorProcedureProtection, type AdvisorQueryRead, type AdvisorR2sqlCall, type AdvisorRelation, type AdvisorRlsProcedure, type AdvisorSchema, type AdvisorSecretLiteral, type AdvisorShape, type AdvisorShardTraffic, type AdvisorSqlInterpolation, type AdvisorTable, type AdvisorTableSample, type AdvisorTableScan, type AdvisorWorkflow, type AdvisorWorkflowCall, type AnalyticsMetricsOptions, type AnalyticsMetricsSource, type AnalyticsRuntimeMetrics, type Category, type Facing, type Finding, type Level, type Lint, type LintContext, type LintSource, RUNTIME_LINTS, RunAdvisorOptions, STATIC_LINTS, adminRouteWithoutGuard, authApiCallWithoutHeaders, circularFk, constraintValidator, containerOversizedInstance, containerPublicInternet, duplicateIndex, emptyIndex, filterWithoutIndex, fromServerSchema, hardcodedSecret, hotShard, hyperdriveOutsideAction, indexReferencesUnknownField, indexUtilization, loadAnalyticsRuntimeMetrics, maskUncoveredPiiColumn, mutatorFullRowReplace, nondeterministicQueryMutation, policyReferencesUnknownTable, publicArgumentUsesAny, publicMutationWithoutRatelimit, r2sqlOutsideAction, relationReferencesUnknownField, relationReferencesUnknownTable, rlsUncoveredTable, runAdvisor, shapeTargetsGlobalTable, shapeUnknownTable, sqlInjectionRisk, tableWithoutInsert, unboundedStringArgument, unindexedForeignKey, unindexedRelationTarget, userCreatingMutationWithoutCaptcha, workflowDuplicateStepName, workflowUnknownTarget, workflowUnused };
|
package/dist/index.d.ts
CHANGED
|
@@ -214,6 +214,28 @@ interface AdvisorMaskProcedure {
|
|
|
214
214
|
visibility: "internal" | "public";
|
|
215
215
|
}
|
|
216
216
|
/**
|
|
217
|
+
* One whole-row `ctx.db.replace(id, document)` write discovered inside a custom
|
|
218
|
+
* mutator's authoritative `server` impl (`lunora/mutators.ts`) — the input the
|
|
219
|
+
* `mutator_full_row_replace` lint consumes.
|
|
220
|
+
*
|
|
221
|
+
* In the local-first sync engine a `replace` overwrites the entire row, so a
|
|
222
|
+
* concurrent edit to a *different* column (committed between this mutator's read
|
|
223
|
+
* and its write) is silently clobbered. `patch(id, { onlyTheField })` merges at
|
|
224
|
+
* the column level instead, letting independent field edits coexist — the
|
|
225
|
+
* blessed pattern for mutators on a synced (poke-live) table. Produced by the
|
|
226
|
+
* codegen feeder, which attributes each `replace` to the mutator export
|
|
227
|
+
* performing it; runtime callers don't supply it, so the lint finds nothing
|
|
228
|
+
* there.
|
|
229
|
+
*/
|
|
230
|
+
interface AdvisorMutatorWrite {
|
|
231
|
+
/** The mutator export whose `server` impl performs the replace (e.g. `renameChannel`). */
|
|
232
|
+
exportName: string;
|
|
233
|
+
/** Openable source path the replace appears in — always `lunora/mutators.ts`. */
|
|
234
|
+
file: string;
|
|
235
|
+
/** 1-based line of the `replace(...)` call, or `0` when unknown. */
|
|
236
|
+
line: number;
|
|
237
|
+
}
|
|
238
|
+
/**
|
|
217
239
|
* One non-deterministic API call discovered lexically inside a `query(...)` or
|
|
218
240
|
* `mutation(...)` handler body — the input the `nondeterministic_query_mutation`
|
|
219
241
|
* lint consumes. Produced by the codegen feeder, which walks each exported
|
|
@@ -384,6 +406,16 @@ interface AdvisorTable {
|
|
|
384
406
|
optionalFields?: ReadonlySet<string>;
|
|
385
407
|
/** Declared relations (`.relations((r) => …)`). */
|
|
386
408
|
relations: ReadonlyArray<AdvisorRelation>;
|
|
409
|
+
/**
|
|
410
|
+
* Storage tier the table is declared in: `"global"` (a `.global()` table,
|
|
411
|
+
* lives in D1 — the cross-shard tier), `"shardBy"` (partitioned across
|
|
412
|
+
* shard DOs by a key), or `"root"` (the default single-DO table). Read by
|
|
413
|
+
* the `shape_*` lints to flag replication shapes targeting a `.global()`
|
|
414
|
+
* table (poll-refreshed/latency-tiered, not poke-live). Optional — the
|
|
415
|
+
* codegen feeder always supplies it, the runtime feeder derives it; a feeder
|
|
416
|
+
* that omits it leaves tier-sensitive lints to treat the table as local.
|
|
417
|
+
*/
|
|
418
|
+
shardKind?: "global" | "root" | "shardBy";
|
|
387
419
|
}
|
|
388
420
|
/**
|
|
389
421
|
* One declared index, flattened across Lunora's index kinds so a single lint can
|
|
@@ -440,6 +472,28 @@ interface AdvisorSecretLiteral {
|
|
|
440
472
|
preview: string;
|
|
441
473
|
}
|
|
442
474
|
/**
|
|
475
|
+
* A replication shape declared via `defineShape({ table, where, columns? })` in
|
|
476
|
+
* `lunora/shapes.ts` (the local-first sync engine's partial-replication unit).
|
|
477
|
+
* The `shape_*` lints cross-reference each shape's {@link AdvisorShape.table}
|
|
478
|
+
* against the declared schema to flag a shape targeting an unknown table or a
|
|
479
|
+
* `.global()` table (which replicates through the latency-tiered D1 poll path,
|
|
480
|
+
* not the poke-live op-log). Supplied by the codegen feeder, which lifts only
|
|
481
|
+
* the export name + the static `table` literal; absent for runtime callers,
|
|
482
|
+
* where the shape lints find nothing.
|
|
483
|
+
*/
|
|
484
|
+
interface AdvisorShape {
|
|
485
|
+
/** Export binding name — the shape's registry key (e.g. `channelMessages`). */
|
|
486
|
+
exportName: string;
|
|
487
|
+
/** File the shape is declared in (relative, for the operator to open). */
|
|
488
|
+
file: string;
|
|
489
|
+
/**
|
|
490
|
+
* The `table` string literal the shape replicates from, or `undefined` when
|
|
491
|
+
* the feeder could not read it as a plain string literal — tier-sensitive
|
|
492
|
+
* lints skip a shape with no resolvable table rather than guessing.
|
|
493
|
+
*/
|
|
494
|
+
table?: string;
|
|
495
|
+
}
|
|
496
|
+
/**
|
|
443
497
|
* One shard's observed traffic share — the input the `hot_shard` runtime lint
|
|
444
498
|
* consumes. Produced by the studio backend, which fans out over a sharded
|
|
445
499
|
* function's shards and reads each shard's recorded request volume from the
|
|
@@ -523,10 +577,27 @@ interface AdvisorTableSample {
|
|
|
523
577
|
* how `AdvisorContainer` tracks `ContainerIR` and `AdvisorInsertWrite` tracks
|
|
524
578
|
* `InsertWriteIR`).
|
|
525
579
|
*/
|
|
580
|
+
/** One durable step call lifted from a workflow handler body — the input the duplicate-step-name lint compares. Structural subset of codegen's `WorkflowStepIR`. */
|
|
581
|
+
interface AdvisorWorkflowStep {
|
|
582
|
+
/** 1-based line of the durable step call, or `0` when unknown. */
|
|
583
|
+
line: number;
|
|
584
|
+
/** The native step method invoked: `do` / `sleep` / `sleepUntil` / `waitForEvent`. */
|
|
585
|
+
method: string;
|
|
586
|
+
/** The step's static label (the first string-literal argument). */
|
|
587
|
+
name: string;
|
|
588
|
+
}
|
|
526
589
|
/** One workflow declared via a `defineWorkflow()` export in `lunora/workflows.ts`. */
|
|
527
590
|
interface AdvisorWorkflow {
|
|
528
591
|
/** The `lunora/workflows.ts` export name, e.g. `orderPipeline`. */
|
|
529
592
|
exportName: string;
|
|
593
|
+
/**
|
|
594
|
+
* The durable step labels discovered in the handler body, in source order —
|
|
595
|
+
* the duplicate-step-name input. Cloudflare memoizes a step by its name, so a
|
|
596
|
+
* name used twice makes the second call silently return the first's cached
|
|
597
|
+
* result. Supplied by the codegen feeder; `undefined` for runtime callers,
|
|
598
|
+
* where the lint finds nothing.
|
|
599
|
+
*/
|
|
600
|
+
steps?: ReadonlyArray<AdvisorWorkflowStep>;
|
|
530
601
|
}
|
|
531
602
|
/** One `ctx.workflows.get("name")` call discovered in a function body. */
|
|
532
603
|
interface AdvisorWorkflowCall {
|
|
@@ -660,6 +731,14 @@ interface LintContext {
|
|
|
660
731
|
*/
|
|
661
732
|
maskProcedures?: ReadonlyArray<AdvisorMaskProcedure>;
|
|
662
733
|
/**
|
|
734
|
+
* Whole-row `ctx.db.replace(id, document)` writes lifted from custom
|
|
735
|
+
* mutators' authoritative `server` impls (the `mutator_full_row_replace`
|
|
736
|
+
* input). Each `replace` overwrites the entire row, clobbering a concurrent
|
|
737
|
+
* edit to a different column on a synced table. Supplied by the codegen
|
|
738
|
+
* feeder; absent for runtime callers, where the lint finds nothing.
|
|
739
|
+
*/
|
|
740
|
+
mutatorWrites?: ReadonlyArray<AdvisorMutatorWrite>;
|
|
741
|
+
/**
|
|
663
742
|
* Non-deterministic API calls (`Date.now`, `Math.random`,
|
|
664
743
|
* `crypto.randomUUID`, `crypto.getRandomValues`, `fetch`) discovered lexically
|
|
665
744
|
* inside `query`/`mutation` handler bodies — the `nondeterministic_query_mutation`
|
|
@@ -708,6 +787,14 @@ interface LintContext {
|
|
|
708
787
|
*/
|
|
709
788
|
secretLiterals?: ReadonlyArray<AdvisorSecretLiteral>;
|
|
710
789
|
/**
|
|
790
|
+
* Replication shapes declared via `defineShape` in `lunora/shapes.ts` — the
|
|
791
|
+
* `shape_unknown_table` and `shape_targets_global_table` lint input. Each
|
|
792
|
+
* carries the export name and its static `table` literal, cross-referenced
|
|
793
|
+
* against {@link LintContext.schema}. Supplied by the codegen feeder; absent
|
|
794
|
+
* for runtime callers, where the shape lints find nothing.
|
|
795
|
+
*/
|
|
796
|
+
shapes?: ReadonlyArray<AdvisorShape>;
|
|
797
|
+
/**
|
|
711
798
|
* Per-shard observed traffic — the `hot_shard` lint input. Supplied by the
|
|
712
799
|
* studio backend, which fans out over a sharded function's shards and reads
|
|
713
800
|
* each shard's recorded request volume from the durable `__lunora_metrics`
|
|
@@ -1133,6 +1220,30 @@ declare const indexReferencesUnknownField: Lint;
|
|
|
1133
1220
|
*/
|
|
1134
1221
|
declare const maskUncoveredPiiColumn: Lint;
|
|
1135
1222
|
/**
|
|
1223
|
+
* Flags a custom mutator whose authoritative `server` impl writes a row with
|
|
1224
|
+
* `ctx.db.replace(id, document)` — a whole-document overwrite.
|
|
1225
|
+
*
|
|
1226
|
+
* The local-first sync engine serializes mutators in the shard DO, so two
|
|
1227
|
+
* mutators that touch the *same row* but *different columns* both run to
|
|
1228
|
+
* completion — but only if each writes its own column. A `replace` overwrites
|
|
1229
|
+
* the entire row from the document the mutator assembled, so a concurrent edit
|
|
1230
|
+
* to another column (committed between this mutator's read and its write, or
|
|
1231
|
+
* carried as a pending optimistic overlay on a client) is silently clobbered:
|
|
1232
|
+
* the kind of "two offline edits to different fields fight each other" data loss
|
|
1233
|
+
* a column-level merge avoids. `ctx.db.patch(id, { onlyTheChangedField })`
|
|
1234
|
+
* merges at the column level instead, so independent field edits coexist.
|
|
1235
|
+
*
|
|
1236
|
+
* `WARN`, not `ERROR`: `replace` is legitimate when the mutator genuinely owns
|
|
1237
|
+
* the whole row (a full-form save, a state-machine transition that rewrites
|
|
1238
|
+
* every field). The lint just surfaces the column-clobber risk so a developer
|
|
1239
|
+
* reaches for `patch` by default on a synced table.
|
|
1240
|
+
*
|
|
1241
|
+
* **Evidence supply**: runs only when the codegen feeder supplies
|
|
1242
|
+
* `context.mutatorWrites` (each a `replace` call lifted from a mutator's inline
|
|
1243
|
+
* `server` body); absent for runtime callers, where the lint finds nothing.
|
|
1244
|
+
*/
|
|
1245
|
+
declare const mutatorFullRowReplace: Lint;
|
|
1246
|
+
/**
|
|
1136
1247
|
* Flags a non-deterministic API call inside a `query(...)` or `mutation(...)`
|
|
1137
1248
|
* handler body.
|
|
1138
1249
|
*
|
|
@@ -1285,6 +1396,48 @@ declare const relationReferencesUnknownTable: Lint;
|
|
|
1285
1396
|
*/
|
|
1286
1397
|
declare const rlsUncoveredTable: Lint;
|
|
1287
1398
|
/**
|
|
1399
|
+
* Flags a replication shape whose `table` is a `.global()` table.
|
|
1400
|
+
*
|
|
1401
|
+
* Poke-live replication is a per-shard-DO property: the shard owns its SQLite
|
|
1402
|
+
* and a monotonic `__cdc_log`, so a write produces an ordered op the DO pokes to
|
|
1403
|
+
* every subscriber at the next flush. A `.global()` table lives outside the
|
|
1404
|
+
* shard DO's SQLite op-log (in a global backend — D1, or Hyperdrive-fronted
|
|
1405
|
+
* Postgres/MySQL) — so a shape over a global table cannot be poke-live. It is
|
|
1406
|
+
* served through the cross-shard tier: **coordinator/poll-refreshed, latency-
|
|
1407
|
+
* tiered**, not live. That is a real and supported tier (it is the recommended
|
|
1408
|
+
* answer for cross-shard reads — denormalize, or move the joined table to
|
|
1409
|
+
* `.global()` and read through the global backend), but its freshness semantics
|
|
1410
|
+
* differ from a sharded shape's, so the boundary is surfaced rather than hidden.
|
|
1411
|
+
*
|
|
1412
|
+
* `WARN`, not `ERROR`: a global-table shape is a legitimate design once you
|
|
1413
|
+
* accept the poll-refresh latency; the lint just makes the tier explicit so a
|
|
1414
|
+
* developer does not assume poke-live freshness.
|
|
1415
|
+
*
|
|
1416
|
+
* **Evidence supply**: runs only when the codegen feeder supplies
|
|
1417
|
+
* `context.shapes`; the table's tier comes from the schema's `shardKind`. A
|
|
1418
|
+
* shape whose table is unknown (caught by `shape_unknown_table`) or whose tier
|
|
1419
|
+
* the feeder didn't supply is skipped.
|
|
1420
|
+
*/
|
|
1421
|
+
declare const shapeTargetsGlobalTable: Lint;
|
|
1422
|
+
/**
|
|
1423
|
+
* Flags a replication shape whose `table` names a table that does not exist in
|
|
1424
|
+
* the schema.
|
|
1425
|
+
*
|
|
1426
|
+
* `defineShape({ table: "messages", … })` binds a shape to a table by a plain
|
|
1427
|
+
* string. A live `subscribeShape("…")` resolves that shape server-side and runs
|
|
1428
|
+
* its membership query against the named table — so a typo, a stale name after a
|
|
1429
|
+
* rename, or a copy-paste mistake produces a shape that can never resolve a
|
|
1430
|
+
* rowset: the subscription seeds empty and then errors at the first flush
|
|
1431
|
+
* (`no such table`). This is a definite, build-time-detectable break, so it is
|
|
1432
|
+
* an `ERROR` — surfaced before the broken shape ever ships.
|
|
1433
|
+
*
|
|
1434
|
+
* **Evidence supply**: runs only when the codegen feeder supplies
|
|
1435
|
+
* `context.shapes`. A shape whose `table` wasn't a static string literal (no
|
|
1436
|
+
* resolvable name) is skipped rather than guessed at, so the lint under-reports
|
|
1437
|
+
* rather than raising false alarms.
|
|
1438
|
+
*/
|
|
1439
|
+
declare const shapeUnknownTable: Lint;
|
|
1440
|
+
/**
|
|
1288
1441
|
* Flags a `ctx.sql` tagged-template that splices an unparameterized
|
|
1289
1442
|
* string-building expression into the query.
|
|
1290
1443
|
*
|
|
@@ -1387,6 +1540,25 @@ declare const unindexedRelationTarget: Lint;
|
|
|
1387
1540
|
*/
|
|
1388
1541
|
declare const userCreatingMutationWithoutCaptcha: Lint;
|
|
1389
1542
|
/**
|
|
1543
|
+
* Flags a durable step name reused within one workflow.
|
|
1544
|
+
*
|
|
1545
|
+
* Cloudflare Workflows memoizes every `step.do` / `step.sleep` / `step.sleepUntil`
|
|
1546
|
+
* / `step.waitForEvent` call by its name: on replay the runtime returns the cached
|
|
1547
|
+
* result for a name it has already seen. Two distinct steps that share a name are
|
|
1548
|
+
* therefore a silent bug — the second call never runs its body and instead yields
|
|
1549
|
+
* the first's result, skipping the work (a charge, a write, an external wait)
|
|
1550
|
+
* without error. Hence `ERROR`/`INTERNAL`: it is a developer-facing correctness
|
|
1551
|
+
* defect in the workflow's own code, not a runtime-data nit.
|
|
1552
|
+
*
|
|
1553
|
+
* Only the first string-literal argument of each step call is compared; a step
|
|
1554
|
+
* named dynamically (`step.do(\`load-${id}\`, …)`) is omitted by the feeder, so a
|
|
1555
|
+
* deliberately-parameterized fan-out is never flagged. `ctx.runStep(stepDef, …)`
|
|
1556
|
+
* names (which come from `defineStep` in another file) are out of scope here.
|
|
1557
|
+
* Only runs when the declaration feeder supplied step evidence
|
|
1558
|
+
* (`workflow.steps` present); a runtime caller flags nothing.
|
|
1559
|
+
*/
|
|
1560
|
+
declare const workflowDuplicateStepName: Lint;
|
|
1561
|
+
/**
|
|
1390
1562
|
* A correctness lint: every `ctx.workflows.get("name")` call must reference a
|
|
1391
1563
|
* workflow that exists — i.e. a `defineWorkflow` export in `lunora/workflows.ts`.
|
|
1392
1564
|
* A `.get("x")` whose `"x"` resolves to no declared workflow is a typo or a
|
|
@@ -1448,4 +1620,4 @@ interface RunAdvisorOptions {
|
|
|
1448
1620
|
* `static` lints at build time and defer `runtime` lints to a live shard.
|
|
1449
1621
|
*/
|
|
1450
1622
|
declare const runAdvisor: (context: LintContext, options?: RunAdvisorOptions) => Finding[];
|
|
1451
|
-
export { AE_METRIC_EVENTS, ALL_LINTS, type AdvisorAdminRoute, type AdvisorArgumentValidator, type AdvisorAuthApiCall, type AdvisorContainer, type AdvisorHyperdriveCall, type AdvisorIndex, type AdvisorIndexHit, type AdvisorInsertWrite, type AdvisorMaskProcedure, type AdvisorNondeterministicCall, type AdvisorProcedureProtection, type AdvisorQueryRead, type AdvisorR2sqlCall, type AdvisorRelation, type AdvisorRlsProcedure, type AdvisorSchema, type AdvisorSecretLiteral, type AdvisorShardTraffic, type AdvisorSqlInterpolation, type AdvisorTable, type AdvisorTableSample, type AdvisorTableScan, type AdvisorWorkflow, type AdvisorWorkflowCall, type AnalyticsMetricsOptions, type AnalyticsMetricsSource, type AnalyticsRuntimeMetrics, type Category, type Facing, type Finding, type Level, type Lint, type LintContext, type LintSource, RUNTIME_LINTS, RunAdvisorOptions, STATIC_LINTS, adminRouteWithoutGuard, authApiCallWithoutHeaders, circularFk, constraintValidator, containerOversizedInstance, containerPublicInternet, duplicateIndex, emptyIndex, filterWithoutIndex, fromServerSchema, hardcodedSecret, hotShard, hyperdriveOutsideAction, indexReferencesUnknownField, indexUtilization, loadAnalyticsRuntimeMetrics, maskUncoveredPiiColumn, nondeterministicQueryMutation, policyReferencesUnknownTable, publicArgumentUsesAny, publicMutationWithoutRatelimit, r2sqlOutsideAction, relationReferencesUnknownField, relationReferencesUnknownTable, rlsUncoveredTable, runAdvisor, sqlInjectionRisk, tableWithoutInsert, unboundedStringArgument, unindexedForeignKey, unindexedRelationTarget, userCreatingMutationWithoutCaptcha, workflowUnknownTarget, workflowUnused };
|
|
1623
|
+
export { AE_METRIC_EVENTS, ALL_LINTS, type AdvisorAdminRoute, type AdvisorArgumentValidator, type AdvisorAuthApiCall, type AdvisorContainer, type AdvisorHyperdriveCall, type AdvisorIndex, type AdvisorIndexHit, type AdvisorInsertWrite, type AdvisorMaskProcedure, type AdvisorMutatorWrite, type AdvisorNondeterministicCall, type AdvisorProcedureProtection, type AdvisorQueryRead, type AdvisorR2sqlCall, type AdvisorRelation, type AdvisorRlsProcedure, type AdvisorSchema, type AdvisorSecretLiteral, type AdvisorShape, type AdvisorShardTraffic, type AdvisorSqlInterpolation, type AdvisorTable, type AdvisorTableSample, type AdvisorTableScan, type AdvisorWorkflow, type AdvisorWorkflowCall, type AnalyticsMetricsOptions, type AnalyticsMetricsSource, type AnalyticsRuntimeMetrics, type Category, type Facing, type Finding, type Level, type Lint, type LintContext, type LintSource, RUNTIME_LINTS, RunAdvisorOptions, STATIC_LINTS, adminRouteWithoutGuard, authApiCallWithoutHeaders, circularFk, constraintValidator, containerOversizedInstance, containerPublicInternet, duplicateIndex, emptyIndex, filterWithoutIndex, fromServerSchema, hardcodedSecret, hotShard, hyperdriveOutsideAction, indexReferencesUnknownField, indexUtilization, loadAnalyticsRuntimeMetrics, maskUncoveredPiiColumn, mutatorFullRowReplace, nondeterministicQueryMutation, policyReferencesUnknownTable, publicArgumentUsesAny, publicMutationWithoutRatelimit, r2sqlOutsideAction, relationReferencesUnknownField, relationReferencesUnknownTable, rlsUncoveredTable, runAdvisor, shapeTargetsGlobalTable, shapeUnknownTable, sqlInjectionRisk, tableWithoutInsert, unboundedStringArgument, unindexedForeignKey, unindexedRelationTarget, userCreatingMutationWithoutCaptcha, workflowDuplicateStepName, workflowUnknownTarget, workflowUnused };
|
package/dist/index.mjs
CHANGED
|
@@ -13,6 +13,7 @@ import hardcodedSecret from './packem_shared/hardcodedSecret-W2pz1UZB.mjs';
|
|
|
13
13
|
import hyperdriveOutsideAction from './packem_shared/hyperdriveOutsideAction-BgZqX7Xg.mjs';
|
|
14
14
|
import indexReferencesUnknownField from './packem_shared/indexReferencesUnknownField-DH0_dbUY.mjs';
|
|
15
15
|
import maskUncoveredPiiColumn from './packem_shared/maskUncoveredPiiColumn-DjGIPG6M.mjs';
|
|
16
|
+
import mutatorFullRowReplace from './packem_shared/mutatorFullRowReplace-BJnNDaIV.mjs';
|
|
16
17
|
import nondeterministicQueryMutation from './packem_shared/nondeterministicQueryMutation-GXES1fLp.mjs';
|
|
17
18
|
import policyReferencesUnknownTable from './packem_shared/policyReferencesUnknownTable-DtaIEovd.mjs';
|
|
18
19
|
import publicArgumentUsesAny from './packem_shared/publicArgumentUsesAny-C71b2NCf.mjs';
|
|
@@ -21,22 +22,27 @@ import r2sqlOutsideAction from './packem_shared/r2sqlOutsideAction-CtqxvMuV.mjs'
|
|
|
21
22
|
import relationReferencesUnknownField from './packem_shared/relationReferencesUnknownField-YznyXt_7.mjs';
|
|
22
23
|
import relationReferencesUnknownTable from './packem_shared/relationReferencesUnknownTable-DrorpKYe.mjs';
|
|
23
24
|
import rlsUncoveredTable from './packem_shared/rlsUncoveredTable-CxEfZ5eZ.mjs';
|
|
25
|
+
import shapeTargetsGlobalTable from './packem_shared/shapeTargetsGlobalTable-DHrf4Koi.mjs';
|
|
26
|
+
import shapeUnknownTable from './packem_shared/shapeUnknownTable-C8aDWFoe.mjs';
|
|
24
27
|
import sqlInjectionRisk from './packem_shared/sqlInjectionRisk-zwytYGLt.mjs';
|
|
25
28
|
import tableWithoutInsert from './packem_shared/tableWithoutInsert-CbbaYIP4.mjs';
|
|
26
29
|
import unboundedStringArgument from './packem_shared/unboundedStringArgument-DThg2-wt.mjs';
|
|
27
30
|
import unindexedForeignKey from './packem_shared/unindexedForeignKey-BgJbKyqK.mjs';
|
|
28
31
|
import unindexedRelationTarget from './packem_shared/unindexedRelationTarget-D6eyj6Xx.mjs';
|
|
29
32
|
import userCreatingMutationWithoutCaptcha from './packem_shared/userCreatingMutationWithoutCaptcha-CH31YsUZ.mjs';
|
|
33
|
+
import workflowDuplicateStepName from './packem_shared/workflowDuplicateStepName-ioBxPBCy.mjs';
|
|
30
34
|
import workflowUnknownTarget from './packem_shared/workflowUnknownTarget-Cdd7WhKQ.mjs';
|
|
31
35
|
import workflowUnused from './packem_shared/workflowUnused-D0jHxdz9.mjs';
|
|
32
36
|
export { AE_METRIC_EVENTS, loadAnalyticsRuntimeMetrics } from './packem_shared/AE_METRIC_EVENTS-DexctYv6.mjs';
|
|
33
|
-
export { fromServerSchema } from './packem_shared/fromServerSchema-
|
|
37
|
+
export { fromServerSchema } from './packem_shared/fromServerSchema-TWb7dTHC.mjs';
|
|
34
38
|
|
|
35
39
|
const STATIC_LINTS = [
|
|
36
40
|
indexReferencesUnknownField,
|
|
37
41
|
relationReferencesUnknownTable,
|
|
38
42
|
relationReferencesUnknownField,
|
|
39
43
|
workflowUnknownTarget,
|
|
44
|
+
workflowDuplicateStepName,
|
|
45
|
+
shapeUnknownTable,
|
|
40
46
|
emptyIndex,
|
|
41
47
|
circularFk,
|
|
42
48
|
unindexedForeignKey,
|
|
@@ -45,6 +51,8 @@ const STATIC_LINTS = [
|
|
|
45
51
|
tableWithoutInsert,
|
|
46
52
|
workflowUnused,
|
|
47
53
|
filterWithoutIndex,
|
|
54
|
+
shapeTargetsGlobalTable,
|
|
55
|
+
mutatorFullRowReplace,
|
|
48
56
|
nondeterministicQueryMutation,
|
|
49
57
|
hyperdriveOutsideAction,
|
|
50
58
|
r2sqlOutsideAction,
|
|
@@ -76,4 +84,4 @@ const runAdvisor = (context, options = {}) => {
|
|
|
76
84
|
return findings;
|
|
77
85
|
};
|
|
78
86
|
|
|
79
|
-
export { ALL_LINTS, RUNTIME_LINTS, STATIC_LINTS, adminRouteWithoutGuard, authApiCallWithoutHeaders, circularFk, constraintValidator, containerOversizedInstance, containerPublicInternet, duplicateIndex, emptyIndex, filterWithoutIndex, hardcodedSecret, hotShard, hyperdriveOutsideAction, indexReferencesUnknownField, indexUtilization, maskUncoveredPiiColumn, nondeterministicQueryMutation, policyReferencesUnknownTable, publicArgumentUsesAny, publicMutationWithoutRatelimit, r2sqlOutsideAction, relationReferencesUnknownField, relationReferencesUnknownTable, rlsUncoveredTable, runAdvisor, sqlInjectionRisk, tableWithoutInsert, unboundedStringArgument, unindexedForeignKey, unindexedRelationTarget, userCreatingMutationWithoutCaptcha, workflowUnknownTarget, workflowUnused };
|
|
87
|
+
export { ALL_LINTS, RUNTIME_LINTS, STATIC_LINTS, adminRouteWithoutGuard, authApiCallWithoutHeaders, circularFk, constraintValidator, containerOversizedInstance, containerPublicInternet, duplicateIndex, emptyIndex, filterWithoutIndex, hardcodedSecret, hotShard, hyperdriveOutsideAction, indexReferencesUnknownField, indexUtilization, maskUncoveredPiiColumn, mutatorFullRowReplace, nondeterministicQueryMutation, policyReferencesUnknownTable, publicArgumentUsesAny, publicMutationWithoutRatelimit, r2sqlOutsideAction, relationReferencesUnknownField, relationReferencesUnknownTable, rlsUncoveredTable, runAdvisor, shapeTargetsGlobalTable, shapeUnknownTable, sqlInjectionRisk, tableWithoutInsert, unboundedStringArgument, unindexedForeignKey, unindexedRelationTarget, userCreatingMutationWithoutCaptcha, workflowDuplicateStepName, workflowUnknownTarget, workflowUnused };
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { e as emit } from './finding-Dm_zvzS1.mjs';
|
|
2
|
+
|
|
3
|
+
const mutatorFullRowReplace = {
|
|
4
|
+
categories: ["SCHEMA"],
|
|
5
|
+
description: "A custom mutator's `server` impl writes with `ctx.db.replace(id, document)` — a whole-row overwrite. On a synced table this clobbers a concurrent edit to a different column; `ctx.db.patch(id, { field })` merges at the column level so independent field edits coexist.",
|
|
6
|
+
facing: "EXTERNAL",
|
|
7
|
+
level: "WARN",
|
|
8
|
+
name: "mutator_full_row_replace",
|
|
9
|
+
remediation: "Prefer `ctx.db.patch(id, { onlyTheChangedField })` so a concurrent edit to another column isn't lost. Keep `replace` only when the mutator genuinely owns the entire row (e.g. a full-form save).",
|
|
10
|
+
run: (context) => {
|
|
11
|
+
if (context.mutatorWrites === void 0) {
|
|
12
|
+
return [];
|
|
13
|
+
}
|
|
14
|
+
return context.mutatorWrites.map(
|
|
15
|
+
(write) => emit(mutatorFullRowReplace, {
|
|
16
|
+
cacheKey: `mutator_full_row_replace:${write.exportName}:${String(write.line)}`,
|
|
17
|
+
detail: `Mutator \`${write.exportName}\` (${write.file}:${String(write.line)}) writes with \`ctx.db.replace(...)\`, overwriting the whole row. A concurrent edit to a different column is clobbered — use \`ctx.db.patch(id, { field })\` to merge at the column level.`,
|
|
18
|
+
metadata: { exportName: write.exportName, file: write.file, line: write.line }
|
|
19
|
+
})
|
|
20
|
+
);
|
|
21
|
+
},
|
|
22
|
+
source: "static",
|
|
23
|
+
title: "Mutator overwrites the whole row (clobbers concurrent column edits)"
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
export { mutatorFullRowReplace as default };
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { e as emit } from './finding-Dm_zvzS1.mjs';
|
|
2
|
+
|
|
3
|
+
const shapeTargetsGlobalTable = {
|
|
4
|
+
categories: ["PERFORMANCE"],
|
|
5
|
+
description: "A `defineShape` replicates from a `.global()` table. Global tables live outside the shard DO's SQLite op-log, so the shape is served through the cross-shard tier — coordinator/poll-refreshed and latency-tiered, not poke-live like a sharded shape.",
|
|
6
|
+
facing: "EXTERNAL",
|
|
7
|
+
level: "WARN",
|
|
8
|
+
name: "shape_targets_global_table",
|
|
9
|
+
remediation: "Expected if you want the cross-shard tier — just don't assume poke-live freshness; global shapes refresh on a poll. For live updates, replicate from a sharded table instead (denormalize the columns you need into the shard).",
|
|
10
|
+
run: (context) => {
|
|
11
|
+
if (context.shapes === void 0) {
|
|
12
|
+
return [];
|
|
13
|
+
}
|
|
14
|
+
const globalTables = new Set(context.schema.tables.filter((table) => table.shardKind === "global").map((table) => table.name));
|
|
15
|
+
const findings = [];
|
|
16
|
+
for (const shape of context.shapes) {
|
|
17
|
+
if (shape.table === void 0 || !globalTables.has(shape.table)) {
|
|
18
|
+
continue;
|
|
19
|
+
}
|
|
20
|
+
findings.push(
|
|
21
|
+
emit(shapeTargetsGlobalTable, {
|
|
22
|
+
cacheKey: `shape_targets_global_table:${shape.exportName}`,
|
|
23
|
+
detail: `Shape \`${shape.exportName}\` (${shape.file}) replicates from the \`.global()\` table \`${shape.table}\`. It is served through the cross-shard global tier — poll-refreshed and latency-tiered, not poke-live.`,
|
|
24
|
+
metadata: { exportName: shape.exportName, file: shape.file, table: shape.table }
|
|
25
|
+
})
|
|
26
|
+
);
|
|
27
|
+
}
|
|
28
|
+
return findings;
|
|
29
|
+
},
|
|
30
|
+
source: "static",
|
|
31
|
+
title: "Shape replicates from a global (cross-shard) table"
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
export { shapeTargetsGlobalTable as default };
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { e as emit } from './finding-Dm_zvzS1.mjs';
|
|
2
|
+
|
|
3
|
+
const shapeUnknownTable = {
|
|
4
|
+
categories: ["SCHEMA"],
|
|
5
|
+
description: "A `defineShape` is bound to a `table` name that does not exist in the schema. The shape can never resolve a rowset — its subscription seeds empty and errors at the first flush.",
|
|
6
|
+
facing: "INTERNAL",
|
|
7
|
+
level: "ERROR",
|
|
8
|
+
name: "shape_unknown_table",
|
|
9
|
+
remediation: "Fix the shape's `table` to a real table name (check for a typo or a table that was renamed/removed).",
|
|
10
|
+
run: (context) => {
|
|
11
|
+
if (context.shapes === void 0) {
|
|
12
|
+
return [];
|
|
13
|
+
}
|
|
14
|
+
const knownTables = new Set(context.schema.tables.map((table) => table.name));
|
|
15
|
+
const findings = [];
|
|
16
|
+
for (const shape of context.shapes) {
|
|
17
|
+
if (shape.table === void 0 || knownTables.has(shape.table)) {
|
|
18
|
+
continue;
|
|
19
|
+
}
|
|
20
|
+
findings.push(
|
|
21
|
+
emit(shapeUnknownTable, {
|
|
22
|
+
cacheKey: `shape_unknown_table:${shape.exportName}`,
|
|
23
|
+
detail: `Shape \`${shape.exportName}\` (${shape.file}) replicates from table \`${shape.table}\`, which is not declared in the schema. The shape can never resolve a rowset.`,
|
|
24
|
+
metadata: { exportName: shape.exportName, file: shape.file, table: shape.table }
|
|
25
|
+
})
|
|
26
|
+
);
|
|
27
|
+
}
|
|
28
|
+
return findings;
|
|
29
|
+
},
|
|
30
|
+
source: "static",
|
|
31
|
+
title: "Shape bound to an unknown table"
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
export { shapeUnknownTable as default };
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { e as emit } from './finding-Dm_zvzS1.mjs';
|
|
2
|
+
|
|
3
|
+
const workflowDuplicateStepName = {
|
|
4
|
+
categories: ["SCHEMA"],
|
|
5
|
+
description: "Two durable steps in this workflow share a name. Cloudflare memoizes a step by its name, so on replay the second call returns the first step's cached result instead of running its body — silently skipping the work without an error.",
|
|
6
|
+
facing: "INTERNAL",
|
|
7
|
+
level: "ERROR",
|
|
8
|
+
// eslint-disable-next-line no-secrets/no-secrets -- the lint's rule id, not a credential
|
|
9
|
+
name: "workflow_duplicate_step_name",
|
|
10
|
+
remediation: "Give every `step.do` / `step.sleep` / `step.sleepUntil` / `step.waitForEvent` call in the workflow a unique name. If a step legitimately repeats (e.g. a loop), make the name distinct per iteration by interpolating the item id into the step name.",
|
|
11
|
+
run: (context) => {
|
|
12
|
+
if (context.workflows === void 0) {
|
|
13
|
+
return [];
|
|
14
|
+
}
|
|
15
|
+
const findings = [];
|
|
16
|
+
for (const workflow of context.workflows) {
|
|
17
|
+
const { steps } = workflow;
|
|
18
|
+
if (steps === void 0 || steps.length === 0) {
|
|
19
|
+
continue;
|
|
20
|
+
}
|
|
21
|
+
const firstLineByName = /* @__PURE__ */ new Map();
|
|
22
|
+
const reported = /* @__PURE__ */ new Set();
|
|
23
|
+
for (const step of steps) {
|
|
24
|
+
const firstLine = firstLineByName.get(step.name);
|
|
25
|
+
if (firstLine === void 0) {
|
|
26
|
+
firstLineByName.set(step.name, step.line);
|
|
27
|
+
continue;
|
|
28
|
+
}
|
|
29
|
+
if (reported.has(step.name)) {
|
|
30
|
+
continue;
|
|
31
|
+
}
|
|
32
|
+
reported.add(step.name);
|
|
33
|
+
findings.push(
|
|
34
|
+
emit(workflowDuplicateStepName, {
|
|
35
|
+
cacheKey: `workflow_duplicate_step_name:${workflow.exportName}:${step.name}`,
|
|
36
|
+
detail: `Workflow "${workflow.exportName}" reuses the durable step name "${step.name}" (first at line ${String(firstLine)}, again at line ${String(step.line)}). The second call returns the first's cached result instead of running.`,
|
|
37
|
+
metadata: { firstLine, line: step.line, stepName: step.name, workflow: workflow.exportName }
|
|
38
|
+
})
|
|
39
|
+
);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
return findings;
|
|
43
|
+
},
|
|
44
|
+
source: "static",
|
|
45
|
+
title: "Duplicate durable step name in workflow"
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
export { workflowDuplicateStepName 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.9",
|
|
4
4
|
"description": "Schema & query lints (splinter-style advisors) for Lunora, feeding the Studio Advisors view",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"advisor",
|
|
@@ -46,7 +46,7 @@
|
|
|
46
46
|
"access": "public"
|
|
47
47
|
},
|
|
48
48
|
"dependencies": {
|
|
49
|
-
"@lunora/server": "1.0.0-alpha.
|
|
49
|
+
"@lunora/server": "1.0.0-alpha.7"
|
|
50
50
|
},
|
|
51
51
|
"engines": {
|
|
52
52
|
"node": "^22.15.0 || >=24.11.0"
|