@lunora/advisor 1.0.0-alpha.21 → 1.0.0-alpha.211

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 (218) hide show
  1. package/README.md +64 -7
  2. package/dist/index.d.mts +4107 -2575
  3. package/dist/index.d.ts +4107 -2575
  4. package/dist/index.mjs +1 -181
  5. package/dist/packem_shared/MAP_VERSION-MQ5W2B6n.mjs +1 -0
  6. package/dist/packem_shared/actionFetchSsrf-BK-kvOPO.mjs +1 -0
  7. package/dist/packem_shared/actionWithoutErrorHandling-4GCBT0_z.mjs +1 -0
  8. package/dist/packem_shared/adminRouteWithoutGuard-DPE7LuNh.mjs +1 -0
  9. package/dist/packem_shared/aiRawRunEscapeHatch-BW7a8kZC.mjs +1 -0
  10. package/dist/packem_shared/aiRunWithoutLogging-CvHHtEN9.mjs +1 -0
  11. package/dist/packem_shared/aiToolSideEffectPromptInjection-9eJXRlKt.mjs +1 -0
  12. package/dist/packem_shared/aiUnboundedGenerationPublic-C17h6sMV.mjs +1 -0
  13. package/dist/packem_shared/allowUnauthenticatedShardAccessEnabled-BQufF5Kb.mjs +1 -0
  14. package/dist/packem_shared/argument-derived-sink-DLXFC79t.mjs +1 -0
  15. package/dist/packem_shared/authApiCallWithoutHeaders-CReJRsGW.mjs +1 -0
  16. package/dist/packem_shared/authCsrfCheckDisabled-CRIVsvb9.mjs +1 -0
  17. package/dist/packem_shared/authEmailVerificationDisabled-BjFlnHr2.mjs +1 -0
  18. package/dist/packem_shared/authScimWithoutTransactions-B5ATiz5A.mjs +1 -0
  19. package/dist/packem_shared/authSecureCookiesDisabled-By8PVum3.mjs +1 -0
  20. package/dist/packem_shared/authSessionFreshageZero-DuxX3Ln9.mjs +1 -0
  21. package/dist/packem_shared/authTrustedOriginsWildcard-BKIh6788.mjs +1 -0
  22. package/dist/packem_shared/browserAllowPrivateTargets-Cj5sizhv.mjs +1 -0
  23. package/dist/packem_shared/browserUserUrlWithoutAllowlist-CzDEraJB.mjs +1 -0
  24. package/dist/packem_shared/callSiteCallers-DArHfQSU.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/commitOrderedHardDelete-BPdwOKA7.mjs +1 -0
  28. package/dist/packem_shared/compareToBaseline-CUG59IOy.mjs +1 -0
  29. package/dist/packem_shared/containerInstanceKeyFromUserInput-Bl-tAzKV.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-4tMrhq1k.mjs +1 -0
  33. package/dist/packem_shared/containerStartEnableInternetOverride-mN_kuRtk.mjs +1 -0
  34. package/dist/packem_shared/crossModuleTableWrite-DjiVTAwK.mjs +1 -0
  35. package/dist/packem_shared/dedupeCacheKeys-DtBOHffV.mjs +1 -0
  36. package/dist/packem_shared/dispatch-namespace-BE_LxN7Z.mjs +1 -0
  37. package/dist/packem_shared/dispatchCycle-CWc5Q0cd.mjs +1 -0
  38. package/dist/packem_shared/duplicateIndex-Cip6-Rpu.mjs +1 -0
  39. package/dist/packem_shared/emptyIndex-BnHDcXza.mjs +1 -0
  40. package/dist/packem_shared/errorWithoutCatalog-BTfvaXHR.mjs +1 -0
  41. package/dist/packem_shared/exportSinkMisconfigured-JfbAx9AI.mjs +1 -0
  42. package/dist/packem_shared/externalSourceIncrementalNoDeletePath-BCzm3HzF.mjs +1 -0
  43. package/dist/packem_shared/externalSourceOnGlobal-CH7xbJ49.mjs +1 -0
  44. package/dist/packem_shared/externalSourceUnscoped-BxU2uSXk.mjs +1 -0
  45. package/dist/packem_shared/fanOutBreadth-CBtmZnoh.mjs +1 -0
  46. package/dist/packem_shared/filterAfterIndexScan-CV8aPq7v.mjs +1 -0
  47. package/dist/packem_shared/filterOnPrimaryKey-BeHVg4w3.mjs +1 -0
  48. package/dist/packem_shared/filterWithoutIndex-BGv3jbgg.mjs +1 -0
  49. package/dist/packem_shared/finding-NrKO8idM.mjs +1 -0
  50. package/dist/packem_shared/fk-index-DXYS17cc.mjs +1 -0
  51. package/dist/packem_shared/flagGatesSecurityWithUnsafeDefault-B5FBDF8c.mjs +1 -0
  52. package/dist/packem_shared/flagReadInSubscription-DabIhYGD.mjs +1 -0
  53. package/dist/packem_shared/fromServerSchema-KWJd4PLE.mjs +1 -0
  54. package/dist/packem_shared/geoIndexFieldNotGeopoint-BMQuok2-.mjs +1 -0
  55. package/dist/packem_shared/geoIndexUnused-D7C9Qr4U.mjs +1 -0
  56. package/dist/packem_shared/globalTableNearColumnLimit-BFbBBd6A.mjs +1 -0
  57. package/dist/packem_shared/gradeFromScore-KSt58rj1.mjs +1 -0
  58. package/dist/packem_shared/hardcodedSecret-Bw_4FYrs.mjs +1 -0
  59. package/dist/packem_shared/helpers-DdZ_pyR3.mjs +1 -0
  60. package/dist/packem_shared/hotShard-BwGYZ3Tq.mjs +1 -0
  61. package/dist/packem_shared/httpActionMissingAuthGuard-DhMIs1EI.mjs +1 -0
  62. package/dist/packem_shared/httpActionResponseHeaderInjection-mcRYd3Zw.mjs +1 -0
  63. package/dist/packem_shared/hyperdriveOutsideAction-CPDdGP2g.mjs +1 -0
  64. package/dist/packem_shared/identityUndeclaredClaimTrusted-D1sEd5lr.mjs +1 -0
  65. package/dist/packem_shared/imagesUrlSourceFromUserInput-BJXotuyO.mjs +1 -0
  66. package/dist/packem_shared/indexReferencesUnknownField-DKz4-bla.mjs +1 -0
  67. package/dist/packem_shared/indexUtilization-CkVPZcVe.mjs +1 -0
  68. package/dist/packem_shared/insertManyUnsafeUserData-DWE8d_DF.mjs +1 -0
  69. package/dist/packem_shared/kvUnscopedUserKeyIdor-DhymglIq.mjs +1 -0
  70. package/dist/packem_shared/mailInboundDispatchWithoutVerify-CWwqXyPX.mjs +1 -0
  71. package/dist/packem_shared/mailRecipientFromRequestInput-BVXsZ4Xf.mjs +1 -0
  72. package/dist/packem_shared/maskUncoveredPiiColumn-axEka4nd.mjs +1 -0
  73. package/dist/packem_shared/maskWeakHashStrategyOnPii-DJasagzX.mjs +1 -0
  74. package/dist/packem_shared/maskedRelationLeakViaWith-CIFij6vE.mjs +1 -0
  75. package/dist/packem_shared/mutatorFullRowReplace-BzRpZ47r.mjs +1 -0
  76. package/dist/packem_shared/mutatorWithoutOwnerScope-BoS81ns0.mjs +1 -0
  77. package/dist/packem_shared/nondeterministicQueryMutation-Bbb-AGOS.mjs +1 -0
  78. package/dist/packem_shared/normalizeIdUsedAsAuthorization-BXN-Can6.mjs +1 -0
  79. package/dist/packem_shared/notifyMissingPushConfig-DeQMoNwm.mjs +1 -0
  80. package/dist/packem_shared/notifySendOutsideAction-BJrDkr4h.mjs +1 -0
  81. package/dist/packem_shared/outputProjectionMissingOnPublicRead-DWSeo7oC.mjs +1 -0
  82. package/dist/packem_shared/ownerFieldFromArgsNotAuth-J8Xbz5uI.mjs +1 -0
  83. package/dist/packem_shared/paymentCreateWithoutAuthorize-CdenWpXD.mjs +1 -0
  84. package/dist/packem_shared/paymentWebhookWideTolerance-DVhJB0Ip.mjs +1 -0
  85. package/dist/packem_shared/plaintextSecretInWranglerVariables-bNkqmNqV.mjs +1 -0
  86. package/dist/packem_shared/policyReferencesUnknownTable-CIKuRZ5Y.mjs +1 -0
  87. package/dist/packem_shared/privilegedDispatchUnvalidatedPayload-BM3NtT57.mjs +1 -0
  88. package/dist/packem_shared/privilegedFanoutFromPublicProcedure-DV188gHQ.mjs +1 -0
  89. package/dist/packem_shared/procedure-protections-DRQotu9I.mjs +1 -0
  90. package/dist/packem_shared/procedureWithoutStructuredEvent-CZ_B23nM.mjs +1 -0
  91. package/dist/packem_shared/publicArgumentUsesAny-BDEvMA8Q.mjs +1 -0
  92. package/dist/packem_shared/publicMutationWithoutRatelimit-Bd-rRxnz.mjs +1 -0
  93. package/dist/packem_shared/publicTableRlsOptoutConfusion-CbNk75Xo.mjs +1 -0
  94. package/dist/packem_shared/queueWithoutDlq-BvHz3Opg.mjs +1 -0
  95. package/dist/packem_shared/r2sqlOutsideAction-BdLiLOAX.mjs +1 -0
  96. package/dist/packem_shared/ratelimitDefaultMemoryStore-Dj0Kk7t9.mjs +1 -0
  97. package/dist/packem_shared/ratelimitKeySpoofableOrGlobal-DHMl74hR.mjs +1 -0
  98. package/dist/packem_shared/ratelimitMiddlewareFailOpen-Di2t8pDc.mjs +1 -0
  99. package/dist/packem_shared/relationReferencesUnknownField-Cl6mik8w.mjs +1 -0
  100. package/dist/packem_shared/relationReferencesUnknownTable-vYjirSxf.mjs +1 -0
  101. package/dist/packem_shared/rlsUncoveredTable-CZ4ie4gX.mjs +1 -0
  102. package/dist/packem_shared/selfRescheduleWithoutProgress-Be9d1bQ0.mjs +1 -0
  103. package/dist/packem_shared/shapeTargetsGlobalTable-Bu3eEDic.mjs +1 -0
  104. package/dist/packem_shared/shapeUnknownTable-CREfNnWi.mjs +1 -0
  105. package/dist/packem_shared/signupMutationWithoutDisposableGating-BdKwtxDP.mjs +1 -0
  106. package/dist/packem_shared/softDeleteIncludeDeletedFromArgs-C7Ugg4zb.mjs +1 -0
  107. package/dist/packem_shared/sqlInjectionRisk-BYRllgPV.mjs +1 -0
  108. package/dist/packem_shared/storageGenerateUploadUrlNoContentTypePin-CrwFuo83.mjs +1 -0
  109. package/dist/packem_shared/storageKeyFromUserArgs-3vkxmI3v.mjs +1 -0
  110. package/dist/packem_shared/storagePresignedUrlForPrivateContent-C3KeV5tV.mjs +1 -0
  111. package/dist/packem_shared/storageUploadWithoutContentTypeAllowlist-BXoK_QWE.mjs +1 -0
  112. package/dist/packem_shared/storageUploadWithoutMaxSize-CW7uKvSi.mjs +1 -0
  113. package/dist/packem_shared/sweeps-JWMwcakB.mjs +1 -0
  114. package/dist/packem_shared/tableWithoutInsert-ClJp_jDr.mjs +1 -0
  115. package/dist/packem_shared/throughRelationMissingUniqueIndex-Cg5e3GkS.mjs +1 -0
  116. package/dist/packem_shared/topicTooManySubscriptions-CiZhFm9r.mjs +1 -0
  117. package/dist/packem_shared/ttlFieldNotTimestamp-lnb5nmM6.mjs +1 -0
  118. package/dist/packem_shared/unboundedCollectOnOpenRange-DHy2RUL7.mjs +1 -0
  119. package/dist/packem_shared/unboundedLoop-DxN3_apu.mjs +1 -0
  120. package/dist/packem_shared/unboundedStringArgument-jZnCbwB0.mjs +1 -0
  121. package/dist/packem_shared/unindexedForeignKey-Dypgn8uH.mjs +1 -0
  122. package/dist/packem_shared/unindexedRelationTarget-Dj2_NOCm.mjs +1 -0
  123. package/dist/packem_shared/unrestrictedWhereBranch-CcIBmHik.mjs +1 -0
  124. package/dist/packem_shared/userCreatingMutationWithoutCaptcha-D4TZEpgz.mjs +1 -0
  125. package/dist/packem_shared/vectorsNamespaceFromUserInput-BWvGjJzp.mjs +1 -0
  126. package/dist/packem_shared/workflowDuplicateStepName-B6eAJzwA.mjs +1 -0
  127. package/dist/packem_shared/workflowUnknownTarget-DEKB5H0g.mjs +1 -0
  128. package/dist/packem_shared/workflowUnused-DHwwE-1P.mjs +1 -0
  129. package/package.json +3 -3
  130. package/dist/packem_shared/AE_METRIC_EVENTS-BM14d0lm.mjs +0 -87
  131. package/dist/packem_shared/actionFetchSsrf-wbmYgzmz.mjs +0 -26
  132. package/dist/packem_shared/adminRouteWithoutGuard-UUGBkAjU.mjs +0 -33
  133. package/dist/packem_shared/aiRawRunEscapeHatch-C29jd32J.mjs +0 -26
  134. package/dist/packem_shared/aiToolSideEffectPromptInjection-BtD3alqB.mjs +0 -33
  135. package/dist/packem_shared/aiUnboundedGenerationPublic-CbAcSxLB.mjs +0 -26
  136. package/dist/packem_shared/allowUnauthenticatedShardAccessEnabled-BcASq-jP.mjs +0 -30
  137. package/dist/packem_shared/argument-derived-sink-C1xTTqAt.mjs +0 -30
  138. package/dist/packem_shared/authApiCallWithoutHeaders-BeJhCZaf.mjs +0 -38
  139. package/dist/packem_shared/authCsrfCheckDisabled-DCf9FSoD.mjs +0 -26
  140. package/dist/packem_shared/authEmailVerificationDisabled-Dk-Tc5oU.mjs +0 -26
  141. package/dist/packem_shared/authSecureCookiesDisabled-osJrHW9Y.mjs +0 -26
  142. package/dist/packem_shared/authSessionFreshageZero-CjufCc_Q.mjs +0 -26
  143. package/dist/packem_shared/authTrustedOriginsWildcard-ylv4PKzC.mjs +0 -26
  144. package/dist/packem_shared/browserAllowPrivateTargets-CBvQxLAM.mjs +0 -26
  145. package/dist/packem_shared/browserUserUrlWithoutAllowlist-CvhA6w59.mjs +0 -26
  146. package/dist/packem_shared/circularFk-B2freHrP.mjs +0 -84
  147. package/dist/packem_shared/constraintValidator-Dr9Py3FD.mjs +0 -186
  148. package/dist/packem_shared/containerInstanceKeyFromUserInput-BUXj2J4w.mjs +0 -19
  149. package/dist/packem_shared/containerOversizedInstance-5U1VKPRM.mjs +0 -36
  150. package/dist/packem_shared/containerPublicInternet-CuNerJE5.mjs +0 -30
  151. package/dist/packem_shared/containerRuntimeEgressRelaxation-B0LlfmDA.mjs +0 -26
  152. package/dist/packem_shared/containerStartEnableInternetOverride-DDaHZQ1L.mjs +0 -26
  153. package/dist/packem_shared/duplicateIndex-BOublMSt.mjs +0 -57
  154. package/dist/packem_shared/emptyIndex-BX8EuEY7.mjs +0 -32
  155. package/dist/packem_shared/externalSourceOnGlobal-Bg-NfCX9.mjs +0 -30
  156. package/dist/packem_shared/externalSourceUnscoped-5vT-Bup3.mjs +0 -44
  157. package/dist/packem_shared/filterWithoutIndex-BYVeJaSs.mjs +0 -31
  158. package/dist/packem_shared/finding-Dm_zvzS1.mjs +0 -16
  159. package/dist/packem_shared/fk-index-IUK1ukgs.mjs +0 -7
  160. package/dist/packem_shared/flagGatesSecurityWithUnsafeDefault-BhIs0shr.mjs +0 -41
  161. package/dist/packem_shared/fromServerSchema-BiB6JSiW.mjs +0 -59
  162. package/dist/packem_shared/hardcodedSecret-W2pz1UZB.mjs +0 -35
  163. package/dist/packem_shared/helpers-BySnKhVB.mjs +0 -31
  164. package/dist/packem_shared/hotShard-Ir5D0B6J.mjs +0 -48
  165. package/dist/packem_shared/httpActionMissingAuthGuard-CxipddNx.mjs +0 -35
  166. package/dist/packem_shared/httpActionResponseHeaderInjection-DOFS7pFT.mjs +0 -39
  167. package/dist/packem_shared/hyperdriveOutsideAction-BgZqX7Xg.mjs +0 -30
  168. package/dist/packem_shared/identityUndeclaredClaimTrusted-D8nXV2dd.mjs +0 -32
  169. package/dist/packem_shared/imagesUrlSourceFromUserInput-YcDQ0b_Y.mjs +0 -19
  170. package/dist/packem_shared/indexReferencesUnknownField-BSWNngxX.mjs +0 -36
  171. package/dist/packem_shared/indexUtilization-B5DMQ3bI.mjs +0 -45
  172. package/dist/packem_shared/insertManyUnsafeUserData-Dn77XpmX.mjs +0 -26
  173. package/dist/packem_shared/kvUnscopedUserKeyIdor-YWwmfE8X.mjs +0 -19
  174. package/dist/packem_shared/mailInboundDispatchWithoutVerify-CHSRA8zz.mjs +0 -28
  175. package/dist/packem_shared/mailRecipientFromRequestInput-Cka2qu5J.mjs +0 -26
  176. package/dist/packem_shared/maskUncoveredPiiColumn-DjGIPG6M.mjs +0 -61
  177. package/dist/packem_shared/maskWeakHashStrategyOnPii-1c4q8Opf.mjs +0 -35
  178. package/dist/packem_shared/maskedRelationLeakViaWith-CPI4s0sl.mjs +0 -76
  179. package/dist/packem_shared/mutatorFullRowReplace-BJnNDaIV.mjs +0 -26
  180. package/dist/packem_shared/nondeterministicQueryMutation-GXES1fLp.mjs +0 -35
  181. package/dist/packem_shared/normalizeIdUsedAsAuthorization-BVPtCpzT.mjs +0 -50
  182. package/dist/packem_shared/outputProjectionMissingOnPublicRead-Bl5IMx0k.mjs +0 -51
  183. package/dist/packem_shared/ownerFieldFromArgsNotAuth-mOw3hE5z.mjs +0 -26
  184. package/dist/packem_shared/paymentCreateWithoutAuthorize-BYm4JLxo.mjs +0 -26
  185. package/dist/packem_shared/paymentWebhookWideTolerance-D9QQJMJq.mjs +0 -35
  186. package/dist/packem_shared/plaintextSecretInWranglerVariables-NKO4YkKf.mjs +0 -35
  187. package/dist/packem_shared/policyReferencesUnknownTable-DtaIEovd.mjs +0 -38
  188. package/dist/packem_shared/privilegedDispatchUnvalidatedPayload-5Forjckt.mjs +0 -37
  189. package/dist/packem_shared/privilegedFanoutFromPublicProcedure-D3dL01B8.mjs +0 -26
  190. package/dist/packem_shared/publicArgumentUsesAny-C71b2NCf.mjs +0 -32
  191. package/dist/packem_shared/publicMutationWithoutRatelimit-DbIhgi7j.mjs +0 -36
  192. package/dist/packem_shared/publicTableRlsOptoutConfusion-Bn71yoD4.mjs +0 -38
  193. package/dist/packem_shared/queueWithoutDlq-CSkGNb_0.mjs +0 -41
  194. package/dist/packem_shared/r2sqlOutsideAction-CtqxvMuV.mjs +0 -30
  195. package/dist/packem_shared/ratelimitDefaultMemoryStore-BISChG5C.mjs +0 -26
  196. package/dist/packem_shared/ratelimitKeySpoofableOrGlobal-DqlHYQQ3.mjs +0 -26
  197. package/dist/packem_shared/ratelimitMiddlewareFailOpen-CJgDCaUw.mjs +0 -33
  198. package/dist/packem_shared/relationReferencesUnknownField-CjbLScJ1.mjs +0 -54
  199. package/dist/packem_shared/relationReferencesUnknownTable-DrorpKYe.mjs +0 -33
  200. package/dist/packem_shared/rlsUncoveredTable-CxEfZ5eZ.mjs +0 -56
  201. package/dist/packem_shared/shapeTargetsGlobalTable-DHrf4Koi.mjs +0 -34
  202. package/dist/packem_shared/shapeUnknownTable-C8aDWFoe.mjs +0 -34
  203. package/dist/packem_shared/softDeleteIncludeDeletedFromArgs-BLqDKrkM.mjs +0 -38
  204. package/dist/packem_shared/sqlInjectionRisk-zwytYGLt.mjs +0 -26
  205. package/dist/packem_shared/storageGenerateUploadUrlNoContentTypePin-Da4L9Ge8.mjs +0 -26
  206. package/dist/packem_shared/storageKeyFromUserArgs-B86elJgS.mjs +0 -19
  207. package/dist/packem_shared/storagePresignedUrlForPrivateContent-yGmb8uqz.mjs +0 -47
  208. package/dist/packem_shared/storageUploadWithoutContentTypeAllowlist-BV-gF1lT.mjs +0 -27
  209. package/dist/packem_shared/storageUploadWithoutMaxSize-DpxO59wU.mjs +0 -27
  210. package/dist/packem_shared/tableWithoutInsert-CbbaYIP4.mjs +0 -34
  211. package/dist/packem_shared/unboundedStringArgument-DThg2-wt.mjs +0 -32
  212. package/dist/packem_shared/unindexedForeignKey-BgJbKyqK.mjs +0 -45
  213. package/dist/packem_shared/unindexedRelationTarget-D6eyj6Xx.mjs +0 -53
  214. package/dist/packem_shared/userCreatingMutationWithoutCaptcha-2DZtWIPb.mjs +0 -42
  215. package/dist/packem_shared/vectorsNamespaceFromUserInput-CQhr5bVn.mjs +0 -19
  216. package/dist/packem_shared/workflowDuplicateStepName-ioBxPBCy.mjs +0 -48
  217. package/dist/packem_shared/workflowUnknownTarget-Cdd7WhKQ.mjs +0 -34
  218. package/dist/packem_shared/workflowUnused-D0jHxdz9.mjs +0 -38
@@ -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,50 +0,0 @@
1
- import { e as emit } from './finding-Dm_zvzS1.mjs';
2
-
3
- const SINK_LABELS = {
4
- delete: "deletes the row",
5
- get: "reads the row",
6
- patch: "patches the row"
7
- };
8
- const normalizeIdUsedAsAuthorization = {
9
- categories: ["SECURITY"],
10
- description: "A `.public()` `query`/`mutation` gates a `ctx.db.get`/`patch`/`delete` on a null-checked `ctx.db.normalizeId(table, id)` result with no ownership predicate and no RLS. `normalizeId` validates id *shape* only — it never reads the database — so a non-null result proves the id is well-formed, never that the caller owns the row (an IDOR).",
11
- facing: "EXTERNAL",
12
- level: "INFO",
13
- name: "normalize_id_used_as_authorization",
14
- remediation: 'Don\'t treat a non-null `normalizeId` result as authorization — it only validates id shape. After loading the row, compare its ownership column (`userId`/`ownerId`/…) against the server-trusted identity (`ctx.auth.userId`), or protect the procedure with `.use(rls(...))` / put the schema in `.rls("required")` mode so the row policy runs on every access.',
15
- run: (context) => {
16
- if (context.normalizeIdAuthorizations === void 0) {
17
- return [];
18
- }
19
- const rlsRequired = context.schema.rlsMode === "required";
20
- const rlsOptOutTables = new Set(context.schema.tables.filter((table) => table.isPublic).map((table) => table.name));
21
- const findings = [];
22
- for (const row of context.normalizeIdAuthorizations) {
23
- if (row.visibility !== "public" || row.usesRls || row.mentionsOwnership) {
24
- continue;
25
- }
26
- if (rlsRequired && (row.table === "" || !rlsOptOutTables.has(row.table))) {
27
- continue;
28
- }
29
- const target = row.table === "" ? "the row" : `the \`${row.table}\` row`;
30
- findings.push(
31
- emit(normalizeIdUsedAsAuthorization, {
32
- cacheKey: `normalize_id_used_as_authorization:${row.file}:${row.line.toString()}`,
33
- detail: `Public procedure \`${row.exportName}\` (${row.file}:${row.line.toString()}) gates access on \`ctx.db.normalizeId(...)\` and then ${SINK_LABELS[row.sinkMethod]} — but \`normalizeId\` only validates id shape, it never reads the database or checks ownership. Any caller supplying a well-formed id of ${target} reaches it (IDOR). Compare the loaded row's ownership column against \`ctx.auth\`, or cover the procedure with \`.use(rls(...))\`.`,
34
- metadata: {
35
- exportName: row.exportName,
36
- file: row.file,
37
- line: row.line,
38
- sinkMethod: row.sinkMethod,
39
- table: row.table
40
- }
41
- })
42
- );
43
- }
44
- return findings;
45
- },
46
- source: "static",
47
- title: "normalizeId result used as authorization (validates id shape, not ownership)"
48
- };
49
-
50
- export { normalizeIdUsedAsAuthorization as default };
@@ -1,51 +0,0 @@
1
- import { e as emit } from './finding-Dm_zvzS1.mjs';
2
- import { P as PII_FIELD_NAMES } from './helpers-BySnKhVB.mjs';
3
-
4
- const outputProjectionMissingOnPublicRead = {
5
- categories: ["SECURITY"],
6
- description: "A `.public()` `query` returns raw table rows (no `.output(...)` projection, no `.use(mask(...))`) from a table carrying PII-named columns (`email`, `phone`, `ssn`, …). Every column ships to the caller, and a column added to the table later leaks by default.",
7
- facing: "EXTERNAL",
8
- level: "INFO",
9
- name: "output_projection_missing_on_public_read",
10
- remediation: "Add an explicit return projection to the public query — `.output(v.object({ … }))` listing only the fields a client needs — or apply a `.use(mask(...))` policy to the PII columns. This makes the exposed shape intentional and stops a newly-added column from leaking through this query by default.",
11
- run: (context) => {
12
- if (context.rawRowReturns === void 0) {
13
- return [];
14
- }
15
- const piiColumnsByTable = /* @__PURE__ */ new Map();
16
- for (const table of context.schema.tables) {
17
- const piiColumns = table.fields.filter((field) => PII_FIELD_NAMES.has(field));
18
- if (piiColumns.length > 0) {
19
- piiColumnsByTable.set(table.name, piiColumns);
20
- }
21
- }
22
- const findings = [];
23
- for (const row of context.rawRowReturns) {
24
- if (row.visibility !== "public" || row.usesOutput || row.usesMask || row.table === "") {
25
- continue;
26
- }
27
- const piiColumns = piiColumnsByTable.get(row.table);
28
- if (piiColumns === void 0) {
29
- continue;
30
- }
31
- findings.push(
32
- emit(outputProjectionMissingOnPublicRead, {
33
- cacheKey: `output_projection_missing_on_public_read:${row.file}:${row.line.toString()}`,
34
- detail: `Public query \`${row.exportName}\` (${row.file}:${row.line.toString()}) returns raw \`${row.table}\` rows with no \`.output(...)\` projection — shipping PII column(s) ${piiColumns.join(", ")} to every caller, and any column added to \`${row.table}\` later leaks by default. Project the return with \`.output(v.object({ … }))\` or mask the PII columns.`,
35
- metadata: {
36
- columns: piiColumns,
37
- exportName: row.exportName,
38
- file: row.file,
39
- line: row.line,
40
- table: row.table
41
- }
42
- })
43
- );
44
- }
45
- return findings;
46
- },
47
- source: "static",
48
- title: "Public query returns raw rows with PII and no output projection"
49
- };
50
-
51
- export { outputProjectionMissingOnPublicRead as default };
@@ -1,26 +0,0 @@
1
- import { e as emit } from './finding-Dm_zvzS1.mjs';
2
-
3
- const ownerFieldFromArgsNotAuth = {
4
- categories: ["SECURITY"],
5
- description: "A `ctx.db` write sets an ownership/identity column (`userId`, `ownerId`, `tenantId`, …) from the handler's `args`. The caller controls who the row belongs to, so any caller can write rows owned by another user or tenant — an act-as-any-user / cross-tenant IDOR.",
6
- facing: "EXTERNAL",
7
- level: "ERROR",
8
- name: "owner_field_from_args_not_auth",
9
- remediation: "Stamp the ownership column from the server-trusted identity (`ctx.auth.userId` / `ctx.identity`), never from request input. Drop the field from the accepted `args` so a caller cannot supply it.",
10
- run: (context) => {
11
- if (context.ownerFieldWrites === void 0) {
12
- return [];
13
- }
14
- return context.ownerFieldWrites.map(
15
- (write) => emit(ownerFieldFromArgsNotAuth, {
16
- cacheKey: `owner_field_from_args_not_auth:${write.file}:${write.line.toString()}:${write.field}`,
17
- detail: `\`${write.method}\` in \`${write.exportName}\` (${write.file}:${write.line.toString()}) sets the ownership field \`${write.field}\` from \`args\` instead of the server-trusted identity — any caller can write rows owned by another user/tenant (IDOR). Stamp \`${write.field}\` from \`ctx.auth\`/\`ctx.identity\`, never from request input.`,
18
- metadata: { exportName: write.exportName, field: write.field, file: write.file, line: write.line, method: write.method }
19
- })
20
- );
21
- },
22
- source: "static",
23
- title: "Ownership field written from args, not server identity"
24
- };
25
-
26
- export { ownerFieldFromArgsNotAuth as default };
@@ -1,26 +0,0 @@
1
- import { e as emit } from './finding-Dm_zvzS1.mjs';
2
-
3
- const paymentCreateWithoutAuthorize = {
4
- categories: ["SECURITY"],
5
- description: "`createPayment({...})` was constructed without an `authorize` callback, so its access-control gate never runs — any caller that reaches a payment reference id can drive a charge, refund, or subscription change against it (a broken-access-control / IDOR risk on money movement).",
6
- facing: "EXTERNAL",
7
- level: "ERROR",
8
- name: "payment_create_without_authorize",
9
- remediation: "Pass an `authorize(referenceId)` callback to `createPayment({...})` that verifies the current caller (via `ctx.auth`) owns the payment/subscription reference before the operation runs. Prefer the `ctx.payments` accessor, which is authorization-scoped by default.",
10
- run: (context) => {
11
- if (context.configCalls === void 0) {
12
- return [];
13
- }
14
- return context.configCalls.filter((call) => call.callee === "createPayment" && call.analyzable && !call.presentKeys.includes("authorize")).map(
15
- (call) => emit(paymentCreateWithoutAuthorize, {
16
- cacheKey: `payment_create_without_authorize:${call.file}:${call.line.toString()}`,
17
- detail: `\`createPayment({...})\` in ${call.file}:${call.line.toString()} has no \`authorize\` callback — its access-control gate never runs, so any caller reaching the payment reference can move money against it. Add an \`authorize(referenceId)\` gate that checks \`ctx.auth\` ownership before the operation.`,
18
- metadata: { callee: call.callee, file: call.file, line: call.line }
19
- })
20
- );
21
- },
22
- source: "static",
23
- title: "createPayment without an authorize gate"
24
- };
25
-
26
- export { paymentCreateWithoutAuthorize as default };
@@ -1,35 +0,0 @@
1
- import { e as emit } from './finding-Dm_zvzS1.mjs';
2
-
3
- const WIDE_TOLERANCE_THRESHOLD_SECONDS = 60 * 60;
4
- const paymentWebhookWideTolerance = {
5
- categories: ["SECURITY"],
6
- description: "A payment webhook adapter configured with an implausibly wide `webhookToleranceSeconds` (over an hour). The timestamp tolerance is the webhook replay window; widening it far past the 300s default keeps captured signed events replayable long after capture, defeating the replay check.",
7
- facing: "EXTERNAL",
8
- level: "WARN",
9
- name: "payment_webhook_wide_tolerance",
10
- remediation: "Keep `webhookToleranceSeconds` at or near the 300s default — just enough for clock skew. If a wide window was set to work around retries or backfills, handle those with idempotency on the event id instead, not by loosening the replay-window guard.",
11
- run: (context) => {
12
- if (context.paymentWebhooks === void 0) {
13
- return [];
14
- }
15
- const isWide = (row) => row.toleranceSeconds !== void 0 && row.toleranceSeconds >= WIDE_TOLERANCE_THRESHOLD_SECONDS;
16
- return context.paymentWebhooks.filter((row) => isWide(row)).map((row) => {
17
- const location = `\`${row.exportName}\` (${row.file}:${row.line.toString()})`;
18
- return emit(paymentWebhookWideTolerance, {
19
- cacheKey: `payment_webhook_wide_tolerance:${row.file}:${row.line.toString()}`,
20
- detail: `\`${row.callee}\` in ${location} sets \`webhookToleranceSeconds\` to ${(row.toleranceSeconds ?? 0).toString()}s (default 300s). That replay window keeps captured signed events valid long after capture.`,
21
- metadata: {
22
- callee: row.callee,
23
- exportName: row.exportName,
24
- file: row.file,
25
- line: row.line,
26
- toleranceSeconds: row.toleranceSeconds
27
- }
28
- });
29
- });
30
- },
31
- source: "static",
32
- title: "Payment webhook replay-tolerance window is implausibly wide"
33
- };
34
-
35
- export { paymentWebhookWideTolerance as default };
@@ -1,35 +0,0 @@
1
- import { e as emit } from './finding-Dm_zvzS1.mjs';
2
-
3
- const plaintextSecretInWranglerVariables = {
4
- categories: ["SECURITY"],
5
- description: "A plaintext secret is committed to `wrangler.jsonc`'s `vars` block. `vars` are cleartext — baked into the deployed Worker and checked into source control — so the secret leaks via the repo and the bundle, and rotating it needs a config edit plus a redeploy.",
6
- facing: "INTERNAL",
7
- level: "ERROR",
8
- name: "plaintext_secret_in_wrangler_vars",
9
- remediation: "Move the value out of `vars`: bind it through a Secrets Store (`ctx.secrets.get(<NAME>)`) or set it with `wrangler secret put <NAME>` and read it from `env.<NAME>`. Delete the `vars` entry and rotate the exposed value — assume it is already compromised. Public/publishable keys (name them `*_PUBLIC` / `*_PUBLISHABLE`) are exempt.",
10
- run: (context) => {
11
- if (context.wranglerVariables === void 0) {
12
- return [];
13
- }
14
- const findings = [];
15
- const occurrenceCount = /* @__PURE__ */ new Map();
16
- for (const wranglerVariable of context.wranglerVariables) {
17
- const baseKey = `${wranglerVariable.file}:${wranglerVariable.key}`;
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(plaintextSecretInWranglerVariables, {
23
- cacheKey: `plaintext_secret_in_wrangler_vars:${baseKey}${occurrenceSuffix}`,
24
- detail: `The \`vars\` entry \`${wranglerVariable.key}\` in ${wranglerVariable.file} holds a plaintext ${wranglerVariable.kind.replaceAll("_", " ")} (${wranglerVariable.preview}). \`vars\` ship in cleartext to the deployed Worker and are committed to source control — move it to a Secrets Store binding or \`wrangler secret put\` and rotate the exposed value.`,
25
- metadata: { file: wranglerVariable.file, key: wranglerVariable.key, kind: wranglerVariable.kind, preview: wranglerVariable.preview }
26
- })
27
- );
28
- }
29
- return findings;
30
- },
31
- source: "static",
32
- title: "Plaintext secret in wrangler vars"
33
- };
34
-
35
- export { plaintextSecretInWranglerVariables 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,37 +0,0 @@
1
- import { e as emit } from './finding-Dm_zvzS1.mjs';
2
-
3
- const privilegedDispatchUnvalidatedPayload = {
4
- categories: ["SECURITY"],
5
- description: "A `defineQueue`/`defineWorkflow` handler forwards its untrusted payload (queue body / workflow params) straight into a `ctx.run` dispatch whose target is RLS-gated. The handler runs under the system identity with RLS disabled, so the target's row policy — the only thing guarding it — is bypassed, letting a caller act as any user / write across tenants.",
6
- facing: "EXTERNAL",
7
- level: "ERROR",
8
- name: "privileged_dispatch_unvalidated_payload",
9
- remediation: "Don't forward an unchecked queue body / workflow param into an RLS-gated function. Re-derive the ownership/identity keys from server-trusted state (or an argument you validate against it) before dispatching, or route the work through a function that re-checks the caller's identity rather than relying on the row policy the privileged handler skips.",
10
- run: (context) => {
11
- if (context.privilegedDispatches === void 0) {
12
- return [];
13
- }
14
- const rlsProcedures = context.rlsProcedures ?? [];
15
- const targetUsesRls = (targetFile, targetExport) => rlsProcedures.some((procedure) => procedure.usesRls && procedure.file === targetFile && procedure.exportName === targetExport);
16
- return context.privilegedDispatches.filter((dispatch) => targetUsesRls(dispatch.targetFile, dispatch.targetExport)).map((dispatch) => {
17
- const handlerKind = dispatch.dispatchKind === "queue" ? "queue" : "workflow";
18
- const payloadSource = dispatch.dispatchKind === "queue" ? "queue message body" : "workflow params";
19
- return emit(privilegedDispatchUnvalidatedPayload, {
20
- cacheKey: `privileged_dispatch_unvalidated_payload:${dispatch.file}:${dispatch.line.toString()}`,
21
- detail: `The ${handlerKind} handler \`${dispatch.handlerExport}\` (${dispatch.file}:${dispatch.line.toString()}) forwards its untrusted ${payloadSource} straight into \`${dispatch.targetFile}.${dispatch.targetExport}\`, which is guarded by \`.use(rls(...))\`. The handler runs under the system identity with RLS disabled, so the target's row policy is bypassed — any ownership/identity field the payload controls lets a caller act as another user or write across tenants. Re-derive the identity keys from server-trusted state before dispatching, or route through a function that re-checks the caller.`,
22
- metadata: {
23
- dispatchKind: dispatch.dispatchKind,
24
- file: dispatch.file,
25
- handlerExport: dispatch.handlerExport,
26
- line: dispatch.line,
27
- targetExport: dispatch.targetExport,
28
- targetFile: dispatch.targetFile
29
- }
30
- });
31
- });
32
- },
33
- source: "static",
34
- title: "Privileged handler forwards untrusted payload into an RLS-gated function"
35
- };
36
-
37
- export { privilegedDispatchUnvalidatedPayload as default };
@@ -1,26 +0,0 @@
1
- import { e as emit } from './finding-Dm_zvzS1.mjs';
2
-
3
- const privilegedFanoutFromPublicProcedure = {
4
- categories: ["SECURITY"],
5
- description: "A public procedure fans work out to a privileged dispatch surface (`ctx.scheduler.runAfter`/`runAt`, a `ctx.queues.<name>` send, or `ctx.workflows.<name>.create`) with no rate-limit guard. Each dispatch runs later under the system identity and bills the account, so an anonymous caller can amplify cost and flood the async surface in a loop.",
6
- facing: "EXTERNAL",
7
- level: "WARN",
8
- name: "privileged_fanout_from_public_procedure",
9
- remediation: "Gate the public entry point with a rate limit (`.use(rateLimit(...))` or a `protectPublic({ rateLimit })` bundle), or make the fan-out procedure internal and trigger it only from an authenticated, guarded path. Never expose an unbounded scheduler/queue/workflow dispatch to anonymous callers.",
10
- run: (context) => {
11
- if (context.procedureProtections === void 0) {
12
- return [];
13
- }
14
- return context.procedureProtections.filter((procedure) => procedure.fanOut && procedure.visibility === "public" && !procedure.usesRateLimit).map(
15
- (procedure) => emit(privilegedFanoutFromPublicProcedure, {
16
- cacheKey: `privileged_fanout_from_public_procedure:${procedure.file}:${procedure.exportName}`,
17
- detail: `\`${procedure.exportName}\` (${procedure.file}) is public and fans work out to a privileged dispatch surface (scheduler/queue/workflow) with no rate limit — an anonymous caller can drive that billable, system-identity dispatch in a loop. Add a rate-limit guard, or make it internal and trigger it from an authenticated path.`,
18
- metadata: { exportName: procedure.exportName, file: procedure.file, kind: procedure.kind }
19
- })
20
- );
21
- },
22
- source: "static",
23
- title: "Public procedure fans out to a privileged dispatch surface"
24
- };
25
-
26
- export { privilegedFanoutFromPublicProcedure 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
- import { i as isPublicWrite } from './helpers-BySnKhVB.mjs';
3
-
4
- const SENSITIVE_NAME_RE = /contact|forgot|login|magic|otp|register|reset|signin|signup|subscribe|verify/iu;
5
- const publicMutationWithoutRatelimit = {
6
- categories: ["SECURITY"],
7
- 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.",
8
- facing: "EXTERNAL",
9
- level: "WARN",
10
- name: "public_mutation_without_ratelimit",
11
- 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.',
12
- run: (context) => {
13
- if (context.procedureProtections === void 0) {
14
- return [];
15
- }
16
- const findings = [];
17
- for (const procedure of context.procedureProtections) {
18
- if (!isPublicWrite(procedure) || 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,38 +0,0 @@
1
- import { e as emit } from './finding-Dm_zvzS1.mjs';
2
- import { o as ownershipOrPiiColumns } from './helpers-BySnKhVB.mjs';
3
-
4
- const publicTableRlsOptoutConfusion = {
5
- categories: ["SECURITY"],
6
- description: '`.public()` opts a table OUT of the schema\'s `.rls("required")` enforcement — it does not mean the table holds public data. A `.public()` table whose columns look ownership- or PII-shaped (`userId`, `email`, `ssn`, …) is exempt from the row-security guard, not "safe to expose".',
7
- facing: "EXTERNAL",
8
- level: "WARN",
9
- name: "public_table_rls_optout_confusion",
10
- remediation: 'Double-check this table actually needs the RLS opt-out. If it does not, remove `.public()` so `.rls("required")` covers it like every other table. If it genuinely must stay open (a shared lookup table, for instance), rename or comment it so the exemption is intentional rather than a name-implies-safety mistake, and make sure no procedure trusts `.public()` as an authorization boundary.',
11
- run: (context) => {
12
- if (context.schema.rlsMode !== "required") {
13
- return [];
14
- }
15
- const findings = [];
16
- for (const table of context.schema.tables) {
17
- if (!table.isPublic) {
18
- continue;
19
- }
20
- const flaggedColumns = ownershipOrPiiColumns(table);
21
- if (flaggedColumns.length === 0) {
22
- continue;
23
- }
24
- findings.push(
25
- emit(publicTableRlsOptoutConfusion, {
26
- cacheKey: `public_table_rls_optout_confusion:${table.name}`,
27
- detail: `Table "${table.name}" is \`.public()\` (opted OUT of the schema's \`.rls("required")\` enforcement) but carries ownership/PII-shaped column(s): ${flaggedColumns.join(", ")}. \`.public()\` means "exempt from RLS", not "safe to expose" — confirm this table really needs the opt-out.`,
28
- metadata: { columns: flaggedColumns, table: table.name }
29
- })
30
- );
31
- }
32
- return findings;
33
- },
34
- source: "static",
35
- title: "Public table opts out of RLS but carries sensitive columns"
36
- };
37
-
38
- export { publicTableRlsOptoutConfusion as default };
@@ -1,41 +0,0 @@
1
- import { e as emit } from './finding-Dm_zvzS1.mjs';
2
-
3
- const queueWithoutDlq = {
4
- categories: ["SCHEMA"],
5
- description: "A queue consumer drops a message once it exhausts `maxRetries` (default 3) delivery attempts. Without a `deadLetterQueue`, that message is deleted permanently with no record — route exhausted messages to a DLQ so you can inspect, alert on, and replay them.",
6
- facing: "INTERNAL",
7
- level: "WARN",
8
- name: "queue_without_dlq",
9
- remediation: 'Add `deadLetterQueue: "<name>-dlq"` to the `defineQueue({...})` options and declare a consumer for that queue so exhausted messages are inspectable (an unconsumed DLQ still expires at the retention window). `vis generate lunora-queue` with the best-practice setup scaffolds both. If the queue is fire-and-forget and message loss is acceptable, this advisory can be ignored.',
10
- run: (context) => {
11
- if (context.queues === void 0) {
12
- return [];
13
- }
14
- const dlqTargets = new Set(
15
- context.queues.map((queue) => queue.tuning.deadLetterQueue).filter((name) => name !== void 0 && name !== "")
16
- );
17
- const findings = [];
18
- for (const queue of context.queues) {
19
- const dlq = queue.tuning.deadLetterQueue;
20
- if (dlq !== void 0 && dlq !== "") {
21
- continue;
22
- }
23
- if (dlqTargets.has(queue.name)) {
24
- continue;
25
- }
26
- const maxRetries = typeof queue.tuning.maxRetries === "number" ? queue.tuning.maxRetries : 3;
27
- findings.push(
28
- emit(queueWithoutDlq, {
29
- cacheKey: `queue_without_dlq:${queue.exportName}`,
30
- detail: `Queue "${queue.exportName}" declares no \`deadLetterQueue\`, so a message that exhausts its retries (\`maxRetries\` = ${String(maxRetries)}, ~${String(maxRetries + 1)} delivery attempts) is dropped and permanently lost.`,
31
- metadata: { maxRetries, mode: queue.mode, queue: queue.exportName }
32
- })
33
- );
34
- }
35
- return findings;
36
- },
37
- source: "static",
38
- title: "Queue has no dead-letter queue"
39
- };
40
-
41
- export { queueWithoutDlq 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,26 +0,0 @@
1
- import { e as emit } from './finding-Dm_zvzS1.mjs';
2
-
3
- const ratelimitDefaultMemoryStore = {
4
- categories: ["SECURITY"],
5
- description: "`new RateLimiter({...})` has no `store`, so it falls back to a per-instance in-memory store. On Workers each request may hit a different isolate, so the counter never sums across them — the limit silently under-counts and is effectively a no-op under load, giving false protection against brute-force and cost abuse.",
6
- facing: "EXTERNAL",
7
- level: "WARN",
8
- name: "ratelimit_default_memory_store",
9
- remediation: "Pass a shared, durable `store` to `new RateLimiter({...})` (a Durable-Object- or KV-backed store) so every Worker isolate reads and writes the same bucket. The default in-memory store is per-isolate and does not enforce a global limit.",
10
- run: (context) => {
11
- if (context.configCalls === void 0) {
12
- return [];
13
- }
14
- return context.configCalls.filter((call) => call.callee === "RateLimiter" && call.analyzable && !call.presentKeys.includes("store")).map(
15
- (call) => emit(ratelimitDefaultMemoryStore, {
16
- cacheKey: `ratelimit_default_memory_store:${call.file}:${call.line.toString()}`,
17
- detail: `\`new RateLimiter({...})\` in ${call.file}:${call.line.toString()} has no \`store\` — it uses the per-instance in-memory store, which never sums across Worker isolates, so the limit silently under-counts under load. Pass a Durable-Object- or KV-backed \`store\` so the limit is enforced globally.`,
18
- metadata: { callee: call.callee, file: call.file, line: call.line }
19
- })
20
- );
21
- },
22
- source: "static",
23
- title: "RateLimiter using the default in-memory store"
24
- };
25
-
26
- export { ratelimitDefaultMemoryStore as default };
@@ -1,26 +0,0 @@
1
- import { e as emit } from './finding-Dm_zvzS1.mjs';
2
-
3
- const ratelimitKeySpoofableOrGlobal = {
4
- categories: ["SECURITY"],
5
- description: "A `rateLimit`/`dbRateLimit` middleware call's `key` selector is derived from the handler's `args` with no server-side scoping, so an attacker can rotate the key on every request (e.g. by resubmitting a different email/id) and never share a bucket with themselves — the limit never actually limits them.",
6
- facing: "EXTERNAL",
7
- level: "WARN",
8
- name: "ratelimit_key_spoofable_or_global",
9
- remediation: 'Derive the rate-limit `key` from a server-trusted value — `ctx.auth.userId` for authenticated callers, `ctx.ip` for anonymous ones (e.g. `key: (ctx) => ctx.auth.userId ?? ctx.ip ?? "anon"`) — instead of an `args` field the caller controls.',
10
- run: (context) => {
11
- if (context.ratelimitKeySelectors === void 0) {
12
- return [];
13
- }
14
- return context.ratelimitKeySelectors.map(
15
- (selector) => emit(ratelimitKeySpoofableOrGlobal, {
16
- cacheKey: `ratelimit_key_spoofable_or_global:${selector.file}:${selector.line.toString()}`,
17
- detail: `\`${selector.callee}(…, "${selector.limitName}", { key })\` in \`${selector.exportName}\` (${selector.file}:${selector.line.toString()}) derives its \`key\` from \`args\` with no server-side scoping — an attacker can rotate the key per request and bypass the limit. Derive \`key\` from \`ctx.auth.userId\` / \`ctx.ip\` instead.`,
18
- metadata: { callee: selector.callee, exportName: selector.exportName, file: selector.file, limitName: selector.limitName, line: selector.line }
19
- })
20
- );
21
- },
22
- source: "static",
23
- title: "Rate-limit key derived from spoofable user input"
24
- };
25
-
26
- export { ratelimitKeySpoofableOrGlobal as default };