@lunora/advisor 1.0.0-alpha.8 → 1.0.0-alpha.80

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 (157) hide show
  1. package/LICENSE.md +6 -0
  2. package/README.md +60 -5
  3. package/dist/index.d.mts +4033 -1072
  4. package/dist/index.d.ts +4033 -1072
  5. package/dist/index.mjs +1 -85
  6. package/dist/packem_shared/MAP_VERSION-DEJGm0wI.mjs +1 -0
  7. package/dist/packem_shared/actionFetchSsrf-Z61P0o8U.mjs +1 -0
  8. package/dist/packem_shared/actionWithoutErrorHandling-4GCBT0_z.mjs +1 -0
  9. package/dist/packem_shared/adminRouteWithoutGuard-DPE7LuNh.mjs +1 -0
  10. package/dist/packem_shared/aiRawRunEscapeHatch-Dq43DVD9.mjs +1 -0
  11. package/dist/packem_shared/aiRunWithoutLogging-CvHHtEN9.mjs +1 -0
  12. package/dist/packem_shared/aiToolSideEffectPromptInjection-K42X3QzJ.mjs +1 -0
  13. package/dist/packem_shared/aiUnboundedGenerationPublic-C17h6sMV.mjs +1 -0
  14. package/dist/packem_shared/allowUnauthenticatedShardAccessEnabled-CjXI-kda.mjs +1 -0
  15. package/dist/packem_shared/argument-derived-sink-DLXFC79t.mjs +1 -0
  16. package/dist/packem_shared/authApiCallWithoutHeaders-C1OOWML5.mjs +1 -0
  17. package/dist/packem_shared/authCsrfCheckDisabled-Dz27bDzp.mjs +1 -0
  18. package/dist/packem_shared/authEmailVerificationDisabled-QTDD7TAU.mjs +1 -0
  19. package/dist/packem_shared/authScimWithoutTransactions-FqdTmUJs.mjs +1 -0
  20. package/dist/packem_shared/authSecureCookiesDisabled-CrGYulfJ.mjs +1 -0
  21. package/dist/packem_shared/authSessionFreshageZero-yWE6CGYP.mjs +1 -0
  22. package/dist/packem_shared/authTrustedOriginsWildcard-xpeRXfGF.mjs +1 -0
  23. package/dist/packem_shared/browserAllowPrivateTargets-Cj5sizhv.mjs +1 -0
  24. package/dist/packem_shared/browserUserUrlWithoutAllowlist-Wl3xHr7v.mjs +1 -0
  25. package/dist/packem_shared/circularFk-DtcWFJxK.mjs +1 -0
  26. package/dist/packem_shared/classifySensitivity-JnjaTGYi.mjs +1 -0
  27. package/dist/packem_shared/compareToBaseline-DBgN5YqX.mjs +1 -0
  28. package/dist/packem_shared/constraintValidator-DoRJD9Is.mjs +1 -0
  29. package/dist/packem_shared/containerInstanceKeyFromUserInput-uEQQVsEz.mjs +1 -0
  30. package/dist/packem_shared/containerOversizedInstance-Bx89uR7E.mjs +1 -0
  31. package/dist/packem_shared/containerPublicInternet-BFfZf_P4.mjs +1 -0
  32. package/dist/packem_shared/containerRuntimeEgressRelaxation-pkpyXNou.mjs +1 -0
  33. package/dist/packem_shared/containerStartEnableInternetOverride-BBY4PMG1.mjs +1 -0
  34. package/dist/packem_shared/dedupeCacheKeys-DtBOHffV.mjs +1 -0
  35. package/dist/packem_shared/duplicateIndex-Cip6-Rpu.mjs +1 -0
  36. package/dist/packem_shared/emptyIndex-BnHDcXza.mjs +1 -0
  37. package/dist/packem_shared/errorRateOutlier-pr3_ZKbq.mjs +1 -0
  38. package/dist/packem_shared/errorWithoutCatalog-BTfvaXHR.mjs +1 -0
  39. package/dist/packem_shared/exportSinkMisconfigured-JfbAx9AI.mjs +1 -0
  40. package/dist/packem_shared/externalSourceIncrementalNoDeletePath-BCzm3HzF.mjs +1 -0
  41. package/dist/packem_shared/externalSourceOnGlobal-CH7xbJ49.mjs +1 -0
  42. package/dist/packem_shared/externalSourceUnscoped-BxU2uSXk.mjs +1 -0
  43. package/dist/packem_shared/fanOutBreadth-CBtmZnoh.mjs +1 -0
  44. package/dist/packem_shared/filterOnPrimaryKey-c37rjqBk.mjs +1 -0
  45. package/dist/packem_shared/filterWithoutIndex-DXb__LGI.mjs +1 -0
  46. package/dist/packem_shared/finding-NrKO8idM.mjs +1 -0
  47. package/dist/packem_shared/fk-index-DXYS17cc.mjs +1 -0
  48. package/dist/packem_shared/flagGatesSecurityWithUnsafeDefault-i-Befg8b.mjs +1 -0
  49. package/dist/packem_shared/fromServerSchema-w16riDj2.mjs +1 -0
  50. package/dist/packem_shared/geoIndexFieldNotGeopoint-D0lOTm-_.mjs +1 -0
  51. package/dist/packem_shared/geoIndexUnused-D7C9Qr4U.mjs +1 -0
  52. package/dist/packem_shared/globalTableNearColumnLimit-BFbBBd6A.mjs +1 -0
  53. package/dist/packem_shared/gradeFromScore-KSt58rj1.mjs +1 -0
  54. package/dist/packem_shared/hardcodedSecret-Bw_4FYrs.mjs +1 -0
  55. package/dist/packem_shared/helpers-DcWLLrH0.mjs +1 -0
  56. package/dist/packem_shared/hotShard-BwGYZ3Tq.mjs +1 -0
  57. package/dist/packem_shared/httpActionMissingAuthGuard-BS3JZgaz.mjs +1 -0
  58. package/dist/packem_shared/httpActionResponseHeaderInjection-DHnc8c9f.mjs +1 -0
  59. package/dist/packem_shared/hyperdriveOutsideAction-CPDdGP2g.mjs +1 -0
  60. package/dist/packem_shared/identityUndeclaredClaimTrusted-BDFqB7Dw.mjs +1 -0
  61. package/dist/packem_shared/imagesUrlSourceFromUserInput-DizsRh3M.mjs +1 -0
  62. package/dist/packem_shared/indexReferencesUnknownField-Dxd5jcVh.mjs +1 -0
  63. package/dist/packem_shared/indexUtilization-CkVPZcVe.mjs +1 -0
  64. package/dist/packem_shared/insertManyUnsafeUserData-DWE8d_DF.mjs +1 -0
  65. package/dist/packem_shared/kvUnscopedUserKeyIdor-CIgMPzFj.mjs +1 -0
  66. package/dist/packem_shared/mailInboundDispatchWithoutVerify-CWwqXyPX.mjs +1 -0
  67. package/dist/packem_shared/mailRecipientFromRequestInput-SDCchLP7.mjs +1 -0
  68. package/dist/packem_shared/maskUncoveredPiiColumn-axEka4nd.mjs +1 -0
  69. package/dist/packem_shared/maskWeakHashStrategyOnPii-d9BGImzK.mjs +1 -0
  70. package/dist/packem_shared/maskedRelationLeakViaWith-C8n-Mzb1.mjs +1 -0
  71. package/dist/packem_shared/mutatorFullRowReplace-BzRpZ47r.mjs +1 -0
  72. package/dist/packem_shared/nondeterministicQueryMutation-sxmixc0l.mjs +1 -0
  73. package/dist/packem_shared/normalizeIdUsedAsAuthorization-BXN-Can6.mjs +1 -0
  74. package/dist/packem_shared/notifyMissingPushConfig-DeQMoNwm.mjs +1 -0
  75. package/dist/packem_shared/notifySendOutsideAction-CU4T16cR.mjs +1 -0
  76. package/dist/packem_shared/outputProjectionMissingOnPublicRead-BN5-uW0p.mjs +1 -0
  77. package/dist/packem_shared/ownerFieldFromArgsNotAuth-CjOSUDKi.mjs +1 -0
  78. package/dist/packem_shared/paymentCreateWithoutAuthorize-CICGmRFR.mjs +1 -0
  79. package/dist/packem_shared/paymentWebhookWideTolerance-B-F5jOeA.mjs +1 -0
  80. package/dist/packem_shared/plaintextSecretInWranglerVariables-bNkqmNqV.mjs +1 -0
  81. package/dist/packem_shared/policyReferencesUnknownTable-CIKuRZ5Y.mjs +1 -0
  82. package/dist/packem_shared/privilegedDispatchUnvalidatedPayload-C6Qtc8aT.mjs +1 -0
  83. package/dist/packem_shared/privilegedFanoutFromPublicProcedure-Coqt23CQ.mjs +1 -0
  84. package/dist/packem_shared/procedure-protections-DRQotu9I.mjs +1 -0
  85. package/dist/packem_shared/procedureWithoutStructuredEvent-CZ_B23nM.mjs +1 -0
  86. package/dist/packem_shared/publicArgumentUsesAny-BDEvMA8Q.mjs +1 -0
  87. package/dist/packem_shared/publicMutationWithoutRatelimit-DbczxgpL.mjs +1 -0
  88. package/dist/packem_shared/publicTableRlsOptoutConfusion-C9wCtAfg.mjs +1 -0
  89. package/dist/packem_shared/queueWithoutDlq-BvHz3Opg.mjs +1 -0
  90. package/dist/packem_shared/r2sqlOutsideAction-BdLiLOAX.mjs +1 -0
  91. package/dist/packem_shared/ratelimitDefaultMemoryStore-Dj0Kk7t9.mjs +1 -0
  92. package/dist/packem_shared/ratelimitKeySpoofableOrGlobal-BzDzxnXm.mjs +1 -0
  93. package/dist/packem_shared/ratelimitMiddlewareFailOpen-iDJq68qx.mjs +1 -0
  94. package/dist/packem_shared/relationReferencesUnknownField-BmWWEFsB.mjs +1 -0
  95. package/dist/packem_shared/relationReferencesUnknownTable-CP4aWtAJ.mjs +1 -0
  96. package/dist/packem_shared/rlsUncoveredTable-CZ4ie4gX.mjs +1 -0
  97. package/dist/packem_shared/shapeTargetsGlobalTable-Bu3eEDic.mjs +1 -0
  98. package/dist/packem_shared/shapeUnknownTable-CREfNnWi.mjs +1 -0
  99. package/dist/packem_shared/signupMutationWithoutDisposableGating-D9MFN8QY.mjs +1 -0
  100. package/dist/packem_shared/softDeleteIncludeDeletedFromArgs-C7Ugg4zb.mjs +1 -0
  101. package/dist/packem_shared/sqlInjectionRisk-CslaRBxz.mjs +1 -0
  102. package/dist/packem_shared/storageGenerateUploadUrlNoContentTypePin-CTIU_7Fj.mjs +1 -0
  103. package/dist/packem_shared/storageKeyFromUserArgs-C-QUcdJ3.mjs +1 -0
  104. package/dist/packem_shared/storagePresignedUrlForPrivateContent-yeu1s81k.mjs +1 -0
  105. package/dist/packem_shared/storageUploadWithoutContentTypeAllowlist-KSRG81Iu.mjs +1 -0
  106. package/dist/packem_shared/storageUploadWithoutMaxSize-BTiM9Q3A.mjs +1 -0
  107. package/dist/packem_shared/tableWithoutInsert-DQ-GxjFF.mjs +1 -0
  108. package/dist/packem_shared/ttlFieldNotTimestamp-E14I83wY.mjs +1 -0
  109. package/dist/packem_shared/unboundedStringArgument-DzbFJc5q.mjs +1 -0
  110. package/dist/packem_shared/unindexedForeignKey-Dypgn8uH.mjs +1 -0
  111. package/dist/packem_shared/unindexedRelationTarget-CSqRJWYZ.mjs +1 -0
  112. package/dist/packem_shared/unrestrictedWhereBranch-CcIBmHik.mjs +1 -0
  113. package/dist/packem_shared/userCreatingMutationWithoutCaptcha-lGWw1o2a.mjs +1 -0
  114. package/dist/packem_shared/vectorsNamespaceFromUserInput-CDSOrkyX.mjs +1 -0
  115. package/dist/packem_shared/workflowDuplicateStepName-BU4rg5So.mjs +1 -0
  116. package/dist/packem_shared/workflowUnknownTarget-B8H7jwnH.mjs +1 -0
  117. package/dist/packem_shared/workflowUnused-BUSOPdHq.mjs +1 -0
  118. package/package.json +3 -2
  119. package/dist/packem_shared/AE_METRIC_EVENTS-DexctYv6.mjs +0 -85
  120. package/dist/packem_shared/adminRouteWithoutGuard-UUGBkAjU.mjs +0 -33
  121. package/dist/packem_shared/authApiCallWithoutHeaders-BeJhCZaf.mjs +0 -38
  122. package/dist/packem_shared/circularFk-B2freHrP.mjs +0 -84
  123. package/dist/packem_shared/constraintValidator-Dr9Py3FD.mjs +0 -186
  124. package/dist/packem_shared/containerOversizedInstance-5U1VKPRM.mjs +0 -36
  125. package/dist/packem_shared/containerPublicInternet-CuNerJE5.mjs +0 -30
  126. package/dist/packem_shared/duplicateIndex-BOublMSt.mjs +0 -57
  127. package/dist/packem_shared/emptyIndex-BX8EuEY7.mjs +0 -32
  128. package/dist/packem_shared/filterWithoutIndex-BYVeJaSs.mjs +0 -31
  129. package/dist/packem_shared/finding-Dm_zvzS1.mjs +0 -16
  130. package/dist/packem_shared/fk-index-IUK1ukgs.mjs +0 -7
  131. package/dist/packem_shared/fromServerSchema-TWb7dTHC.mjs +0 -51
  132. package/dist/packem_shared/hardcodedSecret-W2pz1UZB.mjs +0 -35
  133. package/dist/packem_shared/helpers-DNCkMWZQ.mjs +0 -4
  134. package/dist/packem_shared/hotShard-Ir5D0B6J.mjs +0 -48
  135. package/dist/packem_shared/hyperdriveOutsideAction-BgZqX7Xg.mjs +0 -30
  136. package/dist/packem_shared/indexReferencesUnknownField-DH0_dbUY.mjs +0 -36
  137. package/dist/packem_shared/indexUtilization-B5DMQ3bI.mjs +0 -45
  138. package/dist/packem_shared/maskUncoveredPiiColumn-DjGIPG6M.mjs +0 -61
  139. package/dist/packem_shared/mutatorFullRowReplace-BJnNDaIV.mjs +0 -26
  140. package/dist/packem_shared/nondeterministicQueryMutation-GXES1fLp.mjs +0 -35
  141. package/dist/packem_shared/policyReferencesUnknownTable-DtaIEovd.mjs +0 -38
  142. package/dist/packem_shared/publicArgumentUsesAny-C71b2NCf.mjs +0 -32
  143. package/dist/packem_shared/publicMutationWithoutRatelimit-xBpJ6GWK.mjs +0 -36
  144. package/dist/packem_shared/r2sqlOutsideAction-CtqxvMuV.mjs +0 -30
  145. package/dist/packem_shared/relationReferencesUnknownField-YznyXt_7.mjs +0 -54
  146. package/dist/packem_shared/relationReferencesUnknownTable-DrorpKYe.mjs +0 -33
  147. package/dist/packem_shared/rlsUncoveredTable-CxEfZ5eZ.mjs +0 -56
  148. package/dist/packem_shared/shapeTargetsGlobalTable-DHrf4Koi.mjs +0 -34
  149. package/dist/packem_shared/shapeUnknownTable-C8aDWFoe.mjs +0 -34
  150. package/dist/packem_shared/sqlInjectionRisk-zwytYGLt.mjs +0 -26
  151. package/dist/packem_shared/tableWithoutInsert-CbbaYIP4.mjs +0 -34
  152. package/dist/packem_shared/unboundedStringArgument-DThg2-wt.mjs +0 -32
  153. package/dist/packem_shared/unindexedForeignKey-BgJbKyqK.mjs +0 -45
  154. package/dist/packem_shared/unindexedRelationTarget-D6eyj6Xx.mjs +0 -53
  155. package/dist/packem_shared/userCreatingMutationWithoutCaptcha-CH31YsUZ.mjs +0 -42
  156. package/dist/packem_shared/workflowUnknownTarget-Cdd7WhKQ.mjs +0 -34
  157. package/dist/packem_shared/workflowUnused-D0jHxdz9.mjs +0 -38
@@ -1,61 +0,0 @@
1
- import { e as emit } from './finding-Dm_zvzS1.mjs';
2
-
3
- const maskUncoveredPiiColumn = {
4
- categories: ["SECURITY"],
5
- description: "A public procedure reads a table whose columns are masked by `.use(mask(...))` elsewhere in the app, but this procedure's builder chain does not include `.use(mask(...))`. It returns those columns in the clear while a sibling procedure redacts them.",
6
- facing: "EXTERNAL",
7
- level: "WARN",
8
- name: "mask_uncovered_pii_column",
9
- remediation: "Add `.use(mask(policies))` to the procedure's builder chain — e.g. `c.use(mask(myMasks)).query(...)` — so its returned rows redact the same columns as the rest of the app. If this procedure is intentionally privileged (e.g. an admin export), declare it with `internalQuery` / `internalMutation` / `internalAction` instead of the public builder so the intent is explicit.",
10
- // eslint-disable-next-line sonarjs/cognitive-complexity -- the procedure/table/column cross-reference reads clearest as one linear pass; splitting it would obscure the flow.
11
- run: (context) => {
12
- if (context.maskProcedures === void 0) {
13
- return [];
14
- }
15
- const maskedColumnsByTable = /* @__PURE__ */ new Map();
16
- for (const procedure of context.maskProcedures) {
17
- for (const { column, table } of procedure.maskColumns) {
18
- if (table === "" || column === "") {
19
- continue;
20
- }
21
- const columns = maskedColumnsByTable.get(table) ?? /* @__PURE__ */ new Set();
22
- columns.add(column);
23
- maskedColumnsByTable.set(table, columns);
24
- }
25
- }
26
- if (maskedColumnsByTable.size === 0) {
27
- return [];
28
- }
29
- const findings = [];
30
- for (const procedure of context.maskProcedures) {
31
- if (procedure.usesMask) {
32
- continue;
33
- }
34
- if (procedure.visibility === "internal") {
35
- continue;
36
- }
37
- const touched = /* @__PURE__ */ new Set();
38
- for (const table of procedure.tablesRead) {
39
- if (table !== "" && maskedColumnsByTable.has(table)) {
40
- touched.add(table);
41
- }
42
- }
43
- for (const table of touched) {
44
- const columns = [...maskedColumnsByTable.get(table) ?? /* @__PURE__ */ new Set()].toSorted((a, b) => a.localeCompare(b));
45
- const columnList = columns.map((column) => `\`${column}\``).join(", ");
46
- findings.push(
47
- emit(maskUncoveredPiiColumn, {
48
- cacheKey: `mask_uncovered_pii_column:${procedure.file}:${procedure.exportName}:${table}`,
49
- detail: `\`${procedure.exportName}\` in ${procedure.file} reads table \`${table}\` without \`.use(mask(...))\` — its column${columns.length === 1 ? "" : "s"} ${columnList} ${columns.length === 1 ? "is" : "are"} masked elsewhere in the app but returned in the clear here.`,
50
- metadata: { columns, exportName: procedure.exportName, file: procedure.file, table }
51
- })
52
- );
53
- }
54
- }
55
- return findings;
56
- },
57
- source: "static",
58
- title: "Maskable column returned without mask() middleware"
59
- };
60
-
61
- export { maskUncoveredPiiColumn as default };
@@ -1,26 +0,0 @@
1
- import { e as emit } from './finding-Dm_zvzS1.mjs';
2
-
3
- const mutatorFullRowReplace = {
4
- categories: ["SCHEMA"],
5
- description: "A custom mutator's `server` impl writes with `ctx.db.replace(id, document)` — a whole-row overwrite. On a synced table this clobbers a concurrent edit to a different column; `ctx.db.patch(id, { field })` merges at the column level so independent field edits coexist.",
6
- facing: "EXTERNAL",
7
- level: "WARN",
8
- name: "mutator_full_row_replace",
9
- remediation: "Prefer `ctx.db.patch(id, { onlyTheChangedField })` so a concurrent edit to another column isn't lost. Keep `replace` only when the mutator genuinely owns the entire row (e.g. a full-form save).",
10
- run: (context) => {
11
- if (context.mutatorWrites === void 0) {
12
- return [];
13
- }
14
- return context.mutatorWrites.map(
15
- (write) => emit(mutatorFullRowReplace, {
16
- cacheKey: `mutator_full_row_replace:${write.exportName}:${String(write.line)}`,
17
- detail: `Mutator \`${write.exportName}\` (${write.file}:${String(write.line)}) writes with \`ctx.db.replace(...)\`, overwriting the whole row. A concurrent edit to a different column is clobbered — use \`ctx.db.patch(id, { field })\` to merge at the column level.`,
18
- metadata: { exportName: write.exportName, file: write.file, line: write.line }
19
- })
20
- );
21
- },
22
- source: "static",
23
- title: "Mutator overwrites the whole row (clobbers concurrent column edits)"
24
- };
25
-
26
- export { mutatorFullRowReplace as default };
@@ -1,35 +0,0 @@
1
- import { e as emit } from './finding-Dm_zvzS1.mjs';
2
-
3
- const nondeterministicQueryMutation = {
4
- categories: ["SCHEMA"],
5
- description: "A `query`/`mutation` handler calls a non-deterministic API (`Date.now`, `Math.random`, `crypto.randomUUID`, `crypto.getRandomValues`, or `fetch`). These handlers may be re-run on OCC retry / subscription re-evaluation, so they must be deterministic — time, randomness, and network belong in an `action`.",
6
- facing: "EXTERNAL",
7
- level: "WARN",
8
- name: "nondeterministic_query_mutation",
9
- remediation: "Move the non-deterministic call into an `action(...)` (which runs once and may use ambient APIs), then pass the computed value into the mutation as an argument — e.g. compute `Date.now()` in the action and call `ctx.runMutation(api.…, { now })`. Take generated ids/timestamps as args instead of producing them in the handler.",
10
- run: (context) => {
11
- if (context.nondeterministicCalls === void 0) {
12
- return [];
13
- }
14
- const findings = [];
15
- const occurrenceCount = /* @__PURE__ */ new Map();
16
- for (const call of context.nondeterministicCalls) {
17
- const baseKey = `${call.file}:${call.line.toString()}:${call.callee}`;
18
- const occurrence = (occurrenceCount.get(baseKey) ?? 0) + 1;
19
- occurrenceCount.set(baseKey, occurrence);
20
- const occurrenceSuffix = occurrence > 1 ? `:${occurrence.toString()}` : "";
21
- findings.push(
22
- emit(nondeterministicQueryMutation, {
23
- cacheKey: `nondeterministic_query_mutation:${baseKey}${occurrenceSuffix}`,
24
- detail: `\`${call.callee}(…)\` in ${call.exportName} (${call.file}:${call.line.toString()}) runs inside a ${call.kind} handler — query/mutation handlers must be deterministic. Compute it in an \`action\` and pass the value into the mutation as an argument.`,
25
- metadata: { callee: call.callee, exportName: call.exportName, file: call.file, kind: call.kind, line: call.line }
26
- })
27
- );
28
- }
29
- return findings;
30
- },
31
- source: "static",
32
- title: "Non-deterministic call in query/mutation handler"
33
- };
34
-
35
- export { nondeterministicQueryMutation as default };
@@ -1,38 +0,0 @@
1
- import { e as emit } from './finding-Dm_zvzS1.mjs';
2
-
3
- const policyReferencesUnknownTable = {
4
- categories: ["SECURITY"],
5
- description: "An RLS policy is bound to a `table` name that does not exist in the schema. The `rls()` middleware never matches it, so the table the policy was meant to protect is left ungated — reads return unrestricted rows and writes are allowed.",
6
- facing: "EXTERNAL",
7
- level: "WARN",
8
- name: "policy_references_unknown_table",
9
- remediation: "Fix the policy's `table` to a real table name (check for a typo or a table that was renamed/removed). If the table is gone, delete the dead policy.",
10
- run: (context) => {
11
- if (context.rlsProcedures === void 0) {
12
- return [];
13
- }
14
- const knownTables = new Set(context.schema.tables.map((table) => table.name));
15
- const reported = /* @__PURE__ */ new Set();
16
- const findings = [];
17
- for (const procedure of context.rlsProcedures) {
18
- for (const table of procedure.rlsTables) {
19
- if (table === "" || knownTables.has(table) || reported.has(table)) {
20
- continue;
21
- }
22
- reported.add(table);
23
- findings.push(
24
- emit(policyReferencesUnknownTable, {
25
- cacheKey: `policy_references_unknown_table:${table}`,
26
- detail: `An RLS policy in \`${procedure.exportName}\` (${procedure.file}) is bound to table \`${table}\`, which is not declared in the schema. The policy never matches, so \`${table}\` — if it exists under another name — is left ungated.`,
27
- metadata: { exportName: procedure.exportName, file: procedure.file, table }
28
- })
29
- );
30
- }
31
- }
32
- return findings;
33
- },
34
- source: "static",
35
- title: "RLS policy bound to an unknown table"
36
- };
37
-
38
- export { policyReferencesUnknownTable as default };
@@ -1,32 +0,0 @@
1
- import { e as emit } from './finding-Dm_zvzS1.mjs';
2
-
3
- const publicArgumentUsesAny = {
4
- categories: ["SECURITY"],
5
- description: "A public procedure declares a `v.any()` argument. `v.any()` accepts arbitrary untyped input from an untrusted client — defeating validation and opening injection / oversized-payload abuse.",
6
- facing: "EXTERNAL",
7
- level: "WARN",
8
- name: "public_arg_uses_any",
9
- remediation: "Replace `v.any()` with a precise validator — `v.object({...})`, `v.string()`, `v.union(...)`, etc. If the shape is genuinely dynamic, model the known variants with `v.union` rather than accepting anything.",
10
- run: (context) => {
11
- if (context.argValidators === void 0) {
12
- return [];
13
- }
14
- const findings = [];
15
- for (const procedure of context.argValidators) {
16
- for (const argument of procedure.anyArgs) {
17
- findings.push(
18
- emit(publicArgumentUsesAny, {
19
- cacheKey: `public_arg_uses_any:${procedure.file}:${procedure.exportName}:${argument}`,
20
- detail: `Arg \`${argument}\` of public procedure \`${procedure.exportName}\` (${procedure.file}:${procedure.line.toString()}) is \`v.any()\` — untrusted input with no validation. Give it a precise validator.`,
21
- metadata: { argument, exportName: procedure.exportName, file: procedure.file, line: procedure.line }
22
- })
23
- );
24
- }
25
- }
26
- return findings;
27
- },
28
- source: "static",
29
- title: "Public argument uses v.any()"
30
- };
31
-
32
- export { publicArgumentUsesAny as default };
@@ -1,36 +0,0 @@
1
- import { e as emit } from './finding-Dm_zvzS1.mjs';
2
-
3
- const SENSITIVE_NAME_RE = /contact|forgot|login|magic|otp|register|reset|signin|signup|subscribe|verify/iu;
4
- const publicMutationWithoutRatelimit = {
5
- categories: ["SECURITY"],
6
- description: "A public `mutation`/`action` has no `rateLimit` middleware. Publicly-callable writes are flood and brute-force targets — an attacker can exhaust writes, mail quota, or credits, or guess credentials on auth-shaped endpoints.",
7
- facing: "EXTERNAL",
8
- level: "WARN",
9
- name: "public_mutation_without_ratelimit",
10
- remediation: 'Attach a rate limit: `.use(rateLimit(limiter, "<bucket>"))` from `@lunora/ratelimit`, or wrap the recommended public-procedure guards with `.use(protectPublic({ rateLimit, captcha }))` from `@lunora/server`. Genuinely-open writes can be acknowledged by adding a permissive limiter.',
11
- run: (context) => {
12
- if (context.procedureProtections === void 0) {
13
- return [];
14
- }
15
- const findings = [];
16
- for (const procedure of context.procedureProtections) {
17
- const isPublicWrite = procedure.visibility === "public" && (procedure.kind === "mutation" || procedure.kind === "action");
18
- if (!isPublicWrite || procedure.usesRateLimit) {
19
- continue;
20
- }
21
- const sensitive = SENSITIVE_NAME_RE.test(procedure.exportName);
22
- findings.push(
23
- emit(publicMutationWithoutRatelimit, {
24
- cacheKey: `public_mutation_without_ratelimit:${procedure.file}:${procedure.exportName}`,
25
- detail: `Public ${procedure.kind} \`${procedure.exportName}\` (${procedure.file}) has no rate limit${sensitive ? " — its name suggests an auth/abuse-sensitive endpoint, so this is high-risk" : ""}. Add \`.use(rateLimit(...))\` or \`.use(protectPublic({ rateLimit }))\`.`,
26
- metadata: { exportName: procedure.exportName, file: procedure.file, kind: procedure.kind, sensitive }
27
- })
28
- );
29
- }
30
- return findings;
31
- },
32
- source: "static",
33
- title: "Public write without a rate limit"
34
- };
35
-
36
- export { publicMutationWithoutRatelimit as default };
@@ -1,30 +0,0 @@
1
- import { e as emit } from './finding-Dm_zvzS1.mjs';
2
-
3
- const r2sqlOutsideAction = {
4
- categories: ["SCHEMA"],
5
- description: "A `query`/`mutation` handler accesses R2 SQL via `ctx.r2sql`. R2 SQL queries external Apache Iceberg tables over REST (no Workers binding): queries are non-deterministic (like `fetch`) and the reads are invisible to live queries. `ctx.r2sql` is available on `ActionCtx` only and must be confined to `action` handlers.",
6
- facing: "EXTERNAL",
7
- level: "WARN",
8
- name: "r2sql_outside_action",
9
- remediation: "Move the `ctx.r2sql` access into an `action(...)` (the only context where it is typed), where external I/O is allowed. If a query/mutation needs the data, have the action read it via `ctx.r2sql` and write a projection into a `defineSchema` DO/D1 table — that write is tracked by live queries, whereas R2 SQL is not.",
10
- run: (context) => {
11
- if (context.r2sqlCalls === void 0) {
12
- return [];
13
- }
14
- const findings = [];
15
- for (const call of context.r2sqlCalls) {
16
- findings.push(
17
- emit(r2sqlOutsideAction, {
18
- cacheKey: `r2sql_outside_action:${call.file}:${call.line.toString()}:${call.callee}`,
19
- detail: `\`${call.callee}(…)\` in ${call.exportName} (${call.file}:${call.line.toString()}) runs inside a ${call.kind} handler — R2 SQL's \`ctx.r2sql\` is non-deterministic and non-reactive, so it is available only in actions. Move the external query into an \`action\` and project the result into a Lunora table if a query/mutation needs it.`,
20
- metadata: { callee: call.callee, exportName: call.exportName, file: call.file, kind: call.kind, line: call.line }
21
- })
22
- );
23
- }
24
- return findings;
25
- },
26
- source: "static",
27
- title: "R2 SQL `ctx.r2sql` used outside an action"
28
- };
29
-
30
- export { r2sqlOutsideAction as default };
@@ -1,54 +0,0 @@
1
- import { e as emit } from './finding-Dm_zvzS1.mjs';
2
- import { t as tableColumnSet } from './helpers-DNCkMWZQ.mjs';
3
-
4
- const missingColumns = (byName, columnsOf, holder, relation) => {
5
- const target = byName.get(relation.table);
6
- if (!target) {
7
- return [];
8
- }
9
- const fkTable = relation.kind === "one" ? holder : target;
10
- const referencedTable = relation.kind === "one" ? target : holder;
11
- const problems = [];
12
- if (!columnsOf(fkTable).has(relation.field)) {
13
- problems.push({ column: relation.field, owner: fkTable.name, side: "field" });
14
- }
15
- if (!columnsOf(referencedTable).has(relation.references)) {
16
- problems.push({ column: relation.references, owner: referencedTable.name, side: "references" });
17
- }
18
- return problems;
19
- };
20
- const relationReferencesUnknownField = {
21
- categories: ["SCHEMA"],
22
- description: "A relation references a foreign-key or referenced column that is not declared on its table, so the join can never resolve.",
23
- facing: "INTERNAL",
24
- level: "ERROR",
25
- name: "relation_references_unknown_field",
26
- remediation: "Fix the `field` / `references` column name in the relation, or add the missing column.",
27
- run: (context) => {
28
- const byName = new Map(context.schema.tables.map((table) => [table.name, table]));
29
- const columnsCache = /* @__PURE__ */ new Map();
30
- const columnsOf = (table) => {
31
- let columns = columnsCache.get(table);
32
- if (!columns) {
33
- columns = tableColumnSet(table);
34
- columnsCache.set(table, columns);
35
- }
36
- return columns;
37
- };
38
- return context.schema.tables.flatMap(
39
- (table) => table.relations.flatMap(
40
- (relation) => missingColumns(byName, columnsOf, table, relation).map(
41
- (problem) => emit(relationReferencesUnknownField, {
42
- cacheKey: `relation_references_unknown_field:${table.name}:${relation.name}:${problem.side}`,
43
- detail: `Relation "${relation.name}" on table "${table.name}" uses ${problem.side} "${problem.column}", which is not declared on table "${problem.owner}".`,
44
- metadata: { column: problem.column, owner: problem.owner, relation: relation.name, side: problem.side, table: table.name }
45
- })
46
- )
47
- )
48
- );
49
- },
50
- source: "static",
51
- title: "Relation references unknown field"
52
- };
53
-
54
- export { relationReferencesUnknownField as default };
@@ -1,33 +0,0 @@
1
- import { e as emit } from './finding-Dm_zvzS1.mjs';
2
-
3
- const relationReferencesUnknownTable = {
4
- categories: ["SCHEMA"],
5
- description: "A relation targets a table that does not exist in the schema. The relation can never load — the target is a typo or a removed/renamed table.",
6
- facing: "INTERNAL",
7
- level: "ERROR",
8
- name: "relation_references_unknown_table",
9
- remediation: "Fix the target table name in the relation, or add the missing table to the schema.",
10
- run: (context) => {
11
- const findings = [];
12
- const tableNames = new Set(context.schema.tables.map((table) => table.name));
13
- for (const table of context.schema.tables) {
14
- for (const relation of table.relations) {
15
- if (tableNames.has(relation.table)) {
16
- continue;
17
- }
18
- findings.push(
19
- emit(relationReferencesUnknownTable, {
20
- cacheKey: `relation_references_unknown_table:${table.name}:${relation.name}`,
21
- detail: `Relation "${relation.name}" on table "${table.name}" targets table "${relation.table}", which does not exist in the schema.`,
22
- metadata: { relation: relation.name, table: table.name, target: relation.table }
23
- })
24
- );
25
- }
26
- }
27
- return findings;
28
- },
29
- source: "static",
30
- title: "Relation references unknown table"
31
- };
32
-
33
- export { relationReferencesUnknownTable as default };
@@ -1,56 +0,0 @@
1
- import { e as emit } from './finding-Dm_zvzS1.mjs';
2
-
3
- const rlsUncoveredTable = {
4
- categories: ["SECURITY"],
5
- description: "A public procedure reads or writes a table that is covered by an RLS policy elsewhere in the app, but this procedure's builder chain does not include `.use(rls(...))`. The raw, unwrapped `ctx.db` silently bypasses every policy in the list.",
6
- facing: "EXTERNAL",
7
- level: "WARN",
8
- name: "rls_uncovered_table",
9
- remediation: "Add `.use(rls(policies))` to the procedure's builder chain — e.g. `c.use(rls(myPolicies)).query(...)` — so its `ctx.db` is wrapped by the same policy evaluator as the rest of the app. If this procedure is intentionally privileged (e.g. a background job), declare it with `internalQuery` / `internalMutation` / `internalAction` instead of the public builder so the intent is explicit.",
10
- // eslint-disable-next-line sonarjs/cognitive-complexity -- the policy/table cross-reference is clearest as one linear pass; splitting it would obscure the flow.
11
- run: (context) => {
12
- if (context.rlsProcedures === void 0) {
13
- return [];
14
- }
15
- const policyCoveredTables = /* @__PURE__ */ new Set();
16
- for (const procedure of context.rlsProcedures) {
17
- for (const table of procedure.rlsTables) {
18
- if (table !== "") {
19
- policyCoveredTables.add(table);
20
- }
21
- }
22
- }
23
- if (policyCoveredTables.size === 0) {
24
- return [];
25
- }
26
- const findings = [];
27
- for (const procedure of context.rlsProcedures) {
28
- if (procedure.usesRls) {
29
- continue;
30
- }
31
- if (procedure.visibility === "internal") {
32
- continue;
33
- }
34
- const touched = /* @__PURE__ */ new Set();
35
- for (const table of [...procedure.tablesRead, ...procedure.tablesWritten]) {
36
- if (table !== "" && policyCoveredTables.has(table)) {
37
- touched.add(table);
38
- }
39
- }
40
- for (const table of touched) {
41
- findings.push(
42
- emit(rlsUncoveredTable, {
43
- cacheKey: `rls_uncovered_table:${procedure.file}:${procedure.exportName}:${table}`,
44
- detail: `\`${procedure.exportName}\` in ${procedure.file} accesses table \`${table}\` without \`.use(rls(...))\` — the table is policy-gated elsewhere in the app but this procedure bypasses those policies entirely.`,
45
- metadata: { exportName: procedure.exportName, file: procedure.file, table }
46
- })
47
- );
48
- }
49
- }
50
- return findings;
51
- },
52
- source: "static",
53
- title: "RLS-gated table accessed without rls() middleware"
54
- };
55
-
56
- export { rlsUncoveredTable as default };
@@ -1,34 +0,0 @@
1
- import { e as emit } from './finding-Dm_zvzS1.mjs';
2
-
3
- const shapeTargetsGlobalTable = {
4
- categories: ["PERFORMANCE"],
5
- description: "A `defineShape` replicates from a `.global()` table. Global tables live outside the shard DO's SQLite op-log, so the shape is served through the cross-shard tier — coordinator/poll-refreshed and latency-tiered, not poke-live like a sharded shape.",
6
- facing: "EXTERNAL",
7
- level: "WARN",
8
- name: "shape_targets_global_table",
9
- remediation: "Expected if you want the cross-shard tier — just don't assume poke-live freshness; global shapes refresh on a poll. For live updates, replicate from a sharded table instead (denormalize the columns you need into the shard).",
10
- run: (context) => {
11
- if (context.shapes === void 0) {
12
- return [];
13
- }
14
- const globalTables = new Set(context.schema.tables.filter((table) => table.shardKind === "global").map((table) => table.name));
15
- const findings = [];
16
- for (const shape of context.shapes) {
17
- if (shape.table === void 0 || !globalTables.has(shape.table)) {
18
- continue;
19
- }
20
- findings.push(
21
- emit(shapeTargetsGlobalTable, {
22
- cacheKey: `shape_targets_global_table:${shape.exportName}`,
23
- detail: `Shape \`${shape.exportName}\` (${shape.file}) replicates from the \`.global()\` table \`${shape.table}\`. It is served through the cross-shard global tier — poll-refreshed and latency-tiered, not poke-live.`,
24
- metadata: { exportName: shape.exportName, file: shape.file, table: shape.table }
25
- })
26
- );
27
- }
28
- return findings;
29
- },
30
- source: "static",
31
- title: "Shape replicates from a global (cross-shard) table"
32
- };
33
-
34
- export { shapeTargetsGlobalTable as default };
@@ -1,34 +0,0 @@
1
- import { e as emit } from './finding-Dm_zvzS1.mjs';
2
-
3
- const shapeUnknownTable = {
4
- categories: ["SCHEMA"],
5
- description: "A `defineShape` is bound to a `table` name that does not exist in the schema. The shape can never resolve a rowset — its subscription seeds empty and errors at the first flush.",
6
- facing: "INTERNAL",
7
- level: "ERROR",
8
- name: "shape_unknown_table",
9
- remediation: "Fix the shape's `table` to a real table name (check for a typo or a table that was renamed/removed).",
10
- run: (context) => {
11
- if (context.shapes === void 0) {
12
- return [];
13
- }
14
- const knownTables = new Set(context.schema.tables.map((table) => table.name));
15
- const findings = [];
16
- for (const shape of context.shapes) {
17
- if (shape.table === void 0 || knownTables.has(shape.table)) {
18
- continue;
19
- }
20
- findings.push(
21
- emit(shapeUnknownTable, {
22
- cacheKey: `shape_unknown_table:${shape.exportName}`,
23
- detail: `Shape \`${shape.exportName}\` (${shape.file}) replicates from table \`${shape.table}\`, which is not declared in the schema. The shape can never resolve a rowset.`,
24
- metadata: { exportName: shape.exportName, file: shape.file, table: shape.table }
25
- })
26
- );
27
- }
28
- return findings;
29
- },
30
- source: "static",
31
- title: "Shape bound to an unknown table"
32
- };
33
-
34
- export { shapeUnknownTable as default };
@@ -1,26 +0,0 @@
1
- import { e as emit } from './finding-Dm_zvzS1.mjs';
2
-
3
- const sqlInjectionRisk = {
4
- categories: ["SECURITY"],
5
- description: "A `ctx.sql` tagged-template interpolates an unparameterized string-building expression (concatenation or nested template) instead of a bound value — splicing raw text into the query and reopening SQL injection.",
6
- facing: "EXTERNAL",
7
- level: "ERROR",
8
- name: "sql_injection_risk",
9
- remediation: "Pass the value through a bound placeholder so the driver parameterizes it — keep request input inside a `ctx.sql` placeholder instead of concatenating it into the query string. Never build SQL text from request input by hand.",
10
- run: (context) => {
11
- if (context.sqlInterpolations === void 0) {
12
- return [];
13
- }
14
- return context.sqlInterpolations.map(
15
- (interpolation) => emit(sqlInjectionRisk, {
16
- cacheKey: `sql_injection_risk:${interpolation.file}:${interpolation.line.toString()}`,
17
- detail: `\`ctx.sql\` in \`${interpolation.exportName}\` (${interpolation.file}:${interpolation.line.toString()}) interpolates a string-building expression instead of a bound value — a SQL-injection vector. Pass the value through a bound placeholder so the driver parameterizes it.`,
18
- metadata: { exportName: interpolation.exportName, file: interpolation.file, line: interpolation.line }
19
- })
20
- );
21
- },
22
- source: "static",
23
- title: "Possible SQL injection in ctx.sql interpolation"
24
- };
25
-
26
- export { sqlInjectionRisk as default };
@@ -1,34 +0,0 @@
1
- import { e as emit } from './finding-Dm_zvzS1.mjs';
2
-
3
- const tableWithoutInsert = {
4
- categories: ["SCHEMA"],
5
- description: 'No function inserts into this table via `ctx.db.insert("<table>", …)`. It may be read-only by design (seeded by a migration, replicated, or written through a path the advisor can\'t see) — or it may be dead schema.',
6
- facing: "INTERNAL",
7
- level: "INFO",
8
- name: "table_without_insert",
9
- remediation: 'If the table should be writable, add a mutation that calls `ctx.db.insert("<table>", …)`. If it is read-only or seeded elsewhere, this advisory can be ignored.',
10
- run: (context) => {
11
- if (context.inserts === void 0) {
12
- return [];
13
- }
14
- const insertedTables = new Set(context.inserts.filter((write) => write.table !== "").map((write) => write.table));
15
- const findings = [];
16
- for (const table of context.schema.tables) {
17
- if (insertedTables.has(table.name) || table.externallyManaged === true) {
18
- continue;
19
- }
20
- findings.push(
21
- emit(tableWithoutInsert, {
22
- cacheKey: `table_without_insert:${table.name}`,
23
- detail: `No function calls \`ctx.db.insert("${table.name}", …)\` — table "${table.name}" has no discovered insert path.`,
24
- metadata: { table: table.name }
25
- })
26
- );
27
- }
28
- return findings;
29
- },
30
- source: "static",
31
- title: "Table has no insert path"
32
- };
33
-
34
- export { tableWithoutInsert as default };
@@ -1,32 +0,0 @@
1
- import { e as emit } from './finding-Dm_zvzS1.mjs';
2
-
3
- const unboundedStringArgument = {
4
- categories: ["SECURITY"],
5
- description: "A public `v.string()` argument has no maximum-length bound. An unbounded string lets a client submit arbitrarily large input — abusing storage and CPU on every request that processes it.",
6
- facing: "EXTERNAL",
7
- level: "INFO",
8
- name: "unbounded_string_arg",
9
- remediation: "Add a max-length bound via `.check(...)` / `.meta({ maxLength })` on the string validator (e.g. cap a name at 256, a body at a few KB). Size the cap to the field's real-world maximum.",
10
- run: (context) => {
11
- if (context.argValidators === void 0) {
12
- return [];
13
- }
14
- const findings = [];
15
- for (const procedure of context.argValidators) {
16
- for (const argument of procedure.unboundedStringArgs) {
17
- findings.push(
18
- emit(unboundedStringArgument, {
19
- cacheKey: `unbounded_string_arg:${procedure.file}:${procedure.exportName}:${argument}`,
20
- detail: `Arg \`${argument}\` of public procedure \`${procedure.exportName}\` (${procedure.file}:${procedure.line.toString()}) is an unbounded \`v.string()\`. Add a max-length bound to cap payload size.`,
21
- metadata: { argument, exportName: procedure.exportName, file: procedure.file, line: procedure.line }
22
- })
23
- );
24
- }
25
- }
26
- return findings;
27
- },
28
- source: "static",
29
- title: "Public string argument has no length bound"
30
- };
31
-
32
- export { unboundedStringArgument as default };
@@ -1,45 +0,0 @@
1
- import { e as emit } from './finding-Dm_zvzS1.mjs';
2
- import { l as leadingIndexedColumns, P as PRIMARY_KEY, s as suggestIndexName } from './fk-index-IUK1ukgs.mjs';
3
-
4
- const unindexedForeignKey = {
5
- categories: ["PERFORMANCE"],
6
- description: "A foreign-key column declared by a `one` relation has no index leading with it. Reads that filter or join on the column full-scan the table, which gets linearly slower as rows accumulate.",
7
- facing: "EXTERNAL",
8
- level: "INFO",
9
- name: "unindexed_foreign_key",
10
- remediation: 'Add a secondary index leading with the FK column, e.g. `.index("byAuthorId", ["authorId"])`.',
11
- run: (context) => {
12
- const findings = [];
13
- for (const table of context.schema.tables) {
14
- const indexed = leadingIndexedColumns(table);
15
- for (const relation of table.relations) {
16
- if (relation.kind !== "one") {
17
- continue;
18
- }
19
- const fkColumn = relation.field;
20
- if (fkColumn === PRIMARY_KEY || indexed.has(fkColumn)) {
21
- continue;
22
- }
23
- const suggestedIndex = suggestIndexName(fkColumn);
24
- findings.push(
25
- emit(unindexedForeignKey, {
26
- cacheKey: `unindexed_foreign_key:${table.name}:${fkColumn}`,
27
- detail: `Relation "${relation.name}" on table "${table.name}" references "${relation.table}" via column "${fkColumn}", which is not the leading column of any index. Reads filtering or joining on "${fkColumn}" full-scan "${table.name}".`,
28
- metadata: {
29
- fkColumn,
30
- references: { column: relation.references, table: relation.table },
31
- relation: relation.name,
32
- suggestedIndex: { fields: [fkColumn], name: suggestedIndex },
33
- table: table.name
34
- }
35
- })
36
- );
37
- }
38
- }
39
- return findings;
40
- },
41
- source: "static",
42
- title: "Unindexed foreign key"
43
- };
44
-
45
- export { unindexedForeignKey as default };