@lunora/advisor 1.0.0-alpha.25 → 1.0.0-alpha.27
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.mts +30 -7
- package/dist/index.d.ts +30 -7
- package/dist/index.mjs +11 -8
- package/dist/packem_shared/circularFk-CNcAVuYa.mjs +127 -0
- package/dist/packem_shared/{constraintValidator-Dr9Py3FD.mjs → constraintValidator-CxwtpJ6E.mjs} +24 -4
- package/dist/packem_shared/dedupeCacheKeys-r5B7u_yq.mjs +13 -0
- package/dist/packem_shared/{hardcodedSecret-W2pz1UZB.mjs → hardcodedSecret-Be-pKVdn.mjs} +3 -7
- package/dist/packem_shared/hotShard-CkC7qpre.mjs +60 -0
- package/package.json +3 -3
- package/dist/packem_shared/circularFk-B2freHrP.mjs +0 -84
- package/dist/packem_shared/hotShard-Ir5D0B6J.mjs +0 -48
package/dist/index.d.mts
CHANGED
|
@@ -2023,6 +2023,22 @@ interface AnalyticsRuntimeMetrics {
|
|
|
2023
2023
|
*/
|
|
2024
2024
|
declare const loadAnalyticsRuntimeMetrics: (source: AnalyticsMetricsSource, options: AnalyticsMetricsOptions) => Promise<AnalyticsRuntimeMetrics>;
|
|
2025
2025
|
/**
|
|
2026
|
+
* Disambiguate {@link Finding}s that share a `cacheKey`.
|
|
2027
|
+
*
|
|
2028
|
+
* A `cacheKey` is the studio's dedup / dismissal id (see {@link Finding.cacheKey}):
|
|
2029
|
+
* two findings with the same key collapse to one row, and a single dismissal
|
|
2030
|
+
* silences both. A lint keyed on `name:file:line` (the argument-derived sinks,
|
|
2031
|
+
* `sql_injection_risk`, `kv_unscoped_user_key_idor`, `hardcoded_secret`, …) can
|
|
2032
|
+
* legitimately emit two occurrences on one physical source line, so without a
|
|
2033
|
+
* within-line discriminator the second finding is silently hidden.
|
|
2034
|
+
*
|
|
2035
|
+
* This suffixes the second-and-later occurrence of any repeated key with
|
|
2036
|
+
* `:<n>` (`:2`, `:3`, …), leaving the first occurrence unsuffixed so existing
|
|
2037
|
+
* single-occurrence keys stay stable across runs. Order is preserved. Keys are
|
|
2038
|
+
* lint-name-prefixed, so this never merges across lints.
|
|
2039
|
+
*/
|
|
2040
|
+
declare const dedupeCacheKeys: (findings: ReadonlyArray<Finding>) => Finding[];
|
|
2041
|
+
/**
|
|
2026
2042
|
* Constraint validator — flag rows that violate declared FK / NOT NULL / UNIQUE
|
|
2027
2043
|
* constraints by cross-checking sampled row data against the schema.
|
|
2028
2044
|
*
|
|
@@ -2351,7 +2367,7 @@ declare const browserAllowPrivateTargets: Lint;
|
|
|
2351
2367
|
*/
|
|
2352
2368
|
declare const browserUserUrlWithoutAllowlist: Lint;
|
|
2353
2369
|
/**
|
|
2354
|
-
* Detect FK cycles in the declared relation graph
|
|
2370
|
+
* Detect FK cycles in the declared relation graph.
|
|
2355
2371
|
*
|
|
2356
2372
|
* A "circular FK" exists when a chain of `one` relations forms a loop — for
|
|
2357
2373
|
* example `A.authorId → B`, `B.ownerId → C`, `C.postId → A`. Such cycles can
|
|
@@ -2370,11 +2386,18 @@ declare const browserUserUrlWithoutAllowlist: Lint;
|
|
|
2370
2386
|
* would be noise. Only multi-table cycles — the ones the description illustrates
|
|
2371
2387
|
* — are surfaced.
|
|
2372
2388
|
*
|
|
2373
|
-
*
|
|
2374
|
-
*
|
|
2375
|
-
*
|
|
2376
|
-
*
|
|
2377
|
-
*
|
|
2389
|
+
* Cycle enumeration uses **Johnson's algorithm** rather than a naive
|
|
2390
|
+
* enumerate-all-paths DFS. A plain path-DFS re-walks every simple path in the
|
|
2391
|
+
* graph, which is worst-case exponential even on a fully **acyclic** schema
|
|
2392
|
+
* (reconverging FK fan-out — many tables referencing shared parents in a chain —
|
|
2393
|
+
* makes codegen appear to hang). Johnson blocks a vertex once a fruitless
|
|
2394
|
+
* subtree is exhausted and only unblocks it when a new cycle through it is
|
|
2395
|
+
* found, so it runs in `O((V + E)(C + 1))` for `C` elementary circuits — no
|
|
2396
|
+
* blowup on acyclic input. Each circuit is enumerated exactly once from its
|
|
2397
|
+
* lowest-indexed member (vertices are ordered lexicographically), so overlapping
|
|
2398
|
+
* / chord cycles that share interior nodes are each detected independently. The
|
|
2399
|
+
* emitted cycle is still canonicalized to its lexicographically smallest
|
|
2400
|
+
* rotation for a stable cacheKey.
|
|
2378
2401
|
*/
|
|
2379
2402
|
declare const circularFk: Lint;
|
|
2380
2403
|
/**
|
|
@@ -3604,4 +3627,4 @@ interface RunAdvisorOptions {
|
|
|
3604
3627
|
* `static` lints at build time and defer `runtime` lints to a live shard.
|
|
3605
3628
|
*/
|
|
3606
3629
|
declare const runAdvisor: (context: LintContext, options?: RunAdvisorOptions) => Finding[];
|
|
3607
|
-
export { AE_METRIC_EVENTS, ALL_LINTS, type AdvisorAdminRoute, type AdvisorAiRawRun, type AdvisorAiToolSideEffect, type AdvisorArgumentDerivedFetch, type AdvisorArgumentValidator, type AdvisorAuthApiCall, type AdvisorAuthConfig, type AdvisorBrowserUrlAccess, type AdvisorConfigCall, type AdvisorContainer, type AdvisorContainerKeyAccess, type AdvisorContainerOverride, type AdvisorFailOpenGuard, type AdvisorFlagSecurityDefault, type AdvisorHttpActionGuard, type AdvisorHttpHeaderWrite, type AdvisorHyperdriveCall, type AdvisorIdentityClaimRead, type AdvisorImageDeliveryUrlAccess, type AdvisorIndex, type AdvisorIndexHit, type AdvisorInsertWrite, type AdvisorKvKeyAccess, type AdvisorMailRecipientAccess, type AdvisorMaskProcedure, type AdvisorMaskStrategy, type AdvisorMutatorWrite, type AdvisorNondeterministicCall, type AdvisorNormalizeIdAuthorization, type AdvisorOwnerFieldWrite, type AdvisorPaymentWebhook, type AdvisorPrivilegedDispatch, type AdvisorProcedureProtection, type AdvisorQueryRead, type AdvisorQueue, type AdvisorQueueTuning, type AdvisorR2sqlCall, type AdvisorRatelimitKeySelector, type AdvisorRawRowReturn, type AdvisorRelation, type AdvisorRelationLoad, type AdvisorRlsProcedure, type AdvisorSchema, type AdvisorSecretLiteral, type AdvisorShape, type AdvisorShardTraffic, type AdvisorSoftDeleteRead, type AdvisorSqlInterpolation, type AdvisorStorageKeyAccess, type AdvisorStorageUpload, type AdvisorTable, type AdvisorTableSample, type AdvisorTableScan, type AdvisorVectorNamespaceAccess, type AdvisorWorkflow, type AdvisorWorkflowCall, type AdvisorWranglerVariable, type AnalyticsMetricsOptions, type AnalyticsMetricsSource, type AnalyticsRuntimeMetrics, type Category, type Facing, type Finding, type Level, type Lint, type LintContext, type LintSource, RUNTIME_LINTS, RunAdvisorOptions, STATIC_LINTS, actionFetchSsrf, adminRouteWithoutGuard, aiRawRunEscapeHatch, aiToolSideEffectPromptInjection, aiUnboundedGenerationPublic, allowUnauthenticatedShardAccessEnabled, authApiCallWithoutHeaders, authCsrfCheckDisabled, authEmailVerificationDisabled, authSecureCookiesDisabled, authSessionFreshageZero, authTrustedOriginsWildcard, browserAllowPrivateTargets, browserUserUrlWithoutAllowlist, circularFk, constraintValidator, containerInstanceKeyFromUserInput, containerOversizedInstance, containerPublicInternet, containerRuntimeEgressRelaxation, containerStartEnableInternetOverride, duplicateIndex, emptyIndex, externalSourceOnGlobal, externalSourceUnscoped, filterWithoutIndex, flagGatesSecurityWithUnsafeDefault, fromServerSchema, hardcodedSecret, hotShard, httpActionMissingAuthGuard, httpActionResponseHeaderInjection, hyperdriveOutsideAction, identityUndeclaredClaimTrusted, imagesUrlSourceFromUserInput, indexReferencesUnknownField, indexUtilization, insertManyUnsafeUserData, kvUnscopedUserKeyIdor, loadAnalyticsRuntimeMetrics, mailInboundDispatchWithoutVerify, mailRecipientFromRequestInput, maskUncoveredPiiColumn, maskWeakHashStrategyOnPii, maskedRelationLeakViaWith, mutatorFullRowReplace, nondeterministicQueryMutation, normalizeIdUsedAsAuthorization, outputProjectionMissingOnPublicRead, ownerFieldFromArgsNotAuth, paymentCreateWithoutAuthorize, paymentWebhookWideTolerance, plaintextSecretInWranglerVariables, policyReferencesUnknownTable, privilegedDispatchUnvalidatedPayload, privilegedFanoutFromPublicProcedure, publicArgumentUsesAny, publicMutationWithoutRatelimit, publicTableRlsOptoutConfusion, queueWithoutDlq, r2sqlOutsideAction, ratelimitDefaultMemoryStore, ratelimitKeySpoofableOrGlobal, ratelimitMiddlewareFailOpen, relationReferencesUnknownField, relationReferencesUnknownTable, rlsUncoveredTable, runAdvisor, shapeTargetsGlobalTable, shapeUnknownTable, softDeleteIncludeDeletedFromArgs, sqlInjectionRisk, storageGenerateUploadUrlNoContentTypePin, storageKeyFromUserArgs, storagePresignedUrlForPrivateContent, storageUploadWithoutContentTypeAllowlist, storageUploadWithoutMaxSize, tableWithoutInsert, unboundedStringArgument, unindexedForeignKey, unindexedRelationTarget, userCreatingMutationWithoutCaptcha, vectorsNamespaceFromUserInput, workflowDuplicateStepName, workflowUnknownTarget, workflowUnused };
|
|
3630
|
+
export { AE_METRIC_EVENTS, ALL_LINTS, type AdvisorAdminRoute, type AdvisorAiRawRun, type AdvisorAiToolSideEffect, type AdvisorArgumentDerivedFetch, type AdvisorArgumentValidator, type AdvisorAuthApiCall, type AdvisorAuthConfig, type AdvisorBrowserUrlAccess, type AdvisorConfigCall, type AdvisorContainer, type AdvisorContainerKeyAccess, type AdvisorContainerOverride, type AdvisorFailOpenGuard, type AdvisorFlagSecurityDefault, type AdvisorHttpActionGuard, type AdvisorHttpHeaderWrite, type AdvisorHyperdriveCall, type AdvisorIdentityClaimRead, type AdvisorImageDeliveryUrlAccess, type AdvisorIndex, type AdvisorIndexHit, type AdvisorInsertWrite, type AdvisorKvKeyAccess, type AdvisorMailRecipientAccess, type AdvisorMaskProcedure, type AdvisorMaskStrategy, type AdvisorMutatorWrite, type AdvisorNondeterministicCall, type AdvisorNormalizeIdAuthorization, type AdvisorOwnerFieldWrite, type AdvisorPaymentWebhook, type AdvisorPrivilegedDispatch, type AdvisorProcedureProtection, type AdvisorQueryRead, type AdvisorQueue, type AdvisorQueueTuning, type AdvisorR2sqlCall, type AdvisorRatelimitKeySelector, type AdvisorRawRowReturn, type AdvisorRelation, type AdvisorRelationLoad, type AdvisorRlsProcedure, type AdvisorSchema, type AdvisorSecretLiteral, type AdvisorShape, type AdvisorShardTraffic, type AdvisorSoftDeleteRead, type AdvisorSqlInterpolation, type AdvisorStorageKeyAccess, type AdvisorStorageUpload, type AdvisorTable, type AdvisorTableSample, type AdvisorTableScan, type AdvisorVectorNamespaceAccess, type AdvisorWorkflow, type AdvisorWorkflowCall, type AdvisorWranglerVariable, type AnalyticsMetricsOptions, type AnalyticsMetricsSource, type AnalyticsRuntimeMetrics, type Category, type Facing, type Finding, type Level, type Lint, type LintContext, type LintSource, RUNTIME_LINTS, RunAdvisorOptions, STATIC_LINTS, actionFetchSsrf, adminRouteWithoutGuard, aiRawRunEscapeHatch, aiToolSideEffectPromptInjection, aiUnboundedGenerationPublic, allowUnauthenticatedShardAccessEnabled, authApiCallWithoutHeaders, authCsrfCheckDisabled, authEmailVerificationDisabled, authSecureCookiesDisabled, authSessionFreshageZero, authTrustedOriginsWildcard, browserAllowPrivateTargets, browserUserUrlWithoutAllowlist, circularFk, constraintValidator, containerInstanceKeyFromUserInput, containerOversizedInstance, containerPublicInternet, containerRuntimeEgressRelaxation, containerStartEnableInternetOverride, dedupeCacheKeys, duplicateIndex, emptyIndex, externalSourceOnGlobal, externalSourceUnscoped, filterWithoutIndex, flagGatesSecurityWithUnsafeDefault, fromServerSchema, hardcodedSecret, hotShard, httpActionMissingAuthGuard, httpActionResponseHeaderInjection, hyperdriveOutsideAction, identityUndeclaredClaimTrusted, imagesUrlSourceFromUserInput, indexReferencesUnknownField, indexUtilization, insertManyUnsafeUserData, kvUnscopedUserKeyIdor, loadAnalyticsRuntimeMetrics, mailInboundDispatchWithoutVerify, mailRecipientFromRequestInput, maskUncoveredPiiColumn, maskWeakHashStrategyOnPii, maskedRelationLeakViaWith, mutatorFullRowReplace, nondeterministicQueryMutation, normalizeIdUsedAsAuthorization, outputProjectionMissingOnPublicRead, ownerFieldFromArgsNotAuth, paymentCreateWithoutAuthorize, paymentWebhookWideTolerance, plaintextSecretInWranglerVariables, policyReferencesUnknownTable, privilegedDispatchUnvalidatedPayload, privilegedFanoutFromPublicProcedure, publicArgumentUsesAny, publicMutationWithoutRatelimit, publicTableRlsOptoutConfusion, queueWithoutDlq, r2sqlOutsideAction, ratelimitDefaultMemoryStore, ratelimitKeySpoofableOrGlobal, ratelimitMiddlewareFailOpen, relationReferencesUnknownField, relationReferencesUnknownTable, rlsUncoveredTable, runAdvisor, shapeTargetsGlobalTable, shapeUnknownTable, softDeleteIncludeDeletedFromArgs, sqlInjectionRisk, storageGenerateUploadUrlNoContentTypePin, storageKeyFromUserArgs, storagePresignedUrlForPrivateContent, storageUploadWithoutContentTypeAllowlist, storageUploadWithoutMaxSize, tableWithoutInsert, unboundedStringArgument, unindexedForeignKey, unindexedRelationTarget, userCreatingMutationWithoutCaptcha, vectorsNamespaceFromUserInput, workflowDuplicateStepName, workflowUnknownTarget, workflowUnused };
|
package/dist/index.d.ts
CHANGED
|
@@ -2023,6 +2023,22 @@ interface AnalyticsRuntimeMetrics {
|
|
|
2023
2023
|
*/
|
|
2024
2024
|
declare const loadAnalyticsRuntimeMetrics: (source: AnalyticsMetricsSource, options: AnalyticsMetricsOptions) => Promise<AnalyticsRuntimeMetrics>;
|
|
2025
2025
|
/**
|
|
2026
|
+
* Disambiguate {@link Finding}s that share a `cacheKey`.
|
|
2027
|
+
*
|
|
2028
|
+
* A `cacheKey` is the studio's dedup / dismissal id (see {@link Finding.cacheKey}):
|
|
2029
|
+
* two findings with the same key collapse to one row, and a single dismissal
|
|
2030
|
+
* silences both. A lint keyed on `name:file:line` (the argument-derived sinks,
|
|
2031
|
+
* `sql_injection_risk`, `kv_unscoped_user_key_idor`, `hardcoded_secret`, …) can
|
|
2032
|
+
* legitimately emit two occurrences on one physical source line, so without a
|
|
2033
|
+
* within-line discriminator the second finding is silently hidden.
|
|
2034
|
+
*
|
|
2035
|
+
* This suffixes the second-and-later occurrence of any repeated key with
|
|
2036
|
+
* `:<n>` (`:2`, `:3`, …), leaving the first occurrence unsuffixed so existing
|
|
2037
|
+
* single-occurrence keys stay stable across runs. Order is preserved. Keys are
|
|
2038
|
+
* lint-name-prefixed, so this never merges across lints.
|
|
2039
|
+
*/
|
|
2040
|
+
declare const dedupeCacheKeys: (findings: ReadonlyArray<Finding>) => Finding[];
|
|
2041
|
+
/**
|
|
2026
2042
|
* Constraint validator — flag rows that violate declared FK / NOT NULL / UNIQUE
|
|
2027
2043
|
* constraints by cross-checking sampled row data against the schema.
|
|
2028
2044
|
*
|
|
@@ -2351,7 +2367,7 @@ declare const browserAllowPrivateTargets: Lint;
|
|
|
2351
2367
|
*/
|
|
2352
2368
|
declare const browserUserUrlWithoutAllowlist: Lint;
|
|
2353
2369
|
/**
|
|
2354
|
-
* Detect FK cycles in the declared relation graph
|
|
2370
|
+
* Detect FK cycles in the declared relation graph.
|
|
2355
2371
|
*
|
|
2356
2372
|
* A "circular FK" exists when a chain of `one` relations forms a loop — for
|
|
2357
2373
|
* example `A.authorId → B`, `B.ownerId → C`, `C.postId → A`. Such cycles can
|
|
@@ -2370,11 +2386,18 @@ declare const browserUserUrlWithoutAllowlist: Lint;
|
|
|
2370
2386
|
* would be noise. Only multi-table cycles — the ones the description illustrates
|
|
2371
2387
|
* — are surfaced.
|
|
2372
2388
|
*
|
|
2373
|
-
*
|
|
2374
|
-
*
|
|
2375
|
-
*
|
|
2376
|
-
*
|
|
2377
|
-
*
|
|
2389
|
+
* Cycle enumeration uses **Johnson's algorithm** rather than a naive
|
|
2390
|
+
* enumerate-all-paths DFS. A plain path-DFS re-walks every simple path in the
|
|
2391
|
+
* graph, which is worst-case exponential even on a fully **acyclic** schema
|
|
2392
|
+
* (reconverging FK fan-out — many tables referencing shared parents in a chain —
|
|
2393
|
+
* makes codegen appear to hang). Johnson blocks a vertex once a fruitless
|
|
2394
|
+
* subtree is exhausted and only unblocks it when a new cycle through it is
|
|
2395
|
+
* found, so it runs in `O((V + E)(C + 1))` for `C` elementary circuits — no
|
|
2396
|
+
* blowup on acyclic input. Each circuit is enumerated exactly once from its
|
|
2397
|
+
* lowest-indexed member (vertices are ordered lexicographically), so overlapping
|
|
2398
|
+
* / chord cycles that share interior nodes are each detected independently. The
|
|
2399
|
+
* emitted cycle is still canonicalized to its lexicographically smallest
|
|
2400
|
+
* rotation for a stable cacheKey.
|
|
2378
2401
|
*/
|
|
2379
2402
|
declare const circularFk: Lint;
|
|
2380
2403
|
/**
|
|
@@ -3604,4 +3627,4 @@ interface RunAdvisorOptions {
|
|
|
3604
3627
|
* `static` lints at build time and defer `runtime` lints to a live shard.
|
|
3605
3628
|
*/
|
|
3606
3629
|
declare const runAdvisor: (context: LintContext, options?: RunAdvisorOptions) => Finding[];
|
|
3607
|
-
export { AE_METRIC_EVENTS, ALL_LINTS, type AdvisorAdminRoute, type AdvisorAiRawRun, type AdvisorAiToolSideEffect, type AdvisorArgumentDerivedFetch, type AdvisorArgumentValidator, type AdvisorAuthApiCall, type AdvisorAuthConfig, type AdvisorBrowserUrlAccess, type AdvisorConfigCall, type AdvisorContainer, type AdvisorContainerKeyAccess, type AdvisorContainerOverride, type AdvisorFailOpenGuard, type AdvisorFlagSecurityDefault, type AdvisorHttpActionGuard, type AdvisorHttpHeaderWrite, type AdvisorHyperdriveCall, type AdvisorIdentityClaimRead, type AdvisorImageDeliveryUrlAccess, type AdvisorIndex, type AdvisorIndexHit, type AdvisorInsertWrite, type AdvisorKvKeyAccess, type AdvisorMailRecipientAccess, type AdvisorMaskProcedure, type AdvisorMaskStrategy, type AdvisorMutatorWrite, type AdvisorNondeterministicCall, type AdvisorNormalizeIdAuthorization, type AdvisorOwnerFieldWrite, type AdvisorPaymentWebhook, type AdvisorPrivilegedDispatch, type AdvisorProcedureProtection, type AdvisorQueryRead, type AdvisorQueue, type AdvisorQueueTuning, type AdvisorR2sqlCall, type AdvisorRatelimitKeySelector, type AdvisorRawRowReturn, type AdvisorRelation, type AdvisorRelationLoad, type AdvisorRlsProcedure, type AdvisorSchema, type AdvisorSecretLiteral, type AdvisorShape, type AdvisorShardTraffic, type AdvisorSoftDeleteRead, type AdvisorSqlInterpolation, type AdvisorStorageKeyAccess, type AdvisorStorageUpload, type AdvisorTable, type AdvisorTableSample, type AdvisorTableScan, type AdvisorVectorNamespaceAccess, type AdvisorWorkflow, type AdvisorWorkflowCall, type AdvisorWranglerVariable, type AnalyticsMetricsOptions, type AnalyticsMetricsSource, type AnalyticsRuntimeMetrics, type Category, type Facing, type Finding, type Level, type Lint, type LintContext, type LintSource, RUNTIME_LINTS, RunAdvisorOptions, STATIC_LINTS, actionFetchSsrf, adminRouteWithoutGuard, aiRawRunEscapeHatch, aiToolSideEffectPromptInjection, aiUnboundedGenerationPublic, allowUnauthenticatedShardAccessEnabled, authApiCallWithoutHeaders, authCsrfCheckDisabled, authEmailVerificationDisabled, authSecureCookiesDisabled, authSessionFreshageZero, authTrustedOriginsWildcard, browserAllowPrivateTargets, browserUserUrlWithoutAllowlist, circularFk, constraintValidator, containerInstanceKeyFromUserInput, containerOversizedInstance, containerPublicInternet, containerRuntimeEgressRelaxation, containerStartEnableInternetOverride, duplicateIndex, emptyIndex, externalSourceOnGlobal, externalSourceUnscoped, filterWithoutIndex, flagGatesSecurityWithUnsafeDefault, fromServerSchema, hardcodedSecret, hotShard, httpActionMissingAuthGuard, httpActionResponseHeaderInjection, hyperdriveOutsideAction, identityUndeclaredClaimTrusted, imagesUrlSourceFromUserInput, indexReferencesUnknownField, indexUtilization, insertManyUnsafeUserData, kvUnscopedUserKeyIdor, loadAnalyticsRuntimeMetrics, mailInboundDispatchWithoutVerify, mailRecipientFromRequestInput, maskUncoveredPiiColumn, maskWeakHashStrategyOnPii, maskedRelationLeakViaWith, mutatorFullRowReplace, nondeterministicQueryMutation, normalizeIdUsedAsAuthorization, outputProjectionMissingOnPublicRead, ownerFieldFromArgsNotAuth, paymentCreateWithoutAuthorize, paymentWebhookWideTolerance, plaintextSecretInWranglerVariables, policyReferencesUnknownTable, privilegedDispatchUnvalidatedPayload, privilegedFanoutFromPublicProcedure, publicArgumentUsesAny, publicMutationWithoutRatelimit, publicTableRlsOptoutConfusion, queueWithoutDlq, r2sqlOutsideAction, ratelimitDefaultMemoryStore, ratelimitKeySpoofableOrGlobal, ratelimitMiddlewareFailOpen, relationReferencesUnknownField, relationReferencesUnknownTable, rlsUncoveredTable, runAdvisor, shapeTargetsGlobalTable, shapeUnknownTable, softDeleteIncludeDeletedFromArgs, sqlInjectionRisk, storageGenerateUploadUrlNoContentTypePin, storageKeyFromUserArgs, storagePresignedUrlForPrivateContent, storageUploadWithoutContentTypeAllowlist, storageUploadWithoutMaxSize, tableWithoutInsert, unboundedStringArgument, unindexedForeignKey, unindexedRelationTarget, userCreatingMutationWithoutCaptcha, vectorsNamespaceFromUserInput, workflowDuplicateStepName, workflowUnknownTarget, workflowUnused };
|
|
3630
|
+
export { AE_METRIC_EVENTS, ALL_LINTS, type AdvisorAdminRoute, type AdvisorAiRawRun, type AdvisorAiToolSideEffect, type AdvisorArgumentDerivedFetch, type AdvisorArgumentValidator, type AdvisorAuthApiCall, type AdvisorAuthConfig, type AdvisorBrowserUrlAccess, type AdvisorConfigCall, type AdvisorContainer, type AdvisorContainerKeyAccess, type AdvisorContainerOverride, type AdvisorFailOpenGuard, type AdvisorFlagSecurityDefault, type AdvisorHttpActionGuard, type AdvisorHttpHeaderWrite, type AdvisorHyperdriveCall, type AdvisorIdentityClaimRead, type AdvisorImageDeliveryUrlAccess, type AdvisorIndex, type AdvisorIndexHit, type AdvisorInsertWrite, type AdvisorKvKeyAccess, type AdvisorMailRecipientAccess, type AdvisorMaskProcedure, type AdvisorMaskStrategy, type AdvisorMutatorWrite, type AdvisorNondeterministicCall, type AdvisorNormalizeIdAuthorization, type AdvisorOwnerFieldWrite, type AdvisorPaymentWebhook, type AdvisorPrivilegedDispatch, type AdvisorProcedureProtection, type AdvisorQueryRead, type AdvisorQueue, type AdvisorQueueTuning, type AdvisorR2sqlCall, type AdvisorRatelimitKeySelector, type AdvisorRawRowReturn, type AdvisorRelation, type AdvisorRelationLoad, type AdvisorRlsProcedure, type AdvisorSchema, type AdvisorSecretLiteral, type AdvisorShape, type AdvisorShardTraffic, type AdvisorSoftDeleteRead, type AdvisorSqlInterpolation, type AdvisorStorageKeyAccess, type AdvisorStorageUpload, type AdvisorTable, type AdvisorTableSample, type AdvisorTableScan, type AdvisorVectorNamespaceAccess, type AdvisorWorkflow, type AdvisorWorkflowCall, type AdvisorWranglerVariable, type AnalyticsMetricsOptions, type AnalyticsMetricsSource, type AnalyticsRuntimeMetrics, type Category, type Facing, type Finding, type Level, type Lint, type LintContext, type LintSource, RUNTIME_LINTS, RunAdvisorOptions, STATIC_LINTS, actionFetchSsrf, adminRouteWithoutGuard, aiRawRunEscapeHatch, aiToolSideEffectPromptInjection, aiUnboundedGenerationPublic, allowUnauthenticatedShardAccessEnabled, authApiCallWithoutHeaders, authCsrfCheckDisabled, authEmailVerificationDisabled, authSecureCookiesDisabled, authSessionFreshageZero, authTrustedOriginsWildcard, browserAllowPrivateTargets, browserUserUrlWithoutAllowlist, circularFk, constraintValidator, containerInstanceKeyFromUserInput, containerOversizedInstance, containerPublicInternet, containerRuntimeEgressRelaxation, containerStartEnableInternetOverride, dedupeCacheKeys, duplicateIndex, emptyIndex, externalSourceOnGlobal, externalSourceUnscoped, filterWithoutIndex, flagGatesSecurityWithUnsafeDefault, fromServerSchema, hardcodedSecret, hotShard, httpActionMissingAuthGuard, httpActionResponseHeaderInjection, hyperdriveOutsideAction, identityUndeclaredClaimTrusted, imagesUrlSourceFromUserInput, indexReferencesUnknownField, indexUtilization, insertManyUnsafeUserData, kvUnscopedUserKeyIdor, loadAnalyticsRuntimeMetrics, mailInboundDispatchWithoutVerify, mailRecipientFromRequestInput, maskUncoveredPiiColumn, maskWeakHashStrategyOnPii, maskedRelationLeakViaWith, mutatorFullRowReplace, nondeterministicQueryMutation, normalizeIdUsedAsAuthorization, outputProjectionMissingOnPublicRead, ownerFieldFromArgsNotAuth, paymentCreateWithoutAuthorize, paymentWebhookWideTolerance, plaintextSecretInWranglerVariables, policyReferencesUnknownTable, privilegedDispatchUnvalidatedPayload, privilegedFanoutFromPublicProcedure, publicArgumentUsesAny, publicMutationWithoutRatelimit, publicTableRlsOptoutConfusion, queueWithoutDlq, r2sqlOutsideAction, ratelimitDefaultMemoryStore, ratelimitKeySpoofableOrGlobal, ratelimitMiddlewareFailOpen, relationReferencesUnknownField, relationReferencesUnknownTable, rlsUncoveredTable, runAdvisor, shapeTargetsGlobalTable, shapeUnknownTable, softDeleteIncludeDeletedFromArgs, sqlInjectionRisk, storageGenerateUploadUrlNoContentTypePin, storageKeyFromUserArgs, storagePresignedUrlForPrivateContent, storageUploadWithoutContentTypeAllowlist, storageUploadWithoutMaxSize, tableWithoutInsert, unboundedStringArgument, unindexedForeignKey, unindexedRelationTarget, userCreatingMutationWithoutCaptcha, vectorsNamespaceFromUserInput, workflowDuplicateStepName, workflowUnknownTarget, workflowUnused };
|
package/dist/index.mjs
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import
|
|
2
|
-
import
|
|
1
|
+
import { dedupeCacheKeys } from './packem_shared/dedupeCacheKeys-r5B7u_yq.mjs';
|
|
2
|
+
import constraintValidator from './packem_shared/constraintValidator-CxwtpJ6E.mjs';
|
|
3
|
+
import hotShard from './packem_shared/hotShard-CkC7qpre.mjs';
|
|
3
4
|
import indexUtilization from './packem_shared/indexUtilization-B5DMQ3bI.mjs';
|
|
4
5
|
import actionFetchSsrf from './packem_shared/actionFetchSsrf-wbmYgzmz.mjs';
|
|
5
6
|
import adminRouteWithoutGuard from './packem_shared/adminRouteWithoutGuard-UUGBkAjU.mjs';
|
|
@@ -15,7 +16,7 @@ import authSessionFreshageZero from './packem_shared/authSessionFreshageZero-Cju
|
|
|
15
16
|
import authTrustedOriginsWildcard from './packem_shared/authTrustedOriginsWildcard-ylv4PKzC.mjs';
|
|
16
17
|
import browserAllowPrivateTargets from './packem_shared/browserAllowPrivateTargets-CBvQxLAM.mjs';
|
|
17
18
|
import browserUserUrlWithoutAllowlist from './packem_shared/browserUserUrlWithoutAllowlist-CvhA6w59.mjs';
|
|
18
|
-
import circularFk from './packem_shared/circularFk-
|
|
19
|
+
import circularFk from './packem_shared/circularFk-CNcAVuYa.mjs';
|
|
19
20
|
import containerInstanceKeyFromUserInput from './packem_shared/containerInstanceKeyFromUserInput-BUXj2J4w.mjs';
|
|
20
21
|
import containerOversizedInstance from './packem_shared/containerOversizedInstance-5U1VKPRM.mjs';
|
|
21
22
|
import containerPublicInternet from './packem_shared/containerPublicInternet-CuNerJE5.mjs';
|
|
@@ -23,9 +24,11 @@ import containerRuntimeEgressRelaxation from './packem_shared/containerRuntimeEg
|
|
|
23
24
|
import containerStartEnableInternetOverride from './packem_shared/containerStartEnableInternetOverride-DDaHZQ1L.mjs';
|
|
24
25
|
import duplicateIndex from './packem_shared/duplicateIndex-BOublMSt.mjs';
|
|
25
26
|
import emptyIndex from './packem_shared/emptyIndex-BX8EuEY7.mjs';
|
|
27
|
+
import externalSourceOnGlobal from './packem_shared/externalSourceOnGlobal-Bg-NfCX9.mjs';
|
|
28
|
+
import externalSourceUnscoped from './packem_shared/externalSourceUnscoped-5vT-Bup3.mjs';
|
|
26
29
|
import filterWithoutIndex from './packem_shared/filterWithoutIndex-BYVeJaSs.mjs';
|
|
27
30
|
import flagGatesSecurityWithUnsafeDefault from './packem_shared/flagGatesSecurityWithUnsafeDefault-BhIs0shr.mjs';
|
|
28
|
-
import hardcodedSecret from './packem_shared/hardcodedSecret-
|
|
31
|
+
import hardcodedSecret from './packem_shared/hardcodedSecret-Be-pKVdn.mjs';
|
|
29
32
|
import httpActionMissingAuthGuard from './packem_shared/httpActionMissingAuthGuard-CxipddNx.mjs';
|
|
30
33
|
import httpActionResponseHeaderInjection from './packem_shared/httpActionResponseHeaderInjection-DOFS7pFT.mjs';
|
|
31
34
|
import hyperdriveOutsideAction from './packem_shared/hyperdriveOutsideAction-BgZqX7Xg.mjs';
|
|
@@ -80,8 +83,6 @@ import workflowDuplicateStepName from './packem_shared/workflowDuplicateStepName
|
|
|
80
83
|
import workflowUnknownTarget from './packem_shared/workflowUnknownTarget-Cdd7WhKQ.mjs';
|
|
81
84
|
import workflowUnused from './packem_shared/workflowUnused-D0jHxdz9.mjs';
|
|
82
85
|
export { AE_METRIC_EVENTS, loadAnalyticsRuntimeMetrics } from './packem_shared/AE_METRIC_EVENTS-BM14d0lm.mjs';
|
|
83
|
-
export { default as externalSourceOnGlobal } from './packem_shared/externalSourceOnGlobal-Bg-NfCX9.mjs';
|
|
84
|
-
export { default as externalSourceUnscoped } from './packem_shared/externalSourceUnscoped-5vT-Bup3.mjs';
|
|
85
86
|
export { fromServerSchema } from './packem_shared/fromServerSchema-BiB6JSiW.mjs';
|
|
86
87
|
|
|
87
88
|
const STATIC_LINTS = [
|
|
@@ -91,6 +92,8 @@ const STATIC_LINTS = [
|
|
|
91
92
|
workflowUnknownTarget,
|
|
92
93
|
workflowDuplicateStepName,
|
|
93
94
|
shapeUnknownTable,
|
|
95
|
+
externalSourceOnGlobal,
|
|
96
|
+
externalSourceUnscoped,
|
|
94
97
|
emptyIndex,
|
|
95
98
|
circularFk,
|
|
96
99
|
unindexedForeignKey,
|
|
@@ -175,7 +178,7 @@ const runAdvisor = (context, options = {}) => {
|
|
|
175
178
|
}
|
|
176
179
|
findings.push(...lint.run(context));
|
|
177
180
|
}
|
|
178
|
-
return findings;
|
|
181
|
+
return dedupeCacheKeys(findings);
|
|
179
182
|
};
|
|
180
183
|
|
|
181
|
-
export { ALL_LINTS, RUNTIME_LINTS, STATIC_LINTS, actionFetchSsrf, adminRouteWithoutGuard, aiRawRunEscapeHatch, aiToolSideEffectPromptInjection, aiUnboundedGenerationPublic, allowUnauthenticatedShardAccessEnabled, authApiCallWithoutHeaders, authCsrfCheckDisabled, authEmailVerificationDisabled, authSecureCookiesDisabled, authSessionFreshageZero, authTrustedOriginsWildcard, browserAllowPrivateTargets, browserUserUrlWithoutAllowlist, circularFk, constraintValidator, containerInstanceKeyFromUserInput, containerOversizedInstance, containerPublicInternet, containerRuntimeEgressRelaxation, containerStartEnableInternetOverride, duplicateIndex, emptyIndex, filterWithoutIndex, flagGatesSecurityWithUnsafeDefault, hardcodedSecret, hotShard, httpActionMissingAuthGuard, httpActionResponseHeaderInjection, hyperdriveOutsideAction, identityUndeclaredClaimTrusted, imagesUrlSourceFromUserInput, indexReferencesUnknownField, indexUtilization, insertManyUnsafeUserData, kvUnscopedUserKeyIdor, mailInboundDispatchWithoutVerify, mailRecipientFromRequestInput, maskUncoveredPiiColumn, maskWeakHashStrategyOnPii, maskedRelationLeakViaWith, mutatorFullRowReplace, nondeterministicQueryMutation, normalizeIdUsedAsAuthorization, outputProjectionMissingOnPublicRead, ownerFieldFromArgsNotAuth, paymentCreateWithoutAuthorize, paymentWebhookWideTolerance, plaintextSecretInWranglerVariables, policyReferencesUnknownTable, privilegedDispatchUnvalidatedPayload, privilegedFanoutFromPublicProcedure, publicArgumentUsesAny, publicMutationWithoutRatelimit, publicTableRlsOptoutConfusion, queueWithoutDlq, r2sqlOutsideAction, ratelimitDefaultMemoryStore, ratelimitKeySpoofableOrGlobal, ratelimitMiddlewareFailOpen, relationReferencesUnknownField, relationReferencesUnknownTable, rlsUncoveredTable, runAdvisor, shapeTargetsGlobalTable, shapeUnknownTable, softDeleteIncludeDeletedFromArgs, sqlInjectionRisk, storageGenerateUploadUrlNoContentTypePin, storageKeyFromUserArgs, storagePresignedUrlForPrivateContent, storageUploadWithoutContentTypeAllowlist, storageUploadWithoutMaxSize, tableWithoutInsert, unboundedStringArgument, unindexedForeignKey, unindexedRelationTarget, userCreatingMutationWithoutCaptcha, vectorsNamespaceFromUserInput, workflowDuplicateStepName, workflowUnknownTarget, workflowUnused };
|
|
184
|
+
export { ALL_LINTS, RUNTIME_LINTS, STATIC_LINTS, actionFetchSsrf, adminRouteWithoutGuard, aiRawRunEscapeHatch, aiToolSideEffectPromptInjection, aiUnboundedGenerationPublic, allowUnauthenticatedShardAccessEnabled, authApiCallWithoutHeaders, authCsrfCheckDisabled, authEmailVerificationDisabled, authSecureCookiesDisabled, authSessionFreshageZero, authTrustedOriginsWildcard, browserAllowPrivateTargets, browserUserUrlWithoutAllowlist, circularFk, constraintValidator, containerInstanceKeyFromUserInput, containerOversizedInstance, containerPublicInternet, containerRuntimeEgressRelaxation, containerStartEnableInternetOverride, dedupeCacheKeys, duplicateIndex, emptyIndex, externalSourceOnGlobal, externalSourceUnscoped, filterWithoutIndex, flagGatesSecurityWithUnsafeDefault, hardcodedSecret, hotShard, httpActionMissingAuthGuard, httpActionResponseHeaderInjection, hyperdriveOutsideAction, identityUndeclaredClaimTrusted, imagesUrlSourceFromUserInput, indexReferencesUnknownField, indexUtilization, insertManyUnsafeUserData, kvUnscopedUserKeyIdor, mailInboundDispatchWithoutVerify, mailRecipientFromRequestInput, maskUncoveredPiiColumn, maskWeakHashStrategyOnPii, maskedRelationLeakViaWith, mutatorFullRowReplace, nondeterministicQueryMutation, normalizeIdUsedAsAuthorization, outputProjectionMissingOnPublicRead, ownerFieldFromArgsNotAuth, paymentCreateWithoutAuthorize, paymentWebhookWideTolerance, plaintextSecretInWranglerVariables, policyReferencesUnknownTable, privilegedDispatchUnvalidatedPayload, privilegedFanoutFromPublicProcedure, publicArgumentUsesAny, publicMutationWithoutRatelimit, publicTableRlsOptoutConfusion, queueWithoutDlq, r2sqlOutsideAction, ratelimitDefaultMemoryStore, ratelimitKeySpoofableOrGlobal, ratelimitMiddlewareFailOpen, relationReferencesUnknownField, relationReferencesUnknownTable, rlsUncoveredTable, runAdvisor, shapeTargetsGlobalTable, shapeUnknownTable, softDeleteIncludeDeletedFromArgs, sqlInjectionRisk, storageGenerateUploadUrlNoContentTypePin, storageKeyFromUserArgs, storagePresignedUrlForPrivateContent, storageUploadWithoutContentTypeAllowlist, storageUploadWithoutMaxSize, tableWithoutInsert, unboundedStringArgument, unindexedForeignKey, unindexedRelationTarget, userCreatingMutationWithoutCaptcha, vectorsNamespaceFromUserInput, workflowDuplicateStepName, workflowUnknownTarget, workflowUnused };
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import { e as emit } from './finding-Dm_zvzS1.mjs';
|
|
2
|
+
|
|
3
|
+
const MAX_CYCLES = 100;
|
|
4
|
+
const circularFk = {
|
|
5
|
+
categories: ["SCHEMA"],
|
|
6
|
+
description: "A chain of foreign-key relations forms a cycle (e.g. A → B → C → A). Circular FK dependencies can cause unexpected behavior during DELETE operations — a CASCADE chain may loop indefinitely, and even a RESTRICT cycle prevents deletion of any row in the loop without temporarily disabling constraints.",
|
|
7
|
+
facing: "INTERNAL",
|
|
8
|
+
level: "WARN",
|
|
9
|
+
name: "circular_fk",
|
|
10
|
+
remediation: "Remove or break the cycle by dropping at least one FK relation from the loop. Consider replacing the circular dependency with a nullable FK and explicit application logic, or restructure the schema using a junction table.",
|
|
11
|
+
run: (context) => {
|
|
12
|
+
const findings = [];
|
|
13
|
+
const order = [...new Set(context.schema.tables.map((table) => table.name))].sort();
|
|
14
|
+
const indexOf = new Map(order.map((name, index) => [name, index]));
|
|
15
|
+
const adjacency = /* @__PURE__ */ new Map();
|
|
16
|
+
for (const table of context.schema.tables) {
|
|
17
|
+
for (const relation of table.relations) {
|
|
18
|
+
if (relation.kind !== "one" || !indexOf.has(relation.table)) {
|
|
19
|
+
continue;
|
|
20
|
+
}
|
|
21
|
+
let targets = adjacency.get(table.name);
|
|
22
|
+
if (targets === void 0) {
|
|
23
|
+
targets = /* @__PURE__ */ new Set();
|
|
24
|
+
adjacency.set(table.name, targets);
|
|
25
|
+
}
|
|
26
|
+
targets.add(relation.table);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
const blocked = /* @__PURE__ */ new Set();
|
|
30
|
+
const blockMap = /* @__PURE__ */ new Map();
|
|
31
|
+
const stack = [];
|
|
32
|
+
const reported = /* @__PURE__ */ new Set();
|
|
33
|
+
let capped = false;
|
|
34
|
+
const unblock = (vertex) => {
|
|
35
|
+
blocked.delete(vertex);
|
|
36
|
+
const dependents = blockMap.get(vertex);
|
|
37
|
+
if (dependents === void 0) {
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
for (const dependent of dependents) {
|
|
41
|
+
dependents.delete(dependent);
|
|
42
|
+
if (blocked.has(dependent)) {
|
|
43
|
+
unblock(dependent);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
};
|
|
47
|
+
const emitCurrentCycle = () => {
|
|
48
|
+
const cycle = [...stack];
|
|
49
|
+
if (cycle.length < 2) {
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
let minIndex = 0;
|
|
53
|
+
for (let index = 1; index < cycle.length; index += 1) {
|
|
54
|
+
if (cycle[index] < cycle[minIndex]) {
|
|
55
|
+
minIndex = index;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
const canonical = [...cycle.slice(minIndex), ...cycle.slice(0, minIndex)];
|
|
59
|
+
const key = canonical.join("→");
|
|
60
|
+
if (reported.has(key)) {
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
reported.add(key);
|
|
64
|
+
const displayPath = [...canonical, canonical[0]].join(" → ");
|
|
65
|
+
findings.push(
|
|
66
|
+
emit(circularFk, {
|
|
67
|
+
cacheKey: `circular_fk:${key}`,
|
|
68
|
+
detail: `Circular foreign-key dependency detected: ${displayPath}. This cycle can cause unexpected behavior during DELETE operations.`,
|
|
69
|
+
metadata: {
|
|
70
|
+
cycle: canonical,
|
|
71
|
+
path: displayPath,
|
|
72
|
+
tables: canonical
|
|
73
|
+
}
|
|
74
|
+
})
|
|
75
|
+
);
|
|
76
|
+
if (reported.size >= MAX_CYCLES) {
|
|
77
|
+
capped = true;
|
|
78
|
+
}
|
|
79
|
+
};
|
|
80
|
+
const circuit = (vertex, start, startIndex) => {
|
|
81
|
+
let foundCycle = false;
|
|
82
|
+
stack.push(vertex);
|
|
83
|
+
blocked.add(vertex);
|
|
84
|
+
for (const neighbor of adjacency.get(vertex) ?? []) {
|
|
85
|
+
if (capped) {
|
|
86
|
+
break;
|
|
87
|
+
}
|
|
88
|
+
if (indexOf.get(neighbor) < startIndex) {
|
|
89
|
+
continue;
|
|
90
|
+
}
|
|
91
|
+
if (neighbor === start) {
|
|
92
|
+
emitCurrentCycle();
|
|
93
|
+
foundCycle = true;
|
|
94
|
+
} else if (!blocked.has(neighbor) && circuit(neighbor, start, startIndex)) {
|
|
95
|
+
foundCycle = true;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
if (foundCycle) {
|
|
99
|
+
unblock(vertex);
|
|
100
|
+
} else {
|
|
101
|
+
for (const neighbor of adjacency.get(vertex) ?? []) {
|
|
102
|
+
if (indexOf.get(neighbor) < startIndex) {
|
|
103
|
+
continue;
|
|
104
|
+
}
|
|
105
|
+
let dependents = blockMap.get(neighbor);
|
|
106
|
+
if (dependents === void 0) {
|
|
107
|
+
dependents = /* @__PURE__ */ new Set();
|
|
108
|
+
blockMap.set(neighbor, dependents);
|
|
109
|
+
}
|
|
110
|
+
dependents.add(vertex);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
stack.pop();
|
|
114
|
+
return foundCycle;
|
|
115
|
+
};
|
|
116
|
+
for (let startIndex = 0; startIndex < order.length && !capped; startIndex += 1) {
|
|
117
|
+
blocked.clear();
|
|
118
|
+
blockMap.clear();
|
|
119
|
+
circuit(order[startIndex], order[startIndex], startIndex);
|
|
120
|
+
}
|
|
121
|
+
return findings;
|
|
122
|
+
},
|
|
123
|
+
source: "static",
|
|
124
|
+
title: "Circular foreign-key dependency"
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
export { circularFk as default };
|
package/dist/packem_shared/{constraintValidator-Dr9Py3FD.mjs → constraintValidator-CxwtpJ6E.mjs}
RENAMED
|
@@ -9,10 +9,28 @@ const formatExamples = (ids, total) => {
|
|
|
9
9
|
const list = examples.map((id) => `"${id}"`).join(", ");
|
|
10
10
|
return more > 0 ? `${list} (+${more.toString()} more)` : list;
|
|
11
11
|
};
|
|
12
|
+
const resolveReferenceValues = (referencedColumn, targetSample) => {
|
|
13
|
+
if (referencedColumn === "_id") {
|
|
14
|
+
return targetSample.existingIds;
|
|
15
|
+
}
|
|
16
|
+
const values = /* @__PURE__ */ new Set();
|
|
17
|
+
for (const row of targetSample.rows) {
|
|
18
|
+
const value = row[referencedColumn];
|
|
19
|
+
if (value !== null && value !== void 0) {
|
|
20
|
+
values.add(String(value));
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
return values;
|
|
24
|
+
};
|
|
12
25
|
const checkFkRelation = (lint, sample, relation, targetSample) => {
|
|
13
26
|
if (relation.kind !== "one") {
|
|
14
27
|
return void 0;
|
|
15
28
|
}
|
|
29
|
+
if (targetSample.truncated) {
|
|
30
|
+
return void 0;
|
|
31
|
+
}
|
|
32
|
+
const referencedColumn = relation.references;
|
|
33
|
+
const referenceValues = resolveReferenceValues(referencedColumn, targetSample);
|
|
16
34
|
const fkColumn = relation.field;
|
|
17
35
|
const danglingIds = [];
|
|
18
36
|
for (const row of sample.rows) {
|
|
@@ -21,16 +39,17 @@ const checkFkRelation = (lint, sample, relation, targetSample) => {
|
|
|
21
39
|
continue;
|
|
22
40
|
}
|
|
23
41
|
const fkString = String(fkValue);
|
|
24
|
-
if (!
|
|
42
|
+
if (!referenceValues.has(fkString)) {
|
|
25
43
|
danglingIds.push(rowId(row));
|
|
26
44
|
}
|
|
27
45
|
}
|
|
28
46
|
if (danglingIds.length === 0) {
|
|
29
47
|
return void 0;
|
|
30
48
|
}
|
|
31
|
-
const isTruncated = sample.truncated
|
|
32
|
-
const cap =
|
|
33
|
-
const
|
|
49
|
+
const isTruncated = sample.truncated;
|
|
50
|
+
const { cap } = sample;
|
|
51
|
+
const referencedLabel = referencedColumn === "_id" ? `"${relation.table}"` : `"${relation.table}"."${referencedColumn}"`;
|
|
52
|
+
const detail = `Table "${sample.table}": ${danglingIds.length.toString()} sampled row(s) have a dangling FK value in column "${fkColumn}" — the referenced ${referencedLabel} row does not exist. Row ids: ${formatExamples(danglingIds, danglingIds.length)}${truncatedSuffix(isTruncated, cap, "more rows may exist beyond the window")}.`;
|
|
34
53
|
return emit(lint, {
|
|
35
54
|
cacheKey: `constraint_validator:fk:${sample.table}:${fkColumn}`,
|
|
36
55
|
detail,
|
|
@@ -40,6 +59,7 @@ const checkFkRelation = (lint, sample, relation, targetSample) => {
|
|
|
40
59
|
count: danglingIds.length,
|
|
41
60
|
examples: danglingIds.slice(0, MAX_EXAMPLE_IDS),
|
|
42
61
|
kind: "fk",
|
|
62
|
+
references: referencedColumn,
|
|
43
63
|
referencesTable: relation.table,
|
|
44
64
|
table: sample.table,
|
|
45
65
|
truncated: isTruncated
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
const dedupeCacheKeys = (findings) => {
|
|
2
|
+
const seen = /* @__PURE__ */ new Map();
|
|
3
|
+
return findings.map((finding) => {
|
|
4
|
+
const occurrence = (seen.get(finding.cacheKey) ?? 0) + 1;
|
|
5
|
+
seen.set(finding.cacheKey, occurrence);
|
|
6
|
+
if (occurrence === 1) {
|
|
7
|
+
return finding;
|
|
8
|
+
}
|
|
9
|
+
return { ...finding, cacheKey: `${finding.cacheKey}:${occurrence.toString()}` };
|
|
10
|
+
});
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
export { dedupeCacheKeys };
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { dedupeCacheKeys } from './dedupeCacheKeys-r5B7u_yq.mjs';
|
|
1
2
|
import { e as emit } from './finding-Dm_zvzS1.mjs';
|
|
2
3
|
|
|
3
4
|
const hardcodedSecret = {
|
|
@@ -12,21 +13,16 @@ const hardcodedSecret = {
|
|
|
12
13
|
return [];
|
|
13
14
|
}
|
|
14
15
|
const findings = [];
|
|
15
|
-
const occurrenceCount = /* @__PURE__ */ new Map();
|
|
16
16
|
for (const secret of context.secretLiterals) {
|
|
17
|
-
const baseKey = `${secret.file}:${secret.line.toString()}:${secret.kind}`;
|
|
18
|
-
const occurrence = (occurrenceCount.get(baseKey) ?? 0) + 1;
|
|
19
|
-
occurrenceCount.set(baseKey, occurrence);
|
|
20
|
-
const occurrenceSuffix = occurrence > 1 ? `:${occurrence.toString()}` : "";
|
|
21
17
|
findings.push(
|
|
22
18
|
emit(hardcodedSecret, {
|
|
23
|
-
cacheKey: `hardcoded_secret:${
|
|
19
|
+
cacheKey: `hardcoded_secret:${secret.file}:${secret.line.toString()}:${secret.kind}`,
|
|
24
20
|
detail: `A ${secret.kind.replaceAll("_", " ")} (${secret.preview}) is hard-coded at ${secret.file}:${secret.line.toString()}. Move it to \`.dev.vars\` / \`wrangler secret put\` and read it from \`env\`. Rotate the exposed value.`,
|
|
25
21
|
metadata: { file: secret.file, kind: secret.kind, line: secret.line, preview: secret.preview }
|
|
26
22
|
})
|
|
27
23
|
);
|
|
28
24
|
}
|
|
29
|
-
return findings;
|
|
25
|
+
return dedupeCacheKeys(findings);
|
|
30
26
|
},
|
|
31
27
|
source: "static",
|
|
32
28
|
title: "Hard-coded secret in source"
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { e as emit } from './finding-Dm_zvzS1.mjs';
|
|
2
|
+
|
|
3
|
+
const HOT_SHARE_THRESHOLD = 0.5;
|
|
4
|
+
const MIN_TOTAL_REQUESTS = 50;
|
|
5
|
+
const hotShard = {
|
|
6
|
+
categories: ["PERFORMANCE"],
|
|
7
|
+
description: "One shard is taking a disproportionate share of a sharded function's traffic, so the busy shard becomes a single-DO bottleneck while its siblings sit idle — the hot-key skew sharding is meant to avoid.",
|
|
8
|
+
facing: "EXTERNAL",
|
|
9
|
+
level: "WARN",
|
|
10
|
+
name: "hot_shard",
|
|
11
|
+
remediation: "Re-shard on a higher-cardinality / more evenly-distributed key, or split the hot entity's state, so request volume spreads across shards instead of concentrating on one.",
|
|
12
|
+
run: (context) => {
|
|
13
|
+
const active = (context.shardTraffic ?? []).filter((shard) => shard.requests > 0);
|
|
14
|
+
const byGroup = /* @__PURE__ */ new Map();
|
|
15
|
+
for (const shard of active) {
|
|
16
|
+
const groupKey = shard.group ?? "";
|
|
17
|
+
const bucket = byGroup.get(groupKey);
|
|
18
|
+
if (bucket === void 0) {
|
|
19
|
+
byGroup.set(groupKey, [shard]);
|
|
20
|
+
} else {
|
|
21
|
+
bucket.push(shard);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
const findings = [];
|
|
25
|
+
for (const [groupKey, groupShards] of byGroup) {
|
|
26
|
+
const totalRequests = groupShards.reduce((sum, shard) => sum + shard.requests, 0);
|
|
27
|
+
if (groupShards.length < 2 || totalRequests < MIN_TOTAL_REQUESTS) {
|
|
28
|
+
continue;
|
|
29
|
+
}
|
|
30
|
+
for (const shard of groupShards) {
|
|
31
|
+
const share = shard.requests / totalRequests;
|
|
32
|
+
if (share < HOT_SHARE_THRESHOLD) {
|
|
33
|
+
continue;
|
|
34
|
+
}
|
|
35
|
+
const scope = shard.group !== void 0 && shard.group !== "" ? `"${shard.group}" ` : "";
|
|
36
|
+
const label = shard.shardKey === "" ? "the root shard" : `shard "${shard.shardKey}"`;
|
|
37
|
+
const percent = Math.round(share * 100);
|
|
38
|
+
findings.push(
|
|
39
|
+
emit(hotShard, {
|
|
40
|
+
cacheKey: `hot_shard:${groupKey}:${shard.shardKey}`,
|
|
41
|
+
detail: `${scope}${label} handled ${shard.requests.toString()} of ${totalRequests.toString()} requests (${percent.toString()}%) across ${groupShards.length.toString()} shards — a hot-key skew. Re-shard on a more evenly-distributed key.`,
|
|
42
|
+
metadata: {
|
|
43
|
+
group: shard.group,
|
|
44
|
+
requests: shard.requests,
|
|
45
|
+
shardCount: groupShards.length,
|
|
46
|
+
shardKey: shard.shardKey,
|
|
47
|
+
share,
|
|
48
|
+
totalRequests
|
|
49
|
+
}
|
|
50
|
+
})
|
|
51
|
+
);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
return findings;
|
|
55
|
+
},
|
|
56
|
+
source: "runtime",
|
|
57
|
+
title: "Hot shard"
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
export { hotShard as default };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lunora/advisor",
|
|
3
|
-
"version": "1.0.0-alpha.
|
|
3
|
+
"version": "1.0.0-alpha.27",
|
|
4
4
|
"description": "Schema & query lints (splinter-style advisors) for Lunora, feeding the Studio Advisors view",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"advisor",
|
|
@@ -46,8 +46,8 @@
|
|
|
46
46
|
"access": "public"
|
|
47
47
|
},
|
|
48
48
|
"dependencies": {
|
|
49
|
-
"@lunora/errors": "1.0.0-alpha.
|
|
50
|
-
"@lunora/server": "1.0.0-alpha.
|
|
49
|
+
"@lunora/errors": "1.0.0-alpha.4",
|
|
50
|
+
"@lunora/server": "1.0.0-alpha.24"
|
|
51
51
|
},
|
|
52
52
|
"engines": {
|
|
53
53
|
"node": "^22.15.0 || >=24.11.0"
|
|
@@ -1,84 +0,0 @@
|
|
|
1
|
-
import { e as emit } from './finding-Dm_zvzS1.mjs';
|
|
2
|
-
|
|
3
|
-
const circularFk = {
|
|
4
|
-
categories: ["SCHEMA"],
|
|
5
|
-
description: "A chain of foreign-key relations forms a cycle (e.g. A → B → C → A). Circular FK dependencies can cause unexpected behavior during DELETE operations — a CASCADE chain may loop indefinitely, and even a RESTRICT cycle prevents deletion of any row in the loop without temporarily disabling constraints.",
|
|
6
|
-
facing: "INTERNAL",
|
|
7
|
-
level: "WARN",
|
|
8
|
-
name: "circular_fk",
|
|
9
|
-
remediation: "Remove or break the cycle by dropping at least one FK relation from the loop. Consider replacing the circular dependency with a nullable FK and explicit application logic, or restructure the schema using a junction table.",
|
|
10
|
-
run: (context) => {
|
|
11
|
-
const findings = [];
|
|
12
|
-
const edges = /* @__PURE__ */ new Map();
|
|
13
|
-
for (const table of context.schema.tables) {
|
|
14
|
-
for (const relation of table.relations) {
|
|
15
|
-
if (relation.kind !== "one") {
|
|
16
|
-
continue;
|
|
17
|
-
}
|
|
18
|
-
let targets = edges.get(table.name);
|
|
19
|
-
if (targets === void 0) {
|
|
20
|
-
targets = /* @__PURE__ */ new Set();
|
|
21
|
-
edges.set(table.name, targets);
|
|
22
|
-
}
|
|
23
|
-
targets.add(relation.table);
|
|
24
|
-
}
|
|
25
|
-
}
|
|
26
|
-
const onStack = /* @__PURE__ */ new Set();
|
|
27
|
-
const reported = /* @__PURE__ */ new Set();
|
|
28
|
-
const reportBackEdge = (neighbor, path) => {
|
|
29
|
-
const cycleStart = path.indexOf(neighbor);
|
|
30
|
-
if (cycleStart === -1) {
|
|
31
|
-
return;
|
|
32
|
-
}
|
|
33
|
-
const cycle = path.slice(cycleStart);
|
|
34
|
-
if (cycle.length < 2) {
|
|
35
|
-
return;
|
|
36
|
-
}
|
|
37
|
-
let minIndex = 0;
|
|
38
|
-
for (let index = 1; index < cycle.length; index += 1) {
|
|
39
|
-
if (cycle[index] < cycle[minIndex]) {
|
|
40
|
-
minIndex = index;
|
|
41
|
-
}
|
|
42
|
-
}
|
|
43
|
-
const canonical = [...cycle.slice(minIndex), ...cycle.slice(0, minIndex)];
|
|
44
|
-
const key = canonical.join("→");
|
|
45
|
-
if (reported.has(key)) {
|
|
46
|
-
return;
|
|
47
|
-
}
|
|
48
|
-
reported.add(key);
|
|
49
|
-
const displayPath = [...canonical, canonical[0]].join(" → ");
|
|
50
|
-
findings.push(
|
|
51
|
-
emit(circularFk, {
|
|
52
|
-
cacheKey: `circular_fk:${key}`,
|
|
53
|
-
detail: `Circular foreign-key dependency detected: ${displayPath}. This cycle can cause unexpected behavior during DELETE operations.`,
|
|
54
|
-
metadata: {
|
|
55
|
-
cycle: canonical,
|
|
56
|
-
path: displayPath,
|
|
57
|
-
tables: canonical
|
|
58
|
-
}
|
|
59
|
-
})
|
|
60
|
-
);
|
|
61
|
-
};
|
|
62
|
-
const dfs = (node, path) => {
|
|
63
|
-
onStack.add(node);
|
|
64
|
-
path.push(node);
|
|
65
|
-
for (const neighbor of edges.get(node) ?? []) {
|
|
66
|
-
if (onStack.has(neighbor)) {
|
|
67
|
-
reportBackEdge(neighbor, path);
|
|
68
|
-
} else {
|
|
69
|
-
dfs(neighbor, path);
|
|
70
|
-
}
|
|
71
|
-
}
|
|
72
|
-
path.pop();
|
|
73
|
-
onStack.delete(node);
|
|
74
|
-
};
|
|
75
|
-
for (const table of context.schema.tables) {
|
|
76
|
-
dfs(table.name, []);
|
|
77
|
-
}
|
|
78
|
-
return findings;
|
|
79
|
-
},
|
|
80
|
-
source: "static",
|
|
81
|
-
title: "Circular foreign-key dependency"
|
|
82
|
-
};
|
|
83
|
-
|
|
84
|
-
export { circularFk as default };
|
|
@@ -1,48 +0,0 @@
|
|
|
1
|
-
import { e as emit } from './finding-Dm_zvzS1.mjs';
|
|
2
|
-
|
|
3
|
-
const HOT_SHARE_THRESHOLD = 0.5;
|
|
4
|
-
const MIN_TOTAL_REQUESTS = 50;
|
|
5
|
-
const hotShard = {
|
|
6
|
-
categories: ["PERFORMANCE"],
|
|
7
|
-
description: "One shard is taking a disproportionate share of a sharded function's traffic, so the busy shard becomes a single-DO bottleneck while its siblings sit idle — the hot-key skew sharding is meant to avoid.",
|
|
8
|
-
facing: "EXTERNAL",
|
|
9
|
-
level: "WARN",
|
|
10
|
-
name: "hot_shard",
|
|
11
|
-
remediation: "Re-shard on a higher-cardinality / more evenly-distributed key, or split the hot entity's state, so request volume spreads across shards instead of concentrating on one.",
|
|
12
|
-
run: (context) => {
|
|
13
|
-
const active = (context.shardTraffic ?? []).filter((shard) => shard.requests > 0);
|
|
14
|
-
const totalRequests = active.reduce((sum, shard) => sum + shard.requests, 0);
|
|
15
|
-
if (active.length < 2 || totalRequests < MIN_TOTAL_REQUESTS) {
|
|
16
|
-
return [];
|
|
17
|
-
}
|
|
18
|
-
const findings = [];
|
|
19
|
-
for (const shard of active) {
|
|
20
|
-
const share = shard.requests / totalRequests;
|
|
21
|
-
if (share < HOT_SHARE_THRESHOLD) {
|
|
22
|
-
continue;
|
|
23
|
-
}
|
|
24
|
-
const scope = shard.group !== void 0 && shard.group !== "" ? `"${shard.group}" ` : "";
|
|
25
|
-
const label = shard.shardKey === "" ? "the root shard" : `shard "${shard.shardKey}"`;
|
|
26
|
-
const percent = Math.round(share * 100);
|
|
27
|
-
findings.push(
|
|
28
|
-
emit(hotShard, {
|
|
29
|
-
cacheKey: `hot_shard:${shard.group ?? ""}:${shard.shardKey}`,
|
|
30
|
-
detail: `${scope}${label} handled ${shard.requests.toString()} of ${totalRequests.toString()} requests (${percent.toString()}%) across ${active.length.toString()} shards — a hot-key skew. Re-shard on a more evenly-distributed key.`,
|
|
31
|
-
metadata: {
|
|
32
|
-
group: shard.group,
|
|
33
|
-
requests: shard.requests,
|
|
34
|
-
shardCount: active.length,
|
|
35
|
-
shardKey: shard.shardKey,
|
|
36
|
-
share,
|
|
37
|
-
totalRequests
|
|
38
|
-
}
|
|
39
|
-
})
|
|
40
|
-
);
|
|
41
|
-
}
|
|
42
|
-
return findings;
|
|
43
|
-
},
|
|
44
|
-
source: "runtime",
|
|
45
|
-
title: "Hot shard"
|
|
46
|
-
};
|
|
47
|
-
|
|
48
|
-
export { hotShard as default };
|