@lunora/codegen 1.0.0-alpha.291 → 1.0.0-alpha.293

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.
Files changed (59) hide show
  1. package/dist/index.d.mts +1993 -1926
  2. package/dist/index.d.ts +1993 -1926
  3. package/dist/index.mjs +1 -1
  4. package/dist/packem_shared/{AGENTS_FILENAME-CwwSEW6a.mjs → AGENTS_FILENAME-B6-kjzM3.mjs} +1 -1
  5. package/dist/packem_shared/{CONTAINERS_FILENAME-BtX5vWh8.mjs → CONTAINERS_FILENAME-Bk0u9QUu.mjs} +1 -1
  6. package/dist/packem_shared/{MUTATORS_FILENAME-Dl1tElqi.mjs → MUTATORS_FILENAME-Bw0YtaO8.mjs} +1 -1
  7. package/dist/packem_shared/{NOTIFY_FILENAME-BeTurUSC.mjs → NOTIFY_FILENAME-CPjbiHQl.mjs} +1 -1
  8. package/dist/packem_shared/{QUEUES_FILENAME-D588u733.mjs → QUEUES_FILENAME-CsRy4VTF.mjs} +1 -1
  9. package/dist/packem_shared/SCHEMA_SNAPSHOT_FILENAME-2FV2pwdH.mjs +11 -0
  10. package/dist/packem_shared/{SHAPES_FILENAME-D-Q8lSEw.mjs → SHAPES_FILENAME-DkBUN9dK.mjs} +1 -1
  11. package/dist/packem_shared/{WORKFLOWS_FILENAME-BGMSNTDC.mjs → WORKFLOWS_FILENAME-CQR8fE9B.mjs} +1 -1
  12. package/dist/packem_shared/{attribution-BUciBs1M.mjs → attribution-hVBriwzy.mjs} +1 -1
  13. package/dist/packem_shared/chain-CRDaTj2c.mjs +1 -0
  14. package/dist/packem_shared/classify-procedure-call-BAj0D1u2.mjs +1 -0
  15. package/dist/packem_shared/{context-property-calls-DPnUYWSX.mjs → context-property-calls-DJ_A1R5i.mjs} +1 -1
  16. package/dist/packem_shared/{context-root-Dlo21ybw.mjs → context-root-wBahDTzk.mjs} +1 -1
  17. package/dist/packem_shared/createCodegenProject-V51_yBw3.mjs +1 -0
  18. package/dist/packem_shared/{database-calls-ONnjnkMd.mjs → database-calls-CHZbBUCZ.mjs} +1 -1
  19. package/dist/packem_shared/{discoverAuthApiCalls-DAaK0HUK.mjs → discoverAuthApiCalls-xs0ONfI-.mjs} +1 -1
  20. package/dist/packem_shared/{discoverFunctions-DsQwrwWq.mjs → discoverFunctions-DaEZl3uE.mjs} +1 -1
  21. package/dist/packem_shared/{discoverHttpRoutes-Cv-h-Yuc.mjs → discoverHttpRoutes-C_u9YiFb.mjs} +1 -1
  22. package/dist/packem_shared/discoverHyperdriveCalls-BUrA_96X.mjs +1 -0
  23. package/dist/packem_shared/{discoverInserts-CQsoloGk.mjs → discoverInserts-OT_fiEGq.mjs} +1 -1
  24. package/dist/packem_shared/discoverMaskProcedures--hMyCNud.mjs +1 -0
  25. package/dist/packem_shared/{discoverMigrations-uxm9qHrr.mjs → discoverMigrations-C9mLIhGZ.mjs} +1 -1
  26. package/dist/packem_shared/{discoverNondeterministicCalls-Dm7_PJoA.mjs → discoverNondeterministicCalls-9ZDpRptb.mjs} +1 -1
  27. package/dist/packem_shared/discoverQueries-C8V4cMIP.mjs +1 -0
  28. package/dist/packem_shared/discoverR2sqlCalls-COKmDnih.mjs +1 -0
  29. package/dist/packem_shared/{discoverRlsMetadata-Cu4bc5KZ.mjs → discoverRlsMetadata-Cb8JR1b4.mjs} +1 -1
  30. package/dist/packem_shared/discoverRlsProcedures-CuRLSnf9.mjs +1 -0
  31. package/dist/packem_shared/discoverSchema-Btu1KpBX.mjs +1 -0
  32. package/dist/packem_shared/{discoverStorageRulesMetadata-D6rjgKYC.mjs → discoverStorageRulesMetadata-BPJplDfz.mjs} +1 -1
  33. package/dist/packem_shared/emitAgents-D_UUdNe1.mjs +1 -0
  34. package/dist/packem_shared/{emitApp-D7G7gmV7.mjs → emitApp-DAZCUDbr.mjs} +1 -1
  35. package/dist/packem_shared/{emitCrons-Gd8jD8jF.mjs → emitCrons-BQW2lviZ.mjs} +1 -1
  36. package/dist/packem_shared/emitServer-D1vA5_X1.mjs +1 -0
  37. package/dist/packem_shared/emitShard-DY0iaXec.mjs +1 -0
  38. package/dist/packem_shared/{exported-procedure-chains-BZk1-hro.mjs → exported-procedure-chains-DKK5HoK3.mjs} +1 -1
  39. package/dist/packem_shared/{handler-reference-DXkpwoAq.mjs → handler-reference-vIeqkhV_.mjs} +1 -1
  40. package/dist/packem_shared/{index-BoPXSW7g.mjs → index-BW4hJ4Wm.mjs} +1 -1
  41. package/dist/packem_shared/platform-8oj7iXQy.mjs +1 -0
  42. package/dist/packem_shared/schema-BgM8A3XV.mjs +1 -0
  43. package/dist/packem_shared/{server-DSUfd6Tc.mjs → server-DAvi_bv_.mjs} +2 -2
  44. package/dist/packem_shared/{shard-BNhX5R-a.mjs → shard-DfBMIpi9.mjs} +6 -6
  45. package/dist/packem_shared/{shard-runtime-CkaEBSdL.mjs → shard-runtime-BX0xmo0P.mjs} +3 -3
  46. package/package.json +6 -6
  47. package/dist/packem_shared/SCHEMA_SNAPSHOT_FILENAME-3FEkspbx.mjs +0 -11
  48. package/dist/packem_shared/chain-BUxIVIw_.mjs +0 -1
  49. package/dist/packem_shared/classify-procedure-call-BJdf_-M6.mjs +0 -1
  50. package/dist/packem_shared/discoverHyperdriveCalls-xy2G6igG.mjs +0 -1
  51. package/dist/packem_shared/discoverMaskProcedures-Dr1UccD6.mjs +0 -1
  52. package/dist/packem_shared/discoverQueries-BqECbkm9.mjs +0 -1
  53. package/dist/packem_shared/discoverR2sqlCalls-I9IqJFcy.mjs +0 -1
  54. package/dist/packem_shared/discoverRlsProcedures-Bkohn4ZR.mjs +0 -1
  55. package/dist/packem_shared/discoverSchema-BCxeD80d.mjs +0 -1
  56. package/dist/packem_shared/emitAgents-BedQefBj.mjs +0 -1
  57. package/dist/packem_shared/emitServer-DlOheWku.mjs +0 -1
  58. package/dist/packem_shared/emitShard-DguoZWGv.mjs +0 -1
  59. package/dist/packem_shared/ir-DBSvhNa-.mjs +0 -1
package/dist/index.d.mts CHANGED
@@ -251,2237 +251,2301 @@ declare const serializeSchemaSnapshot: (snapshot: SchemaSnapshot) => string;
251
251
  */
252
252
  declare const diffSchemaSnapshots: (baseline: SchemaSnapshot | undefined, current: SchemaSnapshot) => SchemaDrift;
253
253
  /**
254
- * AST-observable subset of a column's modifier chain (`.unique()`, `.default()`,
255
- * …). Function-valued modifiers (`.$defaultFn`/`.$onUpdateFn`) can't be
256
- * serialized, so only their *presence* is recorded.
254
+ * Who a discovered call site runs on behalf of — the one attribution every
255
+ * per-call-site IR carries (see `discover/attribution`).
256
+ *
257
+ * `export`: lexically inside an exported top-level declaration. `name` is the
258
+ * EXPORTED name — `export const send = …` → `send`, `export default …` →
259
+ * `default`, `const run = …; export { run as start }` → `start`.
260
+ *
261
+ * `helper`: inside a top-level, non-exported `function` / `const` of the same
262
+ * file. `callers` are the exports that reach it through same-file value
263
+ * references (a call, or the helper passed on as a value), transitively and
264
+ * sorted; a type-only reference (`typeof helper`) does not count. Empty for an
265
+ * orphan helper no export reaches. `untracked` is set when code attribution
266
+ * cannot follow — module scope, a class body, a destructured declaration — also
267
+ * reaches the helper, so `callers` is incomplete.
268
+ *
269
+ * `module`: outside every top-level `function` / `const` — module scope, or a
270
+ * class body.
271
+ *
272
+ * A site is recorded once; consumers fan out over its callers. The advisor's
273
+ * `AdvisorCallSiteScope` is the same type, so feeder rows pass straight through.
257
274
  */
258
- interface ColumnMetaIR {
259
- /** `.default(...)` or `.$defaultFn(...)` present — field is optional on insert. */
260
- hasDefault?: boolean;
261
- /** `.$onUpdateFn(...)` present. */
262
- hasOnUpdate?: boolean;
263
- /** Default `true`; `.nullable()` flips it to `false`. */
264
- notNull: boolean;
265
- /** `.unique()` present. */
266
- unique?: boolean;
267
- }
268
- /** Reflective representation of a single validator call from the schema. */
269
- interface ValidatorIR {
270
- /** For `v.storage(bucket?)` — the named typed bucket the object key lives in, when given. */
271
- bucket?: string;
272
- /** Column modifiers (`.unique()`, `.default()`, `.nullable()`, …) when present. */
273
- column?: ColumnMetaIR;
274
- /**
275
- * `true` when this validator carries a refinement (`.check(...)`, `.max(n)`,
276
- * `.email()`, …) — anything that narrows the accepted values without changing
277
- * the kind. `schema-drift` hashes this, so a bound counts here even when it is
278
- * modelled below. `.meta(...)` is pure metadata with no parse effect and does
279
- * NOT set it.
280
- */
281
- hasRefinement?: boolean;
282
- /** For `v.optional(inner)` / `v.array(inner)`. */
283
- inner?: ValidatorIR;
284
- /** For `v.record(key, value)`. */
285
- keyType?: ValidatorIR;
286
- /** Kind of validator (string, number, id, object, optional, array, union, literal, record, ...). */
287
- kind: string;
288
- /** For `v.literal(value)` — the literal value as source text. */
289
- literalValue?: string;
290
- /** For `v.union(a, b, ...)`. */
291
- members?: ValidatorIR[];
292
- /** For `v.object({...})`. */
293
- shape?: Record<string, ValidatorIR>;
294
- /** Verbatim source text — used in emitted code when we can't reconstruct from AST. */
295
- sourceText?: string;
296
- /**
297
- * The `n` of a `v.string().max(n)` written with a numeric literal — the one
298
- * refinement predicate the IR can represent exactly (`value.length <= n`).
299
- * The AOT compiler emits it as a guard instead of declining the node, which
300
- * is what keeps a length-bounded public argument on the fast path.
301
- */
302
- stringMaxLength?: number;
303
- /** For `v.id("table")` — the table name. */
304
- tableName?: string;
275
+ type CallSiteScope = AdvisorCallSiteScope;
276
+ /**
277
+ * One call-site edge of the architecture graph, attributed through its
278
+ * {@link CallSiteScope}.
279
+ * Exactly one of `target` / `reason` is set: `target` is a `namespace:export`
280
+ * function key (`call` / `schedule`) or a queue / topic export name (`enqueue` /
281
+ * `publish`); `reason` says why the target could not be read statically.
282
+ */
283
+ interface CallEdgeIR {
305
284
  /**
306
- * For `v.from(externalSchema)` — the wrapped Standard Schema's inferred type,
307
- * rendered as TS source. Recovered through the type checker from
308
- * `~standard.types.output`, mirroring the runtime's `InferStandardOutput`.
309
- * Absent when it could not be recovered safely, in which case the emitted
310
- * type falls back to `unknown`.
285
+ * `true` when the hop may not take effect on every pass of its function —
286
+ * see `isConditionalSite` in `discover/call-edges.ts` for the rules. Set
287
+ * only, never `false`. The `dispatch_cycle` lint's false-positive gate.
311
288
  */
312
- tsType?: string;
289
+ conditional?: true;
290
+ /** Source file relative to `<projectRoot>/lunora/`, without extension. */
291
+ file: string;
292
+ kind: "call" | "enqueue" | "invoke" | "publish" | "schedule";
293
+ /** 1-based line of the call. */
294
+ line: number;
295
+ reason?: string;
313
296
  /**
314
- * `true` when this validator carries a refinement the IR could NOT model — a
315
- * `.check(...)` closure, `.email()`, `.pattern(re)`, a `.max()` whose bound is
316
- * not a literal. The AOT args-validator compiler declines any node with this
317
- * flag, because compiling it would silently skip the predicate.
297
+ * The facts of a scheduled dispatch, present on `schedule` edges only. `delayMs`
298
+ * is the literal `runAfter` delay (absent for `runAt` or a non-literal delay).
299
+ * `argKeys` are the top-level property names of the dispatch's args object:
300
+ * `[]` when there are no args, and `undefined` when the args are not an object
301
+ * literal (or spread into one), so a lint can tell "no cursor" from "cannot tell".
318
302
  */
319
- unmodelledRefinement?: boolean;
320
- valueType?: ValidatorIR;
321
- }
322
- interface IndexIR {
323
- fields: ReadonlyArray<string>;
324
- name: string;
325
- unique?: boolean;
326
- }
327
- interface SearchIndexIR {
328
- /** Primary text-search field; a dot-separated path reads a nested field. */
329
- field: string;
330
- /** Optional filter fields surfaced alongside the FTS column. */
331
- filterFields?: ReadonlyArray<string>;
332
- /** Text-analysis profile (accent folding + that language's stopwords). */
333
- language?: string;
334
- name: string;
335
- /** Skip the migration-time backfill of the search companion (large tables index out-of-band). */
336
- staged?: boolean;
337
- /** `"native"` opts into the engine's own full-text index where it has one (Postgres). */
338
- strategy?: string;
339
- }
340
- /** A `.geoIndex(name, { field, precision? })` declaration — a geohash companion over a `v.geoPoint()` column. */
341
- interface GeoIndexIR {
342
- /** The `v.geoPoint()` column feeding the geohash. */
343
- field: string;
344
- name: string;
345
- /** Geohash precision (characters) maintained on the companion; omitted ⇒ the runtime default. */
346
- precision?: number;
347
- }
348
- /** A `.ttl(field, { after? })` declaration — declarative table-level auto-expiry. */
349
- interface TtlIR {
350
- /** Millisecond offset added to `field` to derive the expiry (`field + after`); omitted ⇒ `field` is the absolute expiry. */
351
- after?: number;
352
- /** The epoch-millisecond expiry column. */
353
- field: string;
354
- }
355
- interface VectorIndexIR {
356
- dimensions?: number;
357
- /** Shape A: the single source column. Shape B: undefined (derived via a `select` fn). */
358
- field?: string;
359
- /** Shape A: metadata field names mirrored into Vectorize. */
360
- metadata?: ReadonlyArray<string>;
361
- metric?: "cosine" | "dot-product" | "euclidean";
362
- name: string;
363
- /** Owning table the vectors are sourced from. */
364
- table: string;
303
+ schedule?: {
304
+ argKeys?: string[];
305
+ delayMs?: number;
306
+ };
307
+ /** Who the call runs on behalf of. */
308
+ scope: CallSiteScope;
309
+ target?: string;
365
310
  }
366
311
  /**
367
- * One ordering key on a rank index's `sortBy`: the column and direction.
368
- * Mirrors the runtime `RankSortKey` (defaults `direction` to `"asc"`).
312
+ * A literal-infinite loop in `lunora/` source — `while (true)`, `for (;;)` /
313
+ * `for (; true;)`, `do { … } while (true)` — with no statically reachable way
314
+ * out: no `break` bound to this loop, no `return`/`throw` outside a nested
315
+ * function, no labeled `break` escaping it. One record per offending loop; a
316
+ * loop that can exit records nothing, so the `unbounded_loop` lint has no
317
+ * bounded loop to sift through.
369
318
  */
370
- interface RankSortKeyIR {
371
- direction: "asc" | "desc";
372
- field: string;
319
+ interface UnboundedLoopIR {
320
+ /** Source file relative to `<projectRoot>/lunora/`, without extension. */
321
+ file: string;
322
+ /** The loop form that never falls out. */
323
+ kind: "do" | "for" | "while";
324
+ /** 1-based line of the `loop` keyword. */
325
+ line: number;
326
+ /** Who the loop runs on behalf of. */
327
+ scope: CallSiteScope;
373
328
  }
374
329
  /**
375
- * A `.rankIndex(name, { sortBy, partitionBy?, where? })` declaration. The owning
376
- * table is always the table the index is declared on, so — unlike a vector index
377
- * — there is no separate `on`/`table` reference to carry: it rides along on its
378
- * {@link TableIR}. Only the fields needed for type emission are captured.
330
+ * A `ctx.workflows.get("name")…` call discovered in a function body — the
331
+ * use-site analog of {@link WorkflowIR} (which is the declaration side). Feeds
332
+ * the `workflow_unused` lint (a declared workflow with zero call sites) and the
333
+ * `workflow_unknown_target` lint (a `.get("x")` whose `x` isn't declared — a
334
+ * typo catcher). {@link WorkflowCallIR.workflow} is `""` when the `get(...)`
335
+ * argument is not a string literal (a dynamic name — which suppresses the
336
+ * unused-workflow heuristic rather than producing a false positive).
379
337
  */
380
- interface RankIndexIR {
381
- name: string;
382
- /** Columns scoping each ranking; omitted ⇒ one global rank over the table. */
383
- partitionBy?: ReadonlyArray<string>;
384
- /** Ordered sort keys driving the rank. */
385
- sortBy: ReadonlyArray<RankSortKeyIR>;
386
- }
387
- interface RelationIR {
388
- /** FK column: on this table for `one`, on the target table for `many`. */
389
- field: string;
390
- kind: "many" | "one";
391
- /** Accessor name the relation is loaded under (the `with` key). */
392
- name: string;
393
- /** FK behaviour applied to holder rows when the referenced parent is deleted. */
394
- onDelete?: "cascade" | "restrict" | "set null";
395
- /** Referenced column (defaults to `_id`). */
396
- references: string;
397
- /** Target table name. */
398
- table: string;
338
+ interface WorkflowCallIR {
339
+ /** Source file relative to `<projectRoot>/lunora/`, without extension (the api namespace). */
340
+ file: string;
341
+ /** 1-based line of the `get(...)` call. */
342
+ line: number;
343
+ /** Who the call runs on behalf of. */
344
+ scope: CallSiteScope;
345
+ /** The referenced workflow export name, or `""` when the argument is not a string literal. */
346
+ workflow: string;
399
347
  }
400
348
  /**
401
- * Statically-discovered `.source(...)` config (plan 077). Only the bits the
402
- * advisor lints + DO wiring need are captured; `map`/`tenantBy` are functions and
403
- * cannot be serialized, so their presence is recorded as `hasTenantBy` rather than
404
- * the function itself.
349
+ * A `ctx.db.query("table")…` read discovered in a function body, reduced to what
350
+ * the query advisor lints need: which table, whether the chain narrows with an
351
+ * index, whether it filters, and which terminal materializes the result.
352
+ * `table` is `""` when the `query(...)` argument is not a string literal (a
353
+ * dynamic table — not lintable).
405
354
  */
406
- interface ExternalSourceIR {
407
- /** The wrangler Hyperdrive binding name. */
408
- binding: string;
409
- /** Whether a `columns` projection allow-list was given. */
410
- columns?: ReadonlyArray<string>;
411
- /** `true` when a `reconcileEveryMs` was given — one of the two incremental delete-visibility paths the `external_source_incremental_no_delete_path` lint checks. */
412
- hasReconcile?: boolean;
413
- /** `true` when a `softDeleteColumn` was given — the other incremental delete-visibility path. */
414
- hasSoftDelete?: boolean;
415
- /** `true` when a `tenantBy` mapper was given — the tenant-isolation boundary the `external_source_unscoped` lint checks. */
416
- hasTenantBy: boolean;
417
- /** The `idColumn` literal, when given (defaults to `"id"` at runtime). */
418
- idColumn?: string;
419
- /** Delete-detection mode literal, when given (`"full-pull"` today). */
420
- mode?: string;
421
- /** The membership query literal, when statically knowable. */
422
- query?: string;
355
+ interface QueryReadIR {
356
+ /** Source file relative to `<projectRoot>/lunora/`, without extension. */
357
+ file: string;
423
358
  /**
424
- * `true` when `.source(...)` was present but its argument was **not** a static
425
- * object literal (e.g. `.source(buildConfig())`), so none of the fields above
426
- * could be read. The source still exists — this flag lets `hasSourcedTables`
427
- * (codegen) and the `external_source_*` lints treat it as a source that can't be
428
- * verified, instead of mistaking it for no `.source()` at all.
359
+ * The fields the chain's `.filter()` predicate reads off its row parameter
360
+ * (`(r) => r.a < cutoff && r.b === x` → `["a", "b"]`). `undefined` when the
361
+ * chain has no filter or the predicate is not a function literal. Optional so
362
+ * a feeder predating this field still typechecks.
429
363
  */
430
- unanalyzable?: boolean;
431
- }
432
- interface TableIR {
364
+ filterFields?: string[];
433
365
  /**
434
- * `true` when the table chain carried `.commitOrdered()` — every row carries
435
- * `_commitSeq`, a per-shard integer allocated once per mutation and strictly
436
- * increasing in commit order. Optional: hand-built IR and tables that never
437
- * called `.commitOrdered()` default it to `false`.
366
+ * True when the chain's `.filter()` predicate compares `_id`
367
+ * (`(d) => d._id === args.id`) — a full scan for a row that `ctx.db.get`
368
+ * addresses directly. Optional so a feeder predating this field still
369
+ * typechecks; absent is treated as "not a primary-key filter".
438
370
  */
439
- commitOrdered?: boolean;
440
- /**
441
- * `true` when that extension was resolved from a package in `node_modules`
442
- * rather than from source under `lunora/` (a registry copy-in). Decides
443
- * whether the component's module owns a `lunora/<key>/` folder.
444
- */
445
- extensionFromPackage?: true;
446
- /**
447
- * The `defineSchemaExtension` key that contributed this table, set when it
448
- * arrived through `defineSchema(...).extend(...)`. Absent for a table the app
449
- * declared itself.
450
- *
451
- * Drives the generated `AppTableName` union: an add-on's tables
452
- * (`ratelimit_buckets`, …) are real tables and stay in `TableName`, but an app
453
- * enumerating "my tables" should not have to know about them.
454
- */
455
- extensionKey?: string;
456
- /**
457
- * `true` when the table chain carried `.externallyManaged()` — its rows are
458
- * written outside Lunora's discoverable insert path (adapter/migration/
459
- * middleware), so advisor insert-path lints skip it. Optional: hand-built
460
- * IR and the runtime `fromServerSchema` path default it to `false`.
461
- */
462
- externallyManaged?: boolean;
463
- /**
464
- * Set when the chain carried `.source(...)` — the table is materialized from an
465
- * external Hyperdrive-backed database by a system poll loop (plan 077). Carries
466
- * the statically-knowable bits the advisor lints read; the functions (`map`,
467
- * `tenantBy`) are not serialized, only their presence (`hasTenantBy`).
468
- */
469
- externalSource?: ExternalSourceIR;
470
- /** Geospatial indexes declared inline via `.geoIndex(name, …)`. Optional so hand-built IR may omit it (discovery always sets it). */
471
- geoIndexes?: ReadonlyArray<GeoIndexIR>;
472
- /**
473
- * Storage backend for a `.global()` table: `"d1"` (default) or
474
- * `"hyperdrive"` (a Postgres/MySQL database via Cloudflare Hyperdrive). Only
475
- * meaningful when `shardMode === "global"`; absent for sharded/root tables.
476
- *
477
- * Read it through `isD1GlobalTable` / `isHyperdriveGlobalTable` rather than
478
- * comparing here — discovery normalises the absent case to `"d1"`, and only
479
- * those predicates encode which comparison is right for hand-built IR.
480
- */
481
- globalBackend?: "d1" | "hyperdrive";
482
- indexes: ReadonlyArray<IndexIR>;
483
- /**
484
- * `true` when the table chain carried `.public()` — an explicit opt-OUT of
485
- * the schema's `.rls("required")` enforcement for this one table. Optional:
486
- * hand-built IR and tables that never called `.public()` default it to
487
- * `false`.
488
- */
489
- isPublic?: boolean;
371
+ filtersPrimaryKey?: boolean;
372
+ /** The chain calls `.filter(...)`. */
373
+ hasFilter: boolean;
374
+ /** The chain narrows with `.withIndex(...)` or `.withSearchIndex(...)`. */
375
+ hasIndex: boolean;
490
376
  /**
491
- * `true` when the table chain carried `.memory()` — its rows are cleared on
492
- * every Durable Object cold start and never reach the CDC changelog.
493
- * Optional: hand-built IR and tables that never called `.memory()` default it
494
- * to `false`.
377
+ * The chain's `.withIndex(…)`, present exactly when the chain has one. `name` is
378
+ * the literal index name, or `undefined` when it is not a string literal.
379
+ * `bounds` are the comparators its range callback calls on the index parameter,
380
+ * sorted and de-duplicated: `[]` for a bare `.withIndex(name)` (the widest range),
381
+ * `undefined` when the callback exists but cannot be read.
495
382
  */
496
- memory?: boolean;
497
- name: string;
498
- /** Rank indexes declared inline via `.rankIndex(name, …)`. */
499
- rankIndexes: ReadonlyArray<RankIndexIR>;
500
- /** Declared relations (via `.relations((r) => …)`), keyed in source order. */
501
- relations: ReadonlyArray<RelationIR>;
502
- searchIndexes: ReadonlyArray<SearchIndexIR>;
503
- shape: Record<string, ValidatorIR>;
504
- shardMode: "global" | "root" | {
505
- field: string;
506
- kind: "shardBy";
507
- };
508
- /** Set when the chain carried `.softDelete()` — the marker column's name (default `deletedAt`). The column is injected into `shape` so `Doc_*` carries it. */
509
- softDelete?: {
510
- field: string;
383
+ index?: {
384
+ bounds?: string[];
385
+ name?: string;
511
386
  };
512
- /** Set when the chain carried `.ttl(field, { after? })` — the declarative auto-expiry policy read by the DO alarm sweep. */
513
- ttl?: TtlIR;
514
- /** Vector indexes declared inline via `.vectorize()` (DSL Shape A). */
515
- vectorIndexes: ReadonlyArray<VectorIndexIR>;
516
- }
517
- /** The Cloudflare DO data-residency jurisdictions the schema may declare. Canonical literal set for the codegen package. */
518
- type JurisdictionIR = "eu" | "fedramp" | "us";
519
- interface SchemaIR {
520
- /**
521
- * Cloudflare data-residency jurisdiction declared via
522
- * `defineSchema(...).jurisdiction("…")`. Emitted into the generated worker's
523
- * `createWorker({ jurisdiction })` (and `ctx.scheduler` / `ctx.containers`).
524
- * Absent ⇒ un-pinned.
525
- */
526
- jurisdiction?: JurisdictionIR;
387
+ /** 1-based line of the `query(...)` call. */
388
+ line: number;
389
+ /** Who the read runs on behalf of. */
390
+ scope: CallSiteScope;
391
+ /** Queried table name, or `""` when the argument is not a string literal. */
392
+ table: string;
527
393
  /**
528
- * `.jurisdiction("…", { pinAuth: true })` — the acknowledgement that
529
- * DO-backed auth, unpinned before, moves into the jurisdiction (and so to a
530
- * new, empty object until its rows are copied). Without it codegen refuses a
531
- * project that has DO-backed auth, and does not pin it.
394
+ * The materializing call the chain ends in — `"collect"`, `"take"`,
395
+ * `"paginate"`, `"first"`, `"unique"`, … — i.e. how much of the narrowed set
396
+ * the read actually loads.
397
+ *
398
+ * `undefined` when the chain reaches no recognised terminal (a reader passed
399
+ * on, a bare `query(...)`) AND when a feeder predating this field produced
400
+ * the read. The two are deliberately not distinguished: no consumer could act
401
+ * on the difference, so the terminal-shaped lints skip the read either way
402
+ * rather than guessing a terminal.
532
403
  */
533
- jurisdictionPinsAuth?: true;
404
+ terminal?: string;
534
405
  /**
535
- * Set when `defineSchema(...).rls("required")` was chained onto the schema —
536
- * every table's `ctx.db` write path is denied without an RLS-covering
537
- * procedure unless the table itself is `.public()` (see {@link TableIR.isPublic}).
538
- * Absent when the schema never called `.rls("required")`.
406
+ * Visibility of the procedure the read runs on behalf of: `internal` when every
407
+ * caller is internal, `public` when any caller is public. Stamped from the
408
+ * registered functions, so it is absent when the feeder could not read them.
539
409
  */
540
- rlsMode?: "required";
541
- tables: ReadonlyArray<TableIR>;
542
- /** All vector indexes (inline Shape A hoisted + standalone Shape B), flattened. */
543
- vectorIndexes: ReadonlyArray<VectorIndexIR>;
410
+ visibility?: "internal" | "public";
544
411
  }
545
412
  /**
546
- * Statically-read mirror of `@lunora/server`'s `RestCacheConfig`. Every field is
547
- * optional because discovery only records literals it could actually read off the
548
- * `.expose({ cache: … })` object.
413
+ * A `ctx.authApi.<method>(...)` call discovered in a function body, attributed
414
+ * to the exported function (and its file = api namespace) that performs it.
415
+ * Structurally identical to `AdvisorAuthApiCall` so it passes straight through
416
+ * to the advisor lint without conversion, exactly as `InsertWriteIR` does for
417
+ * `AdvisorInsertWrite`.
549
418
  */
550
- interface ExposeCacheIR {
551
- maxAge?: number;
552
- scope?: "private" | "public";
553
- staleWhileRevalidate?: number;
554
- tag?: string;
555
- vary?: string;
419
+ interface AuthApiCallIR {
420
+ /** Source file relative to `<projectRoot>/lunora/`, without extension. */
421
+ file: string;
422
+ /** True when the call's argument object includes a `headers` property. */
423
+ hasHeaders: boolean;
424
+ /** 1-based line of the call, or `0` when unknown. */
425
+ line: number;
426
+ /** The better-auth method invoked (e.g. `banUser`); empty when not statically known. */
427
+ method: string;
428
+ /** Who the site runs on behalf of — see {@link CallSiteScope}. */
429
+ scope: CallSiteScope;
556
430
  }
557
- interface FunctionIR {
558
- /**
559
- * Set when the chain carries `.use(platformAdmin(...))`: only a platform admin
560
- * can reach the procedure, so its writes are trusted to name any account.
561
- */
562
- adminOnly?: true;
563
- args: Record<string, ValidatorIR>;
564
- exportName: string;
565
- /**
566
- * Set by the `.expose({ rest: true })` builder modifier (plan 167). When
567
- * `rest` is `true` the function is published on the public REST surface, so the
568
- * OpenAPI emitter describes it as a real `/_lunora/rest/<namespace>/<fn>` path
569
- * (the single source of truth the runtime router also derives from). Absent →
570
- * RPC-only (the default; not on the REST surface).
571
- *
572
- * `cache` mirrors the `RestCacheConfig` the runtime turns into response
573
- * headers, so the emitted spec can document the `Cache-Control` a caller will
574
- * actually observe. Only statically-readable literal fields are carried; a
575
- * computed value is simply absent (the spec under-documents rather than lies).
576
- */
577
- expose?: {
578
- cache?: ExposeCacheIR;
579
- rest?: boolean;
580
- };
581
- /** Path relative to `<projectRoot>/lunora/` without extension, e.g. "messages". */
582
- filePath: string;
583
- kind: "action" | "mutation" | "query" | "stream";
584
- /**
585
- * Set on lifecycle hooks (`onConnect`/`onDisconnect`/`onShardInit`/
586
- * `onQueryChange`/`onWhisper`): the moment the hook fires on. Such a function
587
- * is also an internal registration (so it lands in `LUNORA_FUNCTIONS` for path
588
- * dispatch) — a mutation for every moment except `whisper`, which is a query;
589
- * emit additionally collects it into the `LUNORA_LIFECYCLE_HOOKS` manifest
590
- * keyed by this moment. Absent on ordinary functions.
591
- */
592
- lifecycle?: "connect" | "disconnect" | "init" | "reactor" | "whisper";
593
- /**
594
- * The `.output(validator)` declaration, when the chain has one.
595
- *
596
- * Takes precedence over {@link FunctionIR.returnType} (the handler's
597
- * inferred type) for the emitted `FunctionReference`. `.output()` is what
598
- * validates at runtime and what a reader takes as the contract, so the two
599
- * must agree — see the emit-side note for what went wrong when they did not.
600
- */
601
- output?: ValidatorIR;
431
+ /**
432
+ * A table write other than a plain `ctx.db.insert(...)` (see {@link InsertWriteIR}):
433
+ * a by-id `patch`/`replace`/`delete`/`hardDelete`/`restore`, a batch write, or a
434
+ * `ctx.db.<table>.*` facade write. Feeds the architecture manifest's `write`
435
+ * edges and the `cross_module_table_write` lint.
436
+ */
437
+ interface TableWriteIR {
602
438
  /**
603
- * Serialized TS source for the handler's return type, with `Promise<T>`
604
- * unwrapped so callers see `T` directly. Defaults to `"unknown"` when
605
- * ts-morph cannot resolve the type (typically because the consuming
606
- * project lacks a tsconfig that can reach `@lunora/server`).
439
+ * The fields a `patch` / `patchMany` writes, read off its object-literal
440
+ * argument. `undefined` for every other method, and for a patch whose
441
+ * argument is not an object literal or spreads one: the feeder then cannot
442
+ * say which fields change.
607
443
  */
608
- returnType: string;
444
+ fields?: string[];
445
+ /** Source file relative to `<projectRoot>/lunora/`, without extension. */
446
+ file: string;
447
+ /** 1-based line of the call. */
448
+ line: number;
449
+ /** The writer method called, e.g. `patch`, `deleteMany`, `upsert`. */
450
+ method: string;
451
+ /** Who the write runs on behalf of. */
452
+ scope: CallSiteScope;
609
453
  /**
610
- * Call surface the function is exposed on. Absent (or `"public"`) means it
611
- * lands in the generated `api`; `"internal"` routes it to the separate
612
- * `internal` object and is rejected by the DO's external RPC path.
454
+ * Target table; `""` when it can't be read (an untyped id, a non-literal
455
+ * name). A union id (`Id<"a"> | Id<"b">`) gives one record per table.
613
456
  */
614
- visibility?: "internal" | "public";
457
+ table: string;
615
458
  }
616
459
  /**
617
- * A `defineMigration({...})` declaration discovered in the user's lunora
618
- * sources. The emitted `LUNORA_MIGRATIONS` registry keys on {@link MigrationIR.id}; the
619
- * import wiring needs {@link MigrationIR.exportName}/{@link MigrationIR.filePath}. The runtime object
620
- * carries the authoritative {@link MigrationIR.table}, but the lifted value is
621
- * load-bearing at build time: the schema-drift gate matches it against the
622
- * tables with breaking drift, so a migration left at `""` covers nothing.
460
+ * A row-changing write a function performs directly or through the functions it
461
+ * reaches, attributed to the EXPORT that reaches it. A write in a helper in
462
+ * another file is included under the export that calls the helper, so a sweep
463
+ * that deletes through a helper counts as progress. The fields follow
464
+ * {@link TableWriteIR.fields}.
623
465
  */
624
- interface MigrationIR {
625
- /** Export binding name, used to reference the module member in generated imports. */
466
+ interface RowEffectIR {
467
+ /** The export the write is reachable from. */
626
468
  exportName: string;
627
- /** Path relative to `<projectRoot>/lunora/` without extension, e.g. "migrations". */
628
- filePath: string;
629
- /** Stable migration id — the registry key and per-shard run-state key. */
630
- id: string;
631
- /** Table the migration iterates; `""` when not a static string literal. */
469
+ /** See {@link TableWriteIR.fields}. */
470
+ fields?: string[];
471
+ /** Source file of that export, relative to `<projectRoot>/lunora/`, without extension. */
472
+ file: string;
473
+ /** The write method, as on {@link TableWriteIR.method}. */
474
+ method: string;
475
+ /** The table the write targets. Never `""`: an unreadable table is not evidence of progress. */
632
476
  table: string;
633
477
  }
634
478
  /**
635
- * A `defineShape({...})` declaration discovered in `lunora/shapes.ts`
636
- * (local-first sync engine, Phase 7). The emitted `LUNORA_SHAPES` registry keys
637
- * on {@link ShapeIR.exportName}; the generated DO's `resolveShape` override
638
- * dispatches a `shape_subscribe` to the matching registered shape. Discovery is
639
- * marker-driven (the `__lunoraShape` brand) — no field metadata is lifted here
640
- * because the runtime object (`columns`/`compileWhere`) carries the authority.
479
+ * A `ctx.db.insert("table", …)` write discovered in a function body, attributed
480
+ * to the exported function (and its file = api namespace) that performs it — the
481
+ * write-side analog of {@link QueryReadIR}. Lets tooling wire a table's write
482
+ * action by behavior (which function inserts into it) rather than by naming.
483
+ * {@link InsertWriteIR.table} is `""` when the argument is not a string literal.
641
484
  */
642
- interface ShapeIR {
643
- /**
644
- * The shape's `args` validator map — its partition selector. Lifted so
645
- * `_generated/collections.ts` can type the selector a caller passes instead of
646
- * widening it to `Record<string, unknown>`. `{}` for a parameterless shape.
647
- */
648
- args: Record<string, ValidatorIR>;
649
- /** Export binding name — the shape's registry key and import member. */
650
- exportName: string;
651
- /** Path relative to `<projectRoot>/lunora/` without extension — always `"shapes"`. */
652
- filePath: string;
653
- /**
654
- * The `table` string literal from the `defineShape({ table })` call, lifted
655
- * only for static advisor lints (the runtime object stays authoritative).
656
- * `undefined` when `table` is not a plain string literal — lints skip those.
657
- */
658
- table?: string;
485
+ interface InsertWriteIR {
486
+ /** Source file relative to `<projectRoot>/lunora/`, without extension (the api namespace). */
487
+ file: string;
488
+ /** 1-based line of the `insert(...)` call. */
489
+ line: number;
490
+ /** Who the insert runs on behalf of. */
491
+ scope: CallSiteScope;
492
+ /** Target table name, or `""` when the argument is not a string literal. */
493
+ table: string;
659
494
  }
660
495
  /**
661
- * The single `defineIdentity({...})` claim contract discovered in
662
- * `lunora/identity.ts`. Discovery is **marker-driven** (the `__lunoraIdentity`
663
- * brand, exactly like {@link ShapeIR}) — no claim metadata is lifted here
664
- * because the emitted `_generated/server.ts` recovers the claim *type* from the
665
- * declaration itself (`InferIdentity` over the contract's `typeof`), and the
666
- * runtime object (`validate`/`onInvalid`) carries the authority at the boundary.
667
- * Exactly one per app; absent ⇒ generated output is byte-identical to today.
496
+ * A non-deterministic API call (`Date.now`, `Math.random`, `crypto.randomUUID`,
497
+ * `crypto.getRandomValues`, `fetch`) discovered lexically inside a `query(...)`
498
+ * or `mutation(...)` handler body — the `nondeterministic_query_mutation` lint
499
+ * input. Structurally identical to `AdvisorNondeterministicCall` so values pass
500
+ * straight through to the advisor without conversion, exactly as `AuthApiCallIR`
501
+ * does for `AdvisorAuthApiCall`. `action(...)` handlers are never recorded —
502
+ * actions are the determinism escape hatch.
668
503
  */
669
- interface IdentityIR {
670
- /** Export binding name — the namespace member `_generated/server.ts` reads via `typeof`. */
504
+ interface NondeterministicCallIR {
505
+ /** The non-deterministic API invoked, e.g. `Date.now` / `Math.random` / `crypto.randomUUID` / `fetch`. */
506
+ callee: string;
507
+ /** Export binding name of the function performing the call, e.g. `sendMessage`. */
671
508
  exportName: string;
509
+ /** Source file relative to `<projectRoot>/lunora/`, without extension (the api namespace). */
510
+ file: string;
511
+ /** Which procedure kind the call lives in — only `query`/`mutation` handlers are recorded. */
512
+ kind: "mutation" | "query";
513
+ /** 1-based line of the call, or `0` when unknown. */
514
+ line: number;
672
515
  }
673
516
  /**
674
- * The single `defineEnv({...})` contract discovered in `lunora/env.ts`. Like
675
- * {@link IdentityIR}, only the export binding is lifted — the emitted
676
- * `_generated/server.ts` recovers the validated shape from the declaration
677
- * itself (`ReturnType` over the accessor's `typeof`), and the generated ShardDO
678
- * applies the same accessor to the worker `env` at ctx-build time to populate
679
- * `ctx.env`. Exactly one per app; absent ⇒ generated output is byte-identical.
517
+ * One `ctx.<property>` access lexically inside a `query`/`mutation` handler —
518
+ * the feeder shape behind every "action-only surface used outside an action"
519
+ * advisor lint (`hyperdrive_outside_action` for `ctx.sql`,
520
+ * `r2sql_outside_action` for `ctx.r2sql`). Structurally identical to the
521
+ * advisor's `AdvisorHyperdriveCall` / `AdvisorR2sqlCall` (same field set) so
522
+ * values pass straight through `lintSchema` without conversion, exactly as
523
+ * {@link NondeterministicCallIR} does. Only `query`/`mutation` handlers are
524
+ * recorded; `action(...)` is the only context where these surfaces are even
525
+ * typed, so it is the intended home and is skipped.
680
526
  */
681
- interface EnvIR {
682
- /** Export binding name — the namespace member `_generated/server.ts` reads via `typeof`. */
527
+ interface ContextPropertyCallIR {
528
+ /** The accessed surface, e.g. `ctx.sql.query` / `ctx.r2sql.from` — the property, suffixed with the method when one is called on it. */
529
+ callee: string;
530
+ /** Export binding name of the function performing the access. */
683
531
  exportName: string;
532
+ /** Source file relative to the lunora dir, without extension (the api namespace). */
533
+ file: string;
534
+ /** Which procedure kind the access lives in — only `query`/`mutation` handlers are recorded. */
535
+ kind: "mutation" | "query";
536
+ /** 1-based line of the access. */
537
+ line: number;
684
538
  }
685
539
  /**
686
- * A `defineMutator({...})` declaration discovered in `lunora/mutators.ts`
687
- * (local-first sync engine, Phase 7). The emitted registry registers the
688
- * authoritative `server` impl into the DO's `LUNORA_FUNCTIONS` table (so
689
- * `handleRpc` transaction-wraps it) and records its path in
690
- * `LUNORA_MUTATOR_PATHS` so the DO's `isCustomMutator` override routes the
691
- * client-watermark push protocol. The client `client` impl is split into the
692
- * browser bundle separately — only the path crosses to the server side.
540
+ * One `ctx.kv.<method>(key, …)` call whose namespace key is derived from the
541
+ * handler's `args` with no server-side scoping — the `kv_unscoped_user_key_idor`
542
+ * lint input. Workers KV is a single flat namespace, so a key taken straight from
543
+ * request input lets any caller read, overwrite, or delete another user's entry
544
+ * (IDOR). Only arg-derived, unscoped keys reach here; a fixed literal, or a key
545
+ * prefixed with a server-trusted identity (`${ctx.auth.userId}:…` — references
546
+ * `ctx`, so treated as scoped), is not recorded. `list` is excluded (it takes a
547
+ * prefix, not a per-entry key). Structurally identical to `AdvisorKvKeyAccess`.
693
548
  */
694
- interface MutatorIR {
695
- /**
696
- * The mutator's `args` validator map, parsed exactly as a procedure's is, so
697
- * the emitted `api.mutators.<name>` reference carries the arg type a client
698
- * `defineMutator` infers instead of restating. `{}` for a parameterless
699
- * mutator (or one whose `args` isn't an inline object literal).
700
- */
701
- args: Record<string, ValidatorIR>;
702
- /** Export binding name — the mutator's registry key and import member. */
703
- exportName: string;
704
- /** Path relative to `<projectRoot>/lunora/` without extension — always `"mutators"`. */
705
- filePath: string;
706
- /** 1-based line of the `defineMutator(...)` call, for diagnostics and advisor findings. */
549
+ interface KvKeyAccessIR {
550
+ /** Source file relative to `<projectRoot>/lunora/`, without extension. */
551
+ file: string;
552
+ /** 1-based line of the `ctx.kv` call, or `0` when unknown. */
707
553
  line: number;
554
+ /** The `ctx.kv` method invoked: `get` / `getRaw` / `getWithMetadata` / `put` / `delete`. */
555
+ method: string;
556
+ /** Who the site runs on behalf of — see {@link CallSiteScope}. */
557
+ scope: CallSiteScope;
708
558
  /**
709
- * The ownership column the declaration scopes the write to
710
- * (`defineMutator({ owner: "userId" })`), or `undefined` when it declares
711
- * none — which is also what the `mutator_without_owner_scope` lint reports,
712
- * since an unowned mutator is a public write endpoint. Only a string-literal
713
- * `owner` is lifted; a computed one reads as `undefined` rather than guessed.
714
- */
715
- owner?: string;
716
- /**
717
- * Serialized TS source for the authoritative `server` impl's return type,
718
- * `Promise<T>` unwrapped. `"unknown"` when ts-morph can't resolve it — same
719
- * contract as {@link FunctionIR.returnType}.
559
+ * Visibility of the enclosing procedure. `internal` procedures have no
560
+ * untrusted caller by construction — see `owner_field_from_args_not_auth`'s
561
+ * identical split — so `kv_unscoped_user_key_idor` drops the finding to
562
+ * INFO rather than ERROR there. `undefined` when the access sits outside any
563
+ * registered procedure the feeder could attribute it to.
720
564
  */
721
- returnType: string;
565
+ visibility?: "internal" | "public";
722
566
  }
723
567
  /**
724
- * A whole-row `ctx.db.replace(id, document)` write discovered inside a custom
725
- * mutator's inline `server` impl (`lunora/mutators.ts`) — the input the
726
- * `mutator_full_row_replace` advisor lint consumes. A `replace` overwrites the
727
- * entire row, so a concurrent edit to a different column on a synced table is
728
- * clobbered; `ctx.db.patch(id, { field })` merges at the column level instead.
729
- * Structurally identical to `AdvisorMutatorWrite` so it passes straight through
730
- * to the advisor without conversion, exactly as `InsertWriteIR` does for
731
- * `AdvisorInsertWrite`.
568
+ * One `ctx.db` write (`insert` / `replace` / `patch` / `insertManyUnsafe`) that sets
569
+ * an ownership / identity column — `userId`, `ownerId`, `tenantId`, and the like —
570
+ * from the handler's `args` instead of the server-trusted identity. The
571
+ * `owner_field_from_args_not_auth` lint input: the ownership column decides who a
572
+ * row belongs to, so a value taken from request input lets any caller write rows
573
+ * owned by another user or tenant (the act-as-any-user / cross-tenant IDOR vector).
574
+ * A column stamped from `ctx.*`, or set to a fixed literal, is not recorded; only an
575
+ * arg-derived identity write reaches here. Structurally identical to
576
+ * `AdvisorOwnerFieldWrite`.
732
577
  */
733
- interface MutatorWriteIR {
734
- /** The mutator export whose `server` impl performs the replace, e.g. `renameChannel`. */
578
+ /**
579
+ * One branching `defineShape({ where })` / `definePolicy({ when })` predicate arm
580
+ * that returns an unrestricted predicate — the `unrestricted_where_branch` lint
581
+ * input. A denial arm must match NO rows (`deny()` / `{ OR: [] }`); `{}` matches
582
+ * every row, so the near-miss silently replicates the whole table.
583
+ */
584
+ interface UnrestrictedWhereBranchIR {
585
+ /** Export binding name of the shape / policy the predicate belongs to. */
735
586
  exportName: string;
736
- /** Openable source path the replace appears in — always `lunora/mutators.ts`. */
587
+ /** Source file relative to `<projectRoot>/lunora/`, without extension. */
737
588
  file: string;
738
- /** 1-based line of the `replace(...)` call. */
589
+ /** Which unrestricted form was returned. */
590
+ form: "empty-object" | "undefined";
591
+ /** The config key carrying the predicate (`where` for a shape, `when` for a policy). */
592
+ key: string;
593
+ /** 1-based line of the offending returned expression. */
739
594
  line: number;
595
+ /** The declaring call (`defineShape` / `definePolicy`). */
596
+ owner: string;
740
597
  }
741
- /**
742
- * A single cron job lifted from a `cronJobs()` builder in `lunora/crons.ts`.
743
- * Mirrors `@lunora/scheduler`'s `CronJob`: {@link CronJobIR.cron} is the compiled
744
- * standard cron expression, {@link CronJobIR.functionPath} is the target
745
- * `__lunoraRef` (`namespace:fn`), and {@link CronJobIR.args} is the static
746
- * argument object passed at registration.
747
- */
748
- interface CronJobIR {
749
- /** Static args object (source-text JSON), defaults to `{}`. */
750
- args: Record<string, unknown>;
751
- /** Compiled standard cron expression, e.g. `"0 9 * * *"`. */
752
- cron: string;
753
- /** Target function ref `namespace:fn`. Present for a function target; absent when {@link CronJobIR.workflow} is set. */
754
- functionPath?: string;
755
- /** Unique, human-readable job name. */
756
- name: string;
598
+ interface OwnerFieldWriteIR {
757
599
  /**
758
- * Set when the job targets a durable workflow (a `lunora/workflows.ts`
759
- * export) instead of a function: the workflow's class name (its
760
- * `ctx.exports` key) plus its export name. On each fire the worker starts a new workflow
761
- * INSTANCE (the {@link CronJobIR.args} become its `params`) rather than
762
- * dispatching a one-shot function.
600
+ * Every procedure that reaches the write carries `.use(platformAdmin(...))`
601
+ * (see {@link FunctionIR.adminOnly}): the caller is a platform admin by design.
763
602
  */
764
- workflow?: {
765
- className: string;
766
- exportName: string;
767
- };
768
- }
769
- /** A normalized container image source, as written into `wrangler.jsonc`. */
770
- type ContainerImageIR = {
771
- buildContext: string;
772
- dockerfilePath: string;
773
- kind: "dockerfile";
774
- } | {
775
- buildDir: string;
776
- kind: "build";
777
- } | {
778
- kind: "registry";
779
- reference: string;
780
- };
781
- /** A `durable_object` container's named image — a Dockerfile or a registry digest, never a Railpack build. */
782
- type ContainerNamedImageIR = Exclude<ContainerImageIR, {
783
- kind: "build";
784
- }>;
785
- /** The fields every container carries, whichever scheduling policy it uses. */
786
- interface ContainerIRBase {
787
- /** Durable Object binding name, e.g. `CONTAINER_TRANSCODER`. */
788
- bindingName: string;
789
- /** Static Dockerfile build args (wrangler `image_vars` / named-image `build_vars`), when declared as literals. */
790
- buildArgs?: Record<string, string>;
791
- /** Generated DO class name, e.g. `TranscoderContainer`. */
792
- className: string;
603
+ adminOnly?: true;
604
+ /** The identity column being written from `args` (e.g. `userId`). */
605
+ field: string;
606
+ /** Source file relative to `<projectRoot>/lunora/`, without extension. */
607
+ file: string;
793
608
  /**
794
- * Whether the container may open outbound internet connections, when the
795
- * value was a static literal. `undefined` means the field was omitted (the
796
- * platform default is `true`) or wasn't a literal. Lifted for the advisor.
609
+ * The handler proves the value it writes equals the server identity before the
610
+ * write: an `if (<args.field> !== <ctx identity>) throw` (also inside an `&&` or
611
+ * `||` condition), or an `assert*(...)` call given both the argument and the
612
+ * identity. The write is then validated, so `owner_field_from_args_not_auth`
613
+ * declines to report it. Only facts that hold before the write count.
797
614
  */
798
- enableInternet?: boolean;
799
- /** The `lunora/containers.ts` export name, e.g. `transcoder`. */
800
- exportName: string;
801
- /** Static `instanceType`, when declared (the default size, under `durable_object`). */
802
- instanceType?: string | {
803
- diskMb?: number;
804
- memoryMib?: number;
805
- vcpu?: number;
806
- };
807
- /** Static wrangler `containers[].name` override, when declared. */
808
- name?: string;
615
+ guarded?: true;
616
+ /** 1-based line of the `ctx.db` write call, or `0` when unknown. */
617
+ line: number;
618
+ /** The `ctx.db` write method (`insert` / `replace` / `patch` / `insertManyUnsafe`). */
619
+ method: string;
809
620
  /**
810
- * `true` when the definition opts in to the Sandbox SDK helpers
811
- * (`defineContainer({ sandbox: true })`). Must be a literal: it decides
812
- * whether the generated `containers.ts` exports the sandbox gateways, and
813
- * it raises the `containerSandboxTools` platform signal.
621
+ * The enclosing `defineMutator` declared this very column as its `owner`, AND
622
+ * the value written resolves, by symbol, to that same `args[owner]` of the
623
+ * `server` impl's own 2nd parameter. A nested closure's or a helper's own
624
+ * `args` (or any binding shadowing it) never qualifies.
625
+ *
626
+ * `applyOwnerScope` requires a verified identity, rejects a client-supplied
627
+ * value that disagrees with it, and overwrites the column with the verified
628
+ * one before `server` runs — so on this exact shape `args[owner]` IS the
629
+ * server identity, and it is what the docs prescribe. Recorded rather than
630
+ * dropped at discovery: the write did happen, and the feeder is otherwise the
631
+ * only place that knows. `owner_field_from_args_not_auth` is what declines to
632
+ * report it.
633
+ *
634
+ * Deliberately NOT set when only the column NAME matches: `owner: "userId"`
635
+ * launders `args.userId` and nothing else, so `{ userId: args.targetUserId }`
636
+ * is a real IDOR and stays reportable.
814
637
  */
815
- sandbox?: boolean;
638
+ ownerScoped?: true;
639
+ /** Who the site runs on behalf of — see {@link CallSiteScope}. */
640
+ scope: CallSiteScope;
816
641
  /**
817
- * The static `sleepAfter` value, when it was a literal. `undefined` means
818
- * omitted (platform default `"10m"`) or non-literal. Lifted for the advisor.
642
+ * Visibility of the enclosing procedure. `internal` procedures are not
643
+ * reachable by a caller, so the lint's premise ("any caller can write rows
644
+ * owned by another user") does not hold there — see
645
+ * `owner_field_from_args_not_auth`. `undefined` when the write sits outside
646
+ * any recognised procedure (a bare helper).
819
647
  */
820
- sleepAfter?: number | string;
648
+ visibility?: "internal" | "public";
821
649
  }
822
- /** A container under the default scheduling policy: one application image, a cap, and a rollout. */
823
- interface DefaultScheduledContainerIR extends ContainerIRBase {
650
+ /**
651
+ * One `ctx.storage.<bucket>.<method>(key, …)` call whose R2 object key is derived
652
+ * from the handler's `args` with no server-side scoping — the
653
+ * `storage_key_from_user_args` lint input. The bucket read/write/URL/delete methods
654
+ * key by their first argument, so an object key taken straight from request input is
655
+ * object-level IDOR (read/overwrite/delete anyone's object). A key referencing a
656
+ * server-trusted `ctx.*` value (e.g. `${ctx.auth.userId}/…`) is treated as scoped
657
+ * and is not recorded; only an arg-derived, `ctx`-free key reaches here.
658
+ * Structurally identical to `AdvisorStorageKeyAccess`.
659
+ */
660
+ interface StorageKeyAccessIR {
661
+ /** Source file relative to `<projectRoot>/lunora/`, without extension. */
662
+ file: string;
663
+ /** 1-based line of the storage call, or `0` when unknown. */
664
+ line: number;
665
+ /** The bucket method invoked with the arg-derived key, e.g. `get` / `put` / `delete` / `download`. */
666
+ method: string;
667
+ /** Who the site runs on behalf of — see {@link CallSiteScope}. */
668
+ scope: CallSiteScope;
824
669
  /**
825
- * Normalized image source: a local Dockerfile (`dockerfile`), a pre-built
826
- * registry reference (`registry`), or a Railpack source directory (`build`)
827
- * that the deploy step builds and pushes before wrangler runs.
670
+ * Visibility of the enclosing procedure. `internal` procedures have no
671
+ * untrusted caller by construction — see `owner_field_from_args_not_auth`'s
672
+ * identical split — so `storage_key_from_user_args` drops the finding to
673
+ * INFO rather than ERROR there. `undefined` when the access sits outside any
674
+ * registered procedure the feeder could attribute it to.
828
675
  */
829
- image: ContainerImageIR;
830
- /** Static `maxInstances`, when declared. */
831
- maxInstances?: number;
832
- /** Static rolling-deploy tuning, when declared as literals. */
833
- rollout?: {
834
- gracePeriodSeconds?: number;
835
- stepPercentage?: number;
836
- };
837
- /** Absent under the default policy — the union's discriminant. */
838
- schedulingPolicy?: never;
676
+ visibility?: "internal" | "public";
839
677
  }
840
678
  /**
841
- * A container under the `durable_object` scheduling policy (wrangler
842
- * `scheduling_policy`): each instance picks its image and size per start. Also
843
- * the `containerRuntimeScheduling` platform signal.
679
+ * One `ctx.ai.run(model, …)` call whose model-id argument is derived from the handler's
680
+ * `args` with no server-side scoping — the `ai_raw_run_escape_hatch` lint input.
681
+ * `ctx.ai.run` is the raw Workers AI binding passthrough, bypassing the typed
682
+ * `ctx.ai.model(...)` + AI-SDK layer (`generateText`/`streamText`/…) that caps output and
683
+ * enforces a schema, so an arg-derived model id lets any caller select an arbitrary model.
684
+ * A fixed literal model, or one scoped by a server-trusted `ctx.*` value, is not recorded;
685
+ * only an arg-derived, unscoped model id reaches here (an arg-derived `inputs` argument is
686
+ * normal usage and is never inspected). Structurally identical to `AdvisorAiRawRun`.
844
687
  */
845
- interface DurableObjectScheduledContainerIR extends ContainerIRBase {
846
- /** The named images an instance can start from (wrangler `containers[].images`). */
847
- images?: Record<string, ContainerNamedImageIR>;
848
- schedulingPolicy: "durable_object";
688
+ interface AiRawRunIR {
689
+ /** Source file relative to `<projectRoot>/lunora/`, without extension. */
690
+ file: string;
691
+ /** 1-based line of the `ctx.ai.run` call, or `0` when unknown. */
692
+ line: number;
693
+ /** Who the site runs on behalf of — see {@link CallSiteScope}. */
694
+ scope: CallSiteScope;
849
695
  }
850
696
  /**
851
- * A container lifted from a `defineContainer()` export in
852
- * `lunora/containers.ts`, discriminated on its scheduling policy. Carries
853
- * everything the emitters and the config layer need to wire wrangler
854
- * (`containers[]` + the Durable Object binding + migration class) and the
855
- * generated `_generated/containers.ts` DO class. Names are derived via
856
- * `@lunora/container`'s shared helpers so codegen and the config layer can
857
- * never disagree.
697
+ * One `ctx.vectors.<method>(indexName, input)` call whose `input.namespace` is derived
698
+ * from the handler's `args` with no server-side scoping — the
699
+ * `vectors_namespace_from_user_input` lint input. A Vectorize namespace partitions one
700
+ * index into isolated sub-collections, so a namespace taken straight from request input
701
+ * lets any caller read or poison another tenant's vectors. A fixed literal namespace, or
702
+ * one prefixed with a server-trusted identity (`${ctx.auth.orgId}` — references `ctx`, so
703
+ * treated as scoped), is not recorded; only an arg-derived, unscoped namespace reaches
704
+ * here. Structurally identical to `AdvisorVectorNamespaceAccess`.
858
705
  */
859
- type ContainerIR = DefaultScheduledContainerIR | DurableObjectScheduledContainerIR;
706
+ interface VectorNamespaceAccessIR {
707
+ /** Source file relative to `<projectRoot>/lunora/`, without extension. */
708
+ file: string;
709
+ /** 1-based line of the `ctx.vectors` call, or `0` when unknown. */
710
+ line: number;
711
+ /** The `ctx.vectors` method invoked: `query` / `upsert` / `upsertNow`. */
712
+ method: string;
713
+ /** Who the site runs on behalf of — see {@link CallSiteScope}. */
714
+ scope: CallSiteScope;
715
+ }
860
716
  /**
861
- * A workflow lifted from a `defineWorkflow()` export in `lunora/workflows.ts`.
862
- * Carries what the emitters and the config layer need to wire the wrangler
863
- * `workflows[]` entry and the generated `_generated/workflows.ts`
864
- * `WorkflowEntrypoint` class. Unlike containers, workflows are NOT Durable
865
- * Objects — wrangler gets only a `workflows[]` entry, never a `durable_objects`
866
- * binding or a migration class. Names are derived via `@lunora/workflow`'s
867
- * shared helpers so codegen and the config layer can never disagree.
717
+ * One `ctx.mail`/`ctx.email` `send`/`queue` call whose recipient field (`to`/`cc`/`bcc`)
718
+ * is derived from the handler's `args` with no server-side scoping — the
719
+ * `mail_recipient_from_request_input` lint input. A recipient taken straight from request
720
+ * input turns the deployment into an open relay / spam amplifier (any caller can direct
721
+ * mail to an arbitrary address). A fixed literal recipient, or one scoped by a
722
+ * server-trusted `ctx.*` value (e.g. `ctx.auth.user.email`), is not recorded; only an
723
+ * arg-derived, unscoped recipient reaches here. Structurally identical to
724
+ * `AdvisorMailRecipientAccess`.
868
725
  */
869
- interface WorkflowIR {
870
- /** Generated `WorkflowEntrypoint` class name, e.g. `OrderPipelineWorkflow` — also its `exports` key and `ctx.exports` property. */
871
- className: string;
872
- /** Static `defaultRetention` literal → `exports.<Class>.default_retention`. */
873
- defaultRetention?: {
874
- errorRetention?: string;
875
- successRetention?: string;
876
- };
877
- /** The `lunora/workflows.ts` export name, e.g. `orderPipeline`. */
878
- exportName: string;
879
- /**
880
- * The file declaring the `handler:` when it is a function passed by reference
881
- * and declared in another file — the file {@link WorkflowIR.steps}' lines are
882
- * in. Lunora-relative inside `lunora/`, project-relative outside it.
883
- */
884
- handlerFile?: string;
885
- /** See {@link HandlerSiteIR}. */
886
- handlerSite?: HandlerSiteIR;
887
- /** Static `limits` literal → `exports.<Class>.limits`. */
888
- limits?: {
889
- steps?: number;
890
- };
891
- /**
892
- * The stable wrangler `exports.<Class>.name`. Defaults to the kebab-cased export
893
- * name (`orderPipeline` → `order-pipeline`); a static `name:` literal in the
894
- * definition overrides it.
895
- */
896
- name: string;
897
- /**
898
- * Static `schedules` cron literals → `exports.<Class>.schedules`. Each one
899
- * starts an instance on Cloudflare; gated by the `workflowSchedules`
900
- * capability, since a host that ignores them would never run the workflow.
901
- */
902
- schedules?: ReadonlyArray<string>;
903
- /**
904
- * Durable step labels lifted from the handler body — the first string-literal
905
- * argument of every `ctx.step.do` / `.sleep` / `.sleepUntil` / `.waitForEvent`
906
- * call. Feeds the duplicate-step-name lint, which flags a name used twice
907
- * (Cloudflare memoizes by name, so the second call silently returns the
908
- * first's cached result). Calls with a non-literal name are omitted (not
909
- * statically comparable). A handler passed by reference is followed to its
910
- * declaration; the lines are then in {@link WorkflowIR.handlerFile}.
911
- */
912
- steps: ReadonlyArray<WorkflowStepIR>;
726
+ interface MailRecipientAccessIR {
727
+ /** Source file relative to `<projectRoot>/lunora/`, without extension. */
728
+ file: string;
729
+ /** 1-based line of the `ctx.mail`/`ctx.email` call, or `0` when unknown. */
730
+ line: number;
731
+ /** The mailer method invoked: `send` / `queue`. */
732
+ method: string;
733
+ /** Who the site runs on behalf of — see {@link CallSiteScope}. */
734
+ scope: CallSiteScope;
913
735
  }
914
736
  /**
915
- * An agent lifted from a `defineAgent()` export in `lunora/agents.ts`. A
916
- * `defineAgent` compiles its durable tool-loop onto a Cloudflare Workflow, so —
917
- * like {@link WorkflowIR} — an agent is NOT a Durable Object: wrangler gets only
918
- * a `workflows[]` entry, never a `durable_objects` binding or a migration class.
919
- * Carries what the emitters and the config layer need to wire the generated
920
- * agent `WorkflowEntrypoint` class (e.g. `SupportAgentWorkflow`), the typed
921
- * per-agent `ctx.agents` producer, and the reconciled wrangler `workflows[]`
922
- * entry. Names are derived via `@lunora/agent`'s shared helpers so codegen and
923
- * the config layer can never disagree.
737
+ * One `ctx.browser.<method>(url, …)` call whose navigation URL (`arguments[0]`)
738
+ * is derived from the handler's `args` with no server-side scoping — the
739
+ * `browser_user_url_without_allowlist` lint input. The lint additionally
740
+ * cross-references `createBrowser` config-call evidence to suppress findings
741
+ * when the browser is hardened with an `allowedHosts` allowlist or
742
+ * `resolveDns`. Structurally identical to `AdvisorBrowserUrlAccess`.
924
743
  */
925
- interface AgentIR {
926
- /** Generated `WorkflowEntrypoint` class name, e.g. `SupportAgentWorkflow` — also its `exports` key and `ctx.exports` property. */
927
- className: string;
928
- /** The `lunora/agents.ts` export name, e.g. `support`. */
929
- exportName: string;
930
- /**
931
- * The stable wrangler `exports.<Class>.name`. Defaults to the kebab-cased export
932
- * name (`support` → `agent-support`); a static `name:` literal in the
933
- * definition overrides it.
934
- */
935
- name: string;
936
- /**
937
- * Whether the definition declares an `onEmail` mapper on
938
- * `defineAgent({ onEmail: … })`. When `true` the emitter wires this agent
939
- * onto the worker's top-level `email()` handler (via `@lunora/agent/inbound`)
940
- * so inbound mail starts a durable run. Detected by AST PRESENCE — the
941
- * closure is never evaluated — and written to IR only when present, so
942
- * email-free agents (and agent-free projects) stay byte-identical.
943
- */
944
- onEmail?: boolean;
945
- /**
946
- * Whether the definition opted into public run-starts via
947
- * `defineAgent({ publicRun: true })` — emitted into the `ctx.agents` wiring
948
- * spec so the public `agents:agentRun` mutation can gate on it fail-closed.
949
- * Absent (falsy) means server-side starts only; the field is written to IR
950
- * only when the literal is `true`, so agent-free and non-opted-in output is
951
- * byte-identical.
952
- */
953
- publicRun?: boolean;
954
- /**
955
- * Whether the definition opted into a real-time voice session via a `voice`
956
- * block on `defineAgent({ voice: … })`. Unlike the durable loop (a Workflow),
957
- * the voice path IS a Durable Object — so when this is `true` the emitter
958
- * generates the `voiceClassName` `VoiceSessionDO` subclass and the
959
- * `api.agents.{name}Voice` client reference, and the config layer reconciles
960
- * a `durable_objects` binding (`voiceBindingName`) + `new_sqlite_classes`
961
- * migration. Written to IR only when the literal is present, so voice-free
962
- * agents (and agent-free projects) stay byte-identical.
963
- */
964
- voice?: boolean;
965
- /** The voice DO's Cloudflare `DurableObjectNamespace` binding name, e.g. `VOICE_SUPPORT`. Present only when `voice`. */
966
- voiceBindingName?: string;
967
- /** Generated `VoiceSessionDO` subclass name, e.g. `SupportVoiceDO`. Present only when `voice`. */
968
- voiceClassName?: string;
969
- }
970
- /** One durable step call lifted from a workflow handler body (the use side of {@link WorkflowIR.steps}). */
971
- interface WorkflowStepIR {
972
- /** 1-based line of the durable step call. */
744
+ interface BrowserUrlAccessIR {
745
+ /** Source file relative to `<projectRoot>/lunora/`, without extension. */
746
+ file: string;
747
+ /** 1-based line of the `ctx.browser` call, or `0` when unknown. */
973
748
  line: number;
974
- /** The native step method invoked: `do` / `sleep` / `sleepUntil` / `waitForEvent`. */
749
+ /** The browser method invoked: `content` / `pdf` / `scrape` / `screenshot`. */
975
750
  method: string;
976
- /** The step's static label (the first string-literal argument). */
977
- name: string;
751
+ /** Who the site runs on behalf of — see {@link CallSiteScope}. */
752
+ scope: CallSiteScope;
978
753
  }
979
754
  /**
980
- * A queue lifted from a `defineQueue()` export in `lunora/queues.ts`. Carries
981
- * what the emitters and the config layer need to wire the typed `ctx.queues`
982
- * producer, the generated worker `queue()` dispatch, and the wrangler
983
- * `queues.producers[]` / `queues.consumers[]` entries. Like workflows, a queue
984
- * is NOT a Durable Object — wrangler gets only `queues.*` entries. Names are
985
- * derived via `@lunora/queue`'s shared helpers so codegen and the config layer
986
- * can never disagree.
987
- */
988
- interface QueueIR {
989
- /** The Cloudflare `Queue` producer binding name, e.g. `QUEUE_EMAIL`. */
990
- bindingName: string;
991
- /** The `lunora/queues.ts` export name, e.g. `emailQueue`. */
992
- exportName: string;
993
- /** The declaring file, lunora-relative without `.ts`: `queues`, or `billing/queues` for a module's own. */
994
- filePath: string;
995
- /** See {@link HandlerSiteIR}. */
996
- handlerSite?: HandlerSiteIR;
997
- /** How the queue is consumed: `"push"` (a worker `queue()` handler) or `"pull"` (external HTTP). */
998
- mode: "pull" | "push";
999
- /**
1000
- * The stable wrangler queue name (`queues.producers[].queue`). Defaults to
1001
- * the kebab-cased export name (`emailQueue` → `email-queue`); a static
1002
- * `name:` literal in the definition overrides it.
1003
- */
1004
- name: string;
1005
- /**
1006
- * Set for a `defineSubscription(topic, …)` export: the `defineTopic` export it
1007
- * consumes. A subscription is an ordinary push queue everywhere except
1008
- * `ctx.*` — it is published to through `ctx.topics.<topic>`, never `ctx.queues`.
1009
- */
1010
- topic?: string;
1011
- /** Push-consumer batch/retry tuning, mirrored onto the wrangler `queues.consumers[]` entry. */
1012
- tuning: {
1013
- deadLetterQueue?: string;
1014
- maxBatchSize?: number;
1015
- maxBatchTimeout?: number;
1016
- maxConcurrency?: number;
1017
- maxRetries?: number;
1018
- retryDelay?: number;
1019
- };
1020
- }
1021
- /**
1022
- * A `defineTopic()` export in `lunora/queues.ts`. A topic deploys nothing of its
1023
- * own: each subscription is a {@link QueueIR} whose `topic` names this export, and
1024
- * that is the only record of the link (see {@link subscriptionsOf}).
755
+ * One payload-derived privileged dispatch — a `ctx.run`/`context.run` back into a
756
+ * Lunora function from inside a `defineQueue` push handler or a `defineWorkflow`
757
+ * handler, whose args reference the handler's untrusted payload (`context.params`
758
+ * for a workflow, a `for (… of batch.messages)` body for a queue) — the
759
+ * `privileged_dispatch_unvalidated_payload` lint input. Both handler kinds run
760
+ * under the **system identity** (RLS disabled), so forwarding attacker-influenced
761
+ * payload into the dispatch bypasses the target's row policy. The resolved
762
+ * `targetFile`/`targetExport` let the lint join RLS-procedure evidence and fire
763
+ * only for RLS-gated targets. Structurally identical to `AdvisorPrivilegedDispatch`.
1025
764
  */
1026
- interface TopicIR {
1027
- /** The `lunora/queues.ts` export name, e.g. `signups`. */
1028
- exportName: string;
1029
- /** The declaring file, as {@link QueueIR.filePath}. */
1030
- filePath: string;
765
+ interface PrivilegedDispatchIR {
766
+ /** `"queue"` for a `defineQueue` handler, `"workflow"` for a `defineWorkflow` handler. */
767
+ dispatchKind: "queue" | "workflow";
768
+ /** Source file relative to `<projectRoot>/lunora/`, without extension. */
769
+ file: string;
770
+ /** 1-based line of the dispatch call, or `0` when unknown. */
771
+ line: number;
772
+ /** Who the site runs on behalf of — see {@link CallSiteScope}. */
773
+ scope: CallSiteScope;
774
+ /** Export name of the dispatched target (`send` in `api.messages.send`). */
775
+ targetExport: string;
776
+ /** File path of the dispatched target relative to `lunora/` (`messages` in `api.messages.send`). */
777
+ targetFile: string;
1031
778
  }
1032
779
  /**
1033
- * A module: a folder under `lunora/` whose `module.ts` default-exports
1034
- * `defineModule(...)`. Metadata for the Studio catalog, the architecture
1035
- * manifest and the `cross_module_table_write` lint — it changes no `api.*` path.
780
+ * One tracked `ctx.storage.<bucket>.<method>(...)` upload/signing call — the
781
+ * shared input for the storage config-hygiene security lints
782
+ * (`storage_upload_without_content_type_allowlist`, `storage_upload_without_max_size`,
783
+ * `storage_generate_upload_url_no_content_type_pin`, `storage_presigned_url_for_private_content`).
784
+ * `upload`/`store` carry the `UploadOptions` guards (`allowedContentTypes` /
785
+ * `maxSize`); `generateUploadUrl` carries the signed-PUT `contentType` pin;
786
+ * `getPresignedUrl`/`getSignedUrl` carry a statically-known `expiresInSeconds`
787
+ * literal. `presentKeys` is empty (and `expiresInSeconds` unset) when the
788
+ * options argument was absent, a non-literal, or a spread — see `analyzable`.
789
+ * Structurally identical to `AdvisorStorageUpload`.
1036
790
  */
1037
- interface ModuleIR {
1038
- /** One-line description from `defineModule({ description })`. */
1039
- description?: string;
1040
- /**
1041
- * `true` for an installed component (a `defineSchemaExtension` key merged with
1042
- * `.extend(...)`) treated as a module: it owns its prefixed tables and the
1043
- * `lunora/<key>/` folder its copy-in code lives in. Absent for a declared module.
1044
- */
1045
- installed?: true;
1046
- /** The folder path relative to `lunora/`, e.g. `billing` — also the module's name. */
1047
- name: string;
1048
- /**
1049
- * `false` for an installed component shipped as a package: its code is in
1050
- * `node_modules`, so it owns its tables but no `lunora/<key>/` folder (an app
1051
- * folder of that name stays the app's). Absent means the module owns its folder.
1052
- */
1053
- ownsFolder?: false;
1054
- /** Tables the module declares it owns (`defineModule({ tables })`); empty when it declares none. */
1055
- tables: ReadonlyArray<string>;
791
+ interface StorageUploadIR {
792
+ /** `true` when the call's options-object argument (or its deliberate absence) was statically resolvable. */
793
+ analyzable: boolean;
794
+ /** Numeric literal value of an `expiresInSeconds` option, when statically known (`getSignedUrl` / `getPresignedUrl` only). */
795
+ expiresInSeconds?: number;
796
+ /** Source file relative to `<projectRoot>/lunora/`, without extension. */
797
+ file: string;
798
+ /** 1-based line of the call, or `0` when unknown. */
799
+ line: number;
800
+ /** The `ctx.storage` method invoked. */
801
+ method: "generateUploadUrl" | "getPresignedUrl" | "getSignedUrl" | "store" | "upload";
802
+ /** Options-object keys present at the call site (empty when not `analyzable`, or when no options argument was passed). */
803
+ presentKeys: string[];
804
+ /** Who the site runs on behalf of — see {@link CallSiteScope}. */
805
+ scope: CallSiteScope;
1056
806
  }
1057
807
  /**
1058
- * Who a discovered call site runs on behalf of — the one attribution every
1059
- * per-call-site IR carries (see `discover/attribution`).
1060
- *
1061
- * `export`: lexically inside an exported top-level declaration. `name` is the
1062
- * EXPORTED name — `export const send = …` → `send`, `export default …` →
1063
- * `default`, `const run = …; export { run as start }` → `start`.
1064
- *
1065
- * `helper`: inside a top-level, non-exported `function` / `const` of the same
1066
- * file. `callers` are the exports that reach it through same-file value
1067
- * references (a call, or the helper passed on as a value), transitively and
1068
- * sorted; a type-only reference (`typeof helper`) does not count. Empty for an
1069
- * orphan helper no export reaches. `untracked` is set when code attribution
1070
- * cannot follow — module scope, a class body, a destructured declaration — also
1071
- * reaches the helper, so `callers` is incomplete.
1072
- *
1073
- * `module`: outside every top-level `function` / `const` — module scope, or a
1074
- * class body.
1075
- *
1076
- * A site is recorded once; consumers fan out over its callers. The advisor's
1077
- * `AdvisorCallSiteScope` is the same type, so feeder rows pass straight through.
808
+ * One discovered `httpAction`/`httpRoute` handler in `lunora/` that performs a
809
+ * side effect (`ctx.runMutation` / `ctx.runAction` / a `ctx.db.{insert,patch,
810
+ * replace,delete,insertManyUnsafe}` write) from the HTTP edge, with whether it
811
+ * reads `ctx.auth` — the `http_action_missing_auth_guard` lint input. A handler
812
+ * that mutates state or dispatches an action without ever consulting the request
813
+ * identity is an unauthenticated write bypassing identity/RLS. Only handlers with
814
+ * a statically-resolvable inline body and `ctx` binding are recorded (fail-safe
815
+ * under-report); read-only handlers are never recorded. Structurally identical to
816
+ * `AdvisorHttpActionGuard`.
1078
817
  */
1079
- type CallSiteScope = AdvisorCallSiteScope;
818
+ interface HttpActionGuardIR {
819
+ /** Source file relative to `<projectRoot>/lunora/`, without extension. */
820
+ file: string;
821
+ /** Which HTTP surface the handler is: a raw `httpAction` or a typed `httpRoute` route. */
822
+ kind: "httpAction" | "httpRoute";
823
+ /** 1-based line of the handler call, or `0` when unknown. */
824
+ line: number;
825
+ /** For an `httpRoute`, the uppercased verb (`"POST"`); absent for a raw `httpAction`. */
826
+ method?: string;
827
+ /** `true` when the handler reads `ctx.auth` (a direct member access or a `const { auth } = ctx` destructure). */
828
+ readsAuth: boolean;
829
+ /** Who the site runs on behalf of — see {@link CallSiteScope}. */
830
+ scope: CallSiteScope;
831
+ /** The first side effect found, as a stable label: `runMutation`, `runAction`, or `db.<method>`. */
832
+ sideEffect: string;
833
+ }
1080
834
  /**
1081
- * Where a queue or workflow `handler:` passed by reference is declared, when it is
1082
- * exported from a lunora source file — `defineWorkflow({ handler: onboard })` with
1083
- * `onboard` exported from `lunora/onboarding/flow.ts` gives
1084
- * `{ exportName: "onboard", file: "onboarding/flow" }`. The architecture graph
1085
- * attributes that export's own call sites to the queue or workflow node, so its
1086
- * calls are not reported as "not a registered function". `exportName` is the
1087
- * exported name (`export { run as start }` → `start`).
835
+ * One response-header write, inside an `httpAction` handler, whose value is derived
836
+ * from raw request input (`request.headers`, `request.url`/query, `await
837
+ * request.json()`) with no CR/LF sanitizer — the
838
+ * `http_action_response_header_injection` lint input. A `Request`-derived string
839
+ * placed verbatim into a response header lets a caller smuggle `\r\n` and inject
840
+ * extra headers or split the response (header injection / response splitting). Only
841
+ * sites whose value is request-tainted AND unguarded are recorded: a value routed
842
+ * through a CR/LF guard (`isSafeHeaderValue`), a URL/URI encoder
843
+ * (`encodeURIComponent`/`encodeURI`), a numeric coercion (`Number`/`parseInt`/
844
+ * `parseFloat`), or `btoa` is treated as safe and never recorded (`String(...)` /
845
+ * `.toString()` are NOT sanitizers — they don't strip CR/LF). Structurally
846
+ * identical to `AdvisorHttpHeaderWrite`.
1088
847
  */
1089
- interface HandlerSiteIR {
1090
- exportName: string;
848
+ interface HttpHeaderWriteIR {
1091
849
  /** Source file relative to `<projectRoot>/lunora/`, without extension. */
1092
850
  file: string;
851
+ /** The header name being written (`"location"`), or `""` when the key is not a string literal. */
852
+ headerName: string;
853
+ /** 1-based line of the request-tainted header value. */
854
+ line: number;
855
+ /** Who the site runs on behalf of — see {@link CallSiteScope}. */
856
+ scope: CallSiteScope;
857
+ /** How the header was written. */
858
+ via: "headers-append" | "headers-ctor" | "headers-set" | "response-init";
1093
859
  }
1094
860
  /**
1095
- * One call-site edge of the architecture graph, attributed through its
1096
- * {@link CallSiteScope}.
1097
- * Exactly one of `target` / `reason` is set: `target` is a `namespace:export`
1098
- * function key (`call` / `schedule`) or a queue / topic export name (`enqueue` /
1099
- * `publish`); `reason` says why the target could not be read statically.
861
+ * One rate-limit / Turnstile middleware call in `lunora/` — the
862
+ * `ratelimit_middleware_fail_open` lint input. `rateLimit`/`dbRateLimit`
863
+ * (`@lunora/ratelimit`) and `verifyTurnstileMiddleware` (`@lunora/auth`) each
864
+ * accept a `failOpen` escape hatch that admits every request when the
865
+ * limiter/siteverify is unavailable; `failOpen` is `true` only when the options
866
+ * literal set it to the boolean literal `true` (anything else is fail-closed).
867
+ * The lint escalates a fail-open guard to a finding when the guarded procedure
868
+ * (`exportName`/`limitName`) looks auth/payment-sensitive. Structurally
869
+ * identical to `AdvisorFailOpenGuard`.
1100
870
  */
1101
- interface CallEdgeIR {
1102
- /**
1103
- * `true` when the hop may not take effect on every pass of its function —
1104
- * see `isConditionalSite` in `discover/call-edges.ts` for the rules. Set
1105
- * only, never `false`. The `dispatch_cycle` lint's false-positive gate.
1106
- */
1107
- conditional?: true;
871
+ interface FailOpenGuardIR {
872
+ /** The middleware factory at the call site: `rateLimit` / `dbRateLimit` / `verifyTurnstileMiddleware`. */
873
+ callee: string;
874
+ /** `true` only when the options literal set `failOpen: true` as a boolean literal; a non-literal or absent option is treated as fail-closed. */
875
+ failOpen: boolean;
1108
876
  /** Source file relative to `<projectRoot>/lunora/`, without extension. */
1109
877
  file: string;
1110
- kind: "call" | "enqueue" | "invoke" | "publish" | "schedule";
1111
- /** 1-based line of the call. */
878
+ /** The rate-limit `name` (second string argument) for `rateLimit`/`dbRateLimit`; `""` for `verifyTurnstileMiddleware`. */
879
+ limitName: string;
880
+ /** 1-based line of the middleware call, or `0` when unknown. */
1112
881
  line: number;
1113
- reason?: string;
1114
- /** Who the call runs on behalf of. */
882
+ /** Who the site runs on behalf of — see {@link CallSiteScope}. */
1115
883
  scope: CallSiteScope;
1116
- target?: string;
1117
884
  }
1118
885
  /**
1119
- * A literal-infinite loop in `lunora/` source — `while (true)`, `for (;;)` /
1120
- * `for (; true;)`, `do { … } while (true)` — with no statically reachable way
1121
- * out: no `break` bound to this loop, no `return`/`throw` outside a nested
1122
- * function, no labeled `break` escaping it. One record per offending loop; a
1123
- * loop that can exit records nothing, so the `unbounded_loop` lint has no
1124
- * bounded loop to sift through.
886
+ * One `ctx.flags.boolean("key", <boolean-literal>)` read in `lunora/` — the
887
+ * `flag_gates_security_with_unsafe_default` lint input. OpenFeature returns the
888
+ * `defaultValue` when the provider errors, so a fail-open default on a
889
+ * security-shaped key silently opens access during an outage. Only reads with a
890
+ * statically-known string key and boolean-literal default are recorded; the lint
891
+ * owns the security-shape + polarity judgment. Structurally identical to
892
+ * AdvisorFlagSecurityDefault.
1125
893
  */
1126
- interface UnboundedLoopIR {
894
+ interface FlagSecurityDefaultIR {
895
+ /** The boolean-literal default returned on a provider outage (fail-open value). */
896
+ defaultValue: boolean;
1127
897
  /** Source file relative to `<projectRoot>/lunora/`, without extension. */
1128
898
  file: string;
1129
- /** The loop form that never falls out. */
1130
- kind: "do" | "for" | "while";
1131
- /** 1-based line of the `loop` keyword. */
899
+ /** The flag key — the first string-literal argument of `ctx.flags.boolean`. */
900
+ key: string;
901
+ /** 1-based line of the `ctx.flags.boolean` call, or `0` when unknown. */
1132
902
  line: number;
1133
- /** Who the loop runs on behalf of. */
903
+ /** Who the site runs on behalf of — see {@link CallSiteScope}. */
1134
904
  scope: CallSiteScope;
1135
905
  }
1136
906
  /**
1137
- * A sibling Worker the app calls through a Cloudflare service binding, declared in
1138
- * `lunora.config.*` `services` (plan 457). Becomes a wrangler `services[]` entry,
1139
- * a typed `ctx.services.<name>` on actions, and a `service` node in the
1140
- * architecture manifest.
907
+ * One `ctx.flags` read lexically inside a `query(...)` handler — the
908
+ * flag_read_in_subscription advisor lint input. Flag changes append nothing to
909
+ * `__cdc_log`, so a subscribed query is never re-run when a flag flips and keeps
910
+ * serving the branch it last picked; `useFlag` is the reactive path. Structurally
911
+ * identical to the advisor's `AdvisorFlagRead` (same field set) so values pass
912
+ * straight through `lintSchema` without conversion, exactly as `ContextPropertyCallIR` does.
913
+ *
914
+ * Only `query` handlers are recorded — unlike `ContextPropertyCallIR` / `NondeterministicCallIR`
915
+ * there is no `kind` field, because `mutation`/`action` handlers run once and have
916
+ * no subscription staleness to warn about, so the feeder drops them rather than
917
+ * handing the lint rows it would discard.
1141
918
  */
1142
- interface ServiceBindingIR {
1143
- /** The `services[].binding` Lunora writes, e.g. `SERVICE_DOCUMENT_PARSER`. */
1144
- binding: string;
1145
- /** The exported `WorkerEntrypoint` class the binding targets; absent for the Worker's default export. */
1146
- entrypoint?: string;
1147
- /**
1148
- * The Worker name per `env.<name>` block the service's own wrangler config
1149
- * sets a `name` in. An environment missing here deploys as wrangler's
1150
- * default `<worker>-<env>`.
1151
- */
1152
- envWorkers: Readonly<Record<string, string>>;
1153
- /** Absolute path of the service's entry module (its wrangler `main`) — what the RPC type is read from. */
1154
- main: string;
1155
- /** The `ctx.services.<name>` key, e.g. `documentParser`. */
1156
- name: string;
1157
- /**
1158
- * The scopes in which the service still serves on `*.workers.dev` with no
1159
- * route of its own: `""` for the top level, else the environment name.
1160
- */
1161
- publicScopes: ReadonlyArray<string>;
1162
- /**
1163
- * The `WorkerEntrypoint` class the service is typed — and wired — as an RPC
1164
- * stub of. Equal to {@link ServiceBindingIR.entrypoint} for an RPC service;
1165
- * absent for a fetch service, including a named entrypoint declared
1166
- * `rpc: false`, which is called with plain `fetch` and so needs no import of
1167
- * the service's sources.
1168
- */
1169
- rpcEntrypoint?: string;
1170
- /** The Worker's own `name` from its wrangler config — the `services[].service` target. */
1171
- worker: string;
1172
- /** Absolute path of the service's wrangler config. */
1173
- wranglerPath: string;
1174
- }
1175
- /**
1176
- * The feature-flag provider declared by the default export of `lunora/flags.ts`
1177
- * (`defineFlags({ provider, … })`). Discovery is **metadata-only** — codegen
1178
- * imports the real module at runtime for the provider value; this IR exists so
1179
- * the config layer can reconcile/validate the wrangler `flagship` binding when
1180
- * the app uses Flagship in binding mode. A `custom` provider (any other
1181
- * OpenFeature factory) carries no binding to reconcile.
1182
- */
1183
- interface FlagsIR {
1184
- /**
1185
- * The wrangler `flagship[].binding` name — set **only** for a flagship
1186
- * `provider` in binding mode (`flagshipProvider({ binding: "FLAGS" })`). The
1187
- * config layer hints/validates a matching `flagship` binding from this.
1188
- */
1189
- bindingName?: string;
1190
- /**
1191
- * Flagship operating mode — `"binding"` (wrangler binding, needs a
1192
- * `flagship` entry) or `"http"` (no binding); `undefined` for a `custom`
1193
- * provider or when the mode can't be read statically.
1194
- */
1195
- mode?: "binding" | "http";
1196
- /** `"flagship"` when the provider is `flagshipProvider(...)`, else `"custom"` (any other OpenFeature provider factory). */
1197
- provider: "custom" | "flagship";
1198
- }
1199
- /**
1200
- * A `ctx.workflows.get("name")…` call discovered in a function body — the
1201
- * use-site analog of {@link WorkflowIR} (which is the declaration side). Feeds
1202
- * the `workflow_unused` lint (a declared workflow with zero call sites) and the
1203
- * `workflow_unknown_target` lint (a `.get("x")` whose `x` isn't declared — a
1204
- * typo catcher). {@link WorkflowCallIR.workflow} is `""` when the `get(...)`
1205
- * argument is not a string literal (a dynamic name — which suppresses the
1206
- * unused-workflow heuristic rather than producing a false positive).
1207
- */
1208
- interface WorkflowCallIR {
919
+ interface FlagReadIR {
920
+ /** The accessed `ctx.flags` surface, e.g. `ctx.flags.boolean` / `ctx.flags.details.string`. */
921
+ callee: string;
922
+ /** Export binding name of the query performing the read. */
923
+ exportName: string;
1209
924
  /** Source file relative to `<projectRoot>/lunora/`, without extension (the api namespace). */
1210
925
  file: string;
1211
- /** 1-based line of the `get(...)` call. */
926
+ /** 1-based line of the read, or `0` when unknown. */
1212
927
  line: number;
1213
- /** Who the call runs on behalf of. */
1214
- scope: CallSiteScope;
1215
- /** The referenced workflow export name, or `""` when the argument is not a string literal. */
1216
- workflow: string;
1217
928
  }
1218
929
  /**
1219
- * A `ctx.db.query("table")…` read discovered in a function body, reduced to what
1220
- * the query advisor lints need: which table, whether the chain narrows with an
1221
- * index, whether it filters, and which terminal materializes the result.
1222
- * `table` is `""` when the `query(...)` argument is not a string literal (a
1223
- * dynamic table — not lintable).
930
+ * One `generateText` / `streamText` call in `lunora/` whose `tools` reach a
931
+ * privileged side effect (a DB write, function dispatch, or outbound
932
+ * fetch/mail/queue send). `userInputDerived` records whether the model input
933
+ * (`prompt`/`messages`/`system`) flows from the handler's `args`; the
934
+ * `ai_tool_side_effect_prompt_injection` lint fires only when it does.
935
+ * Structurally identical to `AdvisorAiToolSideEffect`.
1224
936
  */
1225
- interface QueryReadIR {
937
+ interface AiToolSideEffectIR {
1226
938
  /** Source file relative to `<projectRoot>/lunora/`, without extension. */
1227
939
  file: string;
1228
- /**
1229
- * True when the chain's `.filter()` predicate compares `_id`
1230
- * (`(d) => d._id === args.id`) — a full scan for a row that `ctx.db.get`
1231
- * addresses directly. Optional so a feeder predating this field still
1232
- * typechecks; absent is treated as "not a primary-key filter".
1233
- */
1234
- filtersPrimaryKey?: boolean;
1235
- /** The chain calls `.filter(...)`. */
1236
- hasFilter: boolean;
1237
- /** The chain narrows with `.withIndex(...)` or `.withSearchIndex(...)`. */
1238
- hasIndex: boolean;
1239
- /** 1-based line of the `query(...)` call. */
940
+ /** 1-based line of the generation call, or `0` when unknown. */
1240
941
  line: number;
1241
- /** Who the read runs on behalf of. */
942
+ /** The generation entrypoint invoked. */
943
+ method: "generateText" | "streamText";
944
+ /** Who the site runs on behalf of — see {@link CallSiteScope}. */
1242
945
  scope: CallSiteScope;
1243
- /** Queried table name, or `""` when the argument is not a string literal. */
1244
- table: string;
1245
- /**
1246
- * The materializing call the chain ends in — `"collect"`, `"take"`,
1247
- * `"paginate"`, `"first"`, `"unique"`, … — i.e. how much of the narrowed set
1248
- * the read actually loads.
1249
- *
1250
- * `undefined` when the chain reaches no recognised terminal (a reader passed
1251
- * on, a bare `query(...)`) AND when a feeder predating this field produced
1252
- * the read. The two are deliberately not distinguished: no consumer could act
1253
- * on the difference, so the terminal-shaped lints skip the read either way
1254
- * rather than guessing a terminal.
1255
- */
1256
- terminal?: string;
946
+ /** The privileged side-effect sink a model-callable tool reaches (`ctx.db.insert`, `ctx.run`, `ctx.fetch`, …). */
947
+ sideEffect: string;
948
+ /** `true` when a model-input option is derived from the handler's `args` (a bare `args.x`, or a name destructured from `args`). */
949
+ userInputDerived: boolean;
1257
950
  }
1258
951
  /**
1259
- * A `ctx.authApi.<method>(...)` call discovered in a function body, attributed
1260
- * to the exported function (and its file = api namespace) that performs it.
1261
- * Structurally identical to `AdvisorAuthApiCall` so it passes straight through
1262
- * to the advisor lint without conversion, exactly as `InsertWriteIR` does for
1263
- * `AdvisorInsertWrite`.
952
+ * One `<receiver>.identity.<key>` claim read in `lunora/`, where `<receiver>` is
953
+ * an RLS/mask policy `auth` (or `ctx.auth`/`context.auth`). `declared` records
954
+ * whether `<key>` is in the app's `defineIdentity({ ... })` contract (or the
955
+ * always-present `userId`); the `identity_undeclared_claim_trusted` lint fires on
956
+ * the undeclared reads. Emitted only when a resolvable identity contract exists.
957
+ * Structurally identical to `AdvisorIdentityClaimRead`.
1264
958
  */
1265
- interface AuthApiCallIR {
959
+ interface IdentityClaimReadIR {
960
+ /** `true` when `key` is a declared claim (in the `defineIdentity` contract, or the always-present `userId`). */
961
+ declared: boolean;
1266
962
  /** Source file relative to `<projectRoot>/lunora/`, without extension. */
1267
963
  file: string;
1268
- /** True when the call's argument object includes a `headers` property. */
1269
- hasHeaders: boolean;
1270
- /** 1-based line of the call, or `0` when unknown. */
964
+ /** The claim key read off the identity bag. */
965
+ key: string;
966
+ /** 1-based line of the read, or `0` when unknown. */
1271
967
  line: number;
1272
- /** The better-auth method invoked (e.g. `banUser`); empty when not statically known. */
1273
- method: string;
1274
968
  /** Who the site runs on behalf of — see {@link CallSiteScope}. */
1275
969
  scope: CallSiteScope;
1276
970
  }
1277
971
  /**
1278
- * A table write other than a plain `ctx.db.insert(...)` (see {@link InsertWriteIR}):
1279
- * a by-id `patch`/`replace`/`delete`/`hardDelete`/`restore`, a batch write, or a
1280
- * `ctx.db.<table>.*` facade write. Feeds the architecture manifest's `write`
1281
- * edges and the `cross_module_table_write` lint.
972
+ * One payment webhook-adapter construction in `lunora/` (`createStripeAdapter` /
973
+ * `createPolarAdapter` / `createAutumnAdapter` / `createDodoPaymentsAdapter`).
974
+ * `toleranceSeconds` carries the statically-known `webhookToleranceSeconds`
975
+ * replay window when it is a plain numeric literal; the payment-webhook
976
+ * wide-tolerance lint fires when it exceeds a conservative ceiling. Structurally
977
+ * identical to `AdvisorPaymentWebhook`.
1282
978
  */
1283
- interface TableWriteIR {
979
+ interface PaymentWebhookIR {
980
+ /** The adapter factory invoked. */
981
+ callee: "createAutumnAdapter" | "createDodoPaymentsAdapter" | "createPolarAdapter" | "createStripeAdapter";
1284
982
  /** Source file relative to `<projectRoot>/lunora/`, without extension. */
1285
983
  file: string;
1286
- /** 1-based line of the call. */
984
+ /** 1-based line of the construction, or `0` when unknown. */
1287
985
  line: number;
1288
- /** The writer method called, e.g. `patch`, `deleteMany`, `upsert`. */
1289
- method: string;
1290
- /** Who the write runs on behalf of. */
986
+ /** Who the site runs on behalf of — see {@link CallSiteScope}. */
1291
987
  scope: CallSiteScope;
1292
- /**
1293
- * Target table; `""` when it can't be read (an untyped id, a non-literal
1294
- * name). A union id (`Id<"a"> | Id<"b">`) gives one record per table.
1295
- */
1296
- table: string;
988
+ /** Statically-known `webhookToleranceSeconds` literal, when present and a plain numeric literal. */
989
+ toleranceSeconds?: number;
1297
990
  }
1298
991
  /**
1299
- * A `ctx.db.insert("table", …)` write discovered in a function body, attributed
1300
- * to the exported function (and its file = api namespace) that performs it — the
1301
- * write-side analog of {@link QueryReadIR}. Lets tooling wire a table's write
1302
- * action by behavior (which function inserts into it) rather than by naming.
1303
- * {@link InsertWriteIR.table} is `""` when the argument is not a string literal.
992
+ * One `query`/`mutation` handler that gates a `ctx.db.get`/`patch`/`delete` on a
993
+ * null-checked `ctx.db.normalizeId(table, id)` result — the
994
+ * `normalize_id_used_as_authorization` lint input. `normalizeId` validates an id's
995
+ * structural shape only (it never reads the database), so a non-null result proves
996
+ * the id is well-formed, never that the caller owns the row; gating access on it is
997
+ * an IDOR. The lint owns the negative proof — it keeps only `visibility === "public"`
998
+ * rows with no `.use(rls(...))` and no ownership/identity mention (`mentionsOwnership`),
999
+ * then joins `table` against the schema's RLS mode before flagging. Structurally
1000
+ * identical to AdvisorNormalizeIdAuthorization so values pass straight through.
1304
1001
  */
1305
- interface InsertWriteIR {
1306
- /** Source file relative to `<projectRoot>/lunora/`, without extension (the api namespace). */
1002
+ interface NormalizeIdAuthorizationIR {
1003
+ /** Export binding name of the procedure performing the normalize-then-access. */
1004
+ exportName: string;
1005
+ /** Source file relative to `<projectRoot>/lunora/`, without extension. */
1307
1006
  file: string;
1308
- /** 1-based line of the `insert(...)` call. */
1007
+ /** 1-based line of the `ctx.db.normalizeId(...)` call the access is gated on. */
1309
1008
  line: number;
1310
- /** Who the insert runs on behalf of. */
1311
- scope: CallSiteScope;
1312
- /** Target table name, or `""` when the argument is not a string literal. */
1009
+ /** `true` when the handler anywhere reads an ownership-named identifier or `ctx.auth`/`ctx.identity`/… — an intervening ownership signal. */
1010
+ mentionsOwnership: boolean;
1011
+ /** The id-first `ctx.db` sink the normalized id reaches. */
1012
+ sinkMethod: "delete" | "get" | "patch";
1013
+ /** Table named in the `normalizeId` call, or `""` when its table argument wasn't a string literal. */
1313
1014
  table: string;
1015
+ /** `true` when the procedure's builder chain carries a `.use(rls(...))` step. */
1016
+ usesRls: boolean;
1017
+ /** `"internal"` for `internalQuery`/`internalMutation`; `"public"` for `query`/`mutation`. */
1018
+ visibility: "internal" | "public";
1314
1019
  }
1315
1020
  /**
1316
- * A non-deterministic API call (`Date.now`, `Math.random`, `crypto.randomUUID`,
1317
- * `crypto.getRandomValues`, `fetch`) discovered lexically inside a `query(...)`
1318
- * or `mutation(...)` handler body — the `nondeterministic_query_mutation` lint
1319
- * input. Structurally identical to `AdvisorNondeterministicCall` so values pass
1320
- * straight through to the advisor without conversion, exactly as `AuthApiCallIR`
1321
- * does for `AdvisorAuthApiCall`. `action(...)` handlers are never recorded —
1322
- * actions are the determinism escape hatch.
1021
+ * Schema, tables, indexes, relations, migrations: what the app declares about its data.
1323
1022
  */
1324
- interface NondeterministicCallIR {
1325
- /** The non-deterministic API invoked, e.g. `Date.now` / `Math.random` / `crypto.randomUUID` / `fetch`. */
1326
- callee: string;
1327
- /** Export binding name of the function performing the call, e.g. `sendMessage`. */
1328
- exportName: string;
1329
- /** Source file relative to `<projectRoot>/lunora/`, without extension (the api namespace). */
1330
- file: string;
1331
- /** Which procedure kind the call lives in — only `query`/`mutation` handlers are recorded. */
1332
- kind: "mutation" | "query";
1333
- /** 1-based line of the call, or `0` when unknown. */
1334
- line: number;
1335
- }
1336
1023
  /**
1337
- * One `ctx.<property>` access lexically inside a `query`/`mutation` handler —
1338
- * the feeder shape behind every "action-only surface used outside an action"
1339
- * advisor lint (`hyperdrive_outside_action` for `ctx.sql`,
1340
- * `r2sql_outside_action` for `ctx.r2sql`). Structurally identical to the
1341
- * advisor's `AdvisorHyperdriveCall` / `AdvisorR2sqlCall` (same field set) so
1342
- * values pass straight through `lintSchema` without conversion, exactly as
1343
- * {@link NondeterministicCallIR} does. Only `query`/`mutation` handlers are
1344
- * recorded; `action(...)` is the only context where these surfaces are even
1345
- * typed, so it is the intended home and is skipped.
1024
+ * AST-observable subset of a column's modifier chain (`.unique()`, `.default()`,
1025
+ * …). Function-valued modifiers (`.$defaultFn`/`.$onUpdateFn`) can't be
1026
+ * serialized, so only their *presence* is recorded.
1346
1027
  */
1347
- interface ContextPropertyCallIR {
1348
- /** The accessed surface, e.g. `ctx.sql.query` / `ctx.r2sql.from` — the property, suffixed with the method when one is called on it. */
1349
- callee: string;
1350
- /** Export binding name of the function performing the access. */
1351
- exportName: string;
1352
- /** Source file relative to the lunora dir, without extension (the api namespace). */
1353
- file: string;
1354
- /** Which procedure kind the access lives in — only `query`/`mutation` handlers are recorded. */
1355
- kind: "mutation" | "query";
1356
- /** 1-based line of the access. */
1357
- line: number;
1358
- }
1359
- /**
1360
- * Per-procedure RLS usage snapshot, produced by `discoverRlsProcedures` for the
1361
- * `rls_uncovered_table` advisor lint. Structurally identical to
1362
- * `AdvisorRlsProcedure` (they share the same field set) so values pass straight
1363
- * through to the advisor without conversion, exactly as `AuthApiCallIR` does for
1364
- * `AdvisorAuthApiCall`.
1365
- */
1366
- interface RlsProcedureIR {
1367
- /** Export binding name of the procedure (e.g. `listDocuments`). */
1368
- exportName: string;
1369
- /** Source file relative to `<projectRoot>/lunora/`, without extension. */
1370
- file: string;
1028
+ interface ColumnMetaIR {
1029
+ /** `.default(...)` or `.$defaultFn(...)` present — field is optional on insert. */
1030
+ hasDefault?: boolean;
1031
+ /** `.$onUpdateFn(...)` present. */
1032
+ hasOnUpdate?: boolean;
1033
+ /** Default `true`; `.nullable()` flips it to `false`. */
1034
+ notNull: boolean;
1035
+ /** `.unique()` present. */
1036
+ unique?: boolean;
1037
+ }
1038
+ /** Reflective representation of a single validator call from the schema. */
1039
+ interface ValidatorIR {
1040
+ /** For `v.storage(bucket?)` — the named typed bucket the object key lives in, when given. */
1041
+ bucket?: string;
1042
+ /** Column modifiers (`.unique()`, `.default()`, `.nullable()`, …) when present. */
1043
+ column?: ColumnMetaIR;
1371
1044
  /**
1372
- * Table names extracted from the `rls(policies)` array literal. Empty when the
1373
- * policies argument is not a statically-readable array literal.
1045
+ * `true` when this validator carries a refinement (`.check(...)`, `.max(n)`,
1046
+ * `.email()`, …) — anything that narrows the accepted values without changing
1047
+ * the kind. `schema-drift` hashes this, so a bound counts here even when it is
1048
+ * modelled below. `.meta(...)` is pure metadata with no parse effect and does
1049
+ * NOT set it.
1374
1050
  */
1375
- rlsTables: string[];
1376
- /** Tables read by the procedure via `ctx.db.query/findMany/findFirst/…`. */
1377
- tablesRead: string[];
1378
- /** Tables written by the procedure via `ctx.db.insert/patch/replace/delete`. */
1379
- tablesWritten: string[];
1380
- /** `true` when the procedure's builder chain includes `.use(rls(...))`. */
1381
- usesRls: boolean;
1382
- /** `"internal"` for `internalQuery` / `internalMutation` / `internalAction`. */
1383
- visibility: "internal" | "public";
1384
- }
1385
- /**
1386
- * One procedure reduced to the facts the `mask_uncovered_pii_column` lint needs:
1387
- * whether its builder chain includes `.use(mask(...))`, which `(table, column)`
1388
- * pairs that mask declares, and which tables the procedure reads/writes. The
1389
- * column-level analogue of {@link RlsProcedureIR}. Structurally identical to
1390
- * `AdvisorMaskProcedure` so values pass straight through without conversion.
1391
- */
1392
- interface MaskProcedureIR {
1393
- /** Export binding name of the procedure (e.g. `listUsers`). */
1394
- exportName: string;
1395
- /** Source file relative to `<projectRoot>/lunora/`, without extension. */
1396
- file: string;
1051
+ hasRefinement?: boolean;
1052
+ /** For `v.optional(inner)` / `v.array(inner)`. */
1053
+ inner?: ValidatorIR;
1054
+ /** For `v.record(key, value)`. */
1055
+ keyType?: ValidatorIR;
1056
+ /** Kind of validator (string, number, id, object, optional, array, union, literal, record, ...). */
1057
+ kind: string;
1058
+ /** For `v.literal(value)` — the literal value as source text. */
1059
+ literalValue?: string;
1060
+ /** For `v.union(a, b, ...)`. */
1061
+ members?: ValidatorIR[];
1062
+ /** For `v.object({...})`. */
1063
+ shape?: Record<string, ValidatorIR>;
1064
+ /** Verbatim source text — used in emitted code when we can't reconstruct from AST. */
1065
+ sourceText?: string;
1397
1066
  /**
1398
- * `(table, column)` pairs this procedure's `mask(policies)` object literal
1399
- * declares. Empty when the policies argument is not a statically-readable
1400
- * object literal (conservative: `usesMask` is still `true`).
1067
+ * The `n` of a `v.string().max(n)` written with a numeric literal — the one
1068
+ * refinement predicate the IR can represent exactly (`value.length <= n`).
1069
+ * The AOT compiler emits it as a guard instead of declining the node, which
1070
+ * is what keeps a length-bounded public argument on the fast path.
1401
1071
  */
1402
- maskColumns: {
1403
- column: string;
1404
- table: string;
1405
- }[];
1406
- /** Tables read by the procedure via `ctx.db.query/findMany/findFirst/get`. */
1407
- tablesRead: string[];
1408
- /** Tables written by the procedure via `ctx.db.insert/patch/replace/delete`. */
1409
- tablesWritten: string[];
1410
- /** `true` when the procedure's builder chain includes `.use(mask(...))`. */
1411
- usesMask: boolean;
1412
- /** `"internal"` for `internalQuery` / `internalMutation` / `internalAction`. */
1413
- visibility: "internal" | "public";
1072
+ stringMaxLength?: number;
1073
+ /** For `v.id("table")` — the table name. */
1074
+ tableName?: string;
1075
+ /**
1076
+ * For `v.from(externalSchema)` — the wrapped Standard Schema's inferred type,
1077
+ * rendered as TS source. Recovered through the type checker from
1078
+ * `~standard.types.output`, mirroring the runtime's `InferStandardOutput`.
1079
+ * Absent when it could not be recovered safely, in which case the emitted
1080
+ * type falls back to `unknown`.
1081
+ */
1082
+ tsType?: string;
1083
+ /**
1084
+ * `true` when this validator carries a refinement the IR could NOT model — a
1085
+ * `.check(...)` closure, `.email()`, `.pattern(re)`, a `.max()` whose bound is
1086
+ * not a literal. The AOT args-validator compiler declines any node with this
1087
+ * flag, because compiling it would silently skip the predicate.
1088
+ */
1089
+ unmodelledRefinement?: boolean;
1090
+ valueType?: ValidatorIR;
1414
1091
  }
1415
- /**
1416
- * One masked column surfaced to the studio's data-browser mask preview: a
1417
- * `(table, column)` pair plus the declared {@link MaskProcedureIR} strategy so
1418
- * the preview can pick redact-vs-hash-vs-custom rendering. Aggregated across the
1419
- * project's `.use(mask(...))` chains by `discoverMaskMetadata`; the descriptive
1420
- * twin of {@link RlsPolicyIR}. `"custom"` covers any non-string strategy (a
1421
- * `(value, ctx) => …` function) — its logic is an opaque closure, never read by
1422
- * the UI; the preview renders a fixed sentinel for it.
1423
- */
1424
- interface MaskColumnMetadataIR {
1425
- /** Column the mask policy redacts. */
1426
- column: string;
1427
- /** Declared masking strategy: `"redact"`/`"hash"` string literals, else `"custom"` for a `MaskFn`. */
1428
- strategy: "custom" | "hash" | "redact";
1429
- /** Logical table the masked column belongs to. */
1430
- table: string;
1092
+ interface IndexIR {
1093
+ fields: ReadonlyArray<string>;
1094
+ name: string;
1095
+ unique?: boolean;
1431
1096
  }
1432
- /**
1433
- * Schema-wide masking metadata the codegen emits into the generated ShardDO so
1434
- * the studio's data-browser mask toggle can preview what a non-privileged caller
1435
- * would see. Aggregated across every `.use(mask(...))` chain in the project —
1436
- * purely descriptive (table + column + strategy), never the masking closure. The
1437
- * column-level analogue of {@link RlsMetadataIR}.
1438
- */
1439
- interface MaskMetadataIR {
1440
- /** Every statically-discovered masked column, deduped by `(table, column)` (first declaration wins). */
1441
- columns: MaskColumnMetadataIR[];
1097
+ interface SearchIndexIR {
1098
+ /** Primary text-search field; a dot-separated path reads a nested field. */
1099
+ field: string;
1100
+ /** Optional filter fields surfaced alongside the FTS column. */
1101
+ filterFields?: ReadonlyArray<string>;
1102
+ /** Text-analysis profile (accent folding + that language's stopwords). */
1103
+ language?: string;
1104
+ name: string;
1105
+ /** Skip the migration-time backfill of the search companion (large tables index out-of-band). */
1106
+ staged?: boolean;
1107
+ /** `"native"` opts into the engine's own full-text index where it has one (Postgres). */
1108
+ strategy?: string;
1442
1109
  }
1443
- /**
1444
- * One masked column whose `mask(policies)` strategy is a statically-known
1445
- * literal (`"hash"` or `"redact"`) — the `mask_weak_hash_strategy_on_pii` lint
1446
- * input. Unlike {@link MaskColumnMetadataIR} (app-wide, deduped by `(table,
1447
- * column)`, studio-preview evidence), this is per declaration site (file + line
1448
- * + enclosing export), undeduped, so the lint can point at the exact
1449
- * `mask(...)` call that applies a weak strategy. A `MaskFn` (custom, non-literal)
1450
- * strategy carries no lint-relevant signal and is never recorded here.
1451
- * Structurally identical to `AdvisorMaskStrategy`.
1452
- */
1453
- interface MaskStrategyIR {
1454
- /** Masked column name. */
1455
- column: string;
1456
- /** Export binding name of the procedure whose `.use(mask(...))` chain declared this column, or `"<module>"` when declared at file scope. */
1457
- exportName: string;
1458
- /** Source file relative to `<projectRoot>/lunora/`, without extension. */
1459
- file: string;
1460
- /** 1-based line of the masked column's strategy property. */
1461
- line: number;
1462
- /** The statically-known strategy literal: `"hash"` or `"redact"`. */
1463
- strategy: string;
1464
- /** Logical table the masked column belongs to. */
1110
+ /** A `.geoIndex(name, { field, precision? })` declaration — a geohash companion over a `v.geoPoint()` column. */
1111
+ interface GeoIndexIR {
1112
+ /** The `v.geoPoint()` column feeding the geohash. */
1113
+ field: string;
1114
+ name: string;
1115
+ /** Geohash precision (characters) maintained on the companion; omitted ⇒ the runtime default. */
1116
+ precision?: number;
1117
+ }
1118
+ /** A `.ttl(field, { after? })` declaration — declarative table-level auto-expiry. */
1119
+ interface TtlIR {
1120
+ /** Millisecond offset added to `field` to derive the expiry (`field + after`); omitted ⇒ `field` is the absolute expiry. */
1121
+ after?: number;
1122
+ /** The epoch-millisecond expiry column. */
1123
+ field: string;
1124
+ }
1125
+ interface VectorIndexIR {
1126
+ dimensions?: number;
1127
+ /** Shape A: the single source column. Shape B: undefined (derived via a `select` fn). */
1128
+ field?: string;
1129
+ /** Shape A: metadata field names mirrored into Vectorize. */
1130
+ metadata?: ReadonlyArray<string>;
1131
+ metric?: "cosine" | "dot-product" | "euclidean";
1132
+ name: string;
1133
+ /** Owning table the vectors are sourced from. */
1465
1134
  table: string;
1466
1135
  }
1467
1136
  /**
1468
- * One statically-readable policy entry from an `rls([...])` array literal,
1469
- * surfaced to the studio's read-only RLS inspector via the generated
1470
- * `rlsPolicies()` hook. Captures the policy's `table` + `on` operation and the
1471
- * procedure it guards — never the `when` predicate, which is an opaque JS
1472
- * closure (its logic stays in code, not the UI). Produced by
1473
- * `discoverRlsProcedures` alongside the lint IR.
1137
+ * One ordering key on a rank index's `sortBy`: the column and direction.
1138
+ * Mirrors the runtime `RankSortKey` (defaults `direction` to `"asc"`).
1474
1139
  */
1475
- interface RlsPolicyIR {
1476
- /** Source file (relative to `lunora/`, without extension) the policy is declared in. */
1477
- file: string;
1478
- /** Operation the policy gates: `read` covers get/query/findMany, the rest are writes. */
1479
- on: "delete" | "insert" | "read" | "update";
1480
- /** Export name of the procedure whose `.use(rls(...))` chain declared this policy. */
1481
- procedure: string;
1482
- /** Logical table the policy applies to. Empty when not a string literal. */
1483
- table: string;
1140
+ interface RankSortKeyIR {
1141
+ direction: "asc" | "desc";
1142
+ field: string;
1484
1143
  }
1485
1144
  /**
1486
- * One statically-readable role entry from an `rls(policies, { roles: [...] })`
1487
- * call, surfaced to the studio's RLS inspector. Captures the role's `name`,
1488
- * optional `description`, and the names of the permissions it grants (string
1489
- * literals or `definePermission("name")` calls). Produced by
1490
- * `discoverRlsProcedures`.
1145
+ * A `.rankIndex(name, { sortBy, partitionBy?, where? })` declaration. The owning
1146
+ * table is always the table the index is declared on, so — unlike a vector index
1147
+ * — there is no separate `on`/`table` reference to carry: it rides along on its
1148
+ * {@link TableIR}. Only the fields needed for type emission are captured.
1491
1149
  */
1492
- interface RlsRoleIR {
1493
- /** Optional human-readable description from `defineRole(name, { description })`. */
1494
- description?: string;
1495
- /** Role label attached to the request identity (e.g. `"admin"`). */
1150
+ interface RankIndexIR {
1496
1151
  name: string;
1497
- /** Permission names this role grants, deduped and in declared order. */
1498
- permissions: string[];
1152
+ /** Columns scoping each ranking; omitted ⇒ one global rank over the table. */
1153
+ partitionBy?: ReadonlyArray<string>;
1154
+ /** Ordered sort keys driving the rank. */
1155
+ sortBy: ReadonlyArray<RankSortKeyIR>;
1156
+ }
1157
+ interface RelationIR {
1158
+ /** FK column: on this table for `one`, on the target table for `many`. */
1159
+ field: string;
1160
+ kind: "many" | "one";
1161
+ /** Accessor name the relation is loaded under (the `with` key). */
1162
+ name: string;
1163
+ /** FK behaviour applied to holder rows when the referenced parent is deleted. */
1164
+ onDelete?: "cascade" | "restrict" | "set null";
1165
+ /** Referenced column (defaults to `_id`). */
1166
+ references: string;
1167
+ /** Target table name. */
1168
+ table: string;
1499
1169
  }
1500
1170
  /**
1501
- * Schema-wide RLS metadata the codegen emits into the generated ShardDO so the
1502
- * studio's read-only inspector can list, per table, which policies guard it and
1503
- * what roles are defined. Aggregated across every `.use(rls(...))` chain in the
1504
- * project — purely descriptive, never the predicate logic.
1171
+ * Statically-discovered `.source(...)` config (plan 077). Only the bits the
1172
+ * advisor lints + DO wiring need are captured; `map`/`tenantBy` are functions and
1173
+ * cannot be serialized, so their presence is recorded as `hasTenantBy` rather than
1174
+ * the function itself.
1505
1175
  */
1506
- interface RlsMetadataIR {
1507
- /** Every statically-discovered policy `(table, on, procedure)` entry. */
1508
- policies: RlsPolicyIR[];
1509
- /** Every statically-discovered role, deduped by name (first declaration wins). */
1510
- roles: RlsRoleIR[];
1176
+ interface ExternalSourceIR {
1177
+ /** The wrangler Hyperdrive binding name. */
1178
+ binding: string;
1179
+ /** Whether a `columns` projection allow-list was given. */
1180
+ columns?: ReadonlyArray<string>;
1181
+ /** `true` when a `reconcileEveryMs` was given — one of the two incremental delete-visibility paths the `external_source_incremental_no_delete_path` lint checks. */
1182
+ hasReconcile?: boolean;
1183
+ /** `true` when a `softDeleteColumn` was given — the other incremental delete-visibility path. */
1184
+ hasSoftDelete?: boolean;
1185
+ /** `true` when a `tenantBy` mapper was given — the tenant-isolation boundary the `external_source_unscoped` lint checks. */
1186
+ hasTenantBy: boolean;
1187
+ /** The `idColumn` literal, when given (defaults to `"id"` at runtime). */
1188
+ idColumn?: string;
1189
+ /** Delete-detection mode literal, when given (`"full-pull"` today). */
1190
+ mode?: string;
1191
+ /** The membership query literal, when statically knowable. */
1192
+ query?: string;
1193
+ /**
1194
+ * `true` when `.source(...)` was present but its argument was **not** a static
1195
+ * object literal (e.g. `.source(buildConfig())`), so none of the fields above
1196
+ * could be read. The source still exists — this flag lets `hasSourcedTables`
1197
+ * (codegen) and the `external_source_*` lints treat it as a source that can't be
1198
+ * verified, instead of mistaking it for no `.source()` at all.
1199
+ */
1200
+ unanalyzable?: boolean;
1511
1201
  }
1512
- /** One statically-discovered `defineStorageRule({ bucket, on, prefix })` entry from a `.use(storageRules(...))` chain. */
1513
- interface StorageRuleIR {
1514
- bucket: string;
1515
- /** Relative to `lunora/`. */
1516
- file: string;
1517
- on: "delete" | "list" | "read" | "write";
1518
- /** Optional key-prefix scope; absent ⇒ the whole bucket. */
1519
- prefix?: string;
1520
- /** The exported procedure that installed the rule. */
1521
- procedure: string;
1202
+ interface TableIR {
1203
+ /**
1204
+ * `true` when the table chain carried `.commitOrdered()` — every row carries
1205
+ * `_commitSeq`, a per-shard integer allocated once per mutation and strictly
1206
+ * increasing in commit order. Optional: hand-built IR and tables that never
1207
+ * called `.commitOrdered()` default it to `false`.
1208
+ */
1209
+ commitOrdered?: boolean;
1210
+ /**
1211
+ * `true` when that extension was resolved from a package in `node_modules`
1212
+ * rather than from source under `lunora/` (a registry copy-in). Decides
1213
+ * whether the component's module owns a `lunora/<key>/` folder.
1214
+ */
1215
+ extensionFromPackage?: true;
1216
+ /**
1217
+ * The `defineSchemaExtension` key that contributed this table, set when it
1218
+ * arrived through `defineSchema(...).extend(...)`. Absent for a table the app
1219
+ * declared itself.
1220
+ *
1221
+ * Drives the generated `AppTableName` union: an add-on's tables
1222
+ * (`ratelimit_buckets`, …) are real tables and stay in `TableName`, but an app
1223
+ * enumerating "my tables" should not have to know about them.
1224
+ */
1225
+ extensionKey?: string;
1226
+ /**
1227
+ * `true` when the table chain carried `.externallyManaged()` — its rows are
1228
+ * written outside Lunora's discoverable insert path (adapter/migration/
1229
+ * middleware), so advisor insert-path lints skip it. Optional: hand-built
1230
+ * IR and the runtime `fromServerSchema` path default it to `false`.
1231
+ */
1232
+ externallyManaged?: boolean;
1233
+ /**
1234
+ * Set when the chain carried `.source(...)` — the table is materialized from an
1235
+ * external Hyperdrive-backed database by a system poll loop (plan 077). Carries
1236
+ * the statically-knowable bits the advisor lints read; the functions (`map`,
1237
+ * `tenantBy`) are not serialized, only their presence (`hasTenantBy`).
1238
+ */
1239
+ externalSource?: ExternalSourceIR;
1240
+ /** Geospatial indexes declared inline via `.geoIndex(name, …)`. Optional so hand-built IR may omit it (discovery always sets it). */
1241
+ geoIndexes?: ReadonlyArray<GeoIndexIR>;
1242
+ /**
1243
+ * Storage backend for a `.global()` table: `"d1"` (default) or
1244
+ * `"hyperdrive"` (a Postgres/MySQL database via Cloudflare Hyperdrive). Only
1245
+ * meaningful when `shardMode === "global"`; absent for sharded/root tables.
1246
+ *
1247
+ * Read it through `isD1GlobalTable` / `isHyperdriveGlobalTable` rather than
1248
+ * comparing here — discovery normalises the absent case to `"d1"`, and only
1249
+ * those predicates encode which comparison is right for hand-built IR.
1250
+ */
1251
+ globalBackend?: "d1" | "hyperdrive";
1252
+ indexes: ReadonlyArray<IndexIR>;
1253
+ /**
1254
+ * `true` when the table chain carried `.public()` — an explicit opt-OUT of
1255
+ * the schema's `.rls("required")` enforcement for this one table. Optional:
1256
+ * hand-built IR and tables that never called `.public()` default it to
1257
+ * `false`.
1258
+ */
1259
+ isPublic?: boolean;
1260
+ /**
1261
+ * `true` when the table chain carried `.memory()` — its rows are cleared on
1262
+ * every Durable Object cold start and never reach the CDC changelog.
1263
+ * Optional: hand-built IR and tables that never called `.memory()` default it
1264
+ * to `false`.
1265
+ */
1266
+ memory?: boolean;
1267
+ name: string;
1268
+ /** Rank indexes declared inline via `.rankIndex(name, …)`. */
1269
+ rankIndexes: ReadonlyArray<RankIndexIR>;
1270
+ /** Declared relations (via `.relations((r) => …)`), keyed in source order. */
1271
+ relations: ReadonlyArray<RelationIR>;
1272
+ searchIndexes: ReadonlyArray<SearchIndexIR>;
1273
+ shape: Record<string, ValidatorIR>;
1274
+ shardMode: "global" | "root" | {
1275
+ field: string;
1276
+ kind: "shardBy";
1277
+ };
1278
+ /** Set when the chain carried `.softDelete()` — the marker column's name (default `deletedAt`). The column is injected into `shape` so `Doc_*` carries it. */
1279
+ softDelete?: {
1280
+ field: string;
1281
+ };
1282
+ /** Set when the chain carried `.ttl(field, { after? })` — the declarative auto-expiry policy read by the DO alarm sweep. */
1283
+ ttl?: TtlIR;
1284
+ /** Vector indexes declared inline via `.vectorize()` (DSL Shape A). */
1285
+ vectorIndexes: ReadonlyArray<VectorIndexIR>;
1286
+ }
1287
+ /** The Cloudflare DO data-residency jurisdictions the schema may declare. Canonical literal set for the codegen package. */
1288
+ type JurisdictionIR = "eu" | "fedramp" | "us";
1289
+ interface SchemaIR {
1290
+ /**
1291
+ * Cloudflare data-residency jurisdiction declared via
1292
+ * `defineSchema(...).jurisdiction("…")`. Emitted into the generated worker's
1293
+ * `createWorker({ jurisdiction })` (and `ctx.scheduler` / `ctx.containers`).
1294
+ * Absent ⇒ un-pinned.
1295
+ */
1296
+ jurisdiction?: JurisdictionIR;
1297
+ /**
1298
+ * `.jurisdiction("…", { pinAuth: true })` — the acknowledgement that
1299
+ * DO-backed auth, unpinned before, moves into the jurisdiction (and so to a
1300
+ * new, empty object until its rows are copied). Without it codegen refuses a
1301
+ * project that has DO-backed auth, and does not pin it.
1302
+ */
1303
+ jurisdictionPinsAuth?: true;
1304
+ /**
1305
+ * Set when `defineSchema(...).rls("required")` was chained onto the schema —
1306
+ * every table's `ctx.db` write path is denied without an RLS-covering
1307
+ * procedure unless the table itself is `.public()` (see {@link TableIR.isPublic}).
1308
+ * Absent when the schema never called `.rls("required")`.
1309
+ */
1310
+ rlsMode?: "required";
1311
+ tables: ReadonlyArray<TableIR>;
1312
+ /** All vector indexes (inline Shape A hoisted + standalone Shape B), flattened. */
1313
+ vectorIndexes: ReadonlyArray<VectorIndexIR>;
1522
1314
  }
1523
1315
  /**
1524
- * Schema-wide storage-access-rule metadata emitted into the generated ShardDO so
1525
- * the studio's read-only inspector can list, per bucket, which operations are
1526
- * gated and under what key prefix. Aggregated across every
1527
- * `.use(storageRules(...))` chain — descriptive only, never the predicate logic.
1316
+ * Statically-read mirror of `@lunora/server`'s `RestCacheConfig`. Every field is
1317
+ * optional because discovery only records literals it could actually read off the
1318
+ * `.expose({ cache: … })` object.
1528
1319
  */
1529
- interface StorageRulesMetadataIR {
1530
- rules: StorageRuleIR[];
1320
+ interface ExposeCacheIR {
1321
+ maxAge?: number;
1322
+ scope?: "private" | "public";
1323
+ staleWhileRevalidate?: number;
1324
+ tag?: string;
1325
+ vary?: string;
1531
1326
  }
1532
1327
  /**
1533
- * A typed REST route declared with the `httpRoute.<verb>("/path")…` builder in
1534
- * `@lunora/server` and mounted on `httpRouter()`. Captured statically from the
1535
- * builder chain so the OpenAPI emitter can render a real `paths` entry: the verb
1536
- * + path become the operation's method + URL, and the accumulated validator maps
1537
- * become its query parameters, path parameters, and request body.
1328
+ * A `defineMigration({...})` declaration discovered in the user's lunora
1329
+ * sources. The emitted `LUNORA_MIGRATIONS` registry keys on {@link MigrationIR.id}; the
1330
+ * import wiring needs {@link MigrationIR.exportName}/{@link MigrationIR.filePath}. The runtime object
1331
+ * carries the authoritative {@link MigrationIR.table}, but the lifted value is
1332
+ * load-bearing at build time: the schema-drift gate matches it against the
1333
+ * tables with breaking drift, so a migration left at `""` covers nothing.
1538
1334
  */
1539
- interface HttpRouteIR {
1540
- /** `v.*` validators decoding the JSON request body (`.body({...})`), keyed by field. */
1541
- body: Record<string, ValidatorIR>;
1542
- /**
1543
- * Rendered TS type of one SSE chunk — the `R` the `.stream(handler)`
1544
- * handler yields — inferred from the handler via the type checker. Present
1545
- * only when {@link HttpRouteIR.stream} is `true`; `"unknown"` when the
1546
- * checker can't resolve enough context. Feeds the emitted
1547
- * `HttpStreamRef<Chunk, …>` so the chunk type flows to the client.
1548
- * @experimental Part of the HTTP-SSE stream surface (the `httpStreams.*` emission).
1549
- */
1550
- chunkType?: string;
1551
- /** Export binding name of the route handler (used only for diagnostics / dedupe). */
1335
+ interface MigrationIR {
1336
+ /** Export binding name, used to reference the module member in generated imports. */
1552
1337
  exportName: string;
1553
- /** Path relative to `<projectRoot>/lunora/` without extension, e.g. "http". */
1338
+ /** Path relative to `<projectRoot>/lunora/` without extension, e.g. "migrations". */
1554
1339
  filePath: string;
1555
- /** HTTP verb the route binds to (uppercased), e.g. `"GET"`. */
1556
- method: string;
1557
- /** The `.output(validator)` return schema when declared; absent ⇒ TS-inferred / best-effort. */
1558
- output?: ValidatorIR;
1559
- /** `v.*` validators decoding the hono path params (`.params({...})`), keyed by `:name`. */
1560
- params: Record<string, ValidatorIR>;
1561
- /** The route path passed to `httpRoute.<verb>(path)`, e.g. `/api/todos/:id`. */
1562
- path: string;
1563
- /** `v.*` validators decoding the URL query string (`.searchParams({...})`), keyed by name. */
1564
- searchParams: Record<string, ValidatorIR>;
1565
- /** `true` when declared via the terminal `.stream(...)` (Server-Sent Events) rather than `.handler(...)`. */
1566
- stream: boolean;
1340
+ /** Stable migration id — the registry key and per-shard run-state key. */
1341
+ id: string;
1342
+ /** Table the migration iterates; `""` when not a static string literal. */
1343
+ table: string;
1567
1344
  }
1568
1345
  /**
1569
- * Per-procedure protective-middleware snapshot, produced by
1570
- * `discoverProcedureMiddleware` for the security lints
1571
- * (`public_mutation_without_ratelimit`, `user_creating_mutation_without_captcha`).
1572
- * Records which `.use(...)` guards a procedure's builder chain carries plus the
1573
- * behavioural facts that decide whether a guard is *expected* (does it write a
1574
- * user/session table, does it send mail). `protectPublic({ rateLimit, captcha })`
1575
- * is unwrapped: the bundle's object-literal keys set `usesRateLimit`/`usesCaptcha`
1576
- * exactly as the individual `.use(rateLimit(...))` / `.use(verifyTurnstile(...))`
1577
- * steps would. Structurally identical to `AdvisorProcedureProtection` so values
1578
- * pass straight through to the advisor without conversion.
1346
+ * One `ctx.db.<table>.findMany({ includeDeleted })` list read whose
1347
+ * `includeDeleted` is either a hardcoded `true` or derived from the handler's
1348
+ * `args` — the `soft_delete_include_deleted_from_args` lint input. The lint joins
1349
+ * `table` against the schema's soft-delete tables and `visibility` against
1350
+ * `.public()` before flagging. Structurally identical to `AdvisorSoftDeleteRead`
1351
+ * so values pass straight through without conversion.
1579
1352
  */
1580
- interface ProcedureMiddlewareIR {
1581
- /**
1582
- * `true` when the handler body could be read statically — an inline function
1583
- * expression/arrow, or a same-file identifier resolved to one. `false` for a
1584
- * genuinely cross-file handler (an imported function, or an identifier that
1585
- * doesn't resolve in this file), in which case every behavioural fact below
1586
- * is `undefined` rather than a false "not observed" — the feeder never saw
1587
- * the body, so it has nothing to report.
1588
- */
1589
- analyzableBody: boolean;
1590
- /** `true` when the handler (or a helper inside it) references `ctx.mail` / `ctx.email`. `undefined` when `analyzableBody` is `false`. */
1591
- callsMail?: boolean;
1592
- /** `true` when the handler emits a structured observability event (`ctx.log` / `ctx.span` / `ctx.trace`). */
1593
- emitsEvent?: boolean;
1594
- /** `true` when a `// lunora-advisor-exempt` directive sits above the export. */
1595
- exempt: boolean;
1596
- /** The `-- reason` from that directive, or `""`. */
1597
- exemptReason: string;
1598
- /** Export binding name of the procedure (e.g. `signUp`). */
1353
+ interface SoftDeleteReadIR {
1354
+ /** Export binding name of the procedure performing the read. */
1599
1355
  exportName: string;
1600
- /** `true` when the handler fans work out to a privileged, cost-bearing dispatch surface (scheduler `runAfter`/`runAt`, a queue producer send, or a workflow create). Feeds the privileged-fanout lint. `undefined` when `analyzableBody` is `false`. */
1601
- fanOut?: boolean;
1602
1356
  /** Source file relative to `<projectRoot>/lunora/`, without extension. */
1603
1357
  file: string;
1604
- /** `true` when the handler wraps work in `try`/`catch`. */
1605
- handlesErrors?: boolean;
1606
- /**
1607
- * `true` when the procedure declares an email-shaped argument (`email`,
1608
- * `emailAddress`, `userEmail`, …), `false` when it provably declares none,
1609
- * and **absent** when the argument list can't be read statically (a
1610
- * `.input(sharedSchema)`, a spread, or a factory whose `args` comes from a
1611
- * variable). Feeds `signup_mutation_without_disposable_gating`, which can
1612
- * only be actioned when there is an address to gate — so "unreadable" must
1613
- * stay distinguishable from "none", or the lint would clear itself on a
1614
- * registration that may well expose one.
1615
- */
1616
- hasEmailArg?: boolean;
1617
- kind: "action" | "mutation" | "query";
1618
- /** `true` when the handler reaches an outbound surface (`ctx.fetch`, mail, queues, storage, sql, ai, …) that can fail. */
1619
- reachesOutbound?: boolean;
1620
- /** `true` when the handler runs any AI generation, bounded or not. */
1621
- runsAiGeneration?: boolean;
1622
- /** `true` when the handler throws a bare `new Error(...)` rather than a coded `LunoraError`. */
1623
- throwsBareError?: boolean;
1624
- /** `true` when the handler runs an AI generation (`generateText`/`streamText`/`generateObject`/`streamObject`) with no `maxOutputTokens` bound in its config literal. Feeds the `ai_unbounded_generation_public` lint. `undefined` when `analyzableBody` is `false`. */
1625
- unboundedAiGeneration?: boolean;
1626
- /** `true` when the chain carries `.use(verifyTurnstile(...))` or a `protectPublic({ captcha })` bundle. */
1627
- usesCaptcha: boolean;
1628
- /** `true` when the chain carries `.use(emailGateMiddleware(...))` (`@lunora/auth`). Feeds the `signup_mutation_without_disposable_gating` lint. */
1629
- usesEmailGate: boolean;
1630
- /** `true` when the handler calls `ctx.db.insertManyUnsafe(...)`, bypassing validators and triggers. Feeds the `insert_many_unsafe_user_data` lint. `undefined` when `analyzableBody` is `false`. */
1631
- usesInsertManyUnsafe?: boolean;
1632
- /** `true` when the chain carries `.use(mask(...))`. */
1633
- usesMask: boolean;
1634
- /** `true` when the chain carries `.use(rateLimit(...))` or a `protectPublic({ rateLimit })` bundle. */
1635
- usesRateLimit: boolean;
1636
- /** `true` when the chain carries `.use(rls(...))`. */
1637
- usesRls: boolean;
1358
+ /** `true` when `includeDeleted` was derived from the handler's `args` (any caller can flip it). */
1359
+ fromArgs: boolean;
1360
+ /** `true` when `includeDeleted` was a hardcoded `true` literal (always resurfaces soft-deleted rows). */
1361
+ hardcodedTrue: boolean;
1362
+ /** 1-based line of the read call. */
1363
+ line: number;
1364
+ /** Table read, or `""` when the table-arg form's first argument wasn't a string literal. */
1365
+ table: string;
1638
1366
  /** `"internal"` for `internalQuery` / `internalMutation` / `internalAction`. */
1639
1367
  visibility: "internal" | "public";
1640
- /** `true` when the handler inserts into a user/session/account-shaped table. `undefined` when `analyzableBody` is `false`. */
1641
- writesUserTable?: boolean;
1642
1368
  }
1643
1369
  /**
1644
- * Per-procedure argument-validator snapshot, produced by the
1645
- * argument-validator discoverer for the input-hardening lints
1646
- * (`public_arg_uses_any`, `unbounded_string_arg`). Only public procedures are
1647
- * recorded — internal functions take server-trusted input. Structurally identical
1648
- * to `AdvisorArgumentValidator` so it passes straight through to the advisor
1649
- * without conversion.
1370
+ * One `ctx.db.<table>.findMany({ with: { <rel> } })` relation-hydrating list read
1371
+ * — the `masked_relation_leak_via_with` lint input. Column masking does not
1372
+ * descend into `with`-hydrated relations, so a masked table surfaced only through
1373
+ * a `with` on an unprotected parent read is returned in the clear. The lint
1374
+ * resolves each relation accessor to its target table and joins it against the
1375
+ * discovered mask evidence before flagging. Structurally identical to
1376
+ * `AdvisorRelationLoad` so values pass straight through without conversion.
1650
1377
  */
1651
- interface ArgumentValidatorIR {
1652
- /** Arg names declared as `v.any()` (unvalidated, untyped input). */
1653
- anyArgs: string[];
1654
- /** Export binding name of the procedure (e.g. `updateProfile`). */
1378
+ interface RelationLoadIR {
1379
+ /** Export binding name of the procedure performing the read. */
1655
1380
  exportName: string;
1656
1381
  /** Source file relative to `<projectRoot>/lunora/`, without extension. */
1657
1382
  file: string;
1658
- /** 1-based line of the registration call, or `0` when unknown. */
1383
+ /** 1-based line of the read call. */
1659
1384
  line: number;
1660
- /** Arg names declared as `v.string()` with no statically-visible max-length bound. */
1661
- unboundedStringArgs: string[];
1385
+ /** Parent table the read targets, or `""` when the table-arg form's first argument wasn't a string literal. */
1386
+ parentTable: string;
1387
+ /** Relation accessor names named in the read's `with: { … }` map — matched against the parent table's declared relations. */
1388
+ relations: string[];
1389
+ /** `"internal"` for `internalQuery` / `internalMutation` / `internalAction`. */
1390
+ visibility: "internal" | "public";
1662
1391
  }
1663
1392
  /**
1664
- * One factory/constructor call in `lunora/` whose config object literal a
1665
- * security lint inspects for a present-or-absent key — the shared input for the
1666
- * config-call security lints (payment authorize, inbound-mail verify, rate-limit
1667
- * store, browser private-targets). Structurally identical to `AdvisorConfigCall`
1668
- * so it passes straight through to the advisor without conversion.
1393
+ * One import of a migrated-away platform's SDK still present in `lunora/`
1394
+ * source. Structurally identical to the advisor's `AdvisorStaleMigrationImport`
1395
+ * so it passes through the feeder without conversion.
1669
1396
  */
1670
- interface ConfigCallIR {
1671
- /** `true` when the config argument was a static object literal the feeder could read. */
1672
- analyzable: boolean;
1673
- /** The factory function or constructor name at the call site, e.g. `createPayment` / `RateLimiter`. */
1674
- callee: string;
1675
- /** Source file relative to `<projectRoot>/lunora/`, without extension. */
1397
+ interface StaleMigrationImportIR {
1398
+ /** Source file relative to the lunora dir, no extension. */
1676
1399
  file: string;
1677
- /** 1-based line of the call site, or `0` when unknown. */
1400
+ /** 1-based line of the import, or `0` when unknown. */
1678
1401
  line: number;
1679
- /** Keys present in the config object literal (empty when not `analyzable`). */
1680
- presentKeys: string[];
1681
- /** Keys in the config object literal explicitly assigned the literal `true`. */
1682
- trueKeys: string[];
1402
+ /** The imported module, e.g. `@supabase/supabase-js`. */
1403
+ moduleSpecifier: string;
1404
+ /** Which migration guide covers this platform. */
1405
+ platform: "convex" | "firebase" | "supabase";
1406
+ }
1407
+ interface FunctionIR {
1408
+ /**
1409
+ * Set when the chain carries `.use(platformAdmin(...))`: only a platform admin
1410
+ * can reach the procedure, so its writes are trusted to name any account.
1411
+ */
1412
+ adminOnly?: true;
1413
+ args: Record<string, ValidatorIR>;
1414
+ exportName: string;
1415
+ /**
1416
+ * Set by the `.expose({ rest: true })` builder modifier (plan 167). When
1417
+ * `rest` is `true` the function is published on the public REST surface, so the
1418
+ * OpenAPI emitter describes it as a real `/_lunora/rest/<namespace>/<fn>` path
1419
+ * (the single source of truth the runtime router also derives from). Absent →
1420
+ * RPC-only (the default; not on the REST surface).
1421
+ *
1422
+ * `cache` mirrors the `RestCacheConfig` the runtime turns into response
1423
+ * headers, so the emitted spec can document the `Cache-Control` a caller will
1424
+ * actually observe. Only statically-readable literal fields are carried; a
1425
+ * computed value is simply absent (the spec under-documents rather than lies).
1426
+ */
1427
+ expose?: {
1428
+ cache?: ExposeCacheIR;
1429
+ rest?: boolean;
1430
+ };
1431
+ /** Path relative to `<projectRoot>/lunora/` without extension, e.g. "messages". */
1432
+ filePath: string;
1433
+ kind: "action" | "mutation" | "query" | "stream";
1434
+ /**
1435
+ * Set on lifecycle hooks (`onConnect`/`onDisconnect`/`onShardInit`/
1436
+ * `onQueryChange`/`onWhisper`): the moment the hook fires on. Such a function
1437
+ * is also an internal registration (so it lands in `LUNORA_FUNCTIONS` for path
1438
+ * dispatch) — a mutation for every moment except `whisper`, which is a query;
1439
+ * emit additionally collects it into the `LUNORA_LIFECYCLE_HOOKS` manifest
1440
+ * keyed by this moment. Absent on ordinary functions.
1441
+ */
1442
+ lifecycle?: "connect" | "disconnect" | "init" | "reactor" | "whisper";
1443
+ /**
1444
+ * The `.output(validator)` declaration, when the chain has one.
1445
+ *
1446
+ * Takes precedence over {@link FunctionIR.returnType} (the handler's
1447
+ * inferred type) for the emitted `FunctionReference`. `.output()` is what
1448
+ * validates at runtime and what a reader takes as the contract, so the two
1449
+ * must agree — see the emit-side note for what went wrong when they did not.
1450
+ */
1451
+ output?: ValidatorIR;
1452
+ /**
1453
+ * Serialized TS source for the handler's return type, with `Promise<T>`
1454
+ * unwrapped so callers see `T` directly. Defaults to `"unknown"` when
1455
+ * ts-morph cannot resolve the type (typically because the consuming
1456
+ * project lacks a tsconfig that can reach `@lunora/server`).
1457
+ */
1458
+ returnType: string;
1459
+ /**
1460
+ * Call surface the function is exposed on. Absent (or `"public"`) means it
1461
+ * lands in the generated `api`; `"internal"` routes it to the separate
1462
+ * `internal` object and is rejected by the DO's external RPC path.
1463
+ */
1464
+ visibility?: "internal" | "public";
1683
1465
  }
1684
1466
  /**
1685
- * One secret-shaped string literal discovered in `lunora/` source — the
1686
- * `hardcoded_secret` lint input. Complements the pre-commit `vis secrets` scan by
1687
- * surfacing the same class of finding in-IDE via the studio Advisors table.
1688
- * Structurally identical to `AdvisorSecretLiteral`.
1467
+ * A `defineShape({...})` declaration discovered in `lunora/shapes.ts`
1468
+ * (local-first sync engine, Phase 7). The emitted `LUNORA_SHAPES` registry keys
1469
+ * on {@link ShapeIR.exportName}; the generated DO's `resolveShape` override
1470
+ * dispatches a `shape_subscribe` to the matching registered shape. Discovery is
1471
+ * marker-driven (the `__lunoraShape` brand) — no field metadata is lifted here
1472
+ * because the runtime object (`columns`/`compileWhere`) carries the authority.
1689
1473
  */
1690
- interface SecretLiteralIR {
1691
- /** Source file relative to `<projectRoot>/lunora/`, without extension. */
1692
- file: string;
1693
- /** Heuristic that matched, e.g. `stripe_live_key` / `aws_access_key` / `pem_private_key` / `high_entropy`. */
1694
- kind: string;
1695
- /** 1-based line of the literal, or `0` when unknown. */
1696
- line: number;
1697
- /** Redacted preview of the literal (first few chars + length) for the finding detail — never the full secret. */
1698
- preview: string;
1474
+ interface ShapeIR {
1475
+ /**
1476
+ * The shape's `args` validator map — its partition selector. Lifted so
1477
+ * `_generated/collections.ts` can type the selector a caller passes instead of
1478
+ * widening it to `Record<string, unknown>`. `{}` for a parameterless shape.
1479
+ */
1480
+ args: Record<string, ValidatorIR>;
1481
+ /** Export binding name — the shape's registry key and import member. */
1482
+ exportName: string;
1483
+ /** Path relative to `<projectRoot>/lunora/` without extension — always `"shapes"`. */
1484
+ filePath: string;
1485
+ /**
1486
+ * The `table` string literal from the `defineShape({ table })` call, lifted
1487
+ * only for static advisor lints (the runtime object stays authoritative).
1488
+ * `undefined` when `table` is not a plain string literal — lints skip those.
1489
+ */
1490
+ table?: string;
1699
1491
  }
1700
1492
  /**
1701
- * One `ctx.sql.query(text, …)` / `ctx.sql.unsafe(text, …)` call whose `text`
1702
- * argument is built in place rather than passed as a fixed statement — the
1703
- * `sql_injection_risk` lint input. The Hyperdrive driver binds ONLY the `params`
1704
- * array; the `text` string is spliced verbatim into the SQL, so a `text` assembled
1705
- * from a string concatenation or a substitution template literal is an injection
1706
- * vector. A fixed string literal / no-substitution template is safe. Structurally
1707
- * identical to `AdvisorSqlInterpolation`.
1493
+ * The single `defineIdentity({...})` claim contract discovered in
1494
+ * `lunora/identity.ts`. Discovery is **marker-driven** (the `__lunoraIdentity`
1495
+ * brand, exactly like {@link ShapeIR}) — no claim metadata is lifted here
1496
+ * because the emitted `_generated/server.ts` recovers the claim *type* from the
1497
+ * declaration itself (`InferIdentity` over the contract's `typeof`), and the
1498
+ * runtime object (`validate`/`onInvalid`) carries the authority at the boundary.
1499
+ * Exactly one per app; absent ⇒ generated output is byte-identical to today.
1708
1500
  */
1709
- interface SqlInterpolationIR {
1710
- /** Source file relative to `<projectRoot>/lunora/`, without extension. */
1711
- file: string;
1712
- /** 1-based line of the interpolation, or `0` when unknown. */
1713
- line: number;
1714
- /** Who the site runs on behalf of — see {@link CallSiteScope}. */
1715
- scope: CallSiteScope;
1501
+ interface IdentityIR {
1502
+ /** Export binding name — the namespace member `_generated/server.ts` reads via `typeof`. */
1503
+ exportName: string;
1716
1504
  }
1717
1505
  /**
1718
- * One `ctx.fetch(url, …)` call inside an action whose URL argument is derived
1719
- * from the handler's `args` — the `action_fetch_ssrf` lint input. `ctx.fetch` is
1720
- * the action-only outbound-request escape hatch with no host allowlist, so a URL
1721
- * assembled from request input is a server-side request forgery vector (cloud
1722
- * metadata endpoints, internal services). Only arg-derived URLs reach here; a
1723
- * fixed literal or a URL built from config/`ctx.*` is not recorded. Structurally
1724
- * identical to `AdvisorArgumentDerivedFetch`.
1506
+ * The single `defineEnv({...})` contract discovered in `lunora/env.ts`. Like
1507
+ * {@link IdentityIR}, only the export binding is lifted — the emitted
1508
+ * `_generated/server.ts` recovers the validated shape from the declaration
1509
+ * itself (`ReturnType` over the accessor's `typeof`), and the generated ShardDO
1510
+ * applies the same accessor to the worker `env` at ctx-build time to populate
1511
+ * `ctx.env`. Exactly one per app; absent ⇒ generated output is byte-identical.
1725
1512
  */
1726
- interface ArgumentDerivedFetchIR {
1727
- /** Source file relative to `<projectRoot>/lunora/`, without extension. */
1728
- file: string;
1729
- /** 1-based line of the `ctx.fetch` call, or `0` when unknown. */
1730
- line: number;
1731
- /** Who the site runs on behalf of — see {@link CallSiteScope}. */
1732
- scope: CallSiteScope;
1513
+ interface EnvIR {
1514
+ /** Export binding name — the namespace member `_generated/server.ts` reads via `typeof`. */
1515
+ exportName: string;
1733
1516
  }
1734
1517
  /**
1735
- * One `ctx.kv.<method>(key, …)` call whose namespace key is derived from the
1736
- * handler's `args` with no server-side scoping — the `kv_unscoped_user_key_idor`
1737
- * lint input. Workers KV is a single flat namespace, so a key taken straight from
1738
- * request input lets any caller read, overwrite, or delete another user's entry
1739
- * (IDOR). Only arg-derived, unscoped keys reach here; a fixed literal, or a key
1740
- * prefixed with a server-trusted identity (`${ctx.auth.userId}:…` — references
1741
- * `ctx`, so treated as scoped), is not recorded. `list` is excluded (it takes a
1742
- * prefix, not a per-entry key). Structurally identical to `AdvisorKvKeyAccess`.
1518
+ * A `defineMutator({...})` declaration discovered in `lunora/mutators.ts`
1519
+ * (local-first sync engine, Phase 7). The emitted registry registers the
1520
+ * authoritative `server` impl into the DO's `LUNORA_FUNCTIONS` table (so
1521
+ * `handleRpc` transaction-wraps it) and records its path in
1522
+ * `LUNORA_MUTATOR_PATHS` so the DO's `isCustomMutator` override routes the
1523
+ * client-watermark push protocol. The client `client` impl is split into the
1524
+ * browser bundle separately — only the path crosses to the server side.
1743
1525
  */
1744
- interface KvKeyAccessIR {
1745
- /** Source file relative to `<projectRoot>/lunora/`, without extension. */
1746
- file: string;
1747
- /** 1-based line of the `ctx.kv` call, or `0` when unknown. */
1748
- line: number;
1749
- /** The `ctx.kv` method invoked: `get` / `getRaw` / `getWithMetadata` / `put` / `delete`. */
1750
- method: string;
1751
- /** Who the site runs on behalf of — see {@link CallSiteScope}. */
1752
- scope: CallSiteScope;
1526
+ interface MutatorIR {
1753
1527
  /**
1754
- * Visibility of the enclosing procedure. `internal` procedures have no
1755
- * untrusted caller by construction — see `owner_field_from_args_not_auth`'s
1756
- * identical split — so `kv_unscoped_user_key_idor` drops the finding to
1757
- * INFO rather than ERROR there. `undefined` when the access sits outside any
1758
- * registered procedure the feeder could attribute it to.
1528
+ * The mutator's `args` validator map, parsed exactly as a procedure's is, so
1529
+ * the emitted `api.mutators.<name>` reference carries the arg type a client
1530
+ * `defineMutator` infers instead of restating. `{}` for a parameterless
1531
+ * mutator (or one whose `args` isn't an inline object literal).
1759
1532
  */
1760
- visibility?: "internal" | "public";
1761
- }
1762
- /**
1763
- * One `ctx.db` write (`insert` / `replace` / `patch` / `insertManyUnsafe`) that sets
1764
- * an ownership / identity column — `userId`, `ownerId`, `tenantId`, and the like —
1765
- * from the handler's `args` instead of the server-trusted identity. The
1766
- * `owner_field_from_args_not_auth` lint input: the ownership column decides who a
1767
- * row belongs to, so a value taken from request input lets any caller write rows
1768
- * owned by another user or tenant (the act-as-any-user / cross-tenant IDOR vector).
1769
- * A column stamped from `ctx.*`, or set to a fixed literal, is not recorded; only an
1770
- * arg-derived identity write reaches here. Structurally identical to
1771
- * `AdvisorOwnerFieldWrite`.
1772
- */
1773
- /**
1774
- * One branching `defineShape({ where })` / `definePolicy({ when })` predicate arm
1775
- * that returns an unrestricted predicate — the `unrestricted_where_branch` lint
1776
- * input. A denial arm must match NO rows (`deny()` / `{ OR: [] }`); `{}` matches
1777
- * every row, so the near-miss silently replicates the whole table.
1778
- */
1779
- interface UnrestrictedWhereBranchIR {
1780
- /** Export binding name of the shape / policy the predicate belongs to. */
1533
+ args: Record<string, ValidatorIR>;
1534
+ /** Export binding name — the mutator's registry key and import member. */
1781
1535
  exportName: string;
1782
- /** Source file relative to `<projectRoot>/lunora/`, without extension. */
1783
- file: string;
1784
- /** Which unrestricted form was returned. */
1785
- form: "empty-object" | "undefined";
1786
- /** The config key carrying the predicate (`where` for a shape, `when` for a policy). */
1787
- key: string;
1788
- /** 1-based line of the offending returned expression. */
1789
- line: number;
1790
- /** The declaring call (`defineShape` / `definePolicy`). */
1791
- owner: string;
1792
- }
1793
- interface OwnerFieldWriteIR {
1794
- /**
1795
- * Every procedure that reaches the write carries `.use(platformAdmin(...))`
1796
- * (see {@link FunctionIR.adminOnly}): the caller is a platform admin by design.
1797
- */
1798
- adminOnly?: true;
1799
- /** The identity column being written from `args` (e.g. `userId`). */
1800
- field: string;
1801
- /** Source file relative to `<projectRoot>/lunora/`, without extension. */
1802
- file: string;
1803
- /**
1804
- * The handler proves the value it writes equals the server identity before the
1805
- * write: an `if (<args.field> !== <ctx identity>) throw` (also inside an `&&` or
1806
- * `||` condition), or an `assert*(...)` call given both the argument and the
1807
- * identity. The write is then validated, so `owner_field_from_args_not_auth`
1808
- * declines to report it. Only facts that hold before the write count.
1809
- */
1810
- guarded?: true;
1811
- /** 1-based line of the `ctx.db` write call, or `0` when unknown. */
1536
+ /** Path relative to `<projectRoot>/lunora/` without extension — always `"mutators"`. */
1537
+ filePath: string;
1538
+ /** 1-based line of the `defineMutator(...)` call, for diagnostics and advisor findings. */
1812
1539
  line: number;
1813
- /** The `ctx.db` write method (`insert` / `replace` / `patch` / `insertManyUnsafe`). */
1814
- method: string;
1815
1540
  /**
1816
- * The enclosing `defineMutator` declared this very column as its `owner`, AND
1817
- * the value written resolves, by symbol, to that same `args[owner]` of the
1818
- * `server` impl's own 2nd parameter. A nested closure's or a helper's own
1819
- * `args` (or any binding shadowing it) never qualifies.
1820
- *
1821
- * `applyOwnerScope` requires a verified identity, rejects a client-supplied
1822
- * value that disagrees with it, and overwrites the column with the verified
1823
- * one before `server` runs — so on this exact shape `args[owner]` IS the
1824
- * server identity, and it is what the docs prescribe. Recorded rather than
1825
- * dropped at discovery: the write did happen, and the feeder is otherwise the
1826
- * only place that knows. `owner_field_from_args_not_auth` is what declines to
1827
- * report it.
1828
- *
1829
- * Deliberately NOT set when only the column NAME matches: `owner: "userId"`
1830
- * launders `args.userId` and nothing else, so `{ userId: args.targetUserId }`
1831
- * is a real IDOR and stays reportable.
1541
+ * The ownership column the declaration scopes the write to
1542
+ * (`defineMutator({ owner: "userId" })`), or `undefined` when it declares
1543
+ * none — which is also what the `mutator_without_owner_scope` lint reports,
1544
+ * since an unowned mutator is a public write endpoint. Only a string-literal
1545
+ * `owner` is lifted; a computed one reads as `undefined` rather than guessed.
1832
1546
  */
1833
- ownerScoped?: true;
1834
- /** Who the site runs on behalf of — see {@link CallSiteScope}. */
1835
- scope: CallSiteScope;
1547
+ owner?: string;
1836
1548
  /**
1837
- * Visibility of the enclosing procedure. `internal` procedures are not
1838
- * reachable by a caller, so the lint's premise ("any caller can write rows
1839
- * owned by another user") does not hold there — see
1840
- * `owner_field_from_args_not_auth`. `undefined` when the write sits outside
1841
- * any recognised procedure (a bare helper).
1549
+ * Serialized TS source for the authoritative `server` impl's return type,
1550
+ * `Promise<T>` unwrapped. `"unknown"` when ts-morph can't resolve it — same
1551
+ * contract as {@link FunctionIR.returnType}.
1842
1552
  */
1843
- visibility?: "internal" | "public";
1553
+ returnType: string;
1844
1554
  }
1845
1555
  /**
1846
- * One `ctx.storage.<bucket>.<method>(key, …)` call whose R2 object key is derived
1847
- * from the handler's `args` with no server-side scoping — the
1848
- * `storage_key_from_user_args` lint input. The bucket read/write/URL/delete methods
1849
- * key by their first argument, so an object key taken straight from request input is
1850
- * object-level IDOR (read/overwrite/delete anyone's object). A key referencing a
1851
- * server-trusted `ctx.*` value (e.g. `${ctx.auth.userId}/…`) is treated as scoped
1852
- * and is not recorded; only an arg-derived, `ctx`-free key reaches here.
1853
- * Structurally identical to `AdvisorStorageKeyAccess`.
1556
+ * A whole-row `ctx.db.replace(id, document)` write discovered inside a custom
1557
+ * mutator's inline `server` impl (`lunora/mutators.ts`) — the input the
1558
+ * `mutator_full_row_replace` advisor lint consumes. A `replace` overwrites the
1559
+ * entire row, so a concurrent edit to a different column on a synced table is
1560
+ * clobbered; `ctx.db.patch(id, { field })` merges at the column level instead.
1561
+ * Structurally identical to `AdvisorMutatorWrite` so it passes straight through
1562
+ * to the advisor without conversion, exactly as `InsertWriteIR` does for
1563
+ * `AdvisorInsertWrite`.
1854
1564
  */
1855
- interface StorageKeyAccessIR {
1856
- /** Source file relative to `<projectRoot>/lunora/`, without extension. */
1565
+ interface MutatorWriteIR {
1566
+ /** The mutator export whose `server` impl performs the replace, e.g. `renameChannel`. */
1567
+ exportName: string;
1568
+ /** Openable source path the replace appears in — always `lunora/mutators.ts`. */
1857
1569
  file: string;
1858
- /** 1-based line of the storage call, or `0` when unknown. */
1570
+ /** 1-based line of the `replace(...)` call. */
1859
1571
  line: number;
1860
- /** The bucket method invoked with the arg-derived key, e.g. `get` / `put` / `delete` / `download`. */
1861
- method: string;
1862
- /** Who the site runs on behalf of — see {@link CallSiteScope}. */
1863
- scope: CallSiteScope;
1864
- /**
1865
- * Visibility of the enclosing procedure. `internal` procedures have no
1866
- * untrusted caller by construction — see `owner_field_from_args_not_auth`'s
1867
- * identical split — so `storage_key_from_user_args` drops the finding to
1868
- * INFO rather than ERROR there. `undefined` when the access sits outside any
1869
- * registered procedure the feeder could attribute it to.
1870
- */
1871
- visibility?: "internal" | "public";
1872
1572
  }
1873
1573
  /**
1874
- * One `ctx.containers.<exportName>.get(name, …)` call whose instance key is derived
1875
- * from the handler's `args` with no server-side scoping — the
1876
- * `container_instance_key_from_user_input` lint input. Each container definition's
1877
- * `.get(name)` accessor routes to one instance per `name`, so a key taken straight from
1878
- * request input lets any caller reach another tenant's container (a cross-tenant IDOR). A
1879
- * fixed literal key, or one derived from a server-trusted identity (`${ctx.auth.userId}` —
1880
- * references `ctx`, so treated as scoped), is not recorded; only an arg-derived, unscoped
1881
- * key reaches here. `.any()`/`.pool()` take no key and are not sinks. Structurally
1882
- * identical to `AdvisorContainerKeyAccess`.
1574
+ * Where a queue or workflow `handler:` passed by reference is declared, when it is
1575
+ * exported from a lunora source file — `defineWorkflow({ handler: onboard })` with
1576
+ * `onboard` exported from `lunora/onboarding/flow.ts` gives
1577
+ * `{ exportName: "onboard", file: "onboarding/flow" }`. The architecture graph
1578
+ * attributes that export's own call sites to the queue or workflow node, so its
1579
+ * calls are not reported as "not a registered function". `exportName` is the
1580
+ * exported name (`export { run as start }` → `start`).
1883
1581
  */
1884
- interface ContainerKeyAccessIR {
1582
+ interface HandlerSiteIR {
1583
+ exportName: string;
1885
1584
  /** Source file relative to `<projectRoot>/lunora/`, without extension. */
1886
1585
  file: string;
1887
- /** 1-based line of the `ctx.containers.*.get` call, or `0` when unknown. */
1888
- line: number;
1889
- /** The container accessor method invoked — always `get`. */
1890
- method: string;
1891
- /** Who the site runs on behalf of — see {@link CallSiteScope}. */
1892
- scope: CallSiteScope;
1893
1586
  }
1894
1587
  /**
1895
- * One `ctx.ai.run(model, …)` call whose model-id argument is derived from the handler's
1896
- * `args` with no server-side scoping — the `ai_raw_run_escape_hatch` lint input.
1897
- * `ctx.ai.run` is the raw Workers AI binding passthrough, bypassing the typed
1898
- * `ctx.ai.model(...)` + AI-SDK layer (`generateText`/`streamText`/…) that caps output and
1899
- * enforces a schema, so an arg-derived model id lets any caller select an arbitrary model.
1900
- * A fixed literal model, or one scoped by a server-trusted `ctx.*` value, is not recorded;
1901
- * only an arg-derived, unscoped model id reaches here (an arg-derived `inputs` argument is
1902
- * normal usage and is never inspected). Structurally identical to `AdvisorAiRawRun`.
1588
+ * A typed REST route declared with the `httpRoute.<verb>("/path")…` builder in
1589
+ * `@lunora/server` and mounted on `httpRouter()`. Captured statically from the
1590
+ * builder chain so the OpenAPI emitter can render a real `paths` entry: the verb
1591
+ * + path become the operation's method + URL, and the accumulated validator maps
1592
+ * become its query parameters, path parameters, and request body.
1903
1593
  */
1904
- interface AiRawRunIR {
1905
- /** Source file relative to `<projectRoot>/lunora/`, without extension. */
1906
- file: string;
1907
- /** 1-based line of the `ctx.ai.run` call, or `0` when unknown. */
1908
- line: number;
1909
- /** Who the site runs on behalf of — see {@link CallSiteScope}. */
1910
- scope: CallSiteScope;
1594
+ interface HttpRouteIR {
1595
+ /** `v.*` validators decoding the JSON request body (`.body({...})`), keyed by field. */
1596
+ body: Record<string, ValidatorIR>;
1597
+ /**
1598
+ * Rendered TS type of one SSE chunk — the `R` the `.stream(handler)`
1599
+ * handler yields — inferred from the handler via the type checker. Present
1600
+ * only when {@link HttpRouteIR.stream} is `true`; `"unknown"` when the
1601
+ * checker can't resolve enough context. Feeds the emitted
1602
+ * `HttpStreamRef<Chunk, …>` so the chunk type flows to the client.
1603
+ * @experimental Part of the HTTP-SSE stream surface (the `httpStreams.*` emission).
1604
+ */
1605
+ chunkType?: string;
1606
+ /** Export binding name of the route handler (used only for diagnostics / dedupe). */
1607
+ exportName: string;
1608
+ /** Path relative to `<projectRoot>/lunora/` without extension, e.g. "http". */
1609
+ filePath: string;
1610
+ /** HTTP verb the route binds to (uppercased), e.g. `"GET"`. */
1611
+ method: string;
1612
+ /** The `.output(validator)` return schema when declared; absent ⇒ TS-inferred / best-effort. */
1613
+ output?: ValidatorIR;
1614
+ /** `v.*` validators decoding the hono path params (`.params({...})`), keyed by `:name`. */
1615
+ params: Record<string, ValidatorIR>;
1616
+ /** The route path passed to `httpRoute.<verb>(path)`, e.g. `/api/todos/:id`. */
1617
+ path: string;
1618
+ /** `v.*` validators decoding the URL query string (`.searchParams({...})`), keyed by name. */
1619
+ searchParams: Record<string, ValidatorIR>;
1620
+ /** `true` when declared via the terminal `.stream(...)` (Server-Sent Events) rather than `.handler(...)`. */
1621
+ stream: boolean;
1911
1622
  }
1912
1623
  /**
1913
- * One `ctx.vectors.<method>(indexName, input)` call whose `input.namespace` is derived
1914
- * from the handler's `args` with no server-side scoping — the
1915
- * `vectors_namespace_from_user_input` lint input. A Vectorize namespace partitions one
1916
- * index into isolated sub-collections, so a namespace taken straight from request input
1917
- * lets any caller read or poison another tenant's vectors. A fixed literal namespace, or
1918
- * one prefixed with a server-trusted identity (`${ctx.auth.orgId}` — references `ctx`, so
1919
- * treated as scoped), is not recorded; only an arg-derived, unscoped namespace reaches
1920
- * here. Structurally identical to `AdvisorVectorNamespaceAccess`.
1624
+ * Per-procedure protective-middleware snapshot, produced by
1625
+ * `discoverProcedureMiddleware` for the security lints
1626
+ * (`public_mutation_without_ratelimit`, `user_creating_mutation_without_captcha`).
1627
+ * Records which `.use(...)` guards a procedure's builder chain carries plus the
1628
+ * behavioural facts that decide whether a guard is *expected* (does it write a
1629
+ * user/session table, does it send mail). `protectPublic({ rateLimit, captcha })`
1630
+ * is unwrapped: the bundle's object-literal keys set `usesRateLimit`/`usesCaptcha`
1631
+ * exactly as the individual `.use(rateLimit(...))` / `.use(verifyTurnstile(...))`
1632
+ * steps would. Structurally identical to `AdvisorProcedureProtection` so values
1633
+ * pass straight through to the advisor without conversion.
1921
1634
  */
1922
- interface VectorNamespaceAccessIR {
1635
+ interface ProcedureMiddlewareIR {
1636
+ /**
1637
+ * `true` when the handler body could be read statically — an inline function
1638
+ * expression/arrow, or a same-file identifier resolved to one. `false` for a
1639
+ * genuinely cross-file handler (an imported function, or an identifier that
1640
+ * doesn't resolve in this file), in which case every behavioural fact below
1641
+ * is `undefined` rather than a false "not observed" — the feeder never saw
1642
+ * the body, so it has nothing to report.
1643
+ */
1644
+ analyzableBody: boolean;
1645
+ /** `true` when the handler (or a helper inside it) references `ctx.mail` / `ctx.email`. `undefined` when `analyzableBody` is `false`. */
1646
+ callsMail?: boolean;
1647
+ /** `true` when the handler emits a structured observability event (`ctx.log` / `ctx.span` / `ctx.trace`). */
1648
+ emitsEvent?: boolean;
1649
+ /** `true` when a `// lunora-advisor-exempt` directive sits above the export. */
1650
+ exempt: boolean;
1651
+ /** The `-- reason` from that directive, or `""`. */
1652
+ exemptReason: string;
1653
+ /** Export binding name of the procedure (e.g. `signUp`). */
1654
+ exportName: string;
1655
+ /** `true` when the handler fans work out to a privileged, cost-bearing dispatch surface (scheduler `runAfter`/`runAt`, a queue producer send, or a workflow create). Feeds the privileged-fanout lint. `undefined` when `analyzableBody` is `false`. */
1656
+ fanOut?: boolean;
1923
1657
  /** Source file relative to `<projectRoot>/lunora/`, without extension. */
1924
1658
  file: string;
1925
- /** 1-based line of the `ctx.vectors` call, or `0` when unknown. */
1926
- line: number;
1927
- /** The `ctx.vectors` method invoked: `query` / `upsert` / `upsertNow`. */
1928
- method: string;
1929
- /** Who the site runs on behalf of — see {@link CallSiteScope}. */
1930
- scope: CallSiteScope;
1931
- }
1932
- /**
1933
- * One `ctx.mail`/`ctx.email` `send`/`queue` call whose recipient field (`to`/`cc`/`bcc`)
1934
- * is derived from the handler's `args` with no server-side scoping — the
1935
- * `mail_recipient_from_request_input` lint input. A recipient taken straight from request
1936
- * input turns the deployment into an open relay / spam amplifier (any caller can direct
1937
- * mail to an arbitrary address). A fixed literal recipient, or one scoped by a
1938
- * server-trusted `ctx.*` value (e.g. `ctx.auth.user.email`), is not recorded; only an
1939
- * arg-derived, unscoped recipient reaches here. Structurally identical to
1940
- * `AdvisorMailRecipientAccess`.
1941
- */
1942
- interface MailRecipientAccessIR {
1943
- /** Source file relative to `<projectRoot>/lunora/`, without extension. */
1944
- file: string;
1945
- /** 1-based line of the `ctx.mail`/`ctx.email` call, or `0` when unknown. */
1946
- line: number;
1947
- /** The mailer method invoked: `send` / `queue`. */
1948
- method: string;
1949
- /** Who the site runs on behalf of — see {@link CallSiteScope}. */
1950
- scope: CallSiteScope;
1659
+ /** `true` when the handler wraps work in `try`/`catch`. */
1660
+ handlesErrors?: boolean;
1661
+ /**
1662
+ * `true` when the procedure declares an email-shaped argument (`email`,
1663
+ * `emailAddress`, `userEmail`, …), `false` when it provably declares none,
1664
+ * and **absent** when the argument list can't be read statically (a
1665
+ * `.input(sharedSchema)`, a spread, or a factory whose `args` comes from a
1666
+ * variable). Feeds `signup_mutation_without_disposable_gating`, which can
1667
+ * only be actioned when there is an address to gate — so "unreadable" must
1668
+ * stay distinguishable from "none", or the lint would clear itself on a
1669
+ * registration that may well expose one.
1670
+ */
1671
+ hasEmailArg?: boolean;
1672
+ kind: "action" | "mutation" | "query";
1673
+ /** `true` when the handler reaches an outbound surface (`ctx.fetch`, mail, queues, storage, sql, ai, …) that can fail. */
1674
+ reachesOutbound?: boolean;
1675
+ /** `true` when the handler runs any AI generation, bounded or not. */
1676
+ runsAiGeneration?: boolean;
1677
+ /** `true` when the handler throws a bare `new Error(...)` rather than a coded `LunoraError`. */
1678
+ throwsBareError?: boolean;
1679
+ /** `true` when the handler runs an AI generation (`generateText`/`streamText`/`generateObject`/`streamObject`) with no `maxOutputTokens` bound in its config literal. Feeds the `ai_unbounded_generation_public` lint. `undefined` when `analyzableBody` is `false`. */
1680
+ unboundedAiGeneration?: boolean;
1681
+ /** `true` when the chain carries `.use(verifyTurnstile(...))` or a `protectPublic({ captcha })` bundle. */
1682
+ usesCaptcha: boolean;
1683
+ /** `true` when the chain carries `.use(emailGateMiddleware(...))` (`@lunora/auth`). Feeds the `signup_mutation_without_disposable_gating` lint. */
1684
+ usesEmailGate: boolean;
1685
+ /** `true` when the handler calls `ctx.db.insertManyUnsafe(...)`, bypassing validators and triggers. Feeds the `insert_many_unsafe_user_data` lint. `undefined` when `analyzableBody` is `false`. */
1686
+ usesInsertManyUnsafe?: boolean;
1687
+ /** `true` when the chain carries `.use(mask(...))`. */
1688
+ usesMask: boolean;
1689
+ /** `true` when the chain carries `.use(rateLimit(...))` or a `protectPublic({ rateLimit })` bundle. */
1690
+ usesRateLimit: boolean;
1691
+ /** `true` when the chain carries `.use(rls(...))`. */
1692
+ usesRls: boolean;
1693
+ /** `"internal"` for `internalQuery` / `internalMutation` / `internalAction`. */
1694
+ visibility: "internal" | "public";
1695
+ /** `true` when the handler inserts into a user/session/account-shaped table. `undefined` when `analyzableBody` is `false`. */
1696
+ writesUserTable?: boolean;
1951
1697
  }
1952
1698
  /**
1953
- * One `ctx.browser.<method>(url, …)` call whose navigation URL (`arguments[0]`)
1954
- * is derived from the handler's `args` with no server-side scoping — the
1955
- * `browser_user_url_without_allowlist` lint input. The lint additionally
1956
- * cross-references `createBrowser` config-call evidence to suppress findings
1957
- * when the browser is hardened with an `allowedHosts` allowlist or
1958
- * `resolveDns`. Structurally identical to `AdvisorBrowserUrlAccess`.
1699
+ * Per-procedure argument-validator snapshot, produced by the
1700
+ * argument-validator discoverer for the input-hardening lints
1701
+ * (`public_arg_uses_any`, `unbounded_string_arg`). Only public procedures are
1702
+ * recorded — internal functions take server-trusted input. Structurally identical
1703
+ * to `AdvisorArgumentValidator` so it passes straight through to the advisor
1704
+ * without conversion.
1959
1705
  */
1960
- interface BrowserUrlAccessIR {
1706
+ interface ArgumentValidatorIR {
1707
+ /** Arg names declared as `v.any()` (unvalidated, untyped input). */
1708
+ anyArgs: string[];
1709
+ /** Export binding name of the procedure (e.g. `updateProfile`). */
1710
+ exportName: string;
1961
1711
  /** Source file relative to `<projectRoot>/lunora/`, without extension. */
1962
1712
  file: string;
1963
- /** 1-based line of the `ctx.browser` call, or `0` when unknown. */
1713
+ /** 1-based line of the registration call, or `0` when unknown. */
1964
1714
  line: number;
1965
- /** The browser method invoked: `content` / `pdf` / `scrape` / `screenshot`. */
1966
- method: string;
1967
- /** Who the site runs on behalf of — see {@link CallSiteScope}. */
1968
- scope: CallSiteScope;
1715
+ /** Arg names declared as `v.string()` with no statically-visible max-length bound. */
1716
+ unboundedStringArgs: string[];
1969
1717
  }
1970
1718
  /**
1971
- * One runtime container-override call: a `<handle>.start({ enableInternet: true, … })`
1972
- * launch override, or a `<handle>.egress.<method>(...)` runtime firewall mutation
1973
- * (`allow` / `deny` / `setAllowed`) — the `container_start_enable_internet_override`
1974
- * and `container_runtime_egress_relaxation` lint input. Both shapes re-open network
1975
- * access the static `defineContainer` declaration (and its `container_public_internet`
1976
- * lint) assumes is locked down. Matched structurally by call shape, independent of the
1977
- * receiver's resolved type. Structurally identical to `AdvisorContainerOverride`.
1719
+ * One factory/constructor call in `lunora/` whose config object literal a
1720
+ * security lint inspects for a present-or-absent key — the shared input for the
1721
+ * config-call security lints (payment authorize, inbound-mail verify, rate-limit
1722
+ * store, browser private-targets). Structurally identical to `AdvisorConfigCall`
1723
+ * so it passes straight through to the advisor without conversion.
1978
1724
  */
1979
- interface ContainerOverrideIR {
1980
- /** e.g. the egress method name, or `"enableInternet: true"`. */
1981
- detail: string;
1725
+ interface ConfigCallIR {
1726
+ /** `true` when the config argument was a static object literal the feeder could read. */
1727
+ analyzable: boolean;
1728
+ /** The factory function or constructor name at the call site, e.g. `createPayment` / `RateLimiter`. */
1729
+ callee: string;
1982
1730
  /** Source file relative to `<projectRoot>/lunora/`, without extension. */
1983
1731
  file: string;
1984
- /** Which override shape matched. */
1985
- kind: "egress_relaxation" | "enable_internet";
1986
- /** 1-based line of the call, or `0` when unknown. */
1732
+ /** 1-based line of the call site, or `0` when unknown. */
1987
1733
  line: number;
1988
- /** Who the site runs on behalf of — see {@link CallSiteScope}. */
1989
- scope: CallSiteScope;
1734
+ /** Keys present in the config object literal (empty when not `analyzable`). */
1735
+ presentKeys: string[];
1736
+ /** Keys in the config object literal explicitly assigned the literal `true`. */
1737
+ trueKeys: string[];
1990
1738
  }
1991
1739
  /**
1992
- * One `buildImageDeliveryUrl({ key, … })` call (`@lunora/bindings/images`) whose
1993
- * `key` — the CDN transform's source image, an absolute URL or an
1994
- * origin-relative key — is derived from the handler's `args` with no
1995
- * server-side scoping — the `images_url_source_from_user_input` lint input.
1996
- * `ctx.images.transform`/`info` take image *bytes*, never a URL, so they are not
1997
- * sinks; only the `key` of `buildImageDeliveryUrl` accepts a URL-or-key source
1998
- * and is inspected. An arg-derived `key` lets any caller point the CDN's
1999
- * `/cdn-cgi/image/` transform at an attacker-chosen origin (SSRF / open proxy)
2000
- * or at an arbitrary key under the account's own store. A fixed literal, or a
2001
- * key scoped by a server-trusted `ctx.*` value, is not recorded. Structurally
2002
- * identical to `AdvisorImageDeliveryUrlAccess`.
1740
+ * One secret-shaped string literal discovered in `lunora/` source — the
1741
+ * `hardcoded_secret` lint input. Complements the pre-commit `vis secrets` scan by
1742
+ * surfacing the same class of finding in-IDE via the studio Advisors table.
1743
+ * Structurally identical to `AdvisorSecretLiteral`.
2003
1744
  */
2004
- interface ImageDeliveryUrlAccessIR {
1745
+ interface SecretLiteralIR {
2005
1746
  /** Source file relative to `<projectRoot>/lunora/`, without extension. */
2006
1747
  file: string;
2007
- /** 1-based line of the `buildImageDeliveryUrl` call, or `0` when unknown. */
1748
+ /** Heuristic that matched, e.g. `stripe_live_key` / `aws_access_key` / `pem_private_key` / `high_entropy`. */
1749
+ kind: string;
1750
+ /** 1-based line of the literal, or `0` when unknown. */
2008
1751
  line: number;
2009
- /** Who the site runs on behalf of — see {@link CallSiteScope}. */
2010
- scope: CallSiteScope;
1752
+ /** Redacted preview of the literal (first few chars + length) for the finding detail — never the full secret. */
1753
+ preview: string;
2011
1754
  }
2012
1755
  /**
2013
- * One `createAuth({...})` call's configuration snapshot — the shared input for
2014
- * the five `auth_*` security lints (trusted-origins wildcard, CSRF check
2015
- * disabled, secure cookies disabled, email verification disabled, session
2016
- * freshAge zero). Matched by callee NAME (an `import`-agnostic, fail-closed
2017
- * convention the other feeders share), so a re-export or alias still resolves.
2018
- * When the config argument isn't a statically-analyzable object literal (a
2019
- * top-level spread, or not an object literal at all), `analyzable` is `false`
2020
- * and every boolean fact defaults to its SAFE (not-flagged) value — an opaque
2021
- * config can't be relied on either way. Structurally identical to
2022
- * `AdvisorAuthConfig`.
1756
+ * One `ctx.sql.query(text, …)` / `ctx.sql.unsafe(text, …)` call whose `text`
1757
+ * argument is built in place rather than passed as a fixed statement — the
1758
+ * `sql_injection_risk` lint input. The Hyperdrive driver binds ONLY the `params`
1759
+ * array; the `text` string is spliced verbatim into the SQL, so a `text` assembled
1760
+ * from a string concatenation or a substitution template literal is an injection
1761
+ * vector. A fixed string literal / no-substitution template is safe. Structurally
1762
+ * identical to `AdvisorSqlInterpolation`.
2023
1763
  */
2024
- interface AuthConfigIR {
2025
- /** `true` when the call's config argument was a static object literal the feeder could read. */
2026
- analyzable: boolean;
2027
- /** `advanced.disableCSRFCheck === true`. */
2028
- disableCsrfCheck: boolean;
2029
- /** `emailAndPassword.enabled === true`. */
2030
- emailPasswordEnabled: boolean;
1764
+ interface SqlInterpolationIR {
2031
1765
  /** Source file relative to `<projectRoot>/lunora/`, without extension. */
2032
1766
  file: string;
2033
- /** 1-based line of the `createAuth(...)` call, or `0` when unknown. */
1767
+ /** 1-based line of the interpolation, or `0` when unknown. */
2034
1768
  line: number;
2035
- /** `emailAndPassword.requireEmailVerification === true` present. */
2036
- requireEmailVerification: boolean;
2037
- /** `trustedOrigins` array literal contains a `"*"` element. */
2038
- /** `plugins` includes `scim(...)` while `database` is a non-transactional Lunora adapter — a combination that throws at runtime. */
2039
- scimOnNonTransactionalAdapter: boolean;
2040
1769
  /** Who the site runs on behalf of — see {@link CallSiteScope}. */
2041
1770
  scope: CallSiteScope;
2042
- /** `advanced.useSecureCookies === false`. */
2043
- secureCookiesDisabled: boolean;
2044
- /** `session.freshAge === 0` (explicit literal). */
2045
- sessionFreshAgeZero: boolean;
2046
- trustedOriginsWildcard: boolean;
2047
1771
  }
2048
1772
  /**
2049
- * One `rateLimit`/`dbRateLimit` middleware call (`@lunora/ratelimit`) whose
2050
- * `key` selector — the per-caller rate-limit sub-key, `(ctx) => string |
2051
- * undefined` — is derived from the handler's `args` with no server-side
2052
- * scoping (no reference to the trusted `ctx` binding anywhere in the selector)
2053
- * — the `ratelimit_key_spoofable_or_global` lint input. A key an attacker
2054
- * controls lets them rotate it per request and bypass the limit entirely,
2055
- * defeating its purpose. A selector scoped by `ctx` (e.g. `ctx.auth.userId`,
2056
- * `ctx.ip`), or one with no `args` reference at all (a fixed/global bucket —
2057
- * the "no key" case this lint deliberately does not flag, to keep it low-FP),
2058
- * is not recorded. Structurally identical to `AdvisorRatelimitKeySelector`.
1773
+ * One `ctx.fetch(url, …)` call inside an action whose URL argument is derived
1774
+ * from the handler's `args` — the `action_fetch_ssrf` lint input. `ctx.fetch` is
1775
+ * the action-only outbound-request escape hatch with no host allowlist, so a URL
1776
+ * assembled from request input is a server-side request forgery vector (cloud
1777
+ * metadata endpoints, internal services). Only arg-derived URLs reach here; a
1778
+ * fixed literal or a URL built from config/`ctx.*` is not recorded. Structurally
1779
+ * identical to `AdvisorArgumentDerivedFetch`.
2059
1780
  */
2060
- interface RatelimitKeySelectorIR {
2061
- /** The `rateLimit`/`dbRateLimit` callee invoked. */
2062
- callee: string;
1781
+ interface ArgumentDerivedFetchIR {
2063
1782
  /** Source file relative to `<projectRoot>/lunora/`, without extension. */
2064
1783
  file: string;
2065
- /** The rate limit's `name` argument (the second positional argument), or `""` when not a string literal. */
2066
- limitName: string;
2067
- /** 1-based line of the `rateLimit`/`dbRateLimit` call, or `0` when unknown. */
1784
+ /** 1-based line of the `ctx.fetch` call, or `0` when unknown. */
2068
1785
  line: number;
2069
1786
  /** Who the site runs on behalf of — see {@link CallSiteScope}. */
2070
1787
  scope: CallSiteScope;
2071
1788
  }
2072
1789
  /**
2073
- * One payload-derived privileged dispatch — a `ctx.run`/`context.run` back into a
2074
- * Lunora function from inside a `defineQueue` push handler or a `defineWorkflow`
2075
- * handler, whose args reference the handler's untrusted payload (`context.params`
2076
- * for a workflow, a `for (… of batch.messages)` body for a queue) — the
2077
- * `privileged_dispatch_unvalidated_payload` lint input. Both handler kinds run
2078
- * under the **system identity** (RLS disabled), so forwarding attacker-influenced
2079
- * payload into the dispatch bypasses the target's row policy. The resolved
2080
- * `targetFile`/`targetExport` let the lint join RLS-procedure evidence and fire
2081
- * only for RLS-gated targets. Structurally identical to `AdvisorPrivilegedDispatch`.
1790
+ * One `query` handler whose `return` hands back the raw rows of a table — the
1791
+ * result of a `ctx.db.<table>.findMany()` / `.findFirst()` / `.get()` read, or a
1792
+ * `ctx.db.query("<table>")…collect()` fluent chain — returned directly (or through
1793
+ * one local `const` hop) with no hand-built projection. The
1794
+ * output_projection_missing_on_public_read lint keeps only `visibility ===
1795
+ * "public"` rows with no `.output(...)` / `.use(mask(...))` on the chain, then
1796
+ * joins `table` against the schema and flags one whose columns are PII-named.
1797
+ * Structurally identical to `AdvisorRawRowReturn` so values pass straight through
1798
+ * without conversion.
2082
1799
  */
2083
- interface PrivilegedDispatchIR {
2084
- /** `"queue"` for a `defineQueue` handler, `"workflow"` for a `defineWorkflow` handler. */
2085
- dispatchKind: "queue" | "workflow";
1800
+ interface RawRowReturnIR {
1801
+ /** Export binding name of the query returning the raw rows. */
1802
+ exportName: string;
2086
1803
  /** Source file relative to `<projectRoot>/lunora/`, without extension. */
2087
1804
  file: string;
2088
- /** 1-based line of the dispatch call, or `0` when unknown. */
1805
+ /** 1-based line of the `return` (or concise-body) expression. */
2089
1806
  line: number;
2090
- /** Who the site runs on behalf of — see {@link CallSiteScope}. */
2091
- scope: CallSiteScope;
2092
- /** Export name of the dispatched target (`send` in `api.messages.send`). */
2093
- targetExport: string;
2094
- /** File path of the dispatched target relative to `lunora/` (`messages` in `api.messages.send`). */
2095
- targetFile: string;
1807
+ /** Table whose raw rows are returned, or `""` when the read's table wasn't a string literal. */
1808
+ table: string;
1809
+ /** `true` when the procedure's builder chain carries a `.use(mask(...))` step. */
1810
+ usesMask: boolean;
1811
+ /** `true` when the procedure's builder chain carries an `.output(...)` return-shape projection. */
1812
+ usesOutput: boolean;
1813
+ /** `"internal"` for `internalQuery`; `"public"` for `query`. */
1814
+ visibility: "internal" | "public";
2096
1815
  }
2097
1816
  /**
2098
- * One discovered `httpRoute.<verb>("/admin/…")` route on an admin/privileged-looking
2099
- * path, with whether its builder chain references an auth/admin guard — the
2100
- * `admin_route_without_guard` lint input. Structurally identical to
2101
- * `AdvisorAdminRoute`.
1817
+ * A single cron job lifted from a `cronJobs()` builder in `lunora/crons.ts`.
1818
+ * Mirrors `@lunora/scheduler`'s `CronJob`: {@link CronJobIR.cron} is the compiled
1819
+ * standard cron expression, {@link CronJobIR.functionPath} is the target
1820
+ * `__lunoraRef` (`namespace:fn`), and {@link CronJobIR.args} is the static
1821
+ * argument object passed at registration.
2102
1822
  */
2103
- interface AdminRouteIR {
2104
- /** Export binding name of the route handler. */
2105
- exportName: string;
2106
- /** Source file relative to `<projectRoot>/lunora/`, without extension. */
2107
- file: string;
2108
- /** HTTP verb the route binds to (uppercased), e.g. `"POST"`. */
1823
+ interface CronJobIR {
1824
+ /** Static args object (source-text JSON), defaults to `{}`. */
1825
+ args: Record<string, unknown>;
1826
+ /** Compiled standard cron expression, e.g. `"0 9 * * *"`. */
1827
+ cron: string;
1828
+ /** Target function ref `namespace:fn`. Present for a function target; absent when {@link CronJobIR.workflow} is set. */
1829
+ functionPath?: string;
1830
+ /** Unique, human-readable job name. */
1831
+ name: string;
1832
+ /**
1833
+ * Set when the job targets a durable workflow (a `lunora/workflows.ts`
1834
+ * export) instead of a function: the workflow's class name (its
1835
+ * `ctx.exports` key) plus its export name. On each fire the worker starts a new workflow
1836
+ * INSTANCE (the {@link CronJobIR.args} become its `params`) rather than
1837
+ * dispatching a one-shot function.
1838
+ */
1839
+ workflow?: {
1840
+ className: string;
1841
+ exportName: string;
1842
+ };
1843
+ }
1844
+ /** A normalized container image source, as written into `wrangler.jsonc`. */
1845
+ type ContainerImageIR = {
1846
+ buildContext: string;
1847
+ dockerfilePath: string;
1848
+ kind: "dockerfile";
1849
+ } | {
1850
+ buildDir: string;
1851
+ kind: "build";
1852
+ } | {
1853
+ kind: "registry";
1854
+ reference: string;
1855
+ };
1856
+ /** A `durable_object` container's named image — a Dockerfile or a registry digest, never a Railpack build. */
1857
+ type ContainerNamedImageIR = Exclude<ContainerImageIR, {
1858
+ kind: "build";
1859
+ }>;
1860
+ /** The fields every container carries, whichever scheduling policy it uses. */
1861
+ interface ContainerIRBase {
1862
+ /** Durable Object binding name, e.g. `CONTAINER_TRANSCODER`. */
1863
+ bindingName: string;
1864
+ /** Static Dockerfile build args (wrangler `image_vars` / named-image `build_vars`), when declared as literals. */
1865
+ buildArgs?: Record<string, string>;
1866
+ /** Generated DO class name, e.g. `TranscoderContainer`. */
1867
+ className: string;
1868
+ /**
1869
+ * Whether the container may open outbound internet connections, when the
1870
+ * value was a static literal. `undefined` means the field was omitted (the
1871
+ * platform default is `true`) or wasn't a literal. Lifted for the advisor.
1872
+ */
1873
+ enableInternet?: boolean;
1874
+ /** The `lunora/containers.ts` export name, e.g. `transcoder`. */
1875
+ exportName: string;
1876
+ /** Static `instanceType`, when declared (the default size, under `durable_object`). */
1877
+ instanceType?: string | {
1878
+ diskMb?: number;
1879
+ memoryMib?: number;
1880
+ vcpu?: number;
1881
+ };
1882
+ /** Static wrangler `containers[].name` override, when declared. */
1883
+ name?: string;
1884
+ /**
1885
+ * `true` when the definition opts in to the Sandbox SDK helpers
1886
+ * (`defineContainer({ sandbox: true })`). Must be a literal: it decides
1887
+ * whether the generated `containers.ts` exports the sandbox gateways, and
1888
+ * it raises the `containerSandboxTools` platform signal.
1889
+ */
1890
+ sandbox?: boolean;
1891
+ /**
1892
+ * The static `sleepAfter` value, when it was a literal. `undefined` means
1893
+ * omitted (platform default `"10m"`) or non-literal. Lifted for the advisor.
1894
+ */
1895
+ sleepAfter?: number | string;
1896
+ }
1897
+ /** A container under the default scheduling policy: one application image, a cap, and a rollout. */
1898
+ interface DefaultScheduledContainerIR extends ContainerIRBase {
1899
+ /**
1900
+ * Normalized image source: a local Dockerfile (`dockerfile`), a pre-built
1901
+ * registry reference (`registry`), or a Railpack source directory (`build`)
1902
+ * that the deploy step builds and pushes before wrangler runs.
1903
+ */
1904
+ image: ContainerImageIR;
1905
+ /** Static `maxInstances`, when declared. */
1906
+ maxInstances?: number;
1907
+ /** Static rolling-deploy tuning, when declared as literals. */
1908
+ rollout?: {
1909
+ gracePeriodSeconds?: number;
1910
+ stepPercentage?: number;
1911
+ };
1912
+ /** Absent under the default policy — the union's discriminant. */
1913
+ schedulingPolicy?: never;
1914
+ }
1915
+ /**
1916
+ * A container under the `durable_object` scheduling policy (wrangler
1917
+ * `scheduling_policy`): each instance picks its image and size per start. Also
1918
+ * the `containerRuntimeScheduling` platform signal.
1919
+ */
1920
+ interface DurableObjectScheduledContainerIR extends ContainerIRBase {
1921
+ /** The named images an instance can start from (wrangler `containers[].images`). */
1922
+ images?: Record<string, ContainerNamedImageIR>;
1923
+ schedulingPolicy: "durable_object";
1924
+ }
1925
+ /**
1926
+ * A container lifted from a `defineContainer()` export in
1927
+ * `lunora/containers.ts`, discriminated on its scheduling policy. Carries
1928
+ * everything the emitters and the config layer need to wire wrangler
1929
+ * (`containers[]` + the Durable Object binding + migration class) and the
1930
+ * generated `_generated/containers.ts` DO class. Names are derived via
1931
+ * `@lunora/container`'s shared helpers so codegen and the config layer can
1932
+ * never disagree.
1933
+ */
1934
+ type ContainerIR = DefaultScheduledContainerIR | DurableObjectScheduledContainerIR;
1935
+ /**
1936
+ * A workflow lifted from a `defineWorkflow()` export in `lunora/workflows.ts`.
1937
+ * Carries what the emitters and the config layer need to wire the wrangler
1938
+ * `workflows[]` entry and the generated `_generated/workflows.ts`
1939
+ * `WorkflowEntrypoint` class. Unlike containers, workflows are NOT Durable
1940
+ * Objects — wrangler gets only a `workflows[]` entry, never a `durable_objects`
1941
+ * binding or a migration class. Names are derived via `@lunora/workflow`'s
1942
+ * shared helpers so codegen and the config layer can never disagree.
1943
+ */
1944
+ interface WorkflowIR {
1945
+ /** Generated `WorkflowEntrypoint` class name, e.g. `OrderPipelineWorkflow` — also its `exports` key and `ctx.exports` property. */
1946
+ className: string;
1947
+ /** Static `defaultRetention` literal → `exports.<Class>.default_retention`. */
1948
+ defaultRetention?: {
1949
+ errorRetention?: string;
1950
+ successRetention?: string;
1951
+ };
1952
+ /** The `lunora/workflows.ts` export name, e.g. `orderPipeline`. */
1953
+ exportName: string;
1954
+ /**
1955
+ * The file declaring the `handler:` when it is a function passed by reference
1956
+ * and declared in another file — the file {@link WorkflowIR.steps}' lines are
1957
+ * in. Lunora-relative inside `lunora/`, project-relative outside it.
1958
+ */
1959
+ handlerFile?: string;
1960
+ /** See {@link HandlerSiteIR}. */
1961
+ handlerSite?: HandlerSiteIR;
1962
+ /** Static `limits` literal → `exports.<Class>.limits`. */
1963
+ limits?: {
1964
+ steps?: number;
1965
+ };
1966
+ /**
1967
+ * The stable wrangler `exports.<Class>.name`. Defaults to the kebab-cased export
1968
+ * name (`orderPipeline` → `order-pipeline`); a static `name:` literal in the
1969
+ * definition overrides it.
1970
+ */
1971
+ name: string;
1972
+ /**
1973
+ * Static `schedules` cron literals → `exports.<Class>.schedules`. Each one
1974
+ * starts an instance on Cloudflare; gated by the `workflowSchedules`
1975
+ * capability, since a host that ignores them would never run the workflow.
1976
+ */
1977
+ schedules?: ReadonlyArray<string>;
1978
+ /**
1979
+ * Durable step labels lifted from the handler body — the first string-literal
1980
+ * argument of every `ctx.step.do` / `.sleep` / `.sleepUntil` / `.waitForEvent`
1981
+ * call. Feeds the duplicate-step-name lint, which flags a name used twice
1982
+ * (Cloudflare memoizes by name, so the second call silently returns the
1983
+ * first's cached result). Calls with a non-literal name are omitted (not
1984
+ * statically comparable). A handler passed by reference is followed to its
1985
+ * declaration; the lines are then in {@link WorkflowIR.handlerFile}.
1986
+ */
1987
+ steps: ReadonlyArray<WorkflowStepIR>;
1988
+ }
1989
+ /**
1990
+ * An agent lifted from a `defineAgent()` export in `lunora/agents.ts`. A
1991
+ * `defineAgent` compiles its durable tool-loop onto a Cloudflare Workflow, so —
1992
+ * like {@link WorkflowIR} — an agent is NOT a Durable Object: wrangler gets only
1993
+ * a `workflows[]` entry, never a `durable_objects` binding or a migration class.
1994
+ * Carries what the emitters and the config layer need to wire the generated
1995
+ * agent `WorkflowEntrypoint` class (e.g. `SupportAgentWorkflow`), the typed
1996
+ * per-agent `ctx.agents` producer, and the reconciled wrangler `workflows[]`
1997
+ * entry. Names are derived via `@lunora/agent`'s shared helpers so codegen and
1998
+ * the config layer can never disagree.
1999
+ */
2000
+ interface AgentIR {
2001
+ /** Generated `WorkflowEntrypoint` class name, e.g. `SupportAgentWorkflow` — also its `exports` key and `ctx.exports` property. */
2002
+ className: string;
2003
+ /** The `lunora/agents.ts` export name, e.g. `support`. */
2004
+ exportName: string;
2005
+ /**
2006
+ * The stable wrangler `exports.<Class>.name`. Defaults to the kebab-cased export
2007
+ * name (`support` → `agent-support`); a static `name:` literal in the
2008
+ * definition overrides it.
2009
+ */
2010
+ name: string;
2011
+ /**
2012
+ * Whether the definition declares an `onEmail` mapper on
2013
+ * `defineAgent({ onEmail: … })`. When `true` the emitter wires this agent
2014
+ * onto the worker's top-level `email()` handler (via `@lunora/agent/inbound`)
2015
+ * so inbound mail starts a durable run. Detected by AST PRESENCE — the
2016
+ * closure is never evaluated — and written to IR only when present, so
2017
+ * email-free agents (and agent-free projects) stay byte-identical.
2018
+ */
2019
+ onEmail?: boolean;
2020
+ /**
2021
+ * Whether the definition opted into public run-starts via
2022
+ * `defineAgent({ publicRun: true })` — emitted into the `ctx.agents` wiring
2023
+ * spec so the public `agents:agentRun` mutation can gate on it fail-closed.
2024
+ * Absent (falsy) means server-side starts only; the field is written to IR
2025
+ * only when the literal is `true`, so agent-free and non-opted-in output is
2026
+ * byte-identical.
2027
+ */
2028
+ publicRun?: boolean;
2029
+ /**
2030
+ * Whether the definition opted into a real-time voice session via a `voice`
2031
+ * block on `defineAgent({ voice: … })`. Unlike the durable loop (a Workflow),
2032
+ * the voice path IS a Durable Object — so when this is `true` the emitter
2033
+ * generates the `voiceClassName` `VoiceSessionDO` subclass and the
2034
+ * `api.agents.{name}Voice` client reference, and the config layer reconciles
2035
+ * a `durable_objects` binding (`voiceBindingName`) + `new_sqlite_classes`
2036
+ * migration. Written to IR only when the literal is present, so voice-free
2037
+ * agents (and agent-free projects) stay byte-identical.
2038
+ */
2039
+ voice?: boolean;
2040
+ /** The voice DO's Cloudflare `DurableObjectNamespace` binding name, e.g. `VOICE_SUPPORT`. Present only when `voice`. */
2041
+ voiceBindingName?: string;
2042
+ /** Generated `VoiceSessionDO` subclass name, e.g. `SupportVoiceDO`. Present only when `voice`. */
2043
+ voiceClassName?: string;
2044
+ }
2045
+ /** One durable step call lifted from a workflow handler body (the use side of {@link WorkflowIR.steps}). */
2046
+ interface WorkflowStepIR {
2047
+ /** 1-based line of the durable step call. */
2048
+ line: number;
2049
+ /** The native step method invoked: `do` / `sleep` / `sleepUntil` / `waitForEvent`. */
2109
2050
  method: string;
2110
- /** The route path, e.g. `/admin/users`. */
2111
- path: string;
2112
- /** `true` when the handler body references an auth/session/admin guard (`ctx.auth`, `getSession`, `requireAdmin`, …). */
2113
- usesGuard: boolean;
2051
+ /** The step's static label (the first string-literal argument). */
2052
+ name: string;
2053
+ }
2054
+ /**
2055
+ * A queue lifted from a `defineQueue()` export in `lunora/queues.ts`. Carries
2056
+ * what the emitters and the config layer need to wire the typed `ctx.queues`
2057
+ * producer, the generated worker `queue()` dispatch, and the wrangler
2058
+ * `queues.producers[]` / `queues.consumers[]` entries. Like workflows, a queue
2059
+ * is NOT a Durable Object — wrangler gets only `queues.*` entries. Names are
2060
+ * derived via `@lunora/queue`'s shared helpers so codegen and the config layer
2061
+ * can never disagree.
2062
+ */
2063
+ interface QueueIR {
2064
+ /** The Cloudflare `Queue` producer binding name, e.g. `QUEUE_EMAIL`. */
2065
+ bindingName: string;
2066
+ /** The `lunora/queues.ts` export name, e.g. `emailQueue`. */
2067
+ exportName: string;
2068
+ /** The declaring file, lunora-relative without `.ts`: `queues`, or `billing/queues` for a module's own. */
2069
+ filePath: string;
2070
+ /** See {@link HandlerSiteIR}. */
2071
+ handlerSite?: HandlerSiteIR;
2072
+ /** How the queue is consumed: `"push"` (a worker `queue()` handler) or `"pull"` (external HTTP). */
2073
+ mode: "pull" | "push";
2074
+ /**
2075
+ * The stable wrangler queue name (`queues.producers[].queue`). Defaults to
2076
+ * the kebab-cased export name (`emailQueue` → `email-queue`); a static
2077
+ * `name:` literal in the definition overrides it.
2078
+ */
2079
+ name: string;
2080
+ /**
2081
+ * Set for a `defineSubscription(topic, …)` export: the `defineTopic` export it
2082
+ * consumes. A subscription is an ordinary push queue everywhere except
2083
+ * `ctx.*` — it is published to through `ctx.topics.<topic>`, never `ctx.queues`.
2084
+ */
2085
+ topic?: string;
2086
+ /** Push-consumer batch/retry tuning, mirrored onto the wrangler `queues.consumers[]` entry. */
2087
+ tuning: {
2088
+ deadLetterQueue?: string;
2089
+ maxBatchSize?: number;
2090
+ maxBatchTimeout?: number;
2091
+ maxConcurrency?: number;
2092
+ maxRetries?: number;
2093
+ retryDelay?: number;
2094
+ };
2114
2095
  }
2115
2096
  /**
2116
- * One tracked `ctx.storage.<bucket>.<method>(...)` upload/signing call — the
2117
- * shared input for the storage config-hygiene security lints
2118
- * (`storage_upload_without_content_type_allowlist`, `storage_upload_without_max_size`,
2119
- * `storage_generate_upload_url_no_content_type_pin`, `storage_presigned_url_for_private_content`).
2120
- * `upload`/`store` carry the `UploadOptions` guards (`allowedContentTypes` /
2121
- * `maxSize`); `generateUploadUrl` carries the signed-PUT `contentType` pin;
2122
- * `getPresignedUrl`/`getSignedUrl` carry a statically-known `expiresInSeconds`
2123
- * literal. `presentKeys` is empty (and `expiresInSeconds` unset) when the
2124
- * options argument was absent, a non-literal, or a spread — see `analyzable`.
2125
- * Structurally identical to `AdvisorStorageUpload`.
2097
+ * A `defineTopic()` export in `lunora/queues.ts`. A topic deploys nothing of its
2098
+ * own: each subscription is a {@link QueueIR} whose `topic` names this export, and
2099
+ * that is the only record of the link (see {@link subscriptionsOf}).
2126
2100
  */
2127
- interface StorageUploadIR {
2128
- /** `true` when the call's options-object argument (or its deliberate absence) was statically resolvable. */
2129
- analyzable: boolean;
2130
- /** Numeric literal value of an `expiresInSeconds` option, when statically known (`getSignedUrl` / `getPresignedUrl` only). */
2131
- expiresInSeconds?: number;
2132
- /** Source file relative to `<projectRoot>/lunora/`, without extension. */
2133
- file: string;
2134
- /** 1-based line of the call, or `0` when unknown. */
2135
- line: number;
2136
- /** The `ctx.storage` method invoked. */
2137
- method: "generateUploadUrl" | "getPresignedUrl" | "getSignedUrl" | "store" | "upload";
2138
- /** Options-object keys present at the call site (empty when not `analyzable`, or when no options argument was passed). */
2139
- presentKeys: string[];
2140
- /** Who the site runs on behalf of — see {@link CallSiteScope}. */
2141
- scope: CallSiteScope;
2101
+ interface TopicIR {
2102
+ /** The `lunora/queues.ts` export name, e.g. `signups`. */
2103
+ exportName: string;
2104
+ /** The declaring file, as {@link QueueIR.filePath}. */
2105
+ filePath: string;
2142
2106
  }
2143
2107
  /**
2144
- * One discovered `httpAction`/`httpRoute` handler in `lunora/` that performs a
2145
- * side effect (`ctx.runMutation` / `ctx.runAction` / a `ctx.db.{insert,patch,
2146
- * replace,delete,insertManyUnsafe}` write) from the HTTP edge, with whether it
2147
- * reads `ctx.auth` — the `http_action_missing_auth_guard` lint input. A handler
2148
- * that mutates state or dispatches an action without ever consulting the request
2149
- * identity is an unauthenticated write bypassing identity/RLS. Only handlers with
2150
- * a statically-resolvable inline body and `ctx` binding are recorded (fail-safe
2151
- * under-report); read-only handlers are never recorded. Structurally identical to
2152
- * `AdvisorHttpActionGuard`.
2108
+ * A module: a folder under `lunora/` whose `module.ts` default-exports
2109
+ * `defineModule(...)`. Metadata for the Studio catalog, the architecture
2110
+ * manifest and the `cross_module_table_write` lint — it changes no `api.*` path.
2153
2111
  */
2154
- interface HttpActionGuardIR {
2155
- /** Source file relative to `<projectRoot>/lunora/`, without extension. */
2156
- file: string;
2157
- /** Which HTTP surface the handler is: a raw `httpAction` or a typed `httpRoute` route. */
2158
- kind: "httpAction" | "httpRoute";
2159
- /** 1-based line of the handler call, or `0` when unknown. */
2160
- line: number;
2161
- /** For an `httpRoute`, the uppercased verb (`"POST"`); absent for a raw `httpAction`. */
2162
- method?: string;
2163
- /** `true` when the handler reads `ctx.auth` (a direct member access or a `const { auth } = ctx` destructure). */
2164
- readsAuth: boolean;
2165
- /** Who the site runs on behalf of — see {@link CallSiteScope}. */
2166
- scope: CallSiteScope;
2167
- /** The first side effect found, as a stable label: `runMutation`, `runAction`, or `db.<method>`. */
2168
- sideEffect: string;
2112
+ interface ModuleIR {
2113
+ /** One-line description from `defineModule({ description })`. */
2114
+ description?: string;
2115
+ /**
2116
+ * `true` for an installed component (a `defineSchemaExtension` key merged with
2117
+ * `.extend(...)`) treated as a module: it owns its prefixed tables and the
2118
+ * `lunora/<key>/` folder its copy-in code lives in. Absent for a declared module.
2119
+ */
2120
+ installed?: true;
2121
+ /** The folder path relative to `lunora/`, e.g. `billing` — also the module's name. */
2122
+ name: string;
2123
+ /**
2124
+ * `false` for an installed component shipped as a package: its code is in
2125
+ * `node_modules`, so it owns its tables but no `lunora/<key>/` folder (an app
2126
+ * folder of that name stays the app's). Absent means the module owns its folder.
2127
+ */
2128
+ ownsFolder?: false;
2129
+ /** Tables the module declares it owns (`defineModule({ tables })`); empty when it declares none. */
2130
+ tables: ReadonlyArray<string>;
2169
2131
  }
2170
2132
  /**
2171
- * One response-header write, inside an `httpAction` handler, whose value is derived
2172
- * from raw request input (`request.headers`, `request.url`/query, `await
2173
- * request.json()`) with no CR/LF sanitizer — the
2174
- * `http_action_response_header_injection` lint input. A `Request`-derived string
2175
- * placed verbatim into a response header lets a caller smuggle `\r\n` and inject
2176
- * extra headers or split the response (header injection / response splitting). Only
2177
- * sites whose value is request-tainted AND unguarded are recorded: a value routed
2178
- * through a CR/LF guard (`isSafeHeaderValue`), a URL/URI encoder
2179
- * (`encodeURIComponent`/`encodeURI`), a numeric coercion (`Number`/`parseInt`/
2180
- * `parseFloat`), or `btoa` is treated as safe and never recorded (`String(...)` /
2181
- * `.toString()` are NOT sanitizers — they don't strip CR/LF). Structurally
2182
- * identical to `AdvisorHttpHeaderWrite`.
2133
+ * A sibling Worker the app calls through a Cloudflare service binding, declared in
2134
+ * `lunora.config.*` `services` (plan 457). Becomes a wrangler `services[]` entry,
2135
+ * a typed `ctx.services.<name>` on actions, and a `service` node in the
2136
+ * architecture manifest.
2183
2137
  */
2184
- interface HttpHeaderWriteIR {
2185
- /** Source file relative to `<projectRoot>/lunora/`, without extension. */
2186
- file: string;
2187
- /** The header name being written (`"location"`), or `""` when the key is not a string literal. */
2188
- headerName: string;
2189
- /** 1-based line of the request-tainted header value. */
2190
- line: number;
2191
- /** Who the site runs on behalf of — see {@link CallSiteScope}. */
2192
- scope: CallSiteScope;
2193
- /** How the header was written. */
2194
- via: "headers-append" | "headers-ctor" | "headers-set" | "response-init";
2138
+ interface ServiceBindingIR {
2139
+ /** The `services[].binding` Lunora writes, e.g. `SERVICE_DOCUMENT_PARSER`. */
2140
+ binding: string;
2141
+ /** The exported `WorkerEntrypoint` class the binding targets; absent for the Worker's default export. */
2142
+ entrypoint?: string;
2143
+ /**
2144
+ * The Worker name per `env.<name>` block the service's own wrangler config
2145
+ * sets a `name` in. An environment missing here deploys as wrangler's
2146
+ * default `<worker>-<env>`.
2147
+ */
2148
+ envWorkers: Readonly<Record<string, string>>;
2149
+ /** Absolute path of the service's entry module (its wrangler `main`) — what the RPC type is read from. */
2150
+ main: string;
2151
+ /** The `ctx.services.<name>` key, e.g. `documentParser`. */
2152
+ name: string;
2153
+ /**
2154
+ * The scopes in which the service still serves on `*.workers.dev` with no
2155
+ * route of its own: `""` for the top level, else the environment name.
2156
+ */
2157
+ publicScopes: ReadonlyArray<string>;
2158
+ /**
2159
+ * The `WorkerEntrypoint` class the service is typed — and wired — as an RPC
2160
+ * stub of. Equal to {@link ServiceBindingIR.entrypoint} for an RPC service;
2161
+ * absent for a fetch service, including a named entrypoint declared
2162
+ * `rpc: false`, which is called with plain `fetch` and so needs no import of
2163
+ * the service's sources.
2164
+ */
2165
+ rpcEntrypoint?: string;
2166
+ /** The Worker's own `name` from its wrangler config — the `services[].service` target. */
2167
+ worker: string;
2168
+ /** Absolute path of the service's wrangler config. */
2169
+ wranglerPath: string;
2195
2170
  }
2196
2171
  /**
2197
- * One rate-limit / Turnstile middleware call in `lunora/` — the
2198
- * `ratelimit_middleware_fail_open` lint input. `rateLimit`/`dbRateLimit`
2199
- * (`@lunora/ratelimit`) and `verifyTurnstileMiddleware` (`@lunora/auth`) each
2200
- * accept a `failOpen` escape hatch that admits every request when the
2201
- * limiter/siteverify is unavailable; `failOpen` is `true` only when the options
2202
- * literal set it to the boolean literal `true` (anything else is fail-closed).
2203
- * The lint escalates a fail-open guard to a finding when the guarded procedure
2204
- * (`exportName`/`limitName`) looks auth/payment-sensitive. Structurally
2205
- * identical to `AdvisorFailOpenGuard`.
2172
+ * The feature-flag provider declared by the default export of `lunora/flags.ts`
2173
+ * (`defineFlags({ provider, … })`). Discovery is **metadata-only** — codegen
2174
+ * imports the real module at runtime for the provider value; this IR exists so
2175
+ * the config layer can reconcile/validate the wrangler `flagship` binding when
2176
+ * the app uses Flagship in binding mode. A `custom` provider (any other
2177
+ * OpenFeature factory) carries no binding to reconcile.
2206
2178
  */
2207
- interface FailOpenGuardIR {
2208
- /** The middleware factory at the call site: `rateLimit` / `dbRateLimit` / `verifyTurnstileMiddleware`. */
2209
- callee: string;
2210
- /** `true` only when the options literal set `failOpen: true` as a boolean literal; a non-literal or absent option is treated as fail-closed. */
2211
- failOpen: boolean;
2212
- /** Source file relative to `<projectRoot>/lunora/`, without extension. */
2213
- file: string;
2214
- /** The rate-limit `name` (second string argument) for `rateLimit`/`dbRateLimit`; `""` for `verifyTurnstileMiddleware`. */
2215
- limitName: string;
2216
- /** 1-based line of the middleware call, or `0` when unknown. */
2217
- line: number;
2218
- /** Who the site runs on behalf of — see {@link CallSiteScope}. */
2219
- scope: CallSiteScope;
2179
+ interface FlagsIR {
2180
+ /**
2181
+ * The wrangler `flagship[].binding` name — set **only** for a flagship
2182
+ * `provider` in binding mode (`flagshipProvider({ binding: "FLAGS" })`). The
2183
+ * config layer hints/validates a matching `flagship` binding from this.
2184
+ */
2185
+ bindingName?: string;
2186
+ /**
2187
+ * Flagship operating mode — `"binding"` (wrangler binding, needs a
2188
+ * `flagship` entry) or `"http"` (no binding); `undefined` for a `custom`
2189
+ * provider or when the mode can't be read statically.
2190
+ */
2191
+ mode?: "binding" | "http";
2192
+ /** `"flagship"` when the provider is `flagshipProvider(...)`, else `"custom"` (any other OpenFeature provider factory). */
2193
+ provider: "custom" | "flagship";
2220
2194
  }
2221
2195
  /**
2222
- * One `ctx.flags.boolean("key", <boolean-literal>)` read in `lunora/` — the
2223
- * `flag_gates_security_with_unsafe_default` lint input. OpenFeature returns the
2224
- * `defaultValue` when the provider errors, so a fail-open default on a
2225
- * security-shaped key silently opens access during an outage. Only reads with a
2226
- * statically-known string key and boolean-literal default are recorded; the lint
2227
- * owns the security-shape + polarity judgment. Structurally identical to
2228
- * `AdvisorFlagSecurityDefault`.
2196
+ * One `ctx.containers.<exportName>.get(name, …)` call whose instance key is derived
2197
+ * from the handler's `args` with no server-side scoping — the
2198
+ * `container_instance_key_from_user_input` lint input. Each container definition's
2199
+ * `.get(name)` accessor routes to one instance per `name`, so a key taken straight from
2200
+ * request input lets any caller reach another tenant's container (a cross-tenant IDOR). A
2201
+ * fixed literal key, or one derived from a server-trusted identity (`${ctx.auth.userId}` —
2202
+ * references `ctx`, so treated as scoped), is not recorded; only an arg-derived, unscoped
2203
+ * key reaches here. `.any()`/`.pool()` take no key and are not sinks. Structurally
2204
+ * identical to `AdvisorContainerKeyAccess`.
2229
2205
  */
2230
- interface FlagSecurityDefaultIR {
2231
- /** The boolean-literal default returned on a provider outage (fail-open value). */
2232
- defaultValue: boolean;
2206
+ interface ContainerKeyAccessIR {
2233
2207
  /** Source file relative to `<projectRoot>/lunora/`, without extension. */
2234
2208
  file: string;
2235
- /** The flag key — the first string-literal argument of `ctx.flags.boolean`. */
2236
- key: string;
2237
- /** 1-based line of the `ctx.flags.boolean` call, or `0` when unknown. */
2209
+ /** 1-based line of the `ctx.containers.*.get` call, or `0` when unknown. */
2238
2210
  line: number;
2211
+ /** The container accessor method invoked — always `get`. */
2212
+ method: string;
2239
2213
  /** Who the site runs on behalf of — see {@link CallSiteScope}. */
2240
2214
  scope: CallSiteScope;
2241
2215
  }
2242
2216
  /**
2243
- * One `ctx.flags` read lexically inside a `query(...)` handler — the
2244
- * `flag_read_in_subscription` advisor lint input. Flag changes append nothing to
2245
- * `__cdc_log`, so a subscribed query is never re-run when a flag flips and keeps
2246
- * serving the branch it last picked; `useFlag` is the reactive path. Structurally
2247
- * identical to the advisor's `AdvisorFlagRead` (same field set) so values pass
2248
- * straight through `lintSchema` without conversion, exactly as `ContextPropertyCallIR` does.
2249
- *
2250
- * Only `query` handlers are recorded — unlike `ContextPropertyCallIR` / `NondeterministicCallIR`
2251
- * there is no `kind` field, because `mutation`/`action` handlers run once and have
2252
- * no subscription staleness to warn about, so the feeder drops them rather than
2253
- * handing the lint rows it would discard.
2254
- */
2255
- interface FlagReadIR {
2256
- /** The accessed `ctx.flags` surface, e.g. `ctx.flags.boolean` / `ctx.flags.details.string`. */
2257
- callee: string;
2258
- /** Export binding name of the query performing the read. */
2259
- exportName: string;
2260
- /** Source file relative to `<projectRoot>/lunora/`, without extension (the api namespace). */
2261
- file: string;
2262
- /** 1-based line of the read, or `0` when unknown. */
2263
- line: number;
2264
- }
2265
- /**
2266
- * One `generateText` / `streamText` call in `lunora/` whose `tools` reach a
2267
- * privileged side effect (a DB write, function dispatch, or outbound
2268
- * fetch/mail/queue send). `userInputDerived` records whether the model input
2269
- * (`prompt`/`messages`/`system`) flows from the handler's `args`; the
2270
- * `ai_tool_side_effect_prompt_injection` lint fires only when it does.
2271
- * Structurally identical to `AdvisorAiToolSideEffect`.
2217
+ * One runtime container-override call: a `<handle>.start({ enableInternet: true, … })`
2218
+ * launch override, or a `<handle>.egress.<method>(...)` runtime firewall mutation
2219
+ * (`allow` / `deny` / `setAllowed`) — the `container_start_enable_internet_override`
2220
+ * and `container_runtime_egress_relaxation` lint input. Both shapes re-open network
2221
+ * access the static `defineContainer` declaration (and its `container_public_internet`
2222
+ * lint) assumes is locked down. Matched structurally by call shape, independent of the
2223
+ * receiver's resolved type. Structurally identical to `AdvisorContainerOverride`.
2272
2224
  */
2273
- interface AiToolSideEffectIR {
2225
+ interface ContainerOverrideIR {
2226
+ /** e.g. the egress method name, or `"enableInternet: true"`. */
2227
+ detail: string;
2274
2228
  /** Source file relative to `<projectRoot>/lunora/`, without extension. */
2275
2229
  file: string;
2276
- /** 1-based line of the generation call, or `0` when unknown. */
2277
- line: number;
2278
- /** The generation entrypoint invoked. */
2279
- method: "generateText" | "streamText";
2230
+ /** Which override shape matched. */
2231
+ kind: "egress_relaxation" | "enable_internet";
2232
+ /** 1-based line of the call, or `0` when unknown. */
2233
+ line: number;
2280
2234
  /** Who the site runs on behalf of — see {@link CallSiteScope}. */
2281
2235
  scope: CallSiteScope;
2282
- /** The privileged side-effect sink a model-callable tool reaches (`ctx.db.insert`, `ctx.run`, `ctx.fetch`, …). */
2283
- sideEffect: string;
2284
- /** `true` when a model-input option is derived from the handler's `args` (a bare `args.x`, or a name destructured from `args`). */
2285
- userInputDerived: boolean;
2286
2236
  }
2287
2237
  /**
2288
- * One `<receiver>.identity.<key>` claim read in `lunora/`, where `<receiver>` is
2289
- * an RLS/mask policy `auth` (or `ctx.auth`/`context.auth`). `declared` records
2290
- * whether `<key>` is in the app's `defineIdentity({ ... })` contract (or the
2291
- * always-present `userId`); the `identity_undeclared_claim_trusted` lint fires on
2292
- * the undeclared reads. Emitted only when a resolvable identity contract exists.
2293
- * Structurally identical to `AdvisorIdentityClaimRead`.
2238
+ * One `buildImageDeliveryUrl({ key, … })` call (`@lunora/bindings/images`) whose
2239
+ * `key` — the CDN transform's source image, an absolute URL or an
2240
+ * origin-relative key — is derived from the handler's `args` with no
2241
+ * server-side scoping — the `images_url_source_from_user_input` lint input.
2242
+ * `ctx.images.transform`/`info` take image *bytes*, never a URL, so they are not
2243
+ * sinks; only the `key` of `buildImageDeliveryUrl` accepts a URL-or-key source
2244
+ * and is inspected. An arg-derived `key` lets any caller point the CDN's
2245
+ * `/cdn-cgi/image/` transform at an attacker-chosen origin (SSRF / open proxy)
2246
+ * or at an arbitrary key under the account's own store. A fixed literal, or a
2247
+ * key scoped by a server-trusted `ctx.*` value, is not recorded. Structurally
2248
+ * identical to `AdvisorImageDeliveryUrlAccess`.
2294
2249
  */
2295
- interface IdentityClaimReadIR {
2296
- /** `true` when `key` is a declared claim (in the `defineIdentity` contract, or the always-present `userId`). */
2297
- declared: boolean;
2250
+ interface ImageDeliveryUrlAccessIR {
2298
2251
  /** Source file relative to `<projectRoot>/lunora/`, without extension. */
2299
2252
  file: string;
2300
- /** The claim key read off the identity bag. */
2301
- key: string;
2302
- /** 1-based line of the read, or `0` when unknown. */
2253
+ /** 1-based line of the `buildImageDeliveryUrl` call, or `0` when unknown. */
2303
2254
  line: number;
2304
2255
  /** Who the site runs on behalf of — see {@link CallSiteScope}. */
2305
2256
  scope: CallSiteScope;
2306
2257
  }
2307
2258
  /**
2308
- * One payment webhook-adapter construction in `lunora/` (`createStripeAdapter` /
2309
- * `createPolarAdapter` / `createAutumnAdapter` / `createDodoPaymentsAdapter`).
2310
- * `toleranceSeconds` carries the statically-known `webhookToleranceSeconds`
2311
- * replay window when it is a plain numeric literal; the payment-webhook
2312
- * wide-tolerance lint fires when it exceeds a conservative ceiling. Structurally
2313
- * identical to `AdvisorPaymentWebhook`.
2259
+ * One `createAuth({...})` call's configuration snapshot — the shared input for
2260
+ * the five `auth_*` security lints (trusted-origins wildcard, CSRF check
2261
+ * disabled, secure cookies disabled, email verification disabled, session
2262
+ * freshAge zero). Matched by callee NAME (an `import`-agnostic, fail-closed
2263
+ * convention the other feeders share), so a re-export or alias still resolves.
2264
+ * When the config argument isn't a statically-analyzable object literal (a
2265
+ * top-level spread, or not an object literal at all), `analyzable` is `false`
2266
+ * and every boolean fact defaults to its SAFE (not-flagged) value — an opaque
2267
+ * config can't be relied on either way. Structurally identical to
2268
+ * `AdvisorAuthConfig`.
2314
2269
  */
2315
- interface PaymentWebhookIR {
2316
- /** The adapter factory invoked. */
2317
- callee: "createAutumnAdapter" | "createDodoPaymentsAdapter" | "createPolarAdapter" | "createStripeAdapter";
2270
+ interface AuthConfigIR {
2271
+ /** `true` when the call's config argument was a static object literal the feeder could read. */
2272
+ analyzable: boolean;
2273
+ /** `advanced.disableCSRFCheck === true`. */
2274
+ disableCsrfCheck: boolean;
2275
+ /** `emailAndPassword.enabled === true`. */
2276
+ emailPasswordEnabled: boolean;
2318
2277
  /** Source file relative to `<projectRoot>/lunora/`, without extension. */
2319
2278
  file: string;
2320
- /** 1-based line of the construction, or `0` when unknown. */
2279
+ /** 1-based line of the `createAuth(...)` call, or `0` when unknown. */
2321
2280
  line: number;
2281
+ /** `emailAndPassword.requireEmailVerification === true` present. */
2282
+ requireEmailVerification: boolean;
2283
+ /** `trustedOrigins` array literal contains a `"*"` element. */
2284
+ /** `plugins` includes `scim(...)` while `database` is a non-transactional Lunora adapter — a combination that throws at runtime. */
2285
+ scimOnNonTransactionalAdapter: boolean;
2322
2286
  /** Who the site runs on behalf of — see {@link CallSiteScope}. */
2323
2287
  scope: CallSiteScope;
2324
- /** Statically-known `webhookToleranceSeconds` literal, when present and a plain numeric literal. */
2325
- toleranceSeconds?: number;
2288
+ /** `advanced.useSecureCookies === false`. */
2289
+ secureCookiesDisabled: boolean;
2290
+ /** `session.freshAge === 0` (explicit literal). */
2291
+ sessionFreshAgeZero: boolean;
2292
+ trustedOriginsWildcard: boolean;
2326
2293
  }
2327
2294
  /**
2328
- * One `ctx.db.<table>.findMany({ includeDeleted })` list read whose
2329
- * `includeDeleted` is either a hardcoded `true` or derived from the handler's
2330
- * `args` — the `soft_delete_include_deleted_from_args` lint input. The lint joins
2331
- * `table` against the schema's soft-delete tables and `visibility` against
2332
- * `.public()` before flagging. Structurally identical to `AdvisorSoftDeleteRead`
2333
- * so values pass straight through without conversion.
2295
+ * One discovered `httpRoute.<verb>("/admin/…")` route on an admin/privileged-looking
2296
+ * path, with whether its builder chain references an auth/admin guard — the
2297
+ * `admin_route_without_guard` lint input. Structurally identical to
2298
+ * `AdvisorAdminRoute`.
2334
2299
  */
2335
- interface SoftDeleteReadIR {
2336
- /** Export binding name of the procedure performing the read. */
2300
+ interface AdminRouteIR {
2301
+ /** Export binding name of the route handler. */
2337
2302
  exportName: string;
2338
2303
  /** Source file relative to `<projectRoot>/lunora/`, without extension. */
2339
2304
  file: string;
2340
- /** `true` when `includeDeleted` was derived from the handler's `args` (any caller can flip it). */
2341
- fromArgs: boolean;
2342
- /** `true` when `includeDeleted` was a hardcoded `true` literal (always resurfaces soft-deleted rows). */
2343
- hardcodedTrue: boolean;
2344
- /** 1-based line of the read call. */
2345
- line: number;
2346
- /** Table read, or `""` when the table-arg form's first argument wasn't a string literal. */
2347
- table: string;
2348
- /** `"internal"` for `internalQuery` / `internalMutation` / `internalAction`. */
2349
- visibility: "internal" | "public";
2305
+ /** HTTP verb the route binds to (uppercased), e.g. `"POST"`. */
2306
+ method: string;
2307
+ /** The route path, e.g. `/admin/users`. */
2308
+ path: string;
2309
+ /** `true` when the handler body references an auth/session/admin guard (`ctx.auth`, `getSession`, `requireAdmin`, …). */
2310
+ usesGuard: boolean;
2350
2311
  }
2351
2312
  /**
2352
- * One `ctx.db.<table>.findMany({ with: { <rel> } })` relation-hydrating list read
2353
- * — the `masked_relation_leak_via_with` lint input. Column masking does not
2354
- * descend into `with`-hydrated relations, so a masked table surfaced only through
2355
- * a `with` on an unprotected parent read is returned in the clear. The lint
2356
- * resolves each relation accessor to its target table and joins it against the
2357
- * discovered mask evidence before flagging. Structurally identical to
2358
- * `AdvisorRelationLoad` so values pass straight through without conversion.
2313
+ * One committed `wrangler.jsonc` `vars` entry whose value is a plaintext secret —
2314
+ * the `plaintext_secret_in_wrangler_vars` lint input. `vars` are baked into the
2315
+ * deployed Worker in cleartext and checked into source control, so a real API key
2316
+ * / token / private key there ships the secret to every reader of the repo and the
2317
+ * bundle; it belongs in a Secrets Store binding or `wrangler secret put`. Produced
2318
+ * by `@lunora/config` (which reads `wrangler.jsonc`), not a ts-morph feeder —
2319
+ * codegen only passes it through. Structurally identical to `AdvisorWranglerVariable`.
2359
2320
  */
2360
- interface RelationLoadIR {
2361
- /** Export binding name of the procedure performing the read. */
2321
+ interface WranglerVariableIR {
2322
+ /** The `wrangler.jsonc` file the var was read from, relative to the project root. */
2323
+ file: string;
2324
+ /** The offending `vars` key (e.g. `STRIPE_SECRET_KEY`). */
2325
+ key: string;
2326
+ /** Heuristic that matched, e.g. `stripe_live_key` / `private_key` / `secret_named_var`. */
2327
+ kind: string;
2328
+ /** Redacted preview of the value (first few chars + length) for the finding detail — never the full secret. */
2329
+ preview: string;
2330
+ }
2331
+ /**
2332
+ * One `queues.producers[]` entry of `wrangler.jsonc`, from the top level or an
2333
+ * `env.<name>` block — how codegen learns a queue's per-environment name. Produced
2334
+ * by `@lunora/config` (which reads `wrangler.jsonc`); codegen only matches it to a
2335
+ * declared queue by `binding`.
2336
+ */
2337
+ interface WranglerQueueProducerIR {
2338
+ /** The `Queue` producer binding, e.g. `QUEUE_JOBS`. */
2339
+ binding: string;
2340
+ /** The `env.<name>` block the entry sits in; absent for the top level. */
2341
+ env?: string;
2342
+ /** The queue name that binding sends to in that scope, e.g. `jobs-preview`. */
2343
+ queue: string;
2344
+ }
2345
+ interface ProjectIR {
2346
+ crons: ReadonlyArray<CronJobIR>;
2347
+ functions: ReadonlyArray<FunctionIR>;
2348
+ /** Typed REST routes discovered from `httpRoute.<verb>(...)` builder chains. */
2349
+ httpRoutes: ReadonlyArray<HttpRouteIR>;
2350
+ migrations: ReadonlyArray<MigrationIR>;
2351
+ schema: SchemaIR;
2352
+ }
2353
+ /**
2354
+ * Per-procedure RLS usage snapshot, produced by `discoverRlsProcedures` for the
2355
+ * `rls_uncovered_table` advisor lint. Structurally identical to
2356
+ * `AdvisorRlsProcedure` (they share the same field set) so values pass straight
2357
+ * through to the advisor without conversion, exactly as `AuthApiCallIR` does for
2358
+ * `AdvisorAuthApiCall`.
2359
+ */
2360
+ interface RlsProcedureIR {
2361
+ /** Export binding name of the procedure (e.g. `listDocuments`). */
2362
2362
  exportName: string;
2363
2363
  /** Source file relative to `<projectRoot>/lunora/`, without extension. */
2364
2364
  file: string;
2365
- /** 1-based line of the read call. */
2366
- line: number;
2367
- /** Parent table the read targets, or `""` when the table-arg form's first argument wasn't a string literal. */
2368
- parentTable: string;
2369
- /** Relation accessor names named in the read's `with: { … }` map — matched against the parent table's declared relations. */
2370
- relations: string[];
2365
+ /**
2366
+ * Table names extracted from the `rls(policies)` array literal. Empty when the
2367
+ * policies argument is not a statically-readable array literal.
2368
+ */
2369
+ rlsTables: string[];
2370
+ /** Tables read by the procedure via `ctx.db.query/findMany/findFirst/…`. */
2371
+ tablesRead: string[];
2372
+ /** Tables written by the procedure via `ctx.db.insert/patch/replace/delete`. */
2373
+ tablesWritten: string[];
2374
+ /** `true` when the procedure's builder chain includes `.use(rls(...))`. */
2375
+ usesRls: boolean;
2371
2376
  /** `"internal"` for `internalQuery` / `internalMutation` / `internalAction`. */
2372
2377
  visibility: "internal" | "public";
2373
2378
  }
2374
2379
  /**
2375
- * One `query` handler whose `return` hands back the raw rows of a table — the
2376
- * result of a `ctx.db.<table>.findMany()` / `.findFirst()` / `.get()` read, or a
2377
- * `ctx.db.query("<table>")…collect()` fluent chain — returned directly (or through
2378
- * one local `const` hop) with no hand-built projection. The
2379
- * `output_projection_missing_on_public_read` lint keeps only `visibility ===
2380
- * "public"` rows with no `.output(...)` / `.use(mask(...))` on the chain, then
2381
- * joins `table` against the schema and flags one whose columns are PII-named.
2382
- * Structurally identical to `AdvisorRawRowReturn` so values pass straight through
2383
- * without conversion.
2380
+ * One procedure reduced to the facts the `mask_uncovered_pii_column` lint needs:
2381
+ * whether its builder chain includes `.use(mask(...))`, which `(table, column)`
2382
+ * pairs that mask declares, and which tables the procedure reads/writes. The
2383
+ * column-level analogue of {@link RlsProcedureIR}. Structurally identical to
2384
+ * `AdvisorMaskProcedure` so values pass straight through without conversion.
2384
2385
  */
2385
- interface RawRowReturnIR {
2386
- /** Export binding name of the query returning the raw rows. */
2386
+ interface MaskProcedureIR {
2387
+ /** Export binding name of the procedure (e.g. `listUsers`). */
2387
2388
  exportName: string;
2388
2389
  /** Source file relative to `<projectRoot>/lunora/`, without extension. */
2389
2390
  file: string;
2390
- /** 1-based line of the `return` (or concise-body) expression. */
2391
- line: number;
2392
- /** Table whose raw rows are returned, or `""` when the read's table wasn't a string literal. */
2393
- table: string;
2394
- /** `true` when the procedure's builder chain carries a `.use(mask(...))` step. */
2391
+ /**
2392
+ * `(table, column)` pairs this procedure's `mask(policies)` object literal
2393
+ * declares. Empty when the policies argument is not a statically-readable
2394
+ * object literal (conservative: `usesMask` is still `true`).
2395
+ */
2396
+ maskColumns: {
2397
+ column: string;
2398
+ table: string;
2399
+ }[];
2400
+ /** Tables read by the procedure via `ctx.db.query/findMany/findFirst/get`. */
2401
+ tablesRead: string[];
2402
+ /** Tables written by the procedure via `ctx.db.insert/patch/replace/delete`. */
2403
+ tablesWritten: string[];
2404
+ /** `true` when the procedure's builder chain includes `.use(mask(...))`. */
2395
2405
  usesMask: boolean;
2396
- /** `true` when the procedure's builder chain carries an `.output(...)` return-shape projection. */
2397
- usesOutput: boolean;
2398
- /** `"internal"` for `internalQuery`; `"public"` for `query`. */
2406
+ /** `"internal"` for `internalQuery` / `internalMutation` / `internalAction`. */
2399
2407
  visibility: "internal" | "public";
2400
2408
  }
2401
2409
  /**
2402
- * One `query`/`mutation` handler that gates a `ctx.db.get`/`patch`/`delete` on a
2403
- * null-checked `ctx.db.normalizeId(table, id)` result — the
2404
- * `normalize_id_used_as_authorization` lint input. `normalizeId` validates an id's
2405
- * structural shape only (it never reads the database), so a non-null result proves
2406
- * the id is well-formed, never that the caller owns the row; gating access on it is
2407
- * an IDOR. The lint owns the negative proof — it keeps only `visibility === "public"`
2408
- * rows with no `.use(rls(...))` and no ownership/identity mention (`mentionsOwnership`),
2409
- * then joins `table` against the schema's RLS mode before flagging. Structurally
2410
- * identical to `AdvisorNormalizeIdAuthorization` so values pass straight through.
2410
+ * One masked column surfaced to the studio's data-browser mask preview: a
2411
+ * `(table, column)` pair plus the declared {@link MaskProcedureIR} strategy so
2412
+ * the preview can pick redact-vs-hash-vs-custom rendering. Aggregated across the
2413
+ * project's `.use(mask(...))` chains by `discoverMaskMetadata`; the descriptive
2414
+ * twin of {@link RlsPolicyIR}. `"custom"` covers any non-string strategy (a
2415
+ * `(value, ctx) => …` function) — its logic is an opaque closure, never read by
2416
+ * the UI; the preview renders a fixed sentinel for it.
2417
+ */
2418
+ interface MaskColumnMetadataIR {
2419
+ /** Column the mask policy redacts. */
2420
+ column: string;
2421
+ /** Declared masking strategy: `"redact"`/`"hash"` string literals, else `"custom"` for a `MaskFn`. */
2422
+ strategy: "custom" | "hash" | "redact";
2423
+ /** Logical table the masked column belongs to. */
2424
+ table: string;
2425
+ }
2426
+ /**
2427
+ * Schema-wide masking metadata the codegen emits into the generated ShardDO so
2428
+ * the studio's data-browser mask toggle can preview what a non-privileged caller
2429
+ * would see. Aggregated across every `.use(mask(...))` chain in the project —
2430
+ * purely descriptive (table + column + strategy), never the masking closure. The
2431
+ * column-level analogue of {@link RlsMetadataIR}.
2432
+ */
2433
+ interface MaskMetadataIR {
2434
+ /** Every statically-discovered masked column, deduped by `(table, column)` (first declaration wins). */
2435
+ columns: MaskColumnMetadataIR[];
2436
+ }
2437
+ /**
2438
+ * One masked column whose `mask(policies)` strategy is a statically-known
2439
+ * literal (`"hash"` or `"redact"`) — the `mask_weak_hash_strategy_on_pii` lint
2440
+ * input. Unlike {@link MaskColumnMetadataIR} (app-wide, deduped by `(table,
2441
+ * column)`, studio-preview evidence), this is per declaration site (file + line
2442
+ * + enclosing export), undeduped, so the lint can point at the exact
2443
+ * `mask(...)` call that applies a weak strategy. A `MaskFn` (custom, non-literal)
2444
+ * strategy carries no lint-relevant signal and is never recorded here.
2445
+ * Structurally identical to `AdvisorMaskStrategy`.
2411
2446
  */
2412
- interface NormalizeIdAuthorizationIR {
2413
- /** Export binding name of the procedure performing the normalize-then-access. */
2447
+ interface MaskStrategyIR {
2448
+ /** Masked column name. */
2449
+ column: string;
2450
+ /** Export binding name of the procedure whose `.use(mask(...))` chain declared this column, or `"<module>"` when declared at file scope. */
2414
2451
  exportName: string;
2415
2452
  /** Source file relative to `<projectRoot>/lunora/`, without extension. */
2416
2453
  file: string;
2417
- /** 1-based line of the `ctx.db.normalizeId(...)` call the access is gated on. */
2454
+ /** 1-based line of the masked column's strategy property. */
2418
2455
  line: number;
2419
- /** `true` when the handler anywhere reads an ownership-named identifier or `ctx.auth`/`ctx.identity`/… — an intervening ownership signal. */
2420
- mentionsOwnership: boolean;
2421
- /** The id-first `ctx.db` sink the normalized id reaches. */
2422
- sinkMethod: "delete" | "get" | "patch";
2423
- /** Table named in the `normalizeId` call, or `""` when its table argument wasn't a string literal. */
2456
+ /** The statically-known strategy literal: `"hash"` or `"redact"`. */
2457
+ strategy: string;
2458
+ /** Logical table the masked column belongs to. */
2424
2459
  table: string;
2425
- /** `true` when the procedure's builder chain carries a `.use(rls(...))` step. */
2426
- usesRls: boolean;
2427
- /** `"internal"` for `internalQuery`/`internalMutation`; `"public"` for `query`/`mutation`. */
2428
- visibility: "internal" | "public";
2429
2460
  }
2430
2461
  /**
2431
- * One committed `wrangler.jsonc` `vars` entry whose value is a plaintext secret —
2432
- * the `plaintext_secret_in_wrangler_vars` lint input. `vars` are baked into the
2433
- * deployed Worker in cleartext and checked into source control, so a real API key
2434
- * / token / private key there ships the secret to every reader of the repo and the
2435
- * bundle; it belongs in a Secrets Store binding or `wrangler secret put`. Produced
2436
- * by `@lunora/config` (which reads `wrangler.jsonc`), not a ts-morph feeder —
2437
- * codegen only passes it through. Structurally identical to `AdvisorWranglerVariable`.
2462
+ * One statically-readable policy entry from an `rls([...])` array literal,
2463
+ * surfaced to the studio's read-only RLS inspector via the generated
2464
+ * `rlsPolicies()` hook. Captures the policy's `table` + `on` operation and the
2465
+ * procedure it guards — never the `when` predicate, which is an opaque JS
2466
+ * closure (its logic stays in code, not the UI). Produced by
2467
+ * `discoverRlsProcedures` alongside the lint IR.
2438
2468
  */
2439
- interface WranglerVariableIR {
2440
- /** The `wrangler.jsonc` file the var was read from, relative to the project root. */
2469
+ interface RlsPolicyIR {
2470
+ /** Source file (relative to `lunora/`, without extension) the policy is declared in. */
2441
2471
  file: string;
2442
- /** The offending `vars` key (e.g. `STRIPE_SECRET_KEY`). */
2443
- key: string;
2444
- /** Heuristic that matched, e.g. `stripe_live_key` / `private_key` / `secret_named_var`. */
2445
- kind: string;
2446
- /** Redacted preview of the value (first few chars + length) for the finding detail — never the full secret. */
2447
- preview: string;
2472
+ /** Operation the policy gates: `read` covers get/query/findMany, the rest are writes. */
2473
+ on: "delete" | "insert" | "read" | "update";
2474
+ /** Export name of the procedure whose `.use(rls(...))` chain declared this policy. */
2475
+ procedure: string;
2476
+ /** Logical table the policy applies to. Empty when not a string literal. */
2477
+ table: string;
2448
2478
  }
2449
2479
  /**
2450
- * One `queues.producers[]` entry of `wrangler.jsonc`, from the top level or an
2451
- * `env.<name>` block — how codegen learns a queue's per-environment name. Produced
2452
- * by `@lunora/config` (which reads `wrangler.jsonc`); codegen only matches it to a
2453
- * declared queue by `binding`.
2480
+ * One statically-readable role entry from an `rls(policies, { roles: [...] })`
2481
+ * call, surfaced to the studio's RLS inspector. Captures the role's `name`,
2482
+ * optional `description`, and the names of the permissions it grants (string
2483
+ * literals or `definePermission("name")` calls). Produced by
2484
+ * `discoverRlsProcedures`.
2454
2485
  */
2455
- interface WranglerQueueProducerIR {
2456
- /** The `Queue` producer binding, e.g. `QUEUE_JOBS`. */
2457
- binding: string;
2458
- /** The `env.<name>` block the entry sits in; absent for the top level. */
2459
- env?: string;
2460
- /** The queue name that binding sends to in that scope, e.g. `jobs-preview`. */
2461
- queue: string;
2486
+ interface RlsRoleIR {
2487
+ /** Optional human-readable description from `defineRole(name, { description })`. */
2488
+ description?: string;
2489
+ /** Role label attached to the request identity (e.g. `"admin"`). */
2490
+ name: string;
2491
+ /** Permission names this role grants, deduped and in declared order. */
2492
+ permissions: string[];
2462
2493
  }
2463
- interface ProjectIR {
2464
- crons: ReadonlyArray<CronJobIR>;
2465
- functions: ReadonlyArray<FunctionIR>;
2466
- /** Typed REST routes discovered from `httpRoute.<verb>(...)` builder chains. */
2467
- httpRoutes: ReadonlyArray<HttpRouteIR>;
2468
- migrations: ReadonlyArray<MigrationIR>;
2469
- schema: SchemaIR;
2494
+ /**
2495
+ * Schema-wide RLS metadata the codegen emits into the generated ShardDO so the
2496
+ * studio's read-only inspector can list, per table, which policies guard it and
2497
+ * what roles are defined. Aggregated across every `.use(rls(...))` chain in the
2498
+ * project — purely descriptive, never the predicate logic.
2499
+ */
2500
+ interface RlsMetadataIR {
2501
+ /** Every statically-discovered policy `(table, on, procedure)` entry. */
2502
+ policies: RlsPolicyIR[];
2503
+ /** Every statically-discovered role, deduped by name (first declaration wins). */
2504
+ roles: RlsRoleIR[];
2505
+ }
2506
+ /** One statically-discovered `defineStorageRule({ bucket, on, prefix })` entry from a `.use(storageRules(...))` chain. */
2507
+ interface StorageRuleIR {
2508
+ bucket: string;
2509
+ /** Relative to `lunora/`. */
2510
+ file: string;
2511
+ on: "delete" | "list" | "read" | "write";
2512
+ /** Optional key-prefix scope; absent ⇒ the whole bucket. */
2513
+ prefix?: string;
2514
+ /** The exported procedure that installed the rule. */
2515
+ procedure: string;
2470
2516
  }
2471
2517
  /**
2472
- * One import of a migrated-away platform's SDK still present in `lunora/`
2473
- * source. Structurally identical to the advisor's `AdvisorStaleMigrationImport`
2474
- * so it passes through the feeder without conversion.
2518
+ * Schema-wide storage-access-rule metadata emitted into the generated ShardDO so
2519
+ * the studio's read-only inspector can list, per bucket, which operations are
2520
+ * gated and under what key prefix. Aggregated across every
2521
+ * `.use(storageRules(...))` chain — descriptive only, never the predicate logic.
2475
2522
  */
2476
- interface StaleMigrationImportIR {
2477
- /** Source file relative to the lunora dir, no extension. */
2523
+ interface StorageRulesMetadataIR {
2524
+ rules: StorageRuleIR[];
2525
+ }
2526
+ /**
2527
+ * One `rateLimit`/`dbRateLimit` middleware call (`@lunora/ratelimit`) whose
2528
+ * `key` selector — the per-caller rate-limit sub-key, `(ctx) => string |
2529
+ * undefined` — is derived from the handler's `args` with no server-side
2530
+ * scoping (no reference to the trusted `ctx` binding anywhere in the selector)
2531
+ * — the `ratelimit_key_spoofable_or_global` lint input. A key an attacker
2532
+ * controls lets them rotate it per request and bypass the limit entirely,
2533
+ * defeating its purpose. A selector scoped by `ctx` (e.g. `ctx.auth.userId`,
2534
+ * `ctx.ip`), or one with no `args` reference at all (a fixed/global bucket —
2535
+ * the "no key" case this lint deliberately does not flag, to keep it low-FP),
2536
+ * is not recorded. Structurally identical to `AdvisorRatelimitKeySelector`.
2537
+ */
2538
+ interface RatelimitKeySelectorIR {
2539
+ /** The `rateLimit`/`dbRateLimit` callee invoked. */
2540
+ callee: string;
2541
+ /** Source file relative to `<projectRoot>/lunora/`, without extension. */
2478
2542
  file: string;
2479
- /** 1-based line of the import, or `0` when unknown. */
2543
+ /** The rate limit's `name` argument (the second positional argument), or `""` when not a string literal. */
2544
+ limitName: string;
2545
+ /** 1-based line of the `rateLimit`/`dbRateLimit` call, or `0` when unknown. */
2480
2546
  line: number;
2481
- /** The imported module, e.g. `@supabase/supabase-js`. */
2482
- moduleSpecifier: string;
2483
- /** Which migration guide covers this platform. */
2484
- platform: "convex" | "firebase" | "supabase";
2547
+ /** Who the site runs on behalf of — see {@link CallSiteScope}. */
2548
+ scope: CallSiteScope;
2485
2549
  }
2486
2550
  /**
2487
2551
  * Named inputs for {@link lintSchema}. Every feeder is a discrete key rather than
@@ -2506,6 +2570,7 @@ interface LintSchemaOptions {
2506
2570
  containerKeyAccesses?: ReadonlyArray<ContainerKeyAccessIR>;
2507
2571
  containerOverrides?: ReadonlyArray<ContainerOverrideIR>;
2508
2572
  containers?: ReadonlyArray<ContainerIR>;
2573
+ cronTargets?: ReadonlyArray<string>;
2509
2574
  exportSinks?: ReadonlyArray<AdvisorExportSink>;
2510
2575
  failOpenGuards?: ReadonlyArray<FailOpenGuardIR>;
2511
2576
  flagReads?: ReadonlyArray<FlagReadIR>;
@@ -2539,6 +2604,7 @@ interface LintSchemaOptions {
2539
2604
  rawRowReturns?: ReadonlyArray<RawRowReturnIR>;
2540
2605
  relationLoads?: ReadonlyArray<RelationLoadIR>;
2541
2606
  rlsProcedures?: ReadonlyArray<RlsProcedureIR>;
2607
+ rowEffects?: ReadonlyArray<RowEffectIR>;
2542
2608
  schema: SchemaIR;
2543
2609
  secretLiterals?: ReadonlyArray<SecretLiteralIR>;
2544
2610
  shapes?: ReadonlyArray<ShapeIR>;
@@ -3050,67 +3116,6 @@ interface PlatformDiagnostic {
3050
3116
  /** The requested deploy target. */
3051
3117
  target: string;
3052
3118
  }
3053
- /**
3054
- * Committed, tracked baseline file holding the blessed structural schema
3055
- * snapshot the pre-deploy drift gate diffs against. Lives in `lunora/` (NOT the
3056
- * gitignored `_generated/`) so it is committed alongside `schema.ts`. Leading
3057
- * dot keeps it tucked away next to the schema it describes.
3058
- */
3059
- declare const SCHEMA_SNAPSHOT_FILENAME = ".lunora-schema.json";
3060
- /**
3061
- * Walk up from `startPath` until we find a `tsconfig.json` or hit the file
3062
- * system root. Returns the absolute path to the tsconfig, or `undefined`.
3063
- *
3064
- * Exported (see the bottom-of-file `export { findTsconfig }`) so a long-lived
3065
- * caller (the Vite dev-loop's cached-Project invalidation) can ask "which
3066
- * tsconfig would {@link createCodegenProject} resolve right now?" without
3067
- * duplicating the walk — recomputing it per call is a handful of `existsSync`
3068
- * checks, negligible next to a Project rebuild.
3069
- */
3070
- declare const findTsconfig: (startPath: string) => string | undefined;
3071
- /**
3072
- * Construct the ts-morph `Project` codegen discovers over. Prefers the user's
3073
- * `tsconfig.json` (when one is found walking up from `lunoraDirectory`) so
3074
- * cross-file type resolution and path aliases work; falls back to an isolated
3075
- * project otherwise. This is the exact construction {@link runCodegen} uses
3076
- * when no `project` is injected — exported so a long-lived caller (the Vite
3077
- * dev-loop) can build one once and reuse it across runs via
3078
- * {@link refreshCodegenProject} instead of re-parsing the user's whole TS
3079
- * program on every save.
3080
- */
3081
- declare const createCodegenProject: (lunoraDirectory: string) => Project;
3082
- /**
3083
- * Synchronise a reused {@link createCodegenProject} Project with the current
3084
- * on-disk state of `lunoraDirectory`, so the next {@link runCodegen} sees the
3085
- * same files a freshly-constructed Project would — without re-parsing the whole
3086
- * TS program. Adds any on-disk source file the Project doesn't yet have, and
3087
- * `refreshFromFileSystemSync()`es the ones it does (picking up edits); then
3088
- * removes Project source files under `lunoraDirectory` that no longer exist on
3089
- * disk (the classic stale-deleted-file cache bug).
3090
- *
3091
- * Files outside `lunoraDirectory` — e.g. a shared validator or type pulled in
3092
- * via the user's tsconfig — are also `refreshFromFileSystemSync()`ed, but only
3093
- * the ones already loaded into the Project; none are added. `resolveValidatorAlias`
3094
- * (parse-validator.ts) follows `getAliasedSymbol()` across module boundaries, so a
3095
- * validator defined outside `lunoraDirectory` is genuinely read from whatever
3096
- * source the Project currently holds — leaving those files stale made a reused
3097
- * Project (the Vite dev loop) silently disagree with a fresh one (`lunora
3098
- * codegen`) about the same source. `node_modules` is excluded: its `.d.ts` set
3099
- * dominates the file count and never changes mid dev-loop, so refreshing it would
3100
- * reinstate close to the full re-parse cost this cache exists to avoid.
3101
- */
3102
- declare const refreshCodegenProject: (project: Project, lunoraDirectory: string) => void;
3103
- /**
3104
- * Top-level codegen entry. Parses `<projectRoot>/lunora/schema.ts` and every
3105
- * function file under `<projectRoot>/lunora/`, then writes
3106
- * `_generated/{api,server,dataModel}.ts` next to them.
3107
- *
3108
- * When `LUNORA_CODEGEN_TIMING` is set (truthy), a single diagnostic summary
3109
- * line is written to stderr with the total wall time and the discovery-vs-emit
3110
- * split — opt-in instrumentation that is otherwise zero-cost and side-effect-free
3111
- * on the returned {@link CodegenResult}.
3112
- */
3113
- declare const runCodegen: (options: CodegenOptions) => CodegenResult;
3114
3119
  interface CodegenOptions {
3115
3120
  /**
3116
3121
  * Which machine-readable API spec(s) to emit into `_generated/`.
@@ -3331,6 +3336,68 @@ interface CodegenResult {
3331
3336
  */
3332
3337
  writtenFiles: ReadonlyArray<string>;
3333
3338
  }
3339
+ /**
3340
+ * Walk up from `startPath` until we find a `tsconfig.json` or hit the file
3341
+ * system root. Returns the absolute path to the tsconfig, or `undefined`.
3342
+ *
3343
+ *
3344
+ * Exported (see the bottom-of-file `export { findTsconfig }`) so a long-lived
3345
+ * caller (the Vite dev-loop's cached-Project invalidation) can ask "which
3346
+ * tsconfig would `createCodegenProject` resolve right now?" without
3347
+ * duplicating the walk — recomputing it per call is a handful of `existsSync`
3348
+ * checks, negligible next to a Project rebuild.
3349
+ */
3350
+ declare const findTsconfig: (startPath: string) => string | undefined;
3351
+ /**
3352
+ * Construct the ts-morph `Project` codegen discovers over. Prefers the user's
3353
+ * `tsconfig.json` (when one is found walking up from `lunoraDirectory`) so
3354
+ * cross-file type resolution and path aliases work; falls back to an isolated
3355
+ * project otherwise. This is the exact construction `runCodegen` uses
3356
+ * when no `project` is injected — exported so a long-lived caller (the Vite
3357
+ * dev-loop) can build one once and reuse it across runs via
3358
+ * {@link refreshCodegenProject} instead of re-parsing the user's whole TS
3359
+ * program on every save.
3360
+ */
3361
+ declare const createCodegenProject: (lunoraDirectory: string) => Project;
3362
+ /**
3363
+ * Synchronise a reused `createCodegenProject` Project with the current
3364
+ * on-disk state of `lunoraDirectory`, so the next `runCodegen` sees the
3365
+ * same files a freshly-constructed Project would — without re-parsing the whole
3366
+ * TS program. Adds any on-disk source file the Project doesn't yet have, and
3367
+ * `refreshFromFileSystemSync()`es the ones it does (picking up edits); then
3368
+ * removes Project source files under `lunoraDirectory` that no longer exist on
3369
+ * disk (the classic stale-deleted-file cache bug).
3370
+ *
3371
+ * Files outside `lunoraDirectory` — e.g. a shared validator or type pulled in
3372
+ * via the user's tsconfig — are also `refreshFromFileSystemSync()`ed, but only
3373
+ * the ones already loaded into the Project; none are added. `resolveValidatorAlias`
3374
+ * (parse-validator.ts) follows `getAliasedSymbol()` across module boundaries, so a
3375
+ * validator defined outside `lunoraDirectory` is genuinely read from whatever
3376
+ * source the Project currently holds — leaving those files stale made a reused
3377
+ * Project (the Vite dev loop) silently disagree with a fresh one (`lunora
3378
+ * codegen`) about the same source. `node_modules` is excluded: its `.d.ts` set
3379
+ * dominates the file count and never changes mid dev-loop, so refreshing it would
3380
+ * reinstate close to the full re-parse cost this cache exists to avoid.
3381
+ */
3382
+ declare const refreshCodegenProject: (project: Project, lunoraDirectory: string) => void;
3383
+ /**
3384
+ * Committed, tracked baseline file holding the blessed structural schema
3385
+ * snapshot the pre-deploy drift gate diffs against. Lives in `lunora/` (NOT the
3386
+ * gitignored `_generated/`) so it is committed alongside `schema.ts`. Leading
3387
+ * dot keeps it tucked away next to the schema it describes.
3388
+ */
3389
+ declare const SCHEMA_SNAPSHOT_FILENAME = ".lunora-schema.json";
3390
+ /**
3391
+ * Top-level codegen entry. Parses `<projectRoot>/lunora/schema.ts` and every
3392
+ * function file under `<projectRoot>/lunora/`, then writes
3393
+ * `_generated/{api,server,dataModel}.ts` next to them.
3394
+ *
3395
+ * When `LUNORA_CODEGEN_TIMING` is set (truthy), a single diagnostic summary
3396
+ * line is written to stderr with the total wall time and the discovery-vs-emit
3397
+ * split — opt-in instrumentation that is otherwise zero-cost and side-effect-free
3398
+ * on the returned {@link CodegenResult}.
3399
+ */
3400
+ declare const runCodegen: (options: CodegenOptions) => CodegenResult;
3334
3401
  declare const sortedUniqueNames: (names: Iterable<string>) => ReadonlyArray<string>;
3335
3402
  /** Deduplicated, sorted names of every ERROR-level advisory in `advisories`. */
3336
3403
  declare const errorAdvisoryNames: (advisories: ReadonlyArray<Pick<Finding, "level" | "name">>) => ReadonlyArray<string>;
@@ -3586,7 +3653,7 @@ declare const readPackageDependencies: (projectRoot: string) => Set<string> | un
3586
3653
  * unfiltered, unindexed `.collect()` is the read `unbounded_collect` exists for
3587
3654
  * — and dropping it here is precisely why nothing could see it.
3588
3655
  */
3589
- declare const discoverQueries: (project: Project, lunoraDirectory: string) => QueryReadIR[];
3656
+ declare const discoverQueries: (project: Project, lunoraDirectory: string, functions?: ReadonlyArray<FunctionIR>) => QueryReadIR[];
3590
3657
  /** The file queues are declared in: `lunora/queues.ts`, and the same name directly inside a module folder. */
3591
3658
  declare const QUEUES_FILENAME = "queues.ts";
3592
3659
  /** The queues half of {@link discoverQueueDeclarations} — what the config layer reconciles into wrangler. */