@api-doctor/cli 0.0.5 → 0.0.6
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/README.md +2 -2
- package/dist/cli.cjs +423 -390
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.mjs +423 -390
- package/dist/cli.mjs.map +1 -1
- package/dist/plugin.d.ts +7 -42
- package/dist/plugin.js +379 -331
- package/dist/plugin.js.map +1 -1
- package/package.json +2 -1
package/dist/cli.mjs
CHANGED
|
@@ -141,7 +141,7 @@ var resendManifest = {
|
|
|
141
141
|
resultRule: "resend/webhook-signature-missing",
|
|
142
142
|
message: "This webhook handler appears to process Resend events without verifying the webhook signature.",
|
|
143
143
|
fix: "Verify incoming webhooks with Svix (Resend uses Svix signatures). Validate headers and payload before handling events.",
|
|
144
|
-
docsUrl: "https://resend.com/docs/
|
|
144
|
+
docsUrl: "https://resend.com/docs/webhooks/verify-webhooks-requests",
|
|
145
145
|
severity: "error"
|
|
146
146
|
},
|
|
147
147
|
{
|
|
@@ -221,7 +221,7 @@ var resendManifest = {
|
|
|
221
221
|
resultRule: "resend/reliability/webhook-no-idempotency",
|
|
222
222
|
message: "Resend webhook handler does not deduplicate retried events.",
|
|
223
223
|
fix: "Track processed event ids (e.g. event.data.email_id) in a store or set, since Resend retries for 24h.",
|
|
224
|
-
docsUrl: "https://resend.com/docs/
|
|
224
|
+
docsUrl: "https://resend.com/docs/webhooks/introduction",
|
|
225
225
|
severity: "warning"
|
|
226
226
|
},
|
|
227
227
|
{
|
|
@@ -249,7 +249,7 @@ var supabaseManifest = {
|
|
|
249
249
|
displayName: "Supabase",
|
|
250
250
|
detect: {
|
|
251
251
|
packages: ["@supabase/supabase-js"],
|
|
252
|
-
imports: ["@supabase/supabase-js"],
|
|
252
|
+
imports: ["@supabase/supabase-js", "@supabase/ssr"],
|
|
253
253
|
urlPatterns: ["supabase.co"]
|
|
254
254
|
},
|
|
255
255
|
oxlintRules: [
|
|
@@ -257,9 +257,9 @@ var supabaseManifest = {
|
|
|
257
257
|
key: "supabase-scope-queries-by-tenant-column",
|
|
258
258
|
resultRule: "supabase/correctness/scope-queries-by-tenant-column",
|
|
259
259
|
message: "Query selects a tenant column but never filters by it.",
|
|
260
|
-
fix: 'Add .eq("<column>", value) (or .match()/.filter()) to scope results to the caller.',
|
|
260
|
+
fix: 'Add .eq("<column>", value) (or .match()/.filter()) to scope results to the caller. If RLS scopes this table the filter is still worth adding \u2014 defense-in-depth, and it avoids overfetching.',
|
|
261
261
|
docsUrl: "https://supabase.com/docs/reference/javascript/eq",
|
|
262
|
-
severity: "
|
|
262
|
+
severity: "warning"
|
|
263
263
|
},
|
|
264
264
|
{
|
|
265
265
|
key: "supabase-validate-uuid-columns",
|
|
@@ -288,25 +288,25 @@ var supabaseManifest = {
|
|
|
288
288
|
{
|
|
289
289
|
key: "supabase-idempotent-mutations",
|
|
290
290
|
resultRule: "supabase/reliability/idempotent-mutations",
|
|
291
|
-
message: "Insert has no idempotency
|
|
292
|
-
fix: '
|
|
291
|
+
message: "Insert payload has no unique/idempotency key field, so a retried request can create a duplicate row.",
|
|
292
|
+
fix: 'Include a client-generated unique key (e.g. an id or *_key field backed by a unique constraint), or use .upsert(..., { onConflict: "<key column>" }).',
|
|
293
293
|
docsUrl: "https://supabase.com/docs/reference/javascript/upsert",
|
|
294
|
-
severity: "
|
|
294
|
+
severity: "info"
|
|
295
295
|
},
|
|
296
296
|
{
|
|
297
297
|
key: "supabase-fail-fast-env-validation",
|
|
298
298
|
resultRule: "supabase/reliability/fail-fast-env-validation",
|
|
299
299
|
message: "createClient is called with env vars that have no presence check.",
|
|
300
|
-
fix:
|
|
300
|
+
fix: `Throw an error naming the env var (e.g. if (!url || !key) throw new Error("SUPABASE_URL/KEY must be set")) before creating the client \u2014 the SDK's own error ("supabaseKey is required.") does not say which variable to fix.`,
|
|
301
301
|
docsUrl: "https://supabase.com/docs/reference/javascript/initializing",
|
|
302
|
-
severity: "
|
|
302
|
+
severity: "info"
|
|
303
303
|
},
|
|
304
304
|
{
|
|
305
305
|
key: "supabase-no-user-metadata-authz",
|
|
306
306
|
resultRule: "supabase/security/no-user-metadata-authz",
|
|
307
307
|
message: "Authorization data is read from or written to user_metadata, which clients can modify.",
|
|
308
308
|
fix: "Store roles in app_metadata via a trusted server path, or in an RLS-protected profiles table \u2014 never user_metadata.",
|
|
309
|
-
docsUrl: "https://supabase.com/docs/guides/
|
|
309
|
+
docsUrl: "https://supabase.com/docs/guides/auth/users",
|
|
310
310
|
severity: "error"
|
|
311
311
|
},
|
|
312
312
|
{
|
|
@@ -411,8 +411,8 @@ var firebaseManifest = {
|
|
|
411
411
|
key: "firebase-missing-app-check",
|
|
412
412
|
resultRule: "firebase/security/missing-app-check",
|
|
413
413
|
message: "Firebase app is initialized with no App Check configured.",
|
|
414
|
-
fix: "Call initializeAppCheck(app, { provider: new ReCaptchaV3Provider(SITE_KEY), isTokenAutoRefreshEnabled: true }) alongside initializeApp.",
|
|
415
|
-
docsUrl: "https://firebase.google.com/docs/
|
|
414
|
+
fix: "Call initializeAppCheck(app, { provider: new ReCaptchaV3Provider(SITE_KEY), isTokenAutoRefreshEnabled: true }) alongside initializeApp, then turn on enforcement in the Firebase console \u2014 App Check does nothing until enforced.",
|
|
415
|
+
docsUrl: "https://firebase.google.com/docs/app-check/web/recaptcha-provider",
|
|
416
416
|
severity: "warning"
|
|
417
417
|
},
|
|
418
418
|
{
|
|
@@ -435,7 +435,7 @@ var firebaseManifest = {
|
|
|
435
435
|
key: "firebase-unvalidated-external-data-to-rtdb",
|
|
436
436
|
resultRule: "firebase/correctness/unvalidated-external-data-to-rtdb",
|
|
437
437
|
message: "Parsed external data is written to the Realtime Database with no validation.",
|
|
438
|
-
fix: "Validate shape/values (e.g. regex-check date fields) before calling set()/update()/push(), and surface a parse-quality warning for skipped items.",
|
|
438
|
+
fix: "Validate shape/values (e.g. with a zod schema.parse/safeParse, or regex-check date fields) before calling set()/update()/push(), and surface a parse-quality warning for skipped items.",
|
|
439
439
|
docsUrl: "https://firebase.google.com/docs/database/web/read-and-write",
|
|
440
440
|
severity: "error"
|
|
441
441
|
},
|
|
@@ -469,7 +469,7 @@ var firebaseManifest = {
|
|
|
469
469
|
message: "A Realtime Database write promise is neither awaited in a try/catch nor .catch-handled.",
|
|
470
470
|
fix: "Wrap state-changing writes in try/catch and surface failures; do not navigate away on a write whose result was never checked.",
|
|
471
471
|
docsUrl: "https://firebase.google.com/docs/database/web/read-and-write",
|
|
472
|
-
severity: "
|
|
472
|
+
severity: "warning"
|
|
473
473
|
},
|
|
474
474
|
{
|
|
475
475
|
key: "firebase-firestore-rules-expired",
|
|
@@ -487,14 +487,6 @@ var firebaseManifest = {
|
|
|
487
487
|
docsUrl: "https://firebase.google.com/docs/auth/admin/manage-cookies",
|
|
488
488
|
severity: "error"
|
|
489
489
|
},
|
|
490
|
-
{
|
|
491
|
-
key: "firebase-middleware-token-not-verified",
|
|
492
|
-
resultRule: "firebase/security/middleware-token-not-verified",
|
|
493
|
-
message: "Middleware reads the auth cookie but never verifies it. Any non-empty cookie value bypasses the guard.",
|
|
494
|
-
fix: "Call adminAuth.verifySessionCookie(cookie, true) in the middleware and redirect on failure.",
|
|
495
|
-
docsUrl: "https://firebase.google.com/docs/auth/admin/manage-cookies#verify_session_cookies_and_check_permissions",
|
|
496
|
-
severity: "error"
|
|
497
|
-
},
|
|
498
490
|
{
|
|
499
491
|
key: "firebase-hardcoded-user-id",
|
|
500
492
|
resultRule: "firebase/security/hardcoded-user-id",
|
|
@@ -523,7 +515,7 @@ var firebaseManifest = {
|
|
|
523
515
|
key: "firebase-use-array-union-remove",
|
|
524
516
|
resultRule: "firebase/correctness/use-array-union-remove",
|
|
525
517
|
message: "Firestore array updated with read-modify-write spread/filter instead of atomic arrayUnion/arrayRemove.",
|
|
526
|
-
fix: "Use arrayUnion(item) or arrayRemove(item) from firebase/firestore for atomic array operations.",
|
|
518
|
+
fix: "Use arrayUnion(item) or arrayRemove(item) from firebase/firestore for atomic array operations. Note: arrayRemove needs the exact element value \u2014 to remove object items by predicate, use a transaction instead.",
|
|
527
519
|
docsUrl: "https://firebase.google.com/docs/firestore/manage-data/add-data#update_elements_in_an_array",
|
|
528
520
|
severity: "warning"
|
|
529
521
|
},
|
|
@@ -555,15 +547,15 @@ var firebaseManifest = {
|
|
|
555
547
|
key: "firebase-firestore-document-size-guard",
|
|
556
548
|
resultRule: "firebase/reliability/firestore-document-size-guard",
|
|
557
549
|
message: "Firestore write includes editor.getJSON() without a document size check.",
|
|
558
|
-
fix: 'Check document size before writing: if (new Blob([payload]).size > 900_000) { setSaveStatus("Document too large"); return; }',
|
|
550
|
+
fix: 'Check document size before writing: if (new Blob([JSON.stringify(payload)]).size > 900_000) { setSaveStatus("Document too large"); return; }',
|
|
559
551
|
docsUrl: "https://firebase.google.com/docs/firestore/quotas#limits",
|
|
560
552
|
severity: "warning"
|
|
561
553
|
},
|
|
562
554
|
{
|
|
563
555
|
key: "firebase-use-timestamp-now",
|
|
564
556
|
resultRule: "firebase/correctness/use-timestamp-now",
|
|
565
|
-
message: "new Date() used for a Firestore timestamp field instead of
|
|
566
|
-
fix: 'Import {
|
|
557
|
+
message: "new Date() used for a Firestore timestamp field instead of serverTimestamp().",
|
|
558
|
+
fix: 'Import { serverTimestamp } from "firebase/firestore" and use it for created/updated fields \u2014 it is server-authoritative, immune to client clock skew, and satisfies rules that compare against request.time.',
|
|
567
559
|
docsUrl: "https://firebase.google.com/docs/reference/js/firestore_.timestamp",
|
|
568
560
|
severity: "info"
|
|
569
561
|
}
|
|
@@ -593,7 +585,7 @@ var lovableManifest = {
|
|
|
593
585
|
resultRule: "lovable/security/paid-flag-without-edge-function",
|
|
594
586
|
message: "A paid-looking flag is set via a direct database update with no payment-provider or Edge Function call.",
|
|
595
587
|
fix: "Move the purchase behind an Edge Function that creates a payment-provider checkout session, and only flip the flag from a server-side webhook after payment is confirmed.",
|
|
596
|
-
docsUrl: "https://docs.lovable.dev/
|
|
588
|
+
docsUrl: "https://docs.lovable.dev/integrations/stripe",
|
|
597
589
|
severity: "error"
|
|
598
590
|
},
|
|
599
591
|
{
|
|
@@ -601,7 +593,7 @@ var lovableManifest = {
|
|
|
601
593
|
resultRule: "lovable/correctness/expiry-column-never-checked",
|
|
602
594
|
message: "An expiry column is written but never compared against the current time anywhere in this file.",
|
|
603
595
|
fix: "Filter or sort by the expiry column against the current time, or add a scheduled Edge Function / pg_cron job that clears the flag once it passes.",
|
|
604
|
-
docsUrl: "https://docs.lovable.dev/
|
|
596
|
+
docsUrl: "https://docs.lovable.dev/integrations/stripe",
|
|
605
597
|
severity: "warning"
|
|
606
598
|
},
|
|
607
599
|
{
|
|
@@ -609,7 +601,7 @@ var lovableManifest = {
|
|
|
609
601
|
resultRule: "lovable/correctness/silent-catch-on-provider-call",
|
|
610
602
|
message: 'A catch block around an LLM provider call has no logging, so failures look like "no key configured."',
|
|
611
603
|
fix: "console.error (or log to your error tracker) the failure reason \u2014 status code and error body \u2014 before falling through.",
|
|
612
|
-
docsUrl: "https://docs.lovable.dev/
|
|
604
|
+
docsUrl: "https://docs.lovable.dev/integrations/cloud",
|
|
613
605
|
severity: "warning"
|
|
614
606
|
}
|
|
615
607
|
]
|
|
@@ -646,7 +638,7 @@ var browserbaseManifest = {
|
|
|
646
638
|
resultRule: "browserbase/security/session-id-requires-ownership-check",
|
|
647
639
|
message: "sessionId flows into a live-view/recording call with no ownership check.",
|
|
648
640
|
fix: "Look up the owning project/org by sessionId and verify the requesting user has access before calling sessions.debug()/recording.retrieve().",
|
|
649
|
-
docsUrl: "https://docs.browserbase.com/
|
|
641
|
+
docsUrl: "https://docs.browserbase.com/platform/browser/observability/session-live-view",
|
|
650
642
|
severity: "warning"
|
|
651
643
|
},
|
|
652
644
|
{
|
|
@@ -654,7 +646,7 @@ var browserbaseManifest = {
|
|
|
654
646
|
resultRule: "browserbase/correctness/no-concurrent-shared-context",
|
|
655
647
|
message: "The same Context id is passed into every session in a concurrent Promise.all batch.",
|
|
656
648
|
fix: "Serialize sessions that share a context id, or create a fresh Context per concurrent run.",
|
|
657
|
-
docsUrl: "https://docs.browserbase.com/features/contexts",
|
|
649
|
+
docsUrl: "https://docs.browserbase.com/platform/browser/core-features/contexts",
|
|
658
650
|
severity: "error"
|
|
659
651
|
},
|
|
660
652
|
{
|
|
@@ -662,7 +654,7 @@ var browserbaseManifest = {
|
|
|
662
654
|
resultRule: "browserbase/correctness/mobile-device-requires-os-setting",
|
|
663
655
|
message: 'A "mobile" device branch resizes the viewport but never sets browserSettings.os.',
|
|
664
656
|
fix: "Set browserSettings.os = 'mobile' (or 'tablet') alongside the viewport resize \u2014 the Node SDK's device-emulation lever is `os`, not a fingerprint object.",
|
|
665
|
-
docsUrl: "https://docs.browserbase.com/
|
|
657
|
+
docsUrl: "https://docs.browserbase.com/platform/identity/verified-customization",
|
|
666
658
|
severity: "error"
|
|
667
659
|
},
|
|
668
660
|
{
|
|
@@ -779,7 +771,7 @@ var openaiCuaManifest = {
|
|
|
779
771
|
resultRule: "openai-cua/integration/set-safety-identifier",
|
|
780
772
|
message: "responses.create() call has no safety_identifier (or user) parameter.",
|
|
781
773
|
fix: "Thread a stable, hashed per-customer identifier through to every responses.create() call as safety_identifier.",
|
|
782
|
-
docsUrl: "https://
|
|
774
|
+
docsUrl: "https://developers.openai.com/api/docs/guides/safety-best-practices",
|
|
783
775
|
severity: "warning"
|
|
784
776
|
}
|
|
785
777
|
]
|
|
@@ -816,7 +808,7 @@ var tiptapManifest = {
|
|
|
816
808
|
resultRule: "tiptap/security/dynamic-script-no-sri",
|
|
817
809
|
message: "Dynamically injected script appended without SRI integrity attribute.",
|
|
818
810
|
fix: 'Add script.setAttribute("integrity", "sha384-...") before appending the script.',
|
|
819
|
-
docsUrl: "https://developer.mozilla.org/en-US/docs/Web/Security/Subresource_Integrity",
|
|
811
|
+
docsUrl: "https://developer.mozilla.org/en-US/docs/Web/Security/Defenses/Subresource_Integrity",
|
|
820
812
|
severity: "warning"
|
|
821
813
|
},
|
|
822
814
|
{
|
|
@@ -851,14 +843,6 @@ var tiptapManifest = {
|
|
|
851
843
|
docsUrl: "https://tiptap.dev/docs/editor/extensions/custom-extensions/create-new/node",
|
|
852
844
|
severity: "warning"
|
|
853
845
|
},
|
|
854
|
-
{
|
|
855
|
-
key: "tiptap-twitter-url-regex",
|
|
856
|
-
resultRule: "tiptap/integration/twitter-url-regex",
|
|
857
|
-
message: "Twitter/X regex matches x.com but not twitter.com \u2014 legacy URLs silently rejected.",
|
|
858
|
-
fix: "Update pattern to (x\\.com|twitter\\.com) to support both domains.",
|
|
859
|
-
docsUrl: "https://tiptap.dev/docs/editor/extensions/nodes",
|
|
860
|
-
severity: "warning"
|
|
861
|
-
},
|
|
862
846
|
{
|
|
863
847
|
key: "tiptap-drop-handler-pos-precedence",
|
|
864
848
|
resultRule: "tiptap/correctness/drop-handler-pos-precedence",
|
|
@@ -879,8 +863,8 @@ var tiptapManifest = {
|
|
|
879
863
|
key: "tiptap-tiptap-markdown-missing-node-spec",
|
|
880
864
|
resultRule: "tiptap/integration/tiptap-markdown-missing-node-spec",
|
|
881
865
|
message: "TipTap node used with tiptap-markdown has no markdown serialization spec \u2014 content lost on export.",
|
|
882
|
-
fix: "
|
|
883
|
-
docsUrl: "https://github.com/
|
|
866
|
+
fix: "Return a markdown serialize/parse spec (MarkdownNodeSpec) from addStorage \u2014 tiptap-markdown reads only extension.storage.markdown.",
|
|
867
|
+
docsUrl: "https://github.com/aguingand/tiptap-markdown",
|
|
884
868
|
severity: "warning"
|
|
885
869
|
}
|
|
886
870
|
]
|
|
@@ -1093,7 +1077,7 @@ var openaiRealtimeManifest = {
|
|
|
1093
1077
|
resultRule: "openai-realtime/reliability/handle-error-server-event",
|
|
1094
1078
|
message: 'This Realtime message handler branches on event types but never checks for the API-level "error" event.',
|
|
1095
1079
|
fix: "Add an explicit branch for message.type === 'error' that logs the error and surfaces/fails over, instead of letting it fall through silently.",
|
|
1096
|
-
docsUrl: "https://developers.openai.com/api/
|
|
1080
|
+
docsUrl: "https://developers.openai.com/api/reference/resources/realtime/server-events",
|
|
1097
1081
|
severity: "error"
|
|
1098
1082
|
},
|
|
1099
1083
|
{
|
|
@@ -1109,7 +1093,7 @@ var openaiRealtimeManifest = {
|
|
|
1109
1093
|
resultRule: "openai-realtime/correctness/avoid-dated-preview-snapshots",
|
|
1110
1094
|
message: "The Realtime connection is pinned to a dated preview model snapshot instead of the GA alias.",
|
|
1111
1095
|
fix: "Use the GA model id (e.g. 'gpt-realtime') or a current dated snapshot tracked against OpenAI's deprecation notices.",
|
|
1112
|
-
docsUrl: "https://developers.openai.com/api/
|
|
1096
|
+
docsUrl: "https://developers.openai.com/api/reference/resources/realtime/subresources/client_secrets",
|
|
1113
1097
|
severity: "warning"
|
|
1114
1098
|
},
|
|
1115
1099
|
{
|
|
@@ -1117,7 +1101,7 @@ var openaiRealtimeManifest = {
|
|
|
1117
1101
|
resultRule: "openai-realtime/correctness/verify-deprecated-session-fields",
|
|
1118
1102
|
message: "The session config sets 'temperature', a field not documented in the current GA Realtime sessions schema.",
|
|
1119
1103
|
fix: "Re-verify this field against the current sessions reference before relying on it, or drop it and control determinism via turn_detection/instructions instead.",
|
|
1120
|
-
docsUrl: "https://developers.openai.com/api/
|
|
1104
|
+
docsUrl: "https://developers.openai.com/api/reference/resources/realtime/subresources/client_secrets",
|
|
1121
1105
|
severity: "warning"
|
|
1122
1106
|
},
|
|
1123
1107
|
{
|
|
@@ -1125,7 +1109,7 @@ var openaiRealtimeManifest = {
|
|
|
1125
1109
|
resultRule: "openai-realtime/reliability/buffer-audio-until-session-ready",
|
|
1126
1110
|
message: "Audio sent before the Realtime socket reaches the open state is dropped instead of buffered.",
|
|
1127
1111
|
fix: "Queue outbound input_audio_buffer.append messages until the open event fires, then flush them in order.",
|
|
1128
|
-
docsUrl: "https://developers.openai.com/api/
|
|
1112
|
+
docsUrl: "https://developers.openai.com/api/reference/resources/realtime/client-events",
|
|
1129
1113
|
severity: "warning"
|
|
1130
1114
|
},
|
|
1131
1115
|
{
|
|
@@ -1139,7 +1123,7 @@ var openaiRealtimeManifest = {
|
|
|
1139
1123
|
{
|
|
1140
1124
|
key: "openai-realtime-transcription-model-choice",
|
|
1141
1125
|
resultRule: "openai-realtime/correctness/transcription-model-choice",
|
|
1142
|
-
message: "
|
|
1126
|
+
message: "The session's input transcription is configured with 'whisper-1', which is not natively streaming and not optimized for realtime sessions.",
|
|
1143
1127
|
fix: "Switch to 'gpt-realtime-whisper' if transcription output is consumed, or drop input_audio_transcription entirely if it isn't.",
|
|
1144
1128
|
docsUrl: "https://developers.openai.com/api/docs/guides/realtime-transcription",
|
|
1145
1129
|
severity: "info"
|
|
@@ -1232,7 +1216,7 @@ var rule = {
|
|
|
1232
1216
|
cwe: "CWE-345",
|
|
1233
1217
|
owasp: "API2:2023 Broken Authentication",
|
|
1234
1218
|
rationale: "Webhook endpoints are public URLs, so anyone who learns the path can POST a forged payload. Without verifying the Svix signature first, an attacker can fake delivery, bounce, or complaint events and drive your application into the wrong state. Validating the signature against your webhook secret before reading the body ensures the event genuinely came from Resend.",
|
|
1235
|
-
docsUrl: "https://resend.com/docs/
|
|
1219
|
+
docsUrl: "https://resend.com/docs/webhooks/verify-webhooks-requests",
|
|
1236
1220
|
recommended: true
|
|
1237
1221
|
},
|
|
1238
1222
|
messages: {
|
|
@@ -1309,6 +1293,15 @@ var rule = {
|
|
|
1309
1293
|
const obj = callee.object;
|
|
1310
1294
|
return obj?.type === "Identifier" && (obj.name === "req" || obj.name === "request");
|
|
1311
1295
|
}
|
|
1296
|
+
function isReqTextCall(n) {
|
|
1297
|
+
if (n?.type !== "CallExpression") return false;
|
|
1298
|
+
const callee = n.callee;
|
|
1299
|
+
if (callee?.type !== "MemberExpression") return false;
|
|
1300
|
+
const prop = callee.property;
|
|
1301
|
+
if (prop?.type !== "Identifier" || prop.name !== "text") return false;
|
|
1302
|
+
const obj = callee.object;
|
|
1303
|
+
return obj?.type === "Identifier" && (obj.name === "req" || obj.name === "request");
|
|
1304
|
+
}
|
|
1312
1305
|
function isBodyMember(n) {
|
|
1313
1306
|
if (n?.type !== "MemberExpression") return false;
|
|
1314
1307
|
const prop = n.property;
|
|
@@ -1333,6 +1326,17 @@ var rule = {
|
|
|
1333
1326
|
const obj = callee.object;
|
|
1334
1327
|
return obj?.type === "Identifier" && svixImports.has(obj.name);
|
|
1335
1328
|
}
|
|
1329
|
+
function isResendWebhooksVerifyCall(n) {
|
|
1330
|
+
if (n?.type !== "CallExpression") return false;
|
|
1331
|
+
const callee = n.callee;
|
|
1332
|
+
if (callee?.type !== "MemberExpression") return false;
|
|
1333
|
+
const prop = callee.property;
|
|
1334
|
+
if (prop?.type !== "Identifier" || prop.name !== "verify") return false;
|
|
1335
|
+
const obj = callee.object;
|
|
1336
|
+
if (obj?.type !== "MemberExpression") return false;
|
|
1337
|
+
const webhooksProp = obj.property;
|
|
1338
|
+
return webhooksProp?.type === "Identifier" && webhooksProp.name === "webhooks";
|
|
1339
|
+
}
|
|
1336
1340
|
function recordFirst(posKey, handler, pos) {
|
|
1337
1341
|
const existing = handler[posKey];
|
|
1338
1342
|
if (!existing) {
|
|
@@ -1347,6 +1351,11 @@ var rule = {
|
|
|
1347
1351
|
ImportDeclaration(node) {
|
|
1348
1352
|
const importSource = node?.source?.value;
|
|
1349
1353
|
if (importSource === "resend") importsResend = true;
|
|
1354
|
+
for (const s of node.specifiers ?? []) {
|
|
1355
|
+
if ((s?.type === "ImportSpecifier" || s?.type === "ImportDefaultSpecifier") && s.local?.type === "Identifier" && /^resend$/i.test(s.local.name)) {
|
|
1356
|
+
importsResend = true;
|
|
1357
|
+
}
|
|
1358
|
+
}
|
|
1350
1359
|
if (importSource === "svix") {
|
|
1351
1360
|
for (const s of node.specifiers ?? []) {
|
|
1352
1361
|
if (s?.type === "ImportSpecifier" && s.local?.type === "Identifier") {
|
|
@@ -1368,8 +1377,10 @@ var rule = {
|
|
|
1368
1377
|
for (const handler of postHandlers) {
|
|
1369
1378
|
if (!within(handler, node)) continue;
|
|
1370
1379
|
if (isReqJsonCall(node)) recordFirst("firstBodyPos", handler, pos);
|
|
1380
|
+
if (isReqTextCall(node)) recordFirst("firstRawBodyPos", handler, pos);
|
|
1371
1381
|
if (isSvixVerifyCall(node)) recordFirst("firstVerifyPos", handler, pos);
|
|
1372
1382
|
if (isCryptoCreateHmacCall(node)) recordFirst("firstVerifyPos", handler, pos);
|
|
1383
|
+
if (isResendWebhooksVerifyCall(node)) recordFirst("firstVerifyPos", handler, pos);
|
|
1373
1384
|
}
|
|
1374
1385
|
},
|
|
1375
1386
|
MemberExpression(node) {
|
|
@@ -1384,6 +1395,8 @@ var rule = {
|
|
|
1384
1395
|
"Program:exit"() {
|
|
1385
1396
|
if (!importsResend) return;
|
|
1386
1397
|
for (const handler of postHandlers) {
|
|
1398
|
+
const consumesRequest = handler.firstBodyPos || handler.firstRawBodyPos;
|
|
1399
|
+
if (!consumesRequest) continue;
|
|
1387
1400
|
if (!handler.firstVerifyPos) {
|
|
1388
1401
|
context.report({ node: handler.node, messageId: "missingVerification" });
|
|
1389
1402
|
continue;
|
|
@@ -1955,7 +1968,7 @@ var rule11 = {
|
|
|
1955
1968
|
description: "Resend webhook handlers should deduplicate retried events",
|
|
1956
1969
|
category: "reliability",
|
|
1957
1970
|
rationale: "Resend retries failed webhook deliveries for up to 24 hours, so the same event can legitimately arrive more than once. A handler that acts on every delivery without deduplication will double-process events \u2014 sending duplicate downstream notifications, double-counting metrics, or corrupting state. Tracking processed event ids (e.g. event.data.email_id) in a store or set and skipping ones already seen makes the handler safely idempotent.",
|
|
1958
|
-
docsUrl: "https://resend.com/docs/
|
|
1971
|
+
docsUrl: "https://resend.com/docs/webhooks/introduction",
|
|
1959
1972
|
recommended: true
|
|
1960
1973
|
},
|
|
1961
1974
|
messages: {
|
|
@@ -2257,12 +2270,12 @@ var rule14 = {
|
|
|
2257
2270
|
docs: {
|
|
2258
2271
|
description: "Supabase queries that select a tenant column must filter by it",
|
|
2259
2272
|
category: "correctness",
|
|
2260
|
-
rationale: "A column like session_id or user_id
|
|
2273
|
+
rationale: "A column like session_id or user_id being selected signals intent to scope rows to one caller, but selecting it is not the same as filtering by it. If RLS is not enabled on the table, the unfiltered query returns every row for every tenant \u2014 a per-user feed becomes a shared, cross-user one. Even when RLS scopes the rows server-side, an explicit .eq()/.match()/.filter() is defense-in-depth (a dropped policy no longer leaks data) and avoids fetching rows the page never uses.",
|
|
2261
2274
|
docsUrl: "https://supabase.com/docs/reference/javascript/eq",
|
|
2262
2275
|
recommended: true
|
|
2263
2276
|
},
|
|
2264
2277
|
messages: {
|
|
2265
|
-
missingTenantFilter:
|
|
2278
|
+
missingTenantFilter: `This query selects "{{column}}" but never filters by it \u2014 without RLS on this table that is every tenant's rows. Add .eq("{{column}}", ...) (or .match()/.filter()) to scope results to the caller.`
|
|
2266
2279
|
},
|
|
2267
2280
|
schema: []
|
|
2268
2281
|
},
|
|
@@ -2534,7 +2547,8 @@ function objectHasIdempotencyKey(objectExpression) {
|
|
|
2534
2547
|
return (objectExpression.properties ?? []).some((p) => {
|
|
2535
2548
|
if (p?.type !== "Property") return false;
|
|
2536
2549
|
const name = p.key?.type === "Identifier" ? p.key.name : p.key?.type === "Literal" ? p.key.value : void 0;
|
|
2537
|
-
|
|
2550
|
+
if (typeof name !== "string") return false;
|
|
2551
|
+
return /idempot|dedupe/i.test(name) || /^(id|uuid)$/i.test(name) || /_(key|uuid)$/i.test(name);
|
|
2538
2552
|
});
|
|
2539
2553
|
}
|
|
2540
2554
|
function insertPayloadHasIdempotencyKey(arg) {
|
|
@@ -2550,12 +2564,12 @@ var rule18 = {
|
|
|
2550
2564
|
docs: {
|
|
2551
2565
|
description: "Supabase insert calls should be retry-safe via an idempotency key",
|
|
2552
2566
|
category: "reliability",
|
|
2553
|
-
rationale: 'Nothing prevents a duplicate row if the client fetch behind an insert is retried (flaky network, double-click, browser replay) \u2014
|
|
2567
|
+
rationale: 'Nothing prevents a duplicate row if the client fetch behind an insert is retried (flaky network, double-click, browser replay) \u2014 no unique key is visible in the payload, and no upsert semantics. Include a client-generated unique key (an id, or a *_key field backed by a unique constraint), or use .upsert(..., { onConflict: "<key column>" }). This is general retry-safety practice rather than a documented Supabase requirement, hence info severity.',
|
|
2554
2568
|
docsUrl: "https://supabase.com/docs/reference/javascript/upsert",
|
|
2555
2569
|
recommended: true
|
|
2556
2570
|
},
|
|
2557
2571
|
messages: {
|
|
2558
|
-
missingIdempotencyKey: 'This insert has no idempotency
|
|
2572
|
+
missingIdempotencyKey: 'This insert payload has no unique/idempotency key field, so a retried request can create a duplicate row. Include a client-generated key, or use .upsert(..., { onConflict: "<key column>" }).'
|
|
2559
2573
|
},
|
|
2560
2574
|
schema: []
|
|
2561
2575
|
},
|
|
@@ -2604,7 +2618,7 @@ var rule19 = {
|
|
|
2604
2618
|
docs: {
|
|
2605
2619
|
description: "createClient must fail fast when required env vars are missing",
|
|
2606
2620
|
category: "reliability",
|
|
2607
|
-
rationale:
|
|
2621
|
+
rationale: `createClient throws immediately when an argument is missing, but with the SDK's message ("supabaseKey is required.") \u2014 it names the SDK parameter, not your env var. An explicit presence check produces an error naming the exact variable to set, which matters most with Next.js NEXT_PUBLIC_* inlining where the fix is a rebuild with the var present, not a server restart.`,
|
|
2608
2622
|
docsUrl: "https://supabase.com/docs/reference/javascript/initializing",
|
|
2609
2623
|
recommended: true
|
|
2610
2624
|
},
|
|
@@ -2614,7 +2628,11 @@ var rule19 = {
|
|
|
2614
2628
|
schema: []
|
|
2615
2629
|
},
|
|
2616
2630
|
create(context) {
|
|
2617
|
-
|
|
2631
|
+
const FACTORY_IMPORTS = {
|
|
2632
|
+
"@supabase/supabase-js": ["createClient"],
|
|
2633
|
+
"@supabase/ssr": ["createBrowserClient", "createServerClient"]
|
|
2634
|
+
};
|
|
2635
|
+
const factoryLocalNames = /* @__PURE__ */ new Set();
|
|
2618
2636
|
const envVarOfVariable = /* @__PURE__ */ new Map();
|
|
2619
2637
|
const validatedVarNames = /* @__PURE__ */ new Set();
|
|
2620
2638
|
const validatedEnvNames = /* @__PURE__ */ new Set();
|
|
@@ -2648,10 +2666,11 @@ var rule19 = {
|
|
|
2648
2666
|
}
|
|
2649
2667
|
return {
|
|
2650
2668
|
ImportDeclaration(node) {
|
|
2651
|
-
|
|
2669
|
+
const factories = FACTORY_IMPORTS[node.source?.value];
|
|
2670
|
+
if (!factories) return;
|
|
2652
2671
|
for (const s of node.specifiers ?? []) {
|
|
2653
|
-
if (s?.type === "ImportSpecifier" && s.imported?.type === "Identifier" && s.imported.name
|
|
2654
|
-
|
|
2672
|
+
if (s?.type === "ImportSpecifier" && s.imported?.type === "Identifier" && factories.includes(s.imported.name) && s.local?.type === "Identifier") {
|
|
2673
|
+
factoryLocalNames.add(s.local.name);
|
|
2655
2674
|
}
|
|
2656
2675
|
}
|
|
2657
2676
|
},
|
|
@@ -2665,8 +2684,8 @@ var rule19 = {
|
|
|
2665
2684
|
collectGuardTargets(node.test);
|
|
2666
2685
|
},
|
|
2667
2686
|
CallExpression(node) {
|
|
2668
|
-
if (
|
|
2669
|
-
if (node.callee?.type !== "Identifier" || node.callee.name
|
|
2687
|
+
if (factoryLocalNames.size === 0) return;
|
|
2688
|
+
if (node.callee?.type !== "Identifier" || !factoryLocalNames.has(node.callee.name)) return;
|
|
2670
2689
|
const missing = [];
|
|
2671
2690
|
for (const rawArg of node.arguments ?? []) {
|
|
2672
2691
|
const arg = unwrapNonNull(rawArg);
|
|
@@ -2734,8 +2753,8 @@ var rule20 = {
|
|
|
2734
2753
|
category: "security",
|
|
2735
2754
|
cwe: "CWE-285",
|
|
2736
2755
|
owasp: "A01:2021 Broken Access Control",
|
|
2737
|
-
rationale:
|
|
2738
|
-
docsUrl: "https://supabase.com/docs/guides/
|
|
2756
|
+
rationale: 'Supabase documents user_metadata as user-editable: "Do not use it in security sensitive context (such as in RLS policies or authorization logic), as this value is editable by the user without any checks." Reading user_metadata.role (or writing role into signUp/updateUser data) lets any signed-in user self-assign privileged roles from the browser. Store roles in app_metadata via a trusted server path, or in an RLS-protected profiles table.',
|
|
2757
|
+
docsUrl: "https://supabase.com/docs/guides/auth/users",
|
|
2739
2758
|
recommended: true
|
|
2740
2759
|
},
|
|
2741
2760
|
messages: {
|
|
@@ -2771,7 +2790,7 @@ var rule21 = {
|
|
|
2771
2790
|
docs: {
|
|
2772
2791
|
description: "Supabase .single() calls must inspect the returned error field",
|
|
2773
2792
|
category: "correctness",
|
|
2774
|
-
rationale: ".single() signals zero-or-one-row intent via the error field (PGRST116), not by leaving data undefined silently. Destructuring only data and never reading error produces infinite spinners on deleted rows, bad IDs, or RLS-denied reads. Prefer .maybeSingle() or branch on error before rendering.",
|
|
2793
|
+
rationale: ".single() signals zero-or-one-row intent via the error field (PGRST116), not by leaving data undefined silently. Destructuring only data and never reading error produces infinite spinners on deleted rows, bad IDs, or RLS-denied reads. Prefer .maybeSingle() or branch on error before rendering. Chains ending in .throwOnError() opt into exceptions instead and are exempt.",
|
|
2775
2794
|
docsUrl: "https://supabase.com/docs/reference/javascript/single",
|
|
2776
2795
|
recommended: true
|
|
2777
2796
|
},
|
|
@@ -2781,14 +2800,29 @@ var rule21 = {
|
|
|
2781
2800
|
schema: []
|
|
2782
2801
|
},
|
|
2783
2802
|
create(context) {
|
|
2803
|
+
const deferredBindings = [];
|
|
2804
|
+
const errorReadNames = /* @__PURE__ */ new Set();
|
|
2784
2805
|
function checkAwaitBinding(node, pattern, awaitExpr) {
|
|
2785
2806
|
if (!isSingleSupabaseQuery(awaitExpr.argument)) return;
|
|
2807
|
+
if (chainHasMethod(awaitExpr.argument, "throwOnError")) return;
|
|
2808
|
+
if (pattern?.type === "Identifier") {
|
|
2809
|
+
deferredBindings.push({ node, name: pattern.name });
|
|
2810
|
+
return;
|
|
2811
|
+
}
|
|
2786
2812
|
const names = destructuredNames(pattern);
|
|
2787
2813
|
if (names.has("error")) return;
|
|
2788
2814
|
context.report({ node, messageId: "missingErrorCheck" });
|
|
2789
2815
|
}
|
|
2790
2816
|
return {
|
|
2817
|
+
MemberExpression(node) {
|
|
2818
|
+
if (!node.computed && node.object?.type === "Identifier" && node.property?.type === "Identifier" && node.property.name === "error") {
|
|
2819
|
+
errorReadNames.add(node.object.name);
|
|
2820
|
+
}
|
|
2821
|
+
},
|
|
2791
2822
|
VariableDeclarator(node) {
|
|
2823
|
+
if (node.init?.type === "Identifier" && node.id?.type === "ObjectPattern") {
|
|
2824
|
+
if (destructuredNames(node.id).has("error")) errorReadNames.add(node.init.name);
|
|
2825
|
+
}
|
|
2792
2826
|
if (node.init?.type !== "AwaitExpression") return;
|
|
2793
2827
|
checkAwaitBinding(node, node.id, node.init);
|
|
2794
2828
|
},
|
|
@@ -2800,9 +2834,17 @@ var rule21 = {
|
|
|
2800
2834
|
const expr = node.expression;
|
|
2801
2835
|
if (expr?.type !== "AwaitExpression") return;
|
|
2802
2836
|
if (!isSingleSupabaseQuery(expr.argument)) return;
|
|
2837
|
+
if (chainHasMethod(expr.argument, "throwOnError")) return;
|
|
2803
2838
|
if (memberPropName(expr.argument) === "single") {
|
|
2804
2839
|
context.report({ node, messageId: "missingErrorCheck" });
|
|
2805
2840
|
}
|
|
2841
|
+
},
|
|
2842
|
+
"Program:exit"() {
|
|
2843
|
+
for (const { node, name } of deferredBindings) {
|
|
2844
|
+
if (!errorReadNames.has(name)) {
|
|
2845
|
+
context.report({ node, messageId: "missingErrorCheck" });
|
|
2846
|
+
}
|
|
2847
|
+
}
|
|
2806
2848
|
}
|
|
2807
2849
|
};
|
|
2808
2850
|
}
|
|
@@ -2911,7 +2953,7 @@ var rule23 = {
|
|
|
2911
2953
|
docs: {
|
|
2912
2954
|
description: "Supabase mutations must check the returned error field",
|
|
2913
2955
|
category: "correctness",
|
|
2914
|
-
rationale: "Unlike fetch(), Supabase client mutations return { data, error } and resolve even when RLS denies the write or a constraint fails. Fire-and-forget awaits or destructuring only data lets optimistic UI state diverge from the database with no toast or rollback.",
|
|
2956
|
+
rationale: "Unlike fetch(), Supabase client mutations return { data, error } and resolve even when RLS denies the write or a constraint fails. Fire-and-forget awaits or destructuring only data lets optimistic UI state diverge from the database with no toast or rollback. Chains ending in .throwOnError() opt into exceptions instead and are exempt.",
|
|
2915
2957
|
docsUrl: "https://supabase.com/docs/reference/javascript/insert",
|
|
2916
2958
|
recommended: true
|
|
2917
2959
|
},
|
|
@@ -2921,30 +2963,52 @@ var rule23 = {
|
|
|
2921
2963
|
schema: []
|
|
2922
2964
|
},
|
|
2923
2965
|
create(context) {
|
|
2966
|
+
const deferredBindings = [];
|
|
2967
|
+
const errorReadNames = /* @__PURE__ */ new Set();
|
|
2924
2968
|
function checkMutationAwait(node, pattern, awaitExpr) {
|
|
2925
2969
|
if (!isSupabaseMutationCall(awaitExpr.argument)) return;
|
|
2970
|
+
if (chainHasMethod(awaitExpr.argument, "throwOnError")) return;
|
|
2926
2971
|
if (!pattern) {
|
|
2927
2972
|
context.report({ node, messageId: "uncheckedMutation" });
|
|
2928
2973
|
return;
|
|
2929
2974
|
}
|
|
2975
|
+
if (pattern.type === "Identifier") {
|
|
2976
|
+
deferredBindings.push({ node, name: pattern.name });
|
|
2977
|
+
return;
|
|
2978
|
+
}
|
|
2930
2979
|
const names = destructuredNames(pattern);
|
|
2931
2980
|
if (!names.has("error")) {
|
|
2932
2981
|
context.report({ node, messageId: "uncheckedMutation" });
|
|
2933
2982
|
}
|
|
2934
2983
|
}
|
|
2935
2984
|
return {
|
|
2985
|
+
MemberExpression(node) {
|
|
2986
|
+
if (!node.computed && node.object?.type === "Identifier" && node.property?.type === "Identifier" && node.property.name === "error") {
|
|
2987
|
+
errorReadNames.add(node.object.name);
|
|
2988
|
+
}
|
|
2989
|
+
},
|
|
2936
2990
|
ExpressionStatement(node) {
|
|
2937
2991
|
const expr = node.expression;
|
|
2938
2992
|
if (expr?.type !== "AwaitExpression") return;
|
|
2939
2993
|
checkMutationAwait(node, void 0, expr);
|
|
2940
2994
|
},
|
|
2941
2995
|
VariableDeclarator(node) {
|
|
2996
|
+
if (node.init?.type === "Identifier" && node.id?.type === "ObjectPattern") {
|
|
2997
|
+
if (destructuredNames(node.id).has("error")) errorReadNames.add(node.init.name);
|
|
2998
|
+
}
|
|
2942
2999
|
if (node.init?.type !== "AwaitExpression") return;
|
|
2943
3000
|
checkMutationAwait(node, node.id, node.init);
|
|
2944
3001
|
},
|
|
2945
3002
|
AssignmentExpression(node) {
|
|
2946
3003
|
if (node.right?.type !== "AwaitExpression") return;
|
|
2947
3004
|
checkMutationAwait(node, node.left, node.right);
|
|
3005
|
+
},
|
|
3006
|
+
"Program:exit"() {
|
|
3007
|
+
for (const { node, name } of deferredBindings) {
|
|
3008
|
+
if (!errorReadNames.has(name)) {
|
|
3009
|
+
context.report({ node, messageId: "uncheckedMutation" });
|
|
3010
|
+
}
|
|
3011
|
+
}
|
|
2948
3012
|
}
|
|
2949
3013
|
};
|
|
2950
3014
|
}
|
|
@@ -2994,11 +3058,20 @@ function isStorageUploadCall(node) {
|
|
|
2994
3058
|
let current = node;
|
|
2995
3059
|
let sawStorage = false;
|
|
2996
3060
|
let sawUpload = false;
|
|
2997
|
-
while (current
|
|
2998
|
-
|
|
2999
|
-
|
|
3000
|
-
|
|
3001
|
-
|
|
3061
|
+
while (current) {
|
|
3062
|
+
if (current.type === "CallExpression") {
|
|
3063
|
+
const prop = memberPropName(current);
|
|
3064
|
+
if (prop === "storage") sawStorage = true;
|
|
3065
|
+
if (prop === "upload") sawUpload = true;
|
|
3066
|
+
current = current.callee?.object;
|
|
3067
|
+
} else if (current.type === "MemberExpression") {
|
|
3068
|
+
const p = current.property;
|
|
3069
|
+
const name = !current.computed && p?.type === "Identifier" ? p.name : p?.type === "Literal" && typeof p.value === "string" ? p.value : void 0;
|
|
3070
|
+
if (name === "storage") sawStorage = true;
|
|
3071
|
+
current = current.object;
|
|
3072
|
+
} else {
|
|
3073
|
+
break;
|
|
3074
|
+
}
|
|
3002
3075
|
}
|
|
3003
3076
|
return sawStorage && sawUpload;
|
|
3004
3077
|
}
|
|
@@ -3579,8 +3652,8 @@ var rule30 = {
|
|
|
3579
3652
|
category: "security",
|
|
3580
3653
|
cwe: "CWE-285",
|
|
3581
3654
|
owasp: "A04:2021 Insecure Design",
|
|
3582
|
-
rationale: "Firebase web API keys are public by design and only identify the project to Google's backend \u2014 they are not a secret. The only things standing between an attacker who clones the client config and your database/auth quota are Security Rules and App Check. A file that calls initializeApp but never initializeAppCheck leaves App Check unconfigured, so any client presenting a valid project config (not necessarily yours) can reach Firebase Auth and, subject to rules, the database.",
|
|
3583
|
-
docsUrl: "https://firebase.google.com/docs/
|
|
3655
|
+
rationale: "Firebase web API keys are public by design and only identify the project to Google's backend \u2014 they are not a secret. The only things standing between an attacker who clones the client config and your database/auth quota are Security Rules and App Check. A file that calls initializeApp but never initializeAppCheck leaves App Check unconfigured, so any client presenting a valid project config (not necessarily yours) can reach Firebase Auth and, subject to rules, the database. Note the client-side call alone is not enough: App Check only blocks traffic once enforcement is turned on per-product in the Firebase console.",
|
|
3656
|
+
docsUrl: "https://firebase.google.com/docs/app-check/web/recaptcha-provider",
|
|
3584
3657
|
recommended: true
|
|
3585
3658
|
},
|
|
3586
3659
|
messages: {
|
|
@@ -3754,7 +3827,13 @@ function isValidationLikeCall(node) {
|
|
|
3754
3827
|
const callee = node.callee;
|
|
3755
3828
|
const name = callee?.type === "Identifier" ? callee.name : callee?.type === "MemberExpression" && !callee.computed && callee.property?.type === "Identifier" ? callee.property.name : void 0;
|
|
3756
3829
|
if (!name) return false;
|
|
3757
|
-
|
|
3830
|
+
if (/valid|^test$|check|assert|schema/i.test(name)) return true;
|
|
3831
|
+
if (callee?.type === "MemberExpression") {
|
|
3832
|
+
if (/^(safeParse|safeParseAsync|parseAsync)$/.test(name)) return true;
|
|
3833
|
+
const objName = callee.object?.type === "Identifier" ? callee.object.name : void 0;
|
|
3834
|
+
if (name === "parse" && objName !== "JSON") return true;
|
|
3835
|
+
}
|
|
3836
|
+
return false;
|
|
3758
3837
|
}
|
|
3759
3838
|
var rule33 = {
|
|
3760
3839
|
meta: {
|
|
@@ -3943,18 +4022,24 @@ var firebaseRtdbListenerErrorNotHandledRule = rule35;
|
|
|
3943
4022
|
function isUseEffectCall(node) {
|
|
3944
4023
|
return node?.type === "CallExpression" && node.callee?.type === "Identifier" && node.callee.name === "useEffect";
|
|
3945
4024
|
}
|
|
4025
|
+
function isNonUidMemberAccess(node, objName) {
|
|
4026
|
+
if (node?.type !== "MemberExpression") return false;
|
|
4027
|
+
if (node.object?.type !== "Identifier" || node.object.name !== objName) return false;
|
|
4028
|
+
if (node.computed) return true;
|
|
4029
|
+
return node.property?.type === "Identifier" && node.property.name !== "uid";
|
|
4030
|
+
}
|
|
3946
4031
|
var rule36 = {
|
|
3947
4032
|
meta: {
|
|
3948
4033
|
type: "suggestion",
|
|
3949
4034
|
docs: {
|
|
3950
4035
|
description: "useEffect should depend on user?.uid, not the whole user object",
|
|
3951
4036
|
category: "reliability",
|
|
3952
|
-
rationale: "
|
|
4037
|
+
rationale: "Auth context providers commonly hand out a new user object reference on every token refresh (roughly hourly) \u2014 providers subscribed to onIdTokenChanged, or ones that clone the user into React state, re-render with a fresh reference even when uid is unchanged. An effect that only reads user.uid but lists the whole user object in its dependency array tears down and re-establishes its RTDB listener on every refresh \u2014 an avoidable unsubscribe/resubscribe cycle that briefly clears local state.",
|
|
3953
4038
|
docsUrl: "https://firebase.google.com/docs/reference/js/auth.md#onauthstatechanged",
|
|
3954
4039
|
recommended: true
|
|
3955
4040
|
},
|
|
3956
4041
|
messages: {
|
|
3957
|
-
wholeUserObjectDep: "This effect only reads {{name}}.uid but depends on the whole {{name}} object, which
|
|
4042
|
+
wholeUserObjectDep: "This effect only reads {{name}}.uid but depends on the whole {{name}} object, which can get a new reference on every token refresh. Depend on {{name}}?.uid instead."
|
|
3958
4043
|
},
|
|
3959
4044
|
schema: []
|
|
3960
4045
|
},
|
|
@@ -3967,9 +4052,9 @@ var rule36 = {
|
|
|
3967
4052
|
for (const el of depsArg.elements ?? []) {
|
|
3968
4053
|
if (el?.type !== "Identifier") continue;
|
|
3969
4054
|
const name = el.name;
|
|
3970
|
-
if (someDescendant(callback, (n) => isUidMemberAccess(n, name)))
|
|
3971
|
-
|
|
3972
|
-
}
|
|
4055
|
+
if (!someDescendant(callback, (n) => isUidMemberAccess(n, name))) continue;
|
|
4056
|
+
if (someDescendant(callback, (n) => isNonUidMemberAccess(n, name))) continue;
|
|
4057
|
+
context.report({ node, messageId: "wholeUserObjectDep", data: { name } });
|
|
3973
4058
|
}
|
|
3974
4059
|
}
|
|
3975
4060
|
};
|
|
@@ -4065,13 +4150,13 @@ var rule38 = {
|
|
|
4065
4150
|
schema: []
|
|
4066
4151
|
},
|
|
4067
4152
|
create(context) {
|
|
4068
|
-
const EXPIRED_YEAR = 2025;
|
|
4069
4153
|
function checkStringForExpiredDate(value, reportNode) {
|
|
4070
|
-
const re = /timestamp\.date\(\s*(\d{4})\s
|
|
4154
|
+
const re = /timestamp\.date\(\s*(\d{4})\s*,\s*(\d{1,2})\s*,\s*(\d{1,2})\s*\)/g;
|
|
4071
4155
|
let match;
|
|
4072
4156
|
while ((match = re.exec(value)) !== null) {
|
|
4073
|
-
const year =
|
|
4074
|
-
|
|
4157
|
+
const [, year, month, day] = match;
|
|
4158
|
+
const expiry = new Date(parseInt(year, 10), parseInt(month, 10) - 1, parseInt(day, 10));
|
|
4159
|
+
if (expiry.getTime() <= Date.now()) {
|
|
4075
4160
|
context.report({ node: reportNode, messageId: "firestoreRulesExpired" });
|
|
4076
4161
|
return;
|
|
4077
4162
|
}
|
|
@@ -4103,6 +4188,18 @@ function findProp(obj, name) {
|
|
|
4103
4188
|
(p) => p?.type === "Property" && (p.key?.type === "Identifier" && p.key.name === name || p.key?.type === "Literal" && p.key.value === name)
|
|
4104
4189
|
);
|
|
4105
4190
|
}
|
|
4191
|
+
var TOKEN_NAME = /token|session/i;
|
|
4192
|
+
function isTokenLiteral(node) {
|
|
4193
|
+
return node?.type === "Literal" && typeof node.value === "string" && TOKEN_NAME.test(node.value);
|
|
4194
|
+
}
|
|
4195
|
+
function isCookieReceiver(obj) {
|
|
4196
|
+
if (obj?.type === "Identifier") return /cookie/i.test(obj.name);
|
|
4197
|
+
if (obj?.type === "CallExpression" && obj.callee?.type === "Identifier") return /^cookies$/i.test(obj.callee.name);
|
|
4198
|
+
if (obj?.type === "MemberExpression" && !obj.computed && obj.property?.type === "Identifier") {
|
|
4199
|
+
return /cookie/i.test(obj.property.name);
|
|
4200
|
+
}
|
|
4201
|
+
return false;
|
|
4202
|
+
}
|
|
4106
4203
|
var rule39 = {
|
|
4107
4204
|
meta: {
|
|
4108
4205
|
type: "problem",
|
|
@@ -4110,110 +4207,71 @@ var rule39 = {
|
|
|
4110
4207
|
description: "Firebase ID token stored in a cookie without httpOnly flag",
|
|
4111
4208
|
category: "security",
|
|
4112
4209
|
cwe: "CWE-1004",
|
|
4113
|
-
owasp: "
|
|
4114
|
-
rationale: "Storing a Firebase ID token in a non-httpOnly cookie makes it readable by any JavaScript on the page. An XSS vulnerability can steal the token and impersonate the user. Use the Firebase Admin SDK createSessionCookie flow to issue a proper httpOnly session cookie instead.",
|
|
4210
|
+
owasp: "A05:2021 Security Misconfiguration",
|
|
4211
|
+
rationale: "Storing a Firebase ID token in a non-httpOnly cookie makes it readable by any JavaScript on the page. An XSS vulnerability can steal the token and impersonate the user. Use the Firebase Admin SDK createSessionCookie flow to issue a proper httpOnly session cookie instead \u2014 note httpOnly can only be set from server-side code, never from client-side JavaScript.",
|
|
4115
4212
|
docsUrl: "https://firebase.google.com/docs/auth/admin/manage-cookies",
|
|
4116
4213
|
recommended: true
|
|
4117
4214
|
},
|
|
4118
4215
|
messages: {
|
|
4119
|
-
idTokenCookieMissingHttpOnly: "
|
|
4216
|
+
idTokenCookieMissingHttpOnly: "Auth token stored in cookie without httpOnly: true. Any XSS vulnerability can steal the token. Use the Firebase Admin SDK createSessionCookie flow instead."
|
|
4120
4217
|
},
|
|
4121
4218
|
schema: []
|
|
4122
4219
|
},
|
|
4123
4220
|
create(context) {
|
|
4124
|
-
function isTokenName(val) {
|
|
4125
|
-
return /token/i.test(val);
|
|
4126
|
-
}
|
|
4127
4221
|
function hasHttpOnlyTrue(optsNode) {
|
|
4128
|
-
if (optsNode?.type !== "ObjectExpression") return false;
|
|
4129
4222
|
const prop = findProp(optsNode, "httpOnly");
|
|
4130
|
-
|
|
4131
|
-
|
|
4132
|
-
|
|
4133
|
-
|
|
4134
|
-
|
|
4135
|
-
const
|
|
4136
|
-
|
|
4137
|
-
if (!
|
|
4138
|
-
const args = node.arguments ?? [];
|
|
4139
|
-
const nameArg = args[0];
|
|
4140
|
-
if (nameArg?.type !== "Literal" || typeof nameArg.value !== "string") return;
|
|
4141
|
-
if (!isTokenName(nameArg.value)) return;
|
|
4142
|
-
const optsArg = args.length >= 3 ? args[2] : null;
|
|
4143
|
-
if (!optsArg || optsArg.type !== "ObjectExpression") {
|
|
4144
|
-
context.report({ node, messageId: "idTokenCookieMissingHttpOnly" });
|
|
4145
|
-
return;
|
|
4146
|
-
}
|
|
4147
|
-
if (!hasHttpOnlyTrue(optsArg)) {
|
|
4223
|
+
return prop?.value?.type === "Literal" && prop.value.value === true;
|
|
4224
|
+
}
|
|
4225
|
+
function checkNameValueOptsCall(node) {
|
|
4226
|
+
const args = node.arguments ?? [];
|
|
4227
|
+
if (args.length === 1 && args[0]?.type === "ObjectExpression") {
|
|
4228
|
+
const nameProp = findProp(args[0], "name");
|
|
4229
|
+
if (!isTokenLiteral(nameProp?.value)) return;
|
|
4230
|
+
if (!hasHttpOnlyTrue(args[0])) {
|
|
4148
4231
|
context.report({ node, messageId: "idTokenCookieMissingHttpOnly" });
|
|
4149
4232
|
}
|
|
4233
|
+
return;
|
|
4234
|
+
}
|
|
4235
|
+
if (!isTokenLiteral(args[0])) return;
|
|
4236
|
+
const optsArg = args.length >= 3 ? args[2] : null;
|
|
4237
|
+
if (!optsArg || optsArg.type !== "ObjectExpression" || !hasHttpOnlyTrue(optsArg)) {
|
|
4238
|
+
context.report({ node, messageId: "idTokenCookieMissingHttpOnly" });
|
|
4150
4239
|
}
|
|
4151
|
-
};
|
|
4152
|
-
}
|
|
4153
|
-
};
|
|
4154
|
-
var firebaseIdTokenCookieFlagsRule = rule39;
|
|
4155
|
-
|
|
4156
|
-
// src/providers/firebase/rules/middleware-token-not-verified.ts
|
|
4157
|
-
var rule40 = {
|
|
4158
|
-
meta: {
|
|
4159
|
-
type: "problem",
|
|
4160
|
-
docs: {
|
|
4161
|
-
description: "Next.js middleware reads auth cookie but never verifies it",
|
|
4162
|
-
category: "security",
|
|
4163
|
-
cwe: "CWE-345",
|
|
4164
|
-
owasp: "A07:2021 Identification and Authentication Failures",
|
|
4165
|
-
rationale: "Checking only that an auth cookie is non-empty does not verify the token. An attacker who sets any non-empty cookie value will bypass the guard entirely. The middleware must call verifySessionCookie or verifyIdToken to confirm the token is valid and unexpired.",
|
|
4166
|
-
docsUrl: "https://firebase.google.com/docs/auth/admin/manage-cookies#verify_session_cookies_and_check_permissions",
|
|
4167
|
-
recommended: true
|
|
4168
|
-
},
|
|
4169
|
-
messages: {
|
|
4170
|
-
tokenNotVerified: "Middleware reads the auth cookie but never verifies it. Any non-empty cookie value bypasses the guard. Call adminAuth.verifySessionCookie() or verifyIdToken() before allowing access."
|
|
4171
|
-
},
|
|
4172
|
-
schema: []
|
|
4173
|
-
},
|
|
4174
|
-
create(context) {
|
|
4175
|
-
const AUTH_COOKIE_NAMES = /token|session/i;
|
|
4176
|
-
const VERIFY_METHOD_NAMES = /* @__PURE__ */ new Set(["verifySessionCookie", "verifyIdToken", "verify"]);
|
|
4177
|
-
let readsCookieWithAuthName = false;
|
|
4178
|
-
let hasVerifyCall = false;
|
|
4179
|
-
const cookieReadNodes = [];
|
|
4180
|
-
function isAuthCookieGet(node) {
|
|
4181
|
-
if (node?.type !== "CallExpression") return false;
|
|
4182
|
-
const callee = node.callee;
|
|
4183
|
-
if (callee?.type !== "MemberExpression") return false;
|
|
4184
|
-
if (callee.property?.type !== "Identifier" || callee.property.name !== "get") return false;
|
|
4185
|
-
const arg = node.arguments?.[0];
|
|
4186
|
-
if (arg?.type !== "Literal" || typeof arg.value !== "string") return false;
|
|
4187
|
-
return AUTH_COOKIE_NAMES.test(arg.value);
|
|
4188
4240
|
}
|
|
4189
4241
|
return {
|
|
4190
4242
|
CallExpression(node) {
|
|
4191
|
-
if (isAuthCookieGet(node)) {
|
|
4192
|
-
readsCookieWithAuthName = true;
|
|
4193
|
-
cookieReadNodes.push(node);
|
|
4194
|
-
}
|
|
4195
4243
|
const callee = node.callee;
|
|
4196
|
-
if (callee?.type === "Identifier" &&
|
|
4197
|
-
|
|
4244
|
+
if (callee?.type === "Identifier" && callee.name === "setCookie") {
|
|
4245
|
+
checkNameValueOptsCall(node);
|
|
4246
|
+
return;
|
|
4247
|
+
}
|
|
4248
|
+
if (callee?.type !== "MemberExpression" || callee.computed || callee.property?.type !== "Identifier") return;
|
|
4249
|
+
if (callee.property.name === "cookie") {
|
|
4250
|
+
checkNameValueOptsCall(node);
|
|
4251
|
+
return;
|
|
4198
4252
|
}
|
|
4199
|
-
if (callee
|
|
4200
|
-
|
|
4253
|
+
if (callee.property.name === "set" && isCookieReceiver(callee.object)) {
|
|
4254
|
+
checkNameValueOptsCall(node);
|
|
4201
4255
|
}
|
|
4202
4256
|
},
|
|
4203
|
-
|
|
4204
|
-
|
|
4205
|
-
|
|
4206
|
-
|
|
4207
|
-
|
|
4257
|
+
// document.cookie = `token=${idToken}` — can never be httpOnly
|
|
4258
|
+
AssignmentExpression(node) {
|
|
4259
|
+
const left = node.left;
|
|
4260
|
+
const isDocumentCookie = left?.type === "MemberExpression" && !left.computed && left.object?.type === "Identifier" && left.object.name === "document" && left.property?.type === "Identifier" && left.property.name === "cookie";
|
|
4261
|
+
if (!isDocumentCookie) return;
|
|
4262
|
+
const right = node.right;
|
|
4263
|
+
const text = right?.type === "Literal" && typeof right.value === "string" ? right.value : right?.type === "TemplateLiteral" ? (right.quasis ?? []).map((q) => q?.value?.cooked ?? "").join("") : "";
|
|
4264
|
+
if (/(?:^|;\s*)[^=;]*(?:token|session)[^=;]*=/i.test(text)) {
|
|
4265
|
+
context.report({ node, messageId: "idTokenCookieMissingHttpOnly" });
|
|
4208
4266
|
}
|
|
4209
4267
|
}
|
|
4210
4268
|
};
|
|
4211
4269
|
}
|
|
4212
4270
|
};
|
|
4213
|
-
var
|
|
4271
|
+
var firebaseIdTokenCookieFlagsRule = rule39;
|
|
4214
4272
|
|
|
4215
4273
|
// src/providers/firebase/rules/hardcoded-user-id.ts
|
|
4216
|
-
var
|
|
4274
|
+
var rule40 = {
|
|
4217
4275
|
meta: {
|
|
4218
4276
|
type: "problem",
|
|
4219
4277
|
docs: {
|
|
@@ -4231,6 +4289,9 @@ var rule41 = {
|
|
|
4231
4289
|
schema: []
|
|
4232
4290
|
},
|
|
4233
4291
|
create(context) {
|
|
4292
|
+
if (isInsideTestFile2(String(context.filename ?? ""))) {
|
|
4293
|
+
return {};
|
|
4294
|
+
}
|
|
4234
4295
|
const USER_ID_NAMES = /* @__PURE__ */ new Set(["userId", "uid", "userID", "user_id"]);
|
|
4235
4296
|
return {
|
|
4236
4297
|
VariableDeclarator(node) {
|
|
@@ -4245,10 +4306,10 @@ var rule41 = {
|
|
|
4245
4306
|
};
|
|
4246
4307
|
}
|
|
4247
4308
|
};
|
|
4248
|
-
var firebaseHardcodedUserIdRule =
|
|
4309
|
+
var firebaseHardcodedUserIdRule = rule40;
|
|
4249
4310
|
|
|
4250
4311
|
// src/providers/firebase/rules/auth-user-not-found-disclosure.ts
|
|
4251
|
-
var
|
|
4312
|
+
var rule41 = {
|
|
4252
4313
|
meta: {
|
|
4253
4314
|
type: "suggestion",
|
|
4254
4315
|
docs: {
|
|
@@ -4287,10 +4348,10 @@ var rule42 = {
|
|
|
4287
4348
|
};
|
|
4288
4349
|
}
|
|
4289
4350
|
};
|
|
4290
|
-
var firebaseAuthUserNotFoundDisclosureRule =
|
|
4351
|
+
var firebaseAuthUserNotFoundDisclosureRule = rule41;
|
|
4291
4352
|
|
|
4292
4353
|
// src/providers/firebase/rules/signup-password-confirm.ts
|
|
4293
|
-
var
|
|
4354
|
+
var rule42 = {
|
|
4294
4355
|
meta: {
|
|
4295
4356
|
type: "suggestion",
|
|
4296
4357
|
docs: {
|
|
@@ -4324,7 +4385,7 @@ var rule43 = {
|
|
|
4324
4385
|
},
|
|
4325
4386
|
BinaryExpression(node) {
|
|
4326
4387
|
if (node.operator !== "===" && node.operator !== "==" && node.operator !== "!==" && node.operator !== "!=") return;
|
|
4327
|
-
const mentionsConfirm = (n) => n?.type === "Identifier" && n.name === "confirmPassword";
|
|
4388
|
+
const mentionsConfirm = (n) => n?.type === "Identifier" && n.name === "confirmPassword" || n?.type === "MemberExpression" && !n.computed && n.property?.type === "Identifier" && n.property.name === "confirmPassword";
|
|
4328
4389
|
if (mentionsConfirm(node.left) || mentionsConfirm(node.right)) {
|
|
4329
4390
|
hasPasswordComparison = true;
|
|
4330
4391
|
}
|
|
@@ -4347,16 +4408,16 @@ var rule43 = {
|
|
|
4347
4408
|
};
|
|
4348
4409
|
}
|
|
4349
4410
|
};
|
|
4350
|
-
var firebaseSignupPasswordConfirmRule =
|
|
4411
|
+
var firebaseSignupPasswordConfirmRule = rule42;
|
|
4351
4412
|
|
|
4352
4413
|
// src/providers/firebase/rules/use-array-union-remove.ts
|
|
4353
|
-
var
|
|
4414
|
+
var rule43 = {
|
|
4354
4415
|
meta: {
|
|
4355
4416
|
type: "suggestion",
|
|
4356
4417
|
docs: {
|
|
4357
4418
|
description: "Firestore array field updated with read-modify-write spread/filter instead of arrayUnion/arrayRemove",
|
|
4358
4419
|
category: "correctness",
|
|
4359
|
-
rationale: "Spreading an array or using filter() inside updateDoc() performs a non-atomic read-modify-write that loses concurrent updates. Firestore provides arrayUnion() and arrayRemove() specifically for atomic array updates that do not require reading the document first.",
|
|
4420
|
+
rationale: "Spreading an array or using filter() inside updateDoc() performs a non-atomic read-modify-write that loses concurrent updates. Firestore provides arrayUnion() and arrayRemove() specifically for atomic array updates that do not require reading the document first. Caveat: arrayRemove() matches whole element values \u2014 to remove object items by a predicate (e.g. by id), use runTransaction() instead so the read-modify-write is atomic.",
|
|
4360
4421
|
docsUrl: "https://firebase.google.com/docs/firestore/manage-data/add-data#update_elements_in_an_array",
|
|
4361
4422
|
recommended: true
|
|
4362
4423
|
},
|
|
@@ -4396,10 +4457,10 @@ var rule44 = {
|
|
|
4396
4457
|
};
|
|
4397
4458
|
}
|
|
4398
4459
|
};
|
|
4399
|
-
var firebaseUseArrayUnionRemoveRule =
|
|
4460
|
+
var firebaseUseArrayUnionRemoveRule = rule43;
|
|
4400
4461
|
|
|
4401
4462
|
// src/providers/firebase/rules/duplicate-initialize-app.ts
|
|
4402
|
-
var
|
|
4463
|
+
var rule44 = {
|
|
4403
4464
|
meta: {
|
|
4404
4465
|
type: "suggestion",
|
|
4405
4466
|
docs: {
|
|
@@ -4432,9 +4493,11 @@ var rule45 = {
|
|
|
4432
4493
|
const callee = node.callee;
|
|
4433
4494
|
if (callee?.type !== "Identifier") return;
|
|
4434
4495
|
if (initializeAppLocalName && callee.name === initializeAppLocalName) {
|
|
4435
|
-
|
|
4496
|
+
if ((node.arguments ?? []).length < 2) {
|
|
4497
|
+
initializeAppCalls.push(node);
|
|
4498
|
+
}
|
|
4436
4499
|
}
|
|
4437
|
-
if (callee.name === "getApps") {
|
|
4500
|
+
if (callee.name === "getApps" || callee.name === "getApp") {
|
|
4438
4501
|
hasGetAppsCall = true;
|
|
4439
4502
|
}
|
|
4440
4503
|
},
|
|
@@ -4447,21 +4510,21 @@ var rule45 = {
|
|
|
4447
4510
|
};
|
|
4448
4511
|
}
|
|
4449
4512
|
};
|
|
4450
|
-
var firebaseDuplicateInitializeAppRule =
|
|
4513
|
+
var firebaseDuplicateInitializeAppRule = rule44;
|
|
4451
4514
|
|
|
4452
4515
|
// src/providers/firebase/rules/onSnapshot-async-throw.ts
|
|
4453
|
-
var
|
|
4516
|
+
var rule45 = {
|
|
4454
4517
|
meta: {
|
|
4455
4518
|
type: "suggestion",
|
|
4456
4519
|
docs: {
|
|
4457
4520
|
description: "throw inside async onSnapshot callback creates an unhandled promise rejection",
|
|
4458
4521
|
category: "reliability",
|
|
4459
|
-
rationale: "onSnapshot
|
|
4522
|
+
rationale: "onSnapshot ignores the promise returned by an async callback, so a throw inside one becomes an unhandled promise rejection: the error never reaches the UI or the onSnapshot error callback (which only fires for stream errors), and in Node it can crash the process. The listener keeps delivering snapshots, but every one that hits the throw fails invisibly.",
|
|
4460
4523
|
docsUrl: "https://firebase.google.com/docs/firestore/query-data/listen#handle_listen_errors",
|
|
4461
4524
|
recommended: true
|
|
4462
4525
|
},
|
|
4463
4526
|
messages: {
|
|
4464
|
-
asyncThrowInSnapshot: "throw inside an async onSnapshot callback creates an unhandled promise rejection
|
|
4527
|
+
asyncThrowInSnapshot: "throw inside an async onSnapshot callback creates an unhandled promise rejection \u2014 the error surfaces nowhere. Catch it in the callback and set error state instead of throwing."
|
|
4465
4528
|
},
|
|
4466
4529
|
schema: []
|
|
4467
4530
|
},
|
|
@@ -4501,10 +4564,10 @@ var rule46 = {
|
|
|
4501
4564
|
};
|
|
4502
4565
|
}
|
|
4503
4566
|
};
|
|
4504
|
-
var firebaseOnSnapshotAsyncThrowRule =
|
|
4567
|
+
var firebaseOnSnapshotAsyncThrowRule = rule45;
|
|
4505
4568
|
|
|
4506
4569
|
// src/providers/firebase/rules/onSnapshot-missing-error-callback.ts
|
|
4507
|
-
var
|
|
4570
|
+
var rule46 = {
|
|
4508
4571
|
meta: {
|
|
4509
4572
|
type: "suggestion",
|
|
4510
4573
|
docs: {
|
|
@@ -4542,21 +4605,21 @@ var rule47 = {
|
|
|
4542
4605
|
};
|
|
4543
4606
|
}
|
|
4544
4607
|
};
|
|
4545
|
-
var firebaseOnSnapshotMissingErrorCallbackRule =
|
|
4608
|
+
var firebaseOnSnapshotMissingErrorCallbackRule = rule46;
|
|
4546
4609
|
|
|
4547
4610
|
// src/providers/firebase/rules/firestore-document-size-guard.ts
|
|
4548
|
-
var
|
|
4611
|
+
var rule47 = {
|
|
4549
4612
|
meta: {
|
|
4550
4613
|
type: "suggestion",
|
|
4551
4614
|
docs: {
|
|
4552
4615
|
description: "Firestore write includes editor.getJSON() without a document size guard",
|
|
4553
4616
|
category: "reliability",
|
|
4554
|
-
rationale: "Firestore documents have a 1 MiB limit. When a rich-text editor stores base64 images inline, a single paste can push the document over the limit. The write
|
|
4617
|
+
rationale: "Firestore documents have a 1 MiB limit. When a rich-text editor stores base64 images inline, a single paste can push the document over the limit. The write is rejected with INVALID_ARGUMENT \u2014 and if that rejection is not handled, nothing surfaces to the user. Check document size (e.g. new Blob([JSON.stringify(payload)]).size) before calling updateDoc/setDoc when the payload includes editor JSON.",
|
|
4555
4618
|
docsUrl: "https://firebase.google.com/docs/firestore/quotas#limits",
|
|
4556
4619
|
recommended: true
|
|
4557
4620
|
},
|
|
4558
4621
|
messages: {
|
|
4559
|
-
missingDocumentSizeGuard: "Firestore write includes editor.getJSON() without a document size check. Base64 images can exceed the 1 MiB Firestore limit
|
|
4622
|
+
missingDocumentSizeGuard: "Firestore write includes editor.getJSON() without a document size check. Base64 images can exceed the 1 MiB Firestore limit and the write is rejected. Add a size guard (new Blob([JSON.stringify(payload)]).size) before calling updateDoc/setDoc."
|
|
4560
4623
|
},
|
|
4561
4624
|
schema: []
|
|
4562
4625
|
},
|
|
@@ -4596,21 +4659,21 @@ var rule48 = {
|
|
|
4596
4659
|
};
|
|
4597
4660
|
}
|
|
4598
4661
|
};
|
|
4599
|
-
var firebaseFirestoreDocumentSizeGuardRule =
|
|
4662
|
+
var firebaseFirestoreDocumentSizeGuardRule = rule47;
|
|
4600
4663
|
|
|
4601
4664
|
// src/providers/firebase/rules/use-timestamp-now.ts
|
|
4602
|
-
var
|
|
4665
|
+
var rule48 = {
|
|
4603
4666
|
meta: {
|
|
4604
4667
|
type: "suggestion",
|
|
4605
4668
|
docs: {
|
|
4606
|
-
description: "new Date() used for Firestore timestamp instead of
|
|
4669
|
+
description: "new Date() used for Firestore timestamp instead of serverTimestamp()",
|
|
4607
4670
|
category: "correctness",
|
|
4608
|
-
rationale: "
|
|
4671
|
+
rationale: "Firestore auto-converts Date objects to Timestamps on write, so new Date() and Timestamp.now() are equivalent \u2014 both use the client clock, which cannot be trusted (skewed or deliberately changed clocks produce wrong orderings). serverTimestamp() stamps the value on the server instead, and it is the only option that satisfies security rules comparing a field against request.time (e.g. createdAt == request.time), which reject both new Date() and Timestamp.now().",
|
|
4609
4672
|
docsUrl: "https://firebase.google.com/docs/reference/js/firestore_.timestamp",
|
|
4610
4673
|
recommended: true
|
|
4611
4674
|
},
|
|
4612
4675
|
messages: {
|
|
4613
|
-
useTimestampNow: "Use
|
|
4676
|
+
useTimestampNow: "Use serverTimestamp() instead of new Date() for Firestore timestamp fields. new Date() uses the untrusted client clock and fails rules that compare against request.time."
|
|
4614
4677
|
},
|
|
4615
4678
|
schema: []
|
|
4616
4679
|
},
|
|
@@ -4619,7 +4682,7 @@ var rule49 = {
|
|
|
4619
4682
|
return {
|
|
4620
4683
|
ImportDeclaration(node) {
|
|
4621
4684
|
const src = node.source?.value;
|
|
4622
|
-
if (typeof src === "string" && (src.startsWith("firebase/") || src === "firebase-admin/firestore")) {
|
|
4685
|
+
if (typeof src === "string" && (src.startsWith("firebase/firestore") || src === "firebase-admin/firestore")) {
|
|
4623
4686
|
importsFromFirestore = true;
|
|
4624
4687
|
}
|
|
4625
4688
|
},
|
|
@@ -4632,7 +4695,7 @@ var rule49 = {
|
|
|
4632
4695
|
};
|
|
4633
4696
|
}
|
|
4634
4697
|
};
|
|
4635
|
-
var firebaseUseTimestampNowRule =
|
|
4698
|
+
var firebaseUseTimestampNowRule = rule48;
|
|
4636
4699
|
|
|
4637
4700
|
// src/providers/lovable/utils.ts
|
|
4638
4701
|
var LLM_HOST_PATTERN = /api\.anthropic\.com|api\.openai\.com/;
|
|
@@ -4647,7 +4710,7 @@ function containsKnownLlmHost(node) {
|
|
|
4647
4710
|
}
|
|
4648
4711
|
|
|
4649
4712
|
// src/providers/lovable/rules/no-client-side-secret-fetch.ts
|
|
4650
|
-
var
|
|
4713
|
+
var rule49 = {
|
|
4651
4714
|
meta: {
|
|
4652
4715
|
type: "problem",
|
|
4653
4716
|
docs: {
|
|
@@ -4720,13 +4783,13 @@ var rule50 = {
|
|
|
4720
4783
|
};
|
|
4721
4784
|
}
|
|
4722
4785
|
};
|
|
4723
|
-
var lovableNoClientSideSecretFetchRule =
|
|
4786
|
+
var lovableNoClientSideSecretFetchRule = rule49;
|
|
4724
4787
|
|
|
4725
4788
|
// src/providers/lovable/rules/paid-flag-without-edge-function.ts
|
|
4726
4789
|
var PRICE_PATTERN = /\$\s?\d/;
|
|
4727
4790
|
var FLAG_PROPERTY_PATTERN = /^is_[a-z0-9_]+$/i;
|
|
4728
4791
|
var FLAG_SUFFIX_PATTERN = /_(unlocked|active|enabled)$/i;
|
|
4729
|
-
var
|
|
4792
|
+
var rule50 = {
|
|
4730
4793
|
meta: {
|
|
4731
4794
|
type: "problem",
|
|
4732
4795
|
docs: {
|
|
@@ -4735,7 +4798,7 @@ var rule51 = {
|
|
|
4735
4798
|
cwe: "CWE-840",
|
|
4736
4799
|
owasp: "A04:2021 \u2013 Insecure Design",
|
|
4737
4800
|
rationale: "Lovable documents payment processing as Edge Function territory specifically so purchase/premium-access flags are only ever set after a payment provider confirms payment server-side. A handler that writes a paid-looking flag straight from the client with no payment call in between either never actually charges anyone, or \u2014 even if a charge happens elsewhere \u2014 leaves the flag itself freely settable by any caller with write access to the row.",
|
|
4738
|
-
docsUrl: "https://docs.lovable.dev/
|
|
4801
|
+
docsUrl: "https://docs.lovable.dev/integrations/stripe",
|
|
4739
4802
|
recommended: true
|
|
4740
4803
|
},
|
|
4741
4804
|
messages: {
|
|
@@ -4868,20 +4931,20 @@ var rule51 = {
|
|
|
4868
4931
|
};
|
|
4869
4932
|
}
|
|
4870
4933
|
};
|
|
4871
|
-
var lovablePaidFlagWithoutEdgeFunctionRule =
|
|
4934
|
+
var lovablePaidFlagWithoutEdgeFunctionRule = rule50;
|
|
4872
4935
|
|
|
4873
4936
|
// src/providers/lovable/rules/expiry-column-never-checked.ts
|
|
4874
4937
|
var EXPIRY_COLUMN_PATTERN = /_(until|expires?_at|expiry)$/i;
|
|
4875
4938
|
var DATE_COMPARISON_OPERATORS = /* @__PURE__ */ new Set([">", "<", ">=", "<="]);
|
|
4876
4939
|
var FILTER_METHODS = /* @__PURE__ */ new Set(["gt", "gte", "lt", "lte"]);
|
|
4877
|
-
var
|
|
4940
|
+
var rule51 = {
|
|
4878
4941
|
meta: {
|
|
4879
4942
|
type: "suggestion",
|
|
4880
4943
|
docs: {
|
|
4881
4944
|
description: "An expiry column must be checked against the current time somewhere",
|
|
4882
4945
|
category: "correctness",
|
|
4883
4946
|
rationale: "Writing a *_until/*_expires_at column without ever comparing it to the current time anywhere in the codebase means the expiry is purely cosmetic \u2014 whatever it was meant to gate (a boost, a trial, a temporary unlock) never actually expires once granted, whether it was granted legitimately or through an unrelated bug.",
|
|
4884
|
-
docsUrl: "https://docs.lovable.dev/
|
|
4947
|
+
docsUrl: "https://docs.lovable.dev/integrations/stripe",
|
|
4885
4948
|
recommended: true
|
|
4886
4949
|
},
|
|
4887
4950
|
messages: {
|
|
@@ -4966,18 +5029,18 @@ var rule52 = {
|
|
|
4966
5029
|
};
|
|
4967
5030
|
}
|
|
4968
5031
|
};
|
|
4969
|
-
var lovableExpiryColumnNeverCheckedRule =
|
|
5032
|
+
var lovableExpiryColumnNeverCheckedRule = rule51;
|
|
4970
5033
|
|
|
4971
5034
|
// src/providers/lovable/rules/silent-catch-on-provider-call.ts
|
|
4972
5035
|
var LOGGING_CONSOLE_METHODS = /* @__PURE__ */ new Set(["error", "warn", "log", "info"]);
|
|
4973
|
-
var
|
|
5036
|
+
var rule52 = {
|
|
4974
5037
|
meta: {
|
|
4975
5038
|
type: "suggestion",
|
|
4976
5039
|
docs: {
|
|
4977
5040
|
description: "A catch block around an LLM provider call must log the failure",
|
|
4978
5041
|
category: "correctness",
|
|
4979
5042
|
rationale: `Returning null/undefined from a bare catch with no logging makes every failure mode \u2014 missing key, expired key, rate limit, malformed response, network error \u2014 look identical to "no key configured." If a deployment's provider key starts failing in production, this is invisible: there is nothing in the browser or error-tracking logs to distinguish a real outage from an intentionally-unconfigured feature.`,
|
|
4980
|
-
docsUrl: "https://docs.lovable.dev/
|
|
5043
|
+
docsUrl: "https://docs.lovable.dev/integrations/cloud",
|
|
4981
5044
|
recommended: true
|
|
4982
5045
|
},
|
|
4983
5046
|
messages: {
|
|
@@ -5043,7 +5106,7 @@ var rule53 = {
|
|
|
5043
5106
|
};
|
|
5044
5107
|
}
|
|
5045
5108
|
};
|
|
5046
|
-
var lovableSilentCatchOnProviderCallRule =
|
|
5109
|
+
var lovableSilentCatchOnProviderCallRule = rule52;
|
|
5047
5110
|
|
|
5048
5111
|
// src/providers/browserbase/utils.ts
|
|
5049
5112
|
function memberPropName3(node) {
|
|
@@ -5156,7 +5219,7 @@ function isTruthyGateTest(test) {
|
|
|
5156
5219
|
}
|
|
5157
5220
|
return false;
|
|
5158
5221
|
}
|
|
5159
|
-
var
|
|
5222
|
+
var rule53 = {
|
|
5160
5223
|
meta: {
|
|
5161
5224
|
type: "problem",
|
|
5162
5225
|
docs: {
|
|
@@ -5228,10 +5291,10 @@ var rule54 = {
|
|
|
5228
5291
|
};
|
|
5229
5292
|
}
|
|
5230
5293
|
};
|
|
5231
|
-
var browserbaseNoConditionalAuthzOnAnonymousUserRule =
|
|
5294
|
+
var browserbaseNoConditionalAuthzOnAnonymousUserRule = rule53;
|
|
5232
5295
|
|
|
5233
5296
|
// src/providers/browserbase/rules/no-connect-url-in-api-response.ts
|
|
5234
|
-
var
|
|
5297
|
+
var rule54 = {
|
|
5235
5298
|
meta: {
|
|
5236
5299
|
type: "problem",
|
|
5237
5300
|
docs: {
|
|
@@ -5261,7 +5324,7 @@ var rule55 = {
|
|
|
5261
5324
|
};
|
|
5262
5325
|
}
|
|
5263
5326
|
};
|
|
5264
|
-
var browserbaseNoConnectUrlInApiResponseRule =
|
|
5327
|
+
var browserbaseNoConnectUrlInApiResponseRule = rule54;
|
|
5265
5328
|
|
|
5266
5329
|
// src/providers/browserbase/rules/session-id-requires-ownership-check.ts
|
|
5267
5330
|
function isOwnershipCheckCall(node) {
|
|
@@ -5278,7 +5341,7 @@ function isSessionIdLikeArg(node) {
|
|
|
5278
5341
|
}
|
|
5279
5342
|
return false;
|
|
5280
5343
|
}
|
|
5281
|
-
var
|
|
5344
|
+
var rule55 = {
|
|
5282
5345
|
meta: {
|
|
5283
5346
|
type: "suggestion",
|
|
5284
5347
|
docs: {
|
|
@@ -5286,7 +5349,7 @@ var rule56 = {
|
|
|
5286
5349
|
category: "security",
|
|
5287
5350
|
cwe: "CWE-862",
|
|
5288
5351
|
rationale: "A session id alone is not an authorization token \u2014 it is an opaque identifier that can leak via links, logs, screenshots, or support tickets. A handler that resolves a sessionId straight into sessions.debug()/sessions.recording.retrieve() with no ownership/org-membership check lets any authenticated caller view or replay a session that belongs to someone else.",
|
|
5289
|
-
docsUrl: "https://docs.browserbase.com/
|
|
5352
|
+
docsUrl: "https://docs.browserbase.com/platform/browser/observability/session-live-view",
|
|
5290
5353
|
recommended: true
|
|
5291
5354
|
},
|
|
5292
5355
|
messages: {
|
|
@@ -5342,7 +5405,7 @@ var rule56 = {
|
|
|
5342
5405
|
};
|
|
5343
5406
|
}
|
|
5344
5407
|
};
|
|
5345
|
-
var browserbaseSessionIdRequiresOwnershipCheckRule =
|
|
5408
|
+
var browserbaseSessionIdRequiresOwnershipCheckRule = rule55;
|
|
5346
5409
|
|
|
5347
5410
|
// src/providers/browserbase/rules/no-concurrent-shared-context.ts
|
|
5348
5411
|
function isPromiseAllCall2(node) {
|
|
@@ -5399,14 +5462,14 @@ function findSharedContextCreateCall(callback) {
|
|
|
5399
5462
|
visit(body);
|
|
5400
5463
|
return found;
|
|
5401
5464
|
}
|
|
5402
|
-
var
|
|
5465
|
+
var rule56 = {
|
|
5403
5466
|
meta: {
|
|
5404
5467
|
type: "problem",
|
|
5405
5468
|
docs: {
|
|
5406
5469
|
description: "A Browserbase Context must not be attached to concurrent sessions",
|
|
5407
5470
|
category: "correctness",
|
|
5408
5471
|
rationale: `Browserbase's docs state explicitly: "Avoid having multiple sessions using the same Context at once. Sites may force a log out." Passing the same browserSettings.context.id into every iteration of a Promise.all(items.map(...)) batch creates 2+ Browserbase sessions simultaneously attached to one Context, producing non-deterministic, flaky failures when a site force-logs-out one session because another concurrently authenticated against the same context.`,
|
|
5409
|
-
docsUrl: "https://docs.browserbase.com/features/contexts",
|
|
5472
|
+
docsUrl: "https://docs.browserbase.com/platform/browser/core-features/contexts",
|
|
5410
5473
|
recommended: true
|
|
5411
5474
|
},
|
|
5412
5475
|
messages: {
|
|
@@ -5446,7 +5509,7 @@ var rule57 = {
|
|
|
5446
5509
|
};
|
|
5447
5510
|
}
|
|
5448
5511
|
};
|
|
5449
|
-
var browserbaseNoConcurrentSharedContextRule =
|
|
5512
|
+
var browserbaseNoConcurrentSharedContextRule = rule56;
|
|
5450
5513
|
|
|
5451
5514
|
// src/providers/browserbase/rules/mobile-device-requires-os-setting.ts
|
|
5452
5515
|
function isMobileLiteral(node) {
|
|
@@ -5479,14 +5542,14 @@ function isOsMobileSetting(node) {
|
|
|
5479
5542
|
}
|
|
5480
5543
|
return false;
|
|
5481
5544
|
}
|
|
5482
|
-
var
|
|
5545
|
+
var rule57 = {
|
|
5483
5546
|
meta: {
|
|
5484
5547
|
type: "problem",
|
|
5485
5548
|
docs: {
|
|
5486
5549
|
description: "Mobile device combos must set browserSettings.os, not just resize the viewport",
|
|
5487
5550
|
category: "correctness",
|
|
5488
5551
|
rationale: `The Node SDK's device emulation lever is browserSettings.os: 'mobile' | 'tablet' \u2014 there is no fingerprint API in this SDK. A "mobile" session that only resizes the Playwright viewport is still a desktop Chrome browser with a desktop user-agent string in a small window. Sites that branch behavior on UA/touch capability (the majority of responsive sites) never actually exercise their mobile code path, silently undermining "test on mobile" results.`,
|
|
5489
|
-
docsUrl: "https://docs.browserbase.com/
|
|
5552
|
+
docsUrl: "https://docs.browserbase.com/platform/identity/verified-customization",
|
|
5490
5553
|
recommended: true
|
|
5491
5554
|
},
|
|
5492
5555
|
messages: {
|
|
@@ -5518,7 +5581,7 @@ var rule58 = {
|
|
|
5518
5581
|
};
|
|
5519
5582
|
}
|
|
5520
5583
|
};
|
|
5521
|
-
var browserbaseMobileDeviceRequiresOsSettingRule =
|
|
5584
|
+
var browserbaseMobileDeviceRequiresOsSettingRule = rule57;
|
|
5522
5585
|
|
|
5523
5586
|
// src/providers/browserbase/rules/use-typed-exception-status-not-substring.ts
|
|
5524
5587
|
function isStringifiedErrorSource(node, errName) {
|
|
@@ -5580,7 +5643,7 @@ function collectStringifiedVars(body, errName) {
|
|
|
5580
5643
|
});
|
|
5581
5644
|
return names;
|
|
5582
5645
|
}
|
|
5583
|
-
var
|
|
5646
|
+
var rule58 = {
|
|
5584
5647
|
meta: {
|
|
5585
5648
|
type: "suggestion",
|
|
5586
5649
|
docs: {
|
|
@@ -5610,7 +5673,7 @@ var rule59 = {
|
|
|
5610
5673
|
};
|
|
5611
5674
|
}
|
|
5612
5675
|
};
|
|
5613
|
-
var browserbaseUseTypedExceptionStatusNotSubstringRule =
|
|
5676
|
+
var browserbaseUseTypedExceptionStatusNotSubstringRule = rule58;
|
|
5614
5677
|
|
|
5615
5678
|
// src/providers/browserbase/rules/release-session-on-connect-failure.ts
|
|
5616
5679
|
function isSessionsCreateAwait(node) {
|
|
@@ -5633,7 +5696,7 @@ function isReleaseCall(node) {
|
|
|
5633
5696
|
const name = callee?.type === "Identifier" ? callee.name : memberPropName3(node);
|
|
5634
5697
|
return !!name && /requeststop|releasesession|release_session/i.test(name);
|
|
5635
5698
|
}
|
|
5636
|
-
var
|
|
5699
|
+
var rule59 = {
|
|
5637
5700
|
meta: {
|
|
5638
5701
|
type: "problem",
|
|
5639
5702
|
docs: {
|
|
@@ -5675,10 +5738,10 @@ var rule60 = {
|
|
|
5675
5738
|
};
|
|
5676
5739
|
}
|
|
5677
5740
|
};
|
|
5678
|
-
var browserbaseReleaseSessionOnConnectFailureRule =
|
|
5741
|
+
var browserbaseReleaseSessionOnConnectFailureRule = rule59;
|
|
5679
5742
|
|
|
5680
5743
|
// src/providers/browserbase/rules/dont-stack-custom-retry-on-sdk-retry.ts
|
|
5681
|
-
var
|
|
5744
|
+
var rule60 = {
|
|
5682
5745
|
meta: {
|
|
5683
5746
|
type: "suggestion",
|
|
5684
5747
|
docs: {
|
|
@@ -5723,7 +5786,7 @@ var rule61 = {
|
|
|
5723
5786
|
};
|
|
5724
5787
|
}
|
|
5725
5788
|
};
|
|
5726
|
-
var browserbaseDontStackCustomRetryOnSdkRetryRule =
|
|
5789
|
+
var browserbaseDontStackCustomRetryOnSdkRetryRule = rule60;
|
|
5727
5790
|
|
|
5728
5791
|
// src/providers/browserbase/rules/no-overbroad-error-substring-match.ts
|
|
5729
5792
|
var OVERBROAD_TERMS = /* @__PURE__ */ new Set(["session", "context", "browser", "browserbase"]);
|
|
@@ -5745,7 +5808,7 @@ function isCleanupCall(node) {
|
|
|
5745
5808
|
const name = callee?.type === "Identifier" ? callee.name : memberPropName3(node);
|
|
5746
5809
|
return !!name && /release|remove|cleanup|stop|teardown/i.test(name);
|
|
5747
5810
|
}
|
|
5748
|
-
var
|
|
5811
|
+
var rule61 = {
|
|
5749
5812
|
meta: {
|
|
5750
5813
|
type: "problem",
|
|
5751
5814
|
docs: {
|
|
@@ -5775,7 +5838,7 @@ var rule62 = {
|
|
|
5775
5838
|
};
|
|
5776
5839
|
}
|
|
5777
5840
|
};
|
|
5778
|
-
var browserbaseNoOverbroadErrorSubstringMatchRule =
|
|
5841
|
+
var browserbaseNoOverbroadErrorSubstringMatchRule = rule61;
|
|
5779
5842
|
|
|
5780
5843
|
// src/providers/browserbase/rules/use-sdk-not-raw-requests.ts
|
|
5781
5844
|
var BROWSERBASE_HOST = "api.browserbase.com";
|
|
@@ -5815,7 +5878,7 @@ function isRawHttpCall(node) {
|
|
|
5815
5878
|
}
|
|
5816
5879
|
return { isCall: false, urlArg: void 0 };
|
|
5817
5880
|
}
|
|
5818
|
-
var
|
|
5881
|
+
var rule62 = {
|
|
5819
5882
|
meta: {
|
|
5820
5883
|
type: "suggestion",
|
|
5821
5884
|
docs: {
|
|
@@ -5849,10 +5912,10 @@ var rule63 = {
|
|
|
5849
5912
|
};
|
|
5850
5913
|
}
|
|
5851
5914
|
};
|
|
5852
|
-
var browserbaseUseSdkNotRawRequestsRule =
|
|
5915
|
+
var browserbaseUseSdkNotRawRequestsRule = rule62;
|
|
5853
5916
|
|
|
5854
5917
|
// src/providers/browserbase/rules/centralize-request-release.ts
|
|
5855
|
-
var
|
|
5918
|
+
var rule63 = {
|
|
5856
5919
|
meta: {
|
|
5857
5920
|
type: "suggestion",
|
|
5858
5921
|
docs: {
|
|
@@ -5885,11 +5948,11 @@ var rule64 = {
|
|
|
5885
5948
|
};
|
|
5886
5949
|
}
|
|
5887
5950
|
};
|
|
5888
|
-
var browserbaseCentralizeRequestReleaseRule =
|
|
5951
|
+
var browserbaseCentralizeRequestReleaseRule = rule63;
|
|
5889
5952
|
|
|
5890
5953
|
// src/providers/openai-cua/rules/no-domain-allowlist.ts
|
|
5891
5954
|
var ACTION_METHOD_NAMES = /* @__PURE__ */ new Set(["click", "type", "press", "move", "goto", "fill", "dblclick", "dragAndDrop"]);
|
|
5892
|
-
var
|
|
5955
|
+
var rule64 = {
|
|
5893
5956
|
meta: {
|
|
5894
5957
|
type: "problem",
|
|
5895
5958
|
docs: {
|
|
@@ -6015,11 +6078,11 @@ var rule65 = {
|
|
|
6015
6078
|
};
|
|
6016
6079
|
}
|
|
6017
6080
|
};
|
|
6018
|
-
var openaiCuaNoDomainAllowlistRule =
|
|
6081
|
+
var openaiCuaNoDomainAllowlistRule = rule64;
|
|
6019
6082
|
|
|
6020
6083
|
// src/providers/openai-cua/rules/scroll-delta-default-zero.ts
|
|
6021
6084
|
var VERTICAL_DELTA_NAME_PATTERN = /^(dy|delta_?y|scroll_?y)$/i;
|
|
6022
|
-
var
|
|
6085
|
+
var rule65 = {
|
|
6023
6086
|
meta: {
|
|
6024
6087
|
type: "problem",
|
|
6025
6088
|
docs: {
|
|
@@ -6095,12 +6158,12 @@ var rule66 = {
|
|
|
6095
6158
|
};
|
|
6096
6159
|
}
|
|
6097
6160
|
};
|
|
6098
|
-
var openaiCuaScrollDeltaDefaultZeroRule =
|
|
6161
|
+
var openaiCuaScrollDeltaDefaultZeroRule = rule65;
|
|
6099
6162
|
|
|
6100
6163
|
// src/providers/openai-cua/rules/structured-step-metadata-not-text-json.ts
|
|
6101
6164
|
var BRACE_SEARCH_METHODS = /* @__PURE__ */ new Set(["indexOf", "lastIndexOf"]);
|
|
6102
6165
|
var SLICE_METHODS = /* @__PURE__ */ new Set(["slice", "substring", "substr"]);
|
|
6103
|
-
var
|
|
6166
|
+
var rule66 = {
|
|
6104
6167
|
meta: {
|
|
6105
6168
|
type: "suggestion",
|
|
6106
6169
|
docs: {
|
|
@@ -6199,10 +6262,10 @@ var rule67 = {
|
|
|
6199
6262
|
};
|
|
6200
6263
|
}
|
|
6201
6264
|
};
|
|
6202
|
-
var openaiCuaStructuredStepMetadataNotTextJsonRule =
|
|
6265
|
+
var openaiCuaStructuredStepMetadataNotTextJsonRule = rule66;
|
|
6203
6266
|
|
|
6204
6267
|
// src/providers/openai-cua/rules/no-blind-safety-check-ack.ts
|
|
6205
|
-
var
|
|
6268
|
+
var rule67 = {
|
|
6206
6269
|
meta: {
|
|
6207
6270
|
type: "suggestion",
|
|
6208
6271
|
docs: {
|
|
@@ -6265,7 +6328,7 @@ var rule68 = {
|
|
|
6265
6328
|
};
|
|
6266
6329
|
}
|
|
6267
6330
|
};
|
|
6268
|
-
var openaiCuaNoBlindSafetyCheckAckRule =
|
|
6331
|
+
var openaiCuaNoBlindSafetyCheckAckRule = rule67;
|
|
6269
6332
|
|
|
6270
6333
|
// src/providers/openai-cua/utils.ts
|
|
6271
6334
|
function propName(node) {
|
|
@@ -6311,7 +6374,7 @@ function findResponsesCreateCall(node, depth = 0) {
|
|
|
6311
6374
|
}
|
|
6312
6375
|
|
|
6313
6376
|
// src/providers/openai-cua/rules/retry-transient-turn-errors.ts
|
|
6314
|
-
var
|
|
6377
|
+
var rule68 = {
|
|
6315
6378
|
meta: {
|
|
6316
6379
|
type: "problem",
|
|
6317
6380
|
docs: {
|
|
@@ -6393,10 +6456,10 @@ var rule69 = {
|
|
|
6393
6456
|
};
|
|
6394
6457
|
}
|
|
6395
6458
|
};
|
|
6396
|
-
var openaiCuaRetryTransientTurnErrorsRule =
|
|
6459
|
+
var openaiCuaRetryTransientTurnErrorsRule = rule68;
|
|
6397
6460
|
|
|
6398
6461
|
// src/providers/openai-cua/rules/check-response-status-incomplete.ts
|
|
6399
|
-
var
|
|
6462
|
+
var rule69 = {
|
|
6400
6463
|
meta: {
|
|
6401
6464
|
type: "problem",
|
|
6402
6465
|
docs: {
|
|
@@ -6505,17 +6568,17 @@ var rule70 = {
|
|
|
6505
6568
|
};
|
|
6506
6569
|
}
|
|
6507
6570
|
};
|
|
6508
|
-
var openaiCuaCheckResponseStatusIncompleteRule =
|
|
6571
|
+
var openaiCuaCheckResponseStatusIncompleteRule = rule69;
|
|
6509
6572
|
|
|
6510
6573
|
// src/providers/openai-cua/rules/set-safety-identifier.ts
|
|
6511
|
-
var
|
|
6574
|
+
var rule70 = {
|
|
6512
6575
|
meta: {
|
|
6513
6576
|
type: "problem",
|
|
6514
6577
|
docs: {
|
|
6515
6578
|
description: "responses.create() must set safety_identifier for per-end-user policy attribution",
|
|
6516
6579
|
category: "integration",
|
|
6517
6580
|
rationale: "OpenAI documents that a stable per-end-user safety_identifier lets policy violations be attributed and acted on per end user. Without one on a multi-tenant integration routing many customers through a single shared API key, a single customer triggering a high-confidence policy-violation heuristic can result in access being temporarily revoked for the entire organization, not just the offending identifier.",
|
|
6518
|
-
docsUrl: "https://
|
|
6581
|
+
docsUrl: "https://developers.openai.com/api/docs/guides/safety-best-practices",
|
|
6519
6582
|
recommended: true
|
|
6520
6583
|
},
|
|
6521
6584
|
messages: {
|
|
@@ -6546,10 +6609,10 @@ var rule71 = {
|
|
|
6546
6609
|
};
|
|
6547
6610
|
}
|
|
6548
6611
|
};
|
|
6549
|
-
var openaiCuaSetSafetyIdentifierRule =
|
|
6612
|
+
var openaiCuaSetSafetyIdentifierRule = rule70;
|
|
6550
6613
|
|
|
6551
6614
|
// src/providers/tiptap/rules/upload-validate-fn-void.ts
|
|
6552
|
-
var
|
|
6615
|
+
var rule71 = {
|
|
6553
6616
|
meta: {
|
|
6554
6617
|
type: "problem",
|
|
6555
6618
|
docs: {
|
|
@@ -6613,10 +6676,10 @@ var rule72 = {
|
|
|
6613
6676
|
};
|
|
6614
6677
|
}
|
|
6615
6678
|
};
|
|
6616
|
-
var tiptapUploadValidateFnVoidRule =
|
|
6679
|
+
var tiptapUploadValidateFnVoidRule = rule71;
|
|
6617
6680
|
|
|
6618
6681
|
// src/providers/tiptap/rules/script-src-hardcoded-api-key.ts
|
|
6619
|
-
var
|
|
6682
|
+
var rule72 = {
|
|
6620
6683
|
meta: {
|
|
6621
6684
|
type: "problem",
|
|
6622
6685
|
docs: {
|
|
@@ -6674,10 +6737,10 @@ var rule73 = {
|
|
|
6674
6737
|
};
|
|
6675
6738
|
}
|
|
6676
6739
|
};
|
|
6677
|
-
var tiptapScriptSrcHardcodedApiKeyRule =
|
|
6740
|
+
var tiptapScriptSrcHardcodedApiKeyRule = rule72;
|
|
6678
6741
|
|
|
6679
6742
|
// src/providers/tiptap/rules/dynamic-script-no-sri.ts
|
|
6680
|
-
var
|
|
6743
|
+
var rule73 = {
|
|
6681
6744
|
meta: {
|
|
6682
6745
|
type: "problem",
|
|
6683
6746
|
docs: {
|
|
@@ -6686,7 +6749,7 @@ var rule74 = {
|
|
|
6686
6749
|
cwe: "CWE-829",
|
|
6687
6750
|
owasp: "API8:2023 Security Misconfiguration",
|
|
6688
6751
|
rationale: 'A script element injected without an integrity attribute will execute whatever the CDN returns, including malicious content if the CDN is compromised. Adding a Subresource Integrity hash (script.setAttribute("integrity", "sha384-...")) lets the browser verify the fetched script matches the expected bytes before executing it.',
|
|
6689
|
-
docsUrl: "https://developer.mozilla.org/en-US/docs/Web/Security/Subresource_Integrity",
|
|
6752
|
+
docsUrl: "https://developer.mozilla.org/en-US/docs/Web/Security/Defenses/Subresource_Integrity",
|
|
6690
6753
|
recommended: true
|
|
6691
6754
|
},
|
|
6692
6755
|
messages: {
|
|
@@ -6773,7 +6836,7 @@ var rule74 = {
|
|
|
6773
6836
|
};
|
|
6774
6837
|
}
|
|
6775
6838
|
};
|
|
6776
|
-
var tiptapDynamicScriptNoSriRule =
|
|
6839
|
+
var tiptapDynamicScriptNoSriRule = rule73;
|
|
6777
6840
|
|
|
6778
6841
|
// src/providers/tiptap/utils.ts
|
|
6779
6842
|
function findProperty3(objectExpression, name) {
|
|
@@ -6798,7 +6861,7 @@ function walk(node, visit) {
|
|
|
6798
6861
|
}
|
|
6799
6862
|
|
|
6800
6863
|
// src/providers/tiptap/rules/addAttributes-missing-renderHTML.ts
|
|
6801
|
-
var
|
|
6864
|
+
var rule74 = {
|
|
6802
6865
|
meta: {
|
|
6803
6866
|
type: "problem",
|
|
6804
6867
|
docs: {
|
|
@@ -6851,10 +6914,10 @@ var rule75 = {
|
|
|
6851
6914
|
};
|
|
6852
6915
|
}
|
|
6853
6916
|
};
|
|
6854
|
-
var tiptapAddAttributesMissingRenderHTMLRule =
|
|
6917
|
+
var tiptapAddAttributesMissingRenderHTMLRule = rule74;
|
|
6855
6918
|
|
|
6856
6919
|
// src/providers/tiptap/rules/appendTransaction-add-to-history.ts
|
|
6857
|
-
var
|
|
6920
|
+
var rule75 = {
|
|
6858
6921
|
meta: {
|
|
6859
6922
|
type: "suggestion",
|
|
6860
6923
|
docs: {
|
|
@@ -6933,10 +6996,10 @@ var rule76 = {
|
|
|
6933
6996
|
};
|
|
6934
6997
|
}
|
|
6935
6998
|
};
|
|
6936
|
-
var tiptapAppendTransactionAddToHistoryRule =
|
|
6999
|
+
var tiptapAppendTransactionAddToHistoryRule = rule75;
|
|
6937
7000
|
|
|
6938
7001
|
// src/providers/tiptap/rules/appendTransaction-full-scan.ts
|
|
6939
|
-
var
|
|
7002
|
+
var rule76 = {
|
|
6940
7003
|
meta: {
|
|
6941
7004
|
type: "suggestion",
|
|
6942
7005
|
docs: {
|
|
@@ -6996,10 +7059,10 @@ var rule77 = {
|
|
|
6996
7059
|
};
|
|
6997
7060
|
}
|
|
6998
7061
|
};
|
|
6999
|
-
var tiptapAppendTransactionFullScanRule =
|
|
7062
|
+
var tiptapAppendTransactionFullScanRule = rule76;
|
|
7000
7063
|
|
|
7001
7064
|
// src/providers/tiptap/rules/atom-node-wrap-in.ts
|
|
7002
|
-
var
|
|
7065
|
+
var rule77 = {
|
|
7003
7066
|
meta: {
|
|
7004
7067
|
type: "problem",
|
|
7005
7068
|
docs: {
|
|
@@ -7062,57 +7125,16 @@ var rule78 = {
|
|
|
7062
7125
|
};
|
|
7063
7126
|
}
|
|
7064
7127
|
};
|
|
7065
|
-
var tiptapAtomNodeWrapInRule =
|
|
7066
|
-
|
|
7067
|
-
// src/providers/tiptap/rules/twitter-url-regex.ts
|
|
7068
|
-
var rule79 = {
|
|
7069
|
-
meta: {
|
|
7070
|
-
type: "suggestion",
|
|
7071
|
-
docs: {
|
|
7072
|
-
description: "Twitter/X URL regex must match both x.com and twitter.com",
|
|
7073
|
-
category: "integration",
|
|
7074
|
-
rationale: "Twitter rebranded to X but twitter.com URLs still resolve and embed correctly. A regex that only matches x.com silently rejects all legacy twitter.com links pasted by users. Update the pattern to (x\\.com|twitter\\.com) so both domains are handled.",
|
|
7075
|
-
docsUrl: "https://tiptap.dev/docs/editor/extensions/nodes",
|
|
7076
|
-
recommended: true
|
|
7077
|
-
},
|
|
7078
|
-
messages: {
|
|
7079
|
-
twitterRegexMissingLegacyDomain: "This regex matches x.com but not twitter.com. Legacy twitter.com URLs are still valid and should be supported. Add (x\\.com|twitter\\.com) to the pattern."
|
|
7080
|
-
},
|
|
7081
|
-
schema: []
|
|
7082
|
-
},
|
|
7083
|
-
create(context) {
|
|
7084
|
-
function checkPattern(pattern, node) {
|
|
7085
|
-
if (!pattern.includes("x\\.com") && !pattern.includes("x.com")) return;
|
|
7086
|
-
if (pattern.includes("twitter\\.com") || pattern.includes("twitter.com")) return;
|
|
7087
|
-
context.report({ node, messageId: "twitterRegexMissingLegacyDomain" });
|
|
7088
|
-
}
|
|
7089
|
-
return {
|
|
7090
|
-
Literal(node) {
|
|
7091
|
-
if (node.regex?.pattern) {
|
|
7092
|
-
checkPattern(node.regex.pattern, node);
|
|
7093
|
-
}
|
|
7094
|
-
},
|
|
7095
|
-
NewExpression(node) {
|
|
7096
|
-
if (node.callee?.type === "Identifier" && node.callee.name === "RegExp") {
|
|
7097
|
-
const firstArg = node.arguments?.[0];
|
|
7098
|
-
if (firstArg?.type === "Literal" && typeof firstArg.value === "string") {
|
|
7099
|
-
checkPattern(firstArg.value, node);
|
|
7100
|
-
}
|
|
7101
|
-
}
|
|
7102
|
-
}
|
|
7103
|
-
};
|
|
7104
|
-
}
|
|
7105
|
-
};
|
|
7106
|
-
var tiptapTwitterUrlRegexRule = rule79;
|
|
7128
|
+
var tiptapAtomNodeWrapInRule = rule77;
|
|
7107
7129
|
|
|
7108
7130
|
// src/providers/tiptap/rules/drop-handler-pos-precedence.ts
|
|
7109
|
-
var
|
|
7131
|
+
var rule78 = {
|
|
7110
7132
|
meta: {
|
|
7111
7133
|
type: "problem",
|
|
7112
7134
|
docs: {
|
|
7113
7135
|
description: "x ?? 0 - 1 is parsed as x ?? (0 - 1) = x ?? -1 due to operator precedence",
|
|
7114
7136
|
category: "correctness",
|
|
7115
|
-
rationale: "The nullish coalescing operator (??) has lower precedence than arithmetic operators. Writing `pos ?? 0 - 1` evaluates as `pos ?? -1`, not `(pos ?? 0) - 1`.
|
|
7137
|
+
rationale: "The nullish coalescing operator (??) has lower precedence than arithmetic operators. Writing `pos ?? 0 - 1` evaluates as `pos ?? -1`, not `(pos ?? 0) - 1`. Whenever pos is defined, the expression yields pos instead of the intended pos - 1 \u2014 an off-by-one on every real drop. In ProseMirror drop handlers this misplaces the decoration/insert position, and the -1 fallback (when pos is nullish) is an invalid document position.",
|
|
7116
7138
|
docsUrl: "https://prosemirror.net/docs/ref/#view.EditorView.posAtCoords",
|
|
7117
7139
|
recommended: true
|
|
7118
7140
|
},
|
|
@@ -7133,7 +7155,7 @@ var rule80 = {
|
|
|
7133
7155
|
};
|
|
7134
7156
|
}
|
|
7135
7157
|
};
|
|
7136
|
-
var tiptapDropHandlerPosPrecedenceRule =
|
|
7158
|
+
var tiptapDropHandlerPosPrecedenceRule = rule78;
|
|
7137
7159
|
|
|
7138
7160
|
// src/providers/tiptap/rules/prefer-table-kit.ts
|
|
7139
7161
|
var INDIVIDUAL_TABLE_PACKAGES = /* @__PURE__ */ new Set([
|
|
@@ -7141,18 +7163,18 @@ var INDIVIDUAL_TABLE_PACKAGES = /* @__PURE__ */ new Set([
|
|
|
7141
7163
|
"@tiptap/extension-table-cell",
|
|
7142
7164
|
"@tiptap/extension-table-header"
|
|
7143
7165
|
]);
|
|
7144
|
-
var
|
|
7166
|
+
var rule79 = {
|
|
7145
7167
|
meta: {
|
|
7146
7168
|
type: "suggestion",
|
|
7147
7169
|
docs: {
|
|
7148
7170
|
description: "Use TableKit from @tiptap/extension-table instead of individual table packages",
|
|
7149
7171
|
category: "integration",
|
|
7150
|
-
rationale: "Importing table extensions individually from their separate packages bypasses the coordinated wiring that TableKit provides: shared configuration
|
|
7172
|
+
rationale: "Importing table extensions individually from their separate packages bypasses the coordinated wiring that TableKit provides: shared configuration and consistent HTMLAttributes across all table elements, configured in one place. TableKit from @tiptap/extension-table is the documented way to register the full table feature set.",
|
|
7151
7173
|
docsUrl: "https://tiptap.dev/docs/editor/extensions/functionality/table-kit",
|
|
7152
7174
|
recommended: true
|
|
7153
7175
|
},
|
|
7154
7176
|
messages: {
|
|
7155
|
-
preferTableKit: "Individual TipTap table extension imported. Use TableKit from @tiptap/extension-table
|
|
7177
|
+
preferTableKit: "Individual TipTap table extension imported. Use TableKit from @tiptap/extension-table to configure all table elements together."
|
|
7156
7178
|
},
|
|
7157
7179
|
schema: []
|
|
7158
7180
|
},
|
|
@@ -7175,21 +7197,21 @@ var rule81 = {
|
|
|
7175
7197
|
};
|
|
7176
7198
|
}
|
|
7177
7199
|
};
|
|
7178
|
-
var tiptapPreferTableKitRule =
|
|
7200
|
+
var tiptapPreferTableKitRule = rule79;
|
|
7179
7201
|
|
|
7180
7202
|
// src/providers/tiptap/rules/tiptap-markdown-missing-node-spec.ts
|
|
7181
|
-
var
|
|
7203
|
+
var rule80 = {
|
|
7182
7204
|
meta: {
|
|
7183
7205
|
type: "suggestion",
|
|
7184
7206
|
docs: {
|
|
7185
7207
|
description: "TipTap nodes used with tiptap-markdown must define a markdown serialization spec",
|
|
7186
7208
|
category: "integration",
|
|
7187
|
-
rationale: "When tiptap-markdown serializes a document, nodes without a markdown spec are silently dropped or emitted as empty strings. Custom node types must register a serialize/parse pair
|
|
7188
|
-
docsUrl: "https://github.com/
|
|
7209
|
+
rationale: "When tiptap-markdown serializes a document, nodes without a markdown spec are silently dropped or emitted as empty strings. Custom node types must register a serialize/parse pair (MarkdownNodeSpec) under a `markdown` key returned from addStorage \u2014 the library reads only extension.storage.markdown \u2014 so content survives markdown export/import cycles.",
|
|
7210
|
+
docsUrl: "https://github.com/aguingand/tiptap-markdown",
|
|
7189
7211
|
recommended: true
|
|
7190
7212
|
},
|
|
7191
7213
|
messages: {
|
|
7192
|
-
missingMarkdownNodeSpec: "This TipTap node is used alongside tiptap-markdown but defines no markdown serialization. Nodes without a markdown spec are silently dropped on export.
|
|
7214
|
+
missingMarkdownNodeSpec: "This TipTap node is used alongside tiptap-markdown but defines no markdown serialization. Nodes without a markdown spec are silently dropped on export. Return a markdown spec (MarkdownNodeSpec) from addStorage."
|
|
7193
7215
|
},
|
|
7194
7216
|
schema: []
|
|
7195
7217
|
},
|
|
@@ -7244,7 +7266,7 @@ var rule82 = {
|
|
|
7244
7266
|
};
|
|
7245
7267
|
}
|
|
7246
7268
|
};
|
|
7247
|
-
var tiptapTiptapMarkdownMissingNodeSpecRule =
|
|
7269
|
+
var tiptapTiptapMarkdownMissingNodeSpecRule = rule80;
|
|
7248
7270
|
|
|
7249
7271
|
// src/providers/elevenlabs/utils.ts
|
|
7250
7272
|
function isElevenLabsUrlArg(node) {
|
|
@@ -7305,7 +7327,7 @@ function posOf(n) {
|
|
|
7305
7327
|
}
|
|
7306
7328
|
|
|
7307
7329
|
// src/providers/elevenlabs/rules/validate-signed-url-response.ts
|
|
7308
|
-
var
|
|
7330
|
+
var rule81 = {
|
|
7309
7331
|
meta: {
|
|
7310
7332
|
type: "problem",
|
|
7311
7333
|
docs: {
|
|
@@ -7404,11 +7426,11 @@ var rule83 = {
|
|
|
7404
7426
|
};
|
|
7405
7427
|
}
|
|
7406
7428
|
};
|
|
7407
|
-
var elevenlabsValidateSignedUrlResponseRule =
|
|
7429
|
+
var elevenlabsValidateSignedUrlResponseRule = rule81;
|
|
7408
7430
|
|
|
7409
7431
|
// src/providers/elevenlabs/rules/no-error-object-logging.ts
|
|
7410
7432
|
var LOGGING_METHODS = /* @__PURE__ */ new Set(["error", "warn", "log"]);
|
|
7411
|
-
var
|
|
7433
|
+
var rule82 = {
|
|
7412
7434
|
meta: {
|
|
7413
7435
|
type: "problem",
|
|
7414
7436
|
docs: {
|
|
@@ -7476,10 +7498,10 @@ var rule84 = {
|
|
|
7476
7498
|
};
|
|
7477
7499
|
}
|
|
7478
7500
|
};
|
|
7479
|
-
var elevenlabsNoErrorObjectLoggingRule =
|
|
7501
|
+
var elevenlabsNoErrorObjectLoggingRule = rule82;
|
|
7480
7502
|
|
|
7481
7503
|
// src/providers/elevenlabs/rules/fetch-timeout-required.ts
|
|
7482
|
-
var
|
|
7504
|
+
var rule83 = {
|
|
7483
7505
|
meta: {
|
|
7484
7506
|
type: "suggestion",
|
|
7485
7507
|
docs: {
|
|
@@ -7512,10 +7534,10 @@ var rule85 = {
|
|
|
7512
7534
|
};
|
|
7513
7535
|
}
|
|
7514
7536
|
};
|
|
7515
|
-
var elevenlabsFetchTimeoutRequiredRule =
|
|
7537
|
+
var elevenlabsFetchTimeoutRequiredRule = rule83;
|
|
7516
7538
|
|
|
7517
7539
|
// src/providers/elevenlabs/rules/validate-agent-id-format.ts
|
|
7518
|
-
var
|
|
7540
|
+
var rule84 = {
|
|
7519
7541
|
meta: {
|
|
7520
7542
|
type: "problem",
|
|
7521
7543
|
docs: {
|
|
@@ -7621,10 +7643,10 @@ var rule86 = {
|
|
|
7621
7643
|
};
|
|
7622
7644
|
}
|
|
7623
7645
|
};
|
|
7624
|
-
var elevenlabsValidateAgentIdFormatRule =
|
|
7646
|
+
var elevenlabsValidateAgentIdFormatRule = rule84;
|
|
7625
7647
|
|
|
7626
7648
|
// src/providers/elevenlabs/rules/secure-session-id-generation.ts
|
|
7627
|
-
var
|
|
7649
|
+
var rule85 = {
|
|
7628
7650
|
meta: {
|
|
7629
7651
|
type: "problem",
|
|
7630
7652
|
docs: {
|
|
@@ -7689,10 +7711,10 @@ var rule87 = {
|
|
|
7689
7711
|
};
|
|
7690
7712
|
}
|
|
7691
7713
|
};
|
|
7692
|
-
var elevenlabsSecureSessionIdGenerationRule =
|
|
7714
|
+
var elevenlabsSecureSessionIdGenerationRule = rule85;
|
|
7693
7715
|
|
|
7694
7716
|
// src/providers/elevenlabs/rules/conversation-error-recovery.ts
|
|
7695
|
-
var
|
|
7717
|
+
var rule86 = {
|
|
7696
7718
|
meta: {
|
|
7697
7719
|
type: "suggestion",
|
|
7698
7720
|
docs: {
|
|
@@ -7792,10 +7814,10 @@ var rule88 = {
|
|
|
7792
7814
|
};
|
|
7793
7815
|
}
|
|
7794
7816
|
};
|
|
7795
|
-
var elevenlabsConversationErrorRecoveryRule =
|
|
7817
|
+
var elevenlabsConversationErrorRecoveryRule = rule86;
|
|
7796
7818
|
|
|
7797
7819
|
// src/providers/elevenlabs/rules/api-version-pinning.ts
|
|
7798
|
-
var
|
|
7820
|
+
var rule87 = {
|
|
7799
7821
|
meta: {
|
|
7800
7822
|
type: "suggestion",
|
|
7801
7823
|
docs: {
|
|
@@ -7835,10 +7857,10 @@ var rule89 = {
|
|
|
7835
7857
|
};
|
|
7836
7858
|
}
|
|
7837
7859
|
};
|
|
7838
|
-
var elevenlabsApiVersionPinningRule =
|
|
7860
|
+
var elevenlabsApiVersionPinningRule = rule87;
|
|
7839
7861
|
|
|
7840
7862
|
// src/providers/elevenlabs/rules/check-http-status-before-json.ts
|
|
7841
|
-
var
|
|
7863
|
+
var rule88 = {
|
|
7842
7864
|
meta: {
|
|
7843
7865
|
type: "problem",
|
|
7844
7866
|
docs: {
|
|
@@ -7934,11 +7956,11 @@ var rule90 = {
|
|
|
7934
7956
|
};
|
|
7935
7957
|
}
|
|
7936
7958
|
};
|
|
7937
|
-
var elevenlabsCheckHttpStatusBeforeJsonRule =
|
|
7959
|
+
var elevenlabsCheckHttpStatusBeforeJsonRule = rule88;
|
|
7938
7960
|
|
|
7939
7961
|
// src/providers/elevenlabs/rules/conversation-cleanup-on-error.ts
|
|
7940
7962
|
var END_METHODS = /* @__PURE__ */ new Set(["endSession", "endConversation"]);
|
|
7941
|
-
var
|
|
7963
|
+
var rule89 = {
|
|
7942
7964
|
meta: {
|
|
7943
7965
|
type: "suggestion",
|
|
7944
7966
|
docs: {
|
|
@@ -7999,11 +8021,11 @@ var rule91 = {
|
|
|
7999
8021
|
};
|
|
8000
8022
|
}
|
|
8001
8023
|
};
|
|
8002
|
-
var elevenlabsConversationCleanupOnErrorRule =
|
|
8024
|
+
var elevenlabsConversationCleanupOnErrorRule = rule89;
|
|
8003
8025
|
|
|
8004
8026
|
// src/providers/elevenlabs/rules/env-var-validation.ts
|
|
8005
8027
|
var ELEVENLABS_ENV_VAR_PATTERN = /^(XI_API_KEY|ELEVENLABS_API_KEY)$/;
|
|
8006
|
-
var
|
|
8028
|
+
var rule90 = {
|
|
8007
8029
|
meta: {
|
|
8008
8030
|
type: "suggestion",
|
|
8009
8031
|
docs: {
|
|
@@ -8064,7 +8086,7 @@ var rule92 = {
|
|
|
8064
8086
|
};
|
|
8065
8087
|
}
|
|
8066
8088
|
};
|
|
8067
|
-
var elevenlabsEnvVarValidationRule =
|
|
8089
|
+
var elevenlabsEnvVarValidationRule = rule90;
|
|
8068
8090
|
|
|
8069
8091
|
// src/providers/twilio/utils.ts
|
|
8070
8092
|
var POST_METHOD_NAMES = /* @__PURE__ */ new Set(["post"]);
|
|
@@ -8118,7 +8140,7 @@ function collectVarDeclarators2(node, out, depth = 0) {
|
|
|
8118
8140
|
}
|
|
8119
8141
|
|
|
8120
8142
|
// src/providers/twilio/rules/validate-webhook-signature.ts
|
|
8121
|
-
var
|
|
8143
|
+
var rule91 = {
|
|
8122
8144
|
meta: {
|
|
8123
8145
|
type: "problem",
|
|
8124
8146
|
docs: {
|
|
@@ -8160,10 +8182,10 @@ var rule93 = {
|
|
|
8160
8182
|
};
|
|
8161
8183
|
}
|
|
8162
8184
|
};
|
|
8163
|
-
var twilioValidateWebhookSignatureRule =
|
|
8185
|
+
var twilioValidateWebhookSignatureRule = rule91;
|
|
8164
8186
|
|
|
8165
8187
|
// src/providers/twilio/rules/taskrouter-attributes-match-consumer.ts
|
|
8166
|
-
var
|
|
8188
|
+
var rule92 = {
|
|
8167
8189
|
meta: {
|
|
8168
8190
|
type: "problem",
|
|
8169
8191
|
docs: {
|
|
@@ -8260,10 +8282,10 @@ var rule94 = {
|
|
|
8260
8282
|
};
|
|
8261
8283
|
}
|
|
8262
8284
|
};
|
|
8263
|
-
var twilioTaskrouterAttributesMatchConsumerRule =
|
|
8285
|
+
var twilioTaskrouterAttributesMatchConsumerRule = rule92;
|
|
8264
8286
|
|
|
8265
8287
|
// src/providers/twilio/rules/enqueue-task-json-stringify.ts
|
|
8266
|
-
var
|
|
8288
|
+
var rule93 = {
|
|
8267
8289
|
meta: {
|
|
8268
8290
|
type: "problem",
|
|
8269
8291
|
docs: {
|
|
@@ -8305,10 +8327,10 @@ var rule95 = {
|
|
|
8305
8327
|
};
|
|
8306
8328
|
}
|
|
8307
8329
|
};
|
|
8308
|
-
var twilioEnqueueTaskJsonStringifyRule =
|
|
8330
|
+
var twilioEnqueueTaskJsonStringifyRule = rule93;
|
|
8309
8331
|
|
|
8310
8332
|
// src/providers/twilio/rules/media-streams-key-by-call-sid.ts
|
|
8311
|
-
var
|
|
8333
|
+
var rule94 = {
|
|
8312
8334
|
meta: {
|
|
8313
8335
|
type: "problem",
|
|
8314
8336
|
docs: {
|
|
@@ -8384,12 +8406,12 @@ var rule96 = {
|
|
|
8384
8406
|
};
|
|
8385
8407
|
}
|
|
8386
8408
|
};
|
|
8387
|
-
var twilioMediaStreamsKeyByCallSidRule =
|
|
8409
|
+
var twilioMediaStreamsKeyByCallSidRule = rule94;
|
|
8388
8410
|
|
|
8389
8411
|
// src/providers/twilio/rules/await-or-catch-rest-calls-in-event-handlers.ts
|
|
8390
8412
|
var REST_RESOURCE_METHODS = /* @__PURE__ */ new Set(["create", "update", "remove", "fetch"]);
|
|
8391
8413
|
var EVENT_REGISTRATION_METHODS = /* @__PURE__ */ new Set(["onStart", "onStop", "onMedia", "onConnected", "on"]);
|
|
8392
|
-
var
|
|
8414
|
+
var rule95 = {
|
|
8393
8415
|
meta: {
|
|
8394
8416
|
type: "problem",
|
|
8395
8417
|
docs: {
|
|
@@ -8497,10 +8519,10 @@ var rule97 = {
|
|
|
8497
8519
|
};
|
|
8498
8520
|
}
|
|
8499
8521
|
};
|
|
8500
|
-
var twilioAwaitOrCatchRestCallsInEventHandlersRule =
|
|
8522
|
+
var twilioAwaitOrCatchRestCallsInEventHandlersRule = rule95;
|
|
8501
8523
|
|
|
8502
8524
|
// src/providers/twilio/rules/use-twiml-builder-not-string-templates.ts
|
|
8503
|
-
var
|
|
8525
|
+
var rule96 = {
|
|
8504
8526
|
meta: {
|
|
8505
8527
|
type: "suggestion",
|
|
8506
8528
|
docs: {
|
|
@@ -8529,10 +8551,10 @@ var rule98 = {
|
|
|
8529
8551
|
};
|
|
8530
8552
|
}
|
|
8531
8553
|
};
|
|
8532
|
-
var twilioUseTwimlBuilderNotStringTemplatesRule =
|
|
8554
|
+
var twilioUseTwimlBuilderNotStringTemplatesRule = rule96;
|
|
8533
8555
|
|
|
8534
8556
|
// src/providers/twilio/rules/media-streams-mark-pacing.ts
|
|
8535
|
-
var
|
|
8557
|
+
var rule97 = {
|
|
8536
8558
|
meta: {
|
|
8537
8559
|
type: "suggestion",
|
|
8538
8560
|
docs: {
|
|
@@ -8608,10 +8630,10 @@ var rule99 = {
|
|
|
8608
8630
|
};
|
|
8609
8631
|
}
|
|
8610
8632
|
};
|
|
8611
|
-
var twilioMediaStreamsMarkPacingRule =
|
|
8633
|
+
var twilioMediaStreamsMarkPacingRule = rule97;
|
|
8612
8634
|
|
|
8613
8635
|
// src/providers/twilio/rules/validate-all-request-inputs.ts
|
|
8614
|
-
var
|
|
8636
|
+
var rule98 = {
|
|
8615
8637
|
meta: {
|
|
8616
8638
|
type: "suggestion",
|
|
8617
8639
|
docs: {
|
|
@@ -8686,10 +8708,10 @@ var rule100 = {
|
|
|
8686
8708
|
};
|
|
8687
8709
|
}
|
|
8688
8710
|
};
|
|
8689
|
-
var twilioValidateAllRequestInputsRule =
|
|
8711
|
+
var twilioValidateAllRequestInputsRule = rule98;
|
|
8690
8712
|
|
|
8691
8713
|
// src/providers/twilio/rules/media-streams-mark-name-string.ts
|
|
8692
|
-
var
|
|
8714
|
+
var rule99 = {
|
|
8693
8715
|
meta: {
|
|
8694
8716
|
type: "suggestion",
|
|
8695
8717
|
docs: {
|
|
@@ -8739,7 +8761,7 @@ var rule101 = {
|
|
|
8739
8761
|
};
|
|
8740
8762
|
}
|
|
8741
8763
|
};
|
|
8742
|
-
var twilioMediaStreamsMarkNameStringRule =
|
|
8764
|
+
var twilioMediaStreamsMarkNameStringRule = rule99;
|
|
8743
8765
|
|
|
8744
8766
|
// src/providers/openai-realtime/utils.ts
|
|
8745
8767
|
function isOpenAIRealtimeUrlArg(node) {
|
|
@@ -8879,7 +8901,7 @@ function resolveStringValue(node, stringVarValues) {
|
|
|
8879
8901
|
}
|
|
8880
8902
|
|
|
8881
8903
|
// src/providers/openai-realtime/rules/migrate-beta-to-ga.ts
|
|
8882
|
-
var
|
|
8904
|
+
var rule100 = {
|
|
8883
8905
|
meta: {
|
|
8884
8906
|
type: "problem",
|
|
8885
8907
|
docs: {
|
|
@@ -8916,7 +8938,7 @@ var rule102 = {
|
|
|
8916
8938
|
};
|
|
8917
8939
|
}
|
|
8918
8940
|
};
|
|
8919
|
-
var openaiRealtimeMigrateBetaToGaRule =
|
|
8941
|
+
var openaiRealtimeMigrateBetaToGaRule = rule100;
|
|
8920
8942
|
|
|
8921
8943
|
// src/providers/openai-realtime/rules/no-log-raw-message-payloads.ts
|
|
8922
8944
|
var LOG_METHOD_NAMES = /* @__PURE__ */ new Set(["info", "warn", "error", "log"]);
|
|
@@ -8952,7 +8974,7 @@ function findCallExpressions(node, out, depth = 0) {
|
|
|
8952
8974
|
if (val && typeof val === "object") findCallExpressions(val, out, depth + 1);
|
|
8953
8975
|
}
|
|
8954
8976
|
}
|
|
8955
|
-
var
|
|
8977
|
+
var rule101 = {
|
|
8956
8978
|
meta: {
|
|
8957
8979
|
type: "problem",
|
|
8958
8980
|
docs: {
|
|
@@ -8996,7 +9018,7 @@ var rule103 = {
|
|
|
8996
9018
|
};
|
|
8997
9019
|
}
|
|
8998
9020
|
};
|
|
8999
|
-
var openaiRealtimeNoLogRawMessagePayloadsRule =
|
|
9021
|
+
var openaiRealtimeNoLogRawMessagePayloadsRule = rule101;
|
|
9000
9022
|
|
|
9001
9023
|
// src/providers/openai-realtime/rules/handle-error-server-event.ts
|
|
9002
9024
|
function isTypeMemberExpression(node) {
|
|
@@ -9027,13 +9049,13 @@ function collectTypeComparisonLiterals(node, out, depth = 0) {
|
|
|
9027
9049
|
if (val && typeof val === "object") collectTypeComparisonLiterals(val, out, depth + 1);
|
|
9028
9050
|
}
|
|
9029
9051
|
}
|
|
9030
|
-
var
|
|
9052
|
+
var rule102 = {
|
|
9031
9053
|
meta: {
|
|
9032
9054
|
type: "problem",
|
|
9033
9055
|
docs: {
|
|
9034
9056
|
description: 'OpenAI Realtime message handlers must check for the API-level "error" server event',
|
|
9035
9057
|
category: "reliability",
|
|
9036
|
-
docsUrl: "https://developers.openai.com/api/
|
|
9058
|
+
docsUrl: "https://developers.openai.com/api/reference/resources/realtime/server-events",
|
|
9037
9059
|
rationale: "The Realtime API's own error server event \u2014 distinct from the WebSocket transport-level error event \u2014 covers rate limits, invalid session.update payloads, content-moderation blocks, and retired/invalid model ids. A message handler that branches on event types but never checks for 'error' lets the call continue silently with translation/processing permanently dead for that leg, with no operator-visible signal of why.",
|
|
9038
9060
|
recommended: true
|
|
9039
9061
|
},
|
|
@@ -9063,7 +9085,7 @@ var rule104 = {
|
|
|
9063
9085
|
};
|
|
9064
9086
|
}
|
|
9065
9087
|
};
|
|
9066
|
-
var openaiRealtimeHandleErrorServerEventRule =
|
|
9088
|
+
var openaiRealtimeHandleErrorServerEventRule = rule102;
|
|
9067
9089
|
|
|
9068
9090
|
// src/providers/openai-realtime/rules/reconnect-on-drop.ts
|
|
9069
9091
|
var RECONNECT_NAME_PATTERN = /reconnect/i;
|
|
@@ -9087,7 +9109,7 @@ function hasReconnectAttempt(node, depth = 0) {
|
|
|
9087
9109
|
}
|
|
9088
9110
|
return false;
|
|
9089
9111
|
}
|
|
9090
|
-
var
|
|
9112
|
+
var rule103 = {
|
|
9091
9113
|
meta: {
|
|
9092
9114
|
type: "problem",
|
|
9093
9115
|
docs: {
|
|
@@ -9120,17 +9142,17 @@ var rule105 = {
|
|
|
9120
9142
|
};
|
|
9121
9143
|
}
|
|
9122
9144
|
};
|
|
9123
|
-
var openaiRealtimeReconnectOnDropRule =
|
|
9145
|
+
var openaiRealtimeReconnectOnDropRule = rule103;
|
|
9124
9146
|
|
|
9125
9147
|
// src/providers/openai-realtime/rules/avoid-dated-preview-snapshots.ts
|
|
9126
9148
|
var DATED_PREVIEW_MODEL_PATTERN = /model=[^&'"`]*-preview-\d{4}-\d{2}-\d{2}/;
|
|
9127
|
-
var
|
|
9149
|
+
var rule104 = {
|
|
9128
9150
|
meta: {
|
|
9129
9151
|
type: "suggestion",
|
|
9130
9152
|
docs: {
|
|
9131
9153
|
description: "OpenAI Realtime connections should not pin to a dated preview model snapshot",
|
|
9132
9154
|
category: "correctness",
|
|
9133
|
-
docsUrl: "https://developers.openai.com/api/
|
|
9155
|
+
docsUrl: "https://developers.openai.com/api/reference/resources/realtime/subresources/client_secrets",
|
|
9134
9156
|
rationale: "Dated preview snapshots are the category of model id OpenAI has historically retired with advance-notice sunset windows. Pinning to a dated preview snapshot rather than the floating GA alias (or a current dated GA snapshot) maximizes exposure to an eventual retirement, with no code path to detect or gracefully react to a model_not_found-style error if/when that happens.",
|
|
9135
9157
|
recommended: true
|
|
9136
9158
|
},
|
|
@@ -9159,16 +9181,16 @@ var rule106 = {
|
|
|
9159
9181
|
};
|
|
9160
9182
|
}
|
|
9161
9183
|
};
|
|
9162
|
-
var openaiRealtimeAvoidDatedPreviewSnapshotsRule =
|
|
9184
|
+
var openaiRealtimeAvoidDatedPreviewSnapshotsRule = rule104;
|
|
9163
9185
|
|
|
9164
9186
|
// src/providers/openai-realtime/rules/verify-deprecated-session-fields.ts
|
|
9165
|
-
var
|
|
9187
|
+
var rule105 = {
|
|
9166
9188
|
meta: {
|
|
9167
9189
|
type: "suggestion",
|
|
9168
9190
|
docs: {
|
|
9169
9191
|
description: "OpenAI Realtime session.update payloads should not rely on an unverified 'temperature' field",
|
|
9170
9192
|
category: "correctness",
|
|
9171
|
-
docsUrl: "https://developers.openai.com/api/
|
|
9193
|
+
docsUrl: "https://developers.openai.com/api/reference/resources/realtime/subresources/client_secrets",
|
|
9172
9194
|
rationale: "Two independent fetches of the current Realtime sessions API reference did not surface 'temperature' as a documented session field. Sending an undocumented field is typically harmless (silently ignored), but code that depends on an assumed constraint (e.g. 'a hard floor of 0.6') for behavior like deterministic output is relying on a claim that is no longer traceable to any current, citable source.",
|
|
9173
9195
|
recommended: true
|
|
9174
9196
|
},
|
|
@@ -9193,15 +9215,20 @@ var rule107 = {
|
|
|
9193
9215
|
};
|
|
9194
9216
|
}
|
|
9195
9217
|
};
|
|
9196
|
-
var openaiRealtimeVerifyDeprecatedSessionFieldsRule =
|
|
9218
|
+
var openaiRealtimeVerifyDeprecatedSessionFieldsRule = rule105;
|
|
9197
9219
|
|
|
9198
9220
|
// src/providers/openai-realtime/rules/buffer-audio-until-session-ready.ts
|
|
9199
9221
|
var QUEUE_CALL_NAME_PATTERN = /^(push|unshift|enqueue|queue)$/i;
|
|
9200
|
-
function
|
|
9201
|
-
if (test?.type !== "BinaryExpression"
|
|
9222
|
+
function readyStateOpenCheckKind(test) {
|
|
9223
|
+
if (test?.type !== "BinaryExpression") return null;
|
|
9224
|
+
const isEq = test.operator === "===" || test.operator === "==";
|
|
9225
|
+
const isNeq = test.operator === "!==" || test.operator === "!=";
|
|
9226
|
+
if (!isEq && !isNeq) return null;
|
|
9202
9227
|
const isReadyStateMember = (n) => n?.type === "MemberExpression" && n.property?.type === "Identifier" && n.property.name === "readyState";
|
|
9203
9228
|
const isOpenValue = (n) => n?.type === "MemberExpression" && n.property?.type === "Identifier" && n.property.name === "OPEN" || n?.type === "Literal" && n.value === 1;
|
|
9204
|
-
|
|
9229
|
+
const matches = isReadyStateMember(test.left) && isOpenValue(test.right) || isReadyStateMember(test.right) && isOpenValue(test.left);
|
|
9230
|
+
if (!matches) return null;
|
|
9231
|
+
return isEq ? "is-open" : "is-not-open";
|
|
9205
9232
|
}
|
|
9206
9233
|
function hasQueueCall(node, depth = 0) {
|
|
9207
9234
|
if (!node || typeof node !== "object" || depth > 40) return false;
|
|
@@ -9218,13 +9245,13 @@ function hasQueueCall(node, depth = 0) {
|
|
|
9218
9245
|
}
|
|
9219
9246
|
return false;
|
|
9220
9247
|
}
|
|
9221
|
-
var
|
|
9248
|
+
var rule106 = {
|
|
9222
9249
|
meta: {
|
|
9223
9250
|
type: "suggestion",
|
|
9224
9251
|
docs: {
|
|
9225
9252
|
description: "Audio sent before an OpenAI Realtime socket is open must be buffered, not dropped",
|
|
9226
9253
|
category: "reliability",
|
|
9227
|
-
docsUrl: "https://developers.openai.com/api/
|
|
9254
|
+
docsUrl: "https://developers.openai.com/api/reference/resources/realtime/client-events",
|
|
9228
9255
|
rationale: "Caller audio can arrive before the Realtime WebSocket finishes its connect + session.update round-trip. A readyState !== OPEN branch that only logs and returns silently drops every audio chunk that arrives in that window, meaning the first fragment of speech is lost to translation/processing on every call.",
|
|
9229
9256
|
recommended: true
|
|
9230
9257
|
},
|
|
@@ -9236,18 +9263,20 @@ var rule108 = {
|
|
|
9236
9263
|
create(context) {
|
|
9237
9264
|
return {
|
|
9238
9265
|
IfStatement(node) {
|
|
9239
|
-
|
|
9240
|
-
if (!
|
|
9241
|
-
|
|
9242
|
-
|
|
9266
|
+
const kind = readyStateOpenCheckKind(node.test);
|
|
9267
|
+
if (!kind) return;
|
|
9268
|
+
const notOpenBranch = kind === "is-open" ? node.alternate : node.consequent;
|
|
9269
|
+
if (!notOpenBranch) return;
|
|
9270
|
+
if (hasQueueCall(notOpenBranch)) return;
|
|
9271
|
+
context.report({ node: notOpenBranch, messageId: "audioDroppedNotBuffered" });
|
|
9243
9272
|
}
|
|
9244
9273
|
};
|
|
9245
9274
|
}
|
|
9246
9275
|
};
|
|
9247
|
-
var openaiRealtimeBufferAudioUntilSessionReadyRule =
|
|
9276
|
+
var openaiRealtimeBufferAudioUntilSessionReadyRule = rule106;
|
|
9248
9277
|
|
|
9249
9278
|
// src/providers/openai-realtime/rules/send-safety-identifier.ts
|
|
9250
|
-
var
|
|
9279
|
+
var rule107 = {
|
|
9251
9280
|
meta: {
|
|
9252
9281
|
type: "suggestion",
|
|
9253
9282
|
docs: {
|
|
@@ -9280,10 +9309,10 @@ var rule109 = {
|
|
|
9280
9309
|
};
|
|
9281
9310
|
}
|
|
9282
9311
|
};
|
|
9283
|
-
var openaiRealtimeSendSafetyIdentifierRule =
|
|
9312
|
+
var openaiRealtimeSendSafetyIdentifierRule = rule107;
|
|
9284
9313
|
|
|
9285
9314
|
// src/providers/openai-realtime/rules/transcription-model-choice.ts
|
|
9286
|
-
var
|
|
9315
|
+
var rule108 = {
|
|
9287
9316
|
meta: {
|
|
9288
9317
|
type: "suggestion",
|
|
9289
9318
|
docs: {
|
|
@@ -9294,7 +9323,7 @@ var rule110 = {
|
|
|
9294
9323
|
recommended: true
|
|
9295
9324
|
},
|
|
9296
9325
|
messages: {
|
|
9297
|
-
nonStreamingTranscriptionModel: "
|
|
9326
|
+
nonStreamingTranscriptionModel: "This session's input transcription is configured with 'whisper-1', which is not natively streaming and not optimized for realtime sessions."
|
|
9298
9327
|
},
|
|
9299
9328
|
schema: []
|
|
9300
9329
|
},
|
|
@@ -9307,7 +9336,12 @@ var rule110 = {
|
|
|
9307
9336
|
if (!isSessionUpdate) return;
|
|
9308
9337
|
const sessionProp = findProperty4(node, "session");
|
|
9309
9338
|
if (sessionProp?.value?.type !== "ObjectExpression") return;
|
|
9310
|
-
|
|
9339
|
+
let transcriptionProp = findProperty4(sessionProp.value, "input_audio_transcription");
|
|
9340
|
+
if (!transcriptionProp) {
|
|
9341
|
+
const audioProp = findProperty4(sessionProp.value, "audio");
|
|
9342
|
+
const inputProp = audioProp?.value?.type === "ObjectExpression" ? findProperty4(audioProp.value, "input") : null;
|
|
9343
|
+
transcriptionProp = inputProp?.value?.type === "ObjectExpression" ? findProperty4(inputProp.value, "transcription") : null;
|
|
9344
|
+
}
|
|
9311
9345
|
if (transcriptionProp?.value?.type !== "ObjectExpression") return;
|
|
9312
9346
|
const modelProp = findProperty4(transcriptionProp.value, "model");
|
|
9313
9347
|
const modelValue = modelProp?.value;
|
|
@@ -9318,7 +9352,7 @@ var rule110 = {
|
|
|
9318
9352
|
};
|
|
9319
9353
|
}
|
|
9320
9354
|
};
|
|
9321
|
-
var openaiRealtimeTranscriptionModelChoiceRule =
|
|
9355
|
+
var openaiRealtimeTranscriptionModelChoiceRule = rule108;
|
|
9322
9356
|
|
|
9323
9357
|
// src/plugin/index.ts
|
|
9324
9358
|
var plugin = {
|
|
@@ -9363,7 +9397,6 @@ var plugin = {
|
|
|
9363
9397
|
"firebase-rtdb-write-promise-not-handled": firebaseRtdbWritePromiseNotHandledRule,
|
|
9364
9398
|
"firebase-firestore-rules-expired": firebaseFirestoreRulesExpiredRule,
|
|
9365
9399
|
"firebase-id-token-cookie-flags": firebaseIdTokenCookieFlagsRule,
|
|
9366
|
-
"firebase-middleware-token-not-verified": firebaseMiddlewareTokenNotVerifiedRule,
|
|
9367
9400
|
"firebase-hardcoded-user-id": firebaseHardcodedUserIdRule,
|
|
9368
9401
|
"firebase-auth-user-not-found-disclosure": firebaseAuthUserNotFoundDisclosureRule,
|
|
9369
9402
|
"firebase-signup-password-confirm": firebaseSignupPasswordConfirmRule,
|
|
@@ -9402,7 +9435,6 @@ var plugin = {
|
|
|
9402
9435
|
"tiptap-appendTransaction-add-to-history": tiptapAppendTransactionAddToHistoryRule,
|
|
9403
9436
|
"tiptap-appendTransaction-full-scan": tiptapAppendTransactionFullScanRule,
|
|
9404
9437
|
"tiptap-atom-node-wrap-in": tiptapAtomNodeWrapInRule,
|
|
9405
|
-
"tiptap-twitter-url-regex": tiptapTwitterUrlRegexRule,
|
|
9406
9438
|
"tiptap-drop-handler-pos-precedence": tiptapDropHandlerPosPrecedenceRule,
|
|
9407
9439
|
"tiptap-prefer-table-kit": tiptapPreferTableKitRule,
|
|
9408
9440
|
"tiptap-tiptap-markdown-missing-node-spec": tiptapTiptapMarkdownMissingNodeSpecRule,
|
|
@@ -9440,8 +9472,8 @@ var plugin = {
|
|
|
9440
9472
|
// src/plugin/rule-registry.ts
|
|
9441
9473
|
function buildRegistry() {
|
|
9442
9474
|
const registry2 = /* @__PURE__ */ new Map();
|
|
9443
|
-
for (const [key,
|
|
9444
|
-
const docs =
|
|
9475
|
+
for (const [key, rule109] of Object.entries(plugin.rules)) {
|
|
9476
|
+
const docs = rule109?.meta?.docs ?? {};
|
|
9445
9477
|
registry2.set(key, {
|
|
9446
9478
|
category: docs.category,
|
|
9447
9479
|
description: docs.description ?? "",
|
|
@@ -9635,9 +9667,10 @@ async function detectProviders(directory, filesContent) {
|
|
|
9635
9667
|
// src/scanner.ts
|
|
9636
9668
|
var SKIP_DIRS = /* @__PURE__ */ new Set(["node_modules", "dist", "build", ".next"]);
|
|
9637
9669
|
var SOURCE_EXT = /\.(tsx?|jsx?)$/;
|
|
9670
|
+
var NPX_CMD = process.platform === "win32" ? "npx.cmd" : "npx";
|
|
9638
9671
|
function runOxlint(args, cwd) {
|
|
9639
9672
|
return new Promise((resolveRun) => {
|
|
9640
|
-
const child = spawn(
|
|
9673
|
+
const child = spawn(NPX_CMD, args, { cwd });
|
|
9641
9674
|
let stdout = "";
|
|
9642
9675
|
let stderr = "";
|
|
9643
9676
|
child.stdout?.on("data", (chunk) => {
|
|
@@ -9676,9 +9709,9 @@ function buildOxlintConfig(detectedNames) {
|
|
|
9676
9709
|
const ruleMetaByKey = /* @__PURE__ */ new Map();
|
|
9677
9710
|
for (const provider of providers) {
|
|
9678
9711
|
if (!detectedNames.has(provider.name)) continue;
|
|
9679
|
-
for (const
|
|
9680
|
-
oxlintRules[`${PLUGIN_NAME}/${
|
|
9681
|
-
ruleMetaByKey.set(
|
|
9712
|
+
for (const rule109 of provider.oxlintRules) {
|
|
9713
|
+
oxlintRules[`${PLUGIN_NAME}/${rule109.key}`] = rule109.severity === "error" || rule109.severity === void 0 ? "error" : "warn";
|
|
9714
|
+
ruleMetaByKey.set(rule109.key, rule109);
|
|
9682
9715
|
}
|
|
9683
9716
|
}
|
|
9684
9717
|
return { oxlintRules, ruleMetaByKey };
|
|
@@ -9796,9 +9829,9 @@ import { basename as basename2 } from "path";
|
|
|
9796
9829
|
function rationaleByRule() {
|
|
9797
9830
|
const map = /* @__PURE__ */ new Map();
|
|
9798
9831
|
for (const provider of providers) {
|
|
9799
|
-
for (const
|
|
9800
|
-
const docs = getRuleDocsMeta(
|
|
9801
|
-
if (docs?.rationale) map.set(
|
|
9832
|
+
for (const rule109 of provider.oxlintRules) {
|
|
9833
|
+
const docs = getRuleDocsMeta(rule109.key);
|
|
9834
|
+
if (docs?.rationale) map.set(rule109.resultRule, docs.rationale);
|
|
9802
9835
|
}
|
|
9803
9836
|
}
|
|
9804
9837
|
return map;
|