@api-doctor/cli 0.0.4 → 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/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/dashboard/webhooks/introduction#verify-webhook-signatures",
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/dashboard/webhooks/introduction",
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: "error"
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/dedupe key, so a retry can create a duplicate row.",
292
- fix: 'Add a client-generated idempotency key field backed by a unique constraint, or use .upsert(..., { onConflict: "<key column>" }).',
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: "warning"
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: "Throw a clear error (e.g. if (!url || !key) throw new Error(...)) before calling createClient.",
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: "warning"
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/database/postgres/row-level-security",
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/database/web/start",
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: "error"
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 Timestamp.now() or serverTimestamp().",
566
- fix: 'Import { Timestamp } from "firebase/firestore" and use Timestamp.now() for consistency with Firestore security rules.',
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/features/cloud",
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/features/cloud",
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/features/cloud",
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/features/session-live-view",
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/features/stealth-mode",
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://help.openai.com/en/articles/5428082-how-to-incorporate-a-safety-identifier",
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,9 +863,270 @@ 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: "Add a markdown serializer to addStorage or addOptions using MarkdownNodeSpec.",
883
- docsUrl: "https://github.com/ueberdosis/tiptap-markdown",
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",
868
+ severity: "warning"
869
+ }
870
+ ]
871
+ };
872
+
873
+ // src/providers/elevenlabs/manifest.ts
874
+ var elevenlabsManifest = {
875
+ name: "elevenlabs",
876
+ displayName: "ElevenLabs",
877
+ detect: {
878
+ packages: ["@11labs/client", "elevenlabs", "@elevenlabs/elevenlabs-js"],
879
+ imports: ["@11labs/client", "elevenlabs", "@elevenlabs/elevenlabs-js"],
880
+ urlPatterns: ["api.elevenlabs.io"]
881
+ },
882
+ oxlintRules: [
883
+ {
884
+ key: "elevenlabs-validate-signed-url-response",
885
+ resultRule: "elevenlabs/correctness/validate-signed-url-response",
886
+ message: "The signed URL response is used without validating that signed_url is present.",
887
+ fix: "Check that data.signed_url exists before using it, and throw/return an error response if it is missing.",
888
+ docsUrl: "https://elevenlabs.io/docs/eleven-agents/api-reference/conversations/get-signed-url",
889
+ severity: "error"
890
+ },
891
+ {
892
+ key: "elevenlabs-no-error-object-logging",
893
+ resultRule: "elevenlabs/security/no-error-object-logging",
894
+ message: "A catch block around an ElevenLabs API call logs the raw error object.",
895
+ fix: 'Log a sanitized message (e.g. error instanceof Error ? error.message : "Unknown error") instead of the raw error object.',
896
+ docsUrl: "https://elevenlabs.io/docs/api-reference/authentication",
897
+ severity: "error"
898
+ },
899
+ {
900
+ key: "elevenlabs-fetch-timeout-required",
901
+ resultRule: "elevenlabs/reliability/fetch-timeout-required",
902
+ message: "A fetch call to the ElevenLabs API has no abort signal/timeout.",
903
+ fix: "Pass an AbortController signal (e.g. via setTimeout(() => controller.abort(), 5000)) so a slow or unresponsive API does not hang the request indefinitely.",
904
+ docsUrl: "https://elevenlabs.io/docs/eleven-agents/api-reference/conversations/get-signed-url",
905
+ severity: "warning"
906
+ },
907
+ {
908
+ key: "elevenlabs-validate-agent-id-format",
909
+ resultRule: "elevenlabs/correctness/validate-agent-id-format",
910
+ message: "agentId is checked for existence but not validated against an expected format.",
911
+ fix: "Validate agentId against an allowed pattern (e.g. /^[a-zA-Z0-9\\-]{1,64}$/) before using it in an ElevenLabs API request.",
912
+ docsUrl: "https://elevenlabs.io/docs/eleven-agents/customization/authentication",
913
+ severity: "error"
914
+ },
915
+ {
916
+ key: "elevenlabs-secure-session-id-generation",
917
+ resultRule: "elevenlabs/security/secure-session-id-generation",
918
+ message: "A session id is generated with Math.random(), which is not cryptographically secure.",
919
+ fix: "Use crypto.getRandomValues() to generate session ids instead of Math.random().",
920
+ docsUrl: "https://elevenlabs.io/docs/eleven-api/guides/how-to/best-practices/security",
921
+ severity: "error"
922
+ },
923
+ {
924
+ key: "elevenlabs-conversation-error-recovery",
925
+ resultRule: "elevenlabs/reliability/conversation-error-recovery",
926
+ message: "A loading flag set before starting a conversation is never reset on failure.",
927
+ fix: "Reset the loading flag in the catch block (or a finally block) so a failed attempt does not leave the UI stuck loading.",
928
+ docsUrl: "https://elevenlabs.io/docs/eleven-agents/libraries/java-script",
929
+ severity: "warning"
930
+ },
931
+ {
932
+ key: "elevenlabs-api-version-pinning",
933
+ resultRule: "elevenlabs/reliability/api-version-pinning",
934
+ message: "A fetch call to the ElevenLabs API has no explicit API version header.",
935
+ fix: "Add an explicit version header (e.g. elevenlabs-version) to the request instead of relying on the URL path alone.",
936
+ docsUrl: "https://elevenlabs.io/docs/eleven-api/resources/breaking-changes-policy",
937
+ severity: "warning"
938
+ },
939
+ {
940
+ key: "elevenlabs-check-http-status-before-json",
941
+ resultRule: "elevenlabs/correctness/check-http-status-before-json",
942
+ message: "response.json() is called on an ElevenLabs API response without checking response.ok/status first.",
943
+ fix: "Check response.ok (or response.status) before calling response.json(), so an error body is not parsed as valid data.",
944
+ docsUrl: "https://elevenlabs.io/docs/eleven-api/resources/errors",
945
+ severity: "error"
946
+ },
947
+ {
948
+ key: "elevenlabs-conversation-cleanup-on-error",
949
+ resultRule: "elevenlabs/reliability/conversation-cleanup-on-error",
950
+ message: "A call to end the conversation has no surrounding try/catch.",
951
+ fix: "Wrap conversation.endSession()/endConversation() in try/catch so a rejection does not leave the conversation state in memory.",
952
+ docsUrl: "https://elevenlabs.io/docs/eleven-agents/libraries/java-script",
953
+ severity: "warning"
954
+ },
955
+ {
956
+ key: "elevenlabs-env-var-validation",
957
+ resultRule: "elevenlabs/correctness/env-var-validation",
958
+ message: "An ElevenLabs API key is only validated inside a request handler, not at module load.",
959
+ fix: "Read and validate the env var at module scope (and throw if missing) so a misconfigured deployment fails at startup.",
960
+ docsUrl: "https://elevenlabs.io/docs/api-reference/authentication",
961
+ severity: "warning"
962
+ }
963
+ ]
964
+ };
965
+
966
+ // src/providers/twilio/manifest.ts
967
+ var twilioManifest = {
968
+ name: "twilio",
969
+ displayName: "Twilio",
970
+ detect: {
971
+ packages: ["twilio"],
972
+ imports: ["twilio"],
973
+ urlPatterns: ["api.twilio.com"]
974
+ },
975
+ oxlintRules: [
976
+ {
977
+ key: "twilio-validate-webhook-signature",
978
+ resultRule: "twilio/security/validate-webhook-signature",
979
+ message: "A POST webhook route reads req.body but never validates the X-Twilio-Signature header.",
980
+ fix: "Validate the X-Twilio-Signature header with twilio.validateRequest()/RequestValidator before trusting the body.",
981
+ docsUrl: "https://www.twilio.com/docs/usage/webhooks/webhooks-security",
982
+ severity: "error"
983
+ },
984
+ {
985
+ key: "twilio-taskrouter-attributes-match-consumer",
986
+ resultRule: "twilio/correctness/taskrouter-attributes-match-consumer",
987
+ message: "A .task() attributes object is missing a field that a reservation handler reads back out of TaskAttributes.",
988
+ fix: "Add the missing field to the Task attributes object so the consumer's JSON.parse(TaskAttributes) destructure actually finds it.",
989
+ docsUrl: "https://www.twilio.com/docs/taskrouter/twiml-queue-calls",
990
+ severity: "error"
991
+ },
992
+ {
993
+ key: "twilio-enqueue-task-json-stringify",
994
+ resultRule: "twilio/security/enqueue-task-json-stringify",
995
+ message: "A .task() call builds JSON attributes via raw string interpolation instead of JSON.stringify().",
996
+ fix: "Build the Task attributes as an object and serialize with JSON.stringify() instead of interpolating values into a JSON string/template literal.",
997
+ docsUrl: "https://www.twilio.com/docs/api/errors/14221",
998
+ severity: "error"
999
+ },
1000
+ {
1001
+ key: "twilio-media-streams-key-by-call-sid",
1002
+ resultRule: "twilio/reliability/media-streams-key-by-call-sid",
1003
+ message: "A Media Streams session map is keyed by a phone-number-like field instead of callSid.",
1004
+ fix: "Key the map by start.callSid instead of the caller phone number, and propagate callSid (not the number) downstream as the lookup key.",
1005
+ docsUrl: "https://www.twilio.com/docs/voice/media-streams/websocket-messages",
1006
+ severity: "error"
1007
+ },
1008
+ {
1009
+ key: "twilio-await-or-catch-rest-calls-in-event-handlers",
1010
+ resultRule: "twilio/reliability/await-or-catch-rest-calls-in-event-handlers",
1011
+ message: "A Twilio REST call inside an event-handler callback has no surrounding try/catch.",
1012
+ fix: "Wrap the REST call in try/catch inside the handler so a rejected promise does not become an unhandled rejection.",
1013
+ docsUrl: "https://www.twilio.com/docs/usage/webhooks/webhooks-faq",
1014
+ severity: "error"
1015
+ },
1016
+ {
1017
+ key: "twilio-use-twiml-builder-not-string-templates",
1018
+ resultRule: "twilio/security/use-twiml-builder-not-string-templates",
1019
+ message: "TwiML is built as a raw template literal instead of the VoiceResponse builder.",
1020
+ fix: "Build TwiML with new VoiceResponse() and its builder methods (say, connect, stream, parameter) instead of interpolating values into an XML template literal.",
1021
+ docsUrl: "https://www.twilio.com/docs/voice/twiml/stream",
1022
+ severity: "warning"
1023
+ },
1024
+ {
1025
+ key: "twilio-media-streams-mark-pacing",
1026
+ resultRule: "twilio/reliability/media-streams-mark-pacing",
1027
+ message: "Media is forwarded to a Stream with no mark-based pacing (isLast) anywhere in the file.",
1028
+ fix: "Pace sends with mark events (pass isLast: true periodically) and wait for the inbound mark acknowledgment before queuing more audio.",
1029
+ docsUrl: "https://www.twilio.com/docs/api/errors/31931",
1030
+ severity: "warning"
1031
+ },
1032
+ {
1033
+ key: "twilio-validate-all-request-inputs",
1034
+ resultRule: "twilio/correctness/validate-all-request-inputs",
1035
+ message: "A webhook route reads a request field that is not covered by its schema (or has no schema at all).",
1036
+ fix: "Add a querystring/body schema covering every field the handler reads, and reject the request with a 400 if a required field is missing.",
1037
+ docsUrl: "https://www.twilio.com/docs/voice/twiml/stream",
1038
+ severity: "warning"
1039
+ },
1040
+ {
1041
+ key: "twilio-media-streams-mark-name-string",
1042
+ resultRule: "twilio/correctness/media-streams-mark-name-string",
1043
+ message: "mark.name is set to a non-string value (Twilio requires a string).",
1044
+ fix: "Wrap the value in String(...) (e.g. String(Date.now())) so mark.name matches Twilio's documented string schema.",
1045
+ docsUrl: "https://www.twilio.com/docs/voice/media-streams/websocket-messages",
1046
+ severity: "warning"
1047
+ }
1048
+ ]
1049
+ };
1050
+
1051
+ // src/providers/openai-realtime/manifest.ts
1052
+ var openaiRealtimeManifest = {
1053
+ name: "openai-realtime",
1054
+ displayName: "OpenAI Realtime API",
1055
+ detect: {
1056
+ urlPatterns: ["api.openai.com/v1/realtime"]
1057
+ },
1058
+ oxlintRules: [
1059
+ {
1060
+ key: "openai-realtime-migrate-beta-to-ga",
1061
+ resultRule: "openai-realtime/correctness/migrate-beta-to-ga",
1062
+ message: "This Realtime connection sends the deprecated OpenAI-Beta: realtime=v1 header.",
1063
+ fix: "Remove the OpenAI-Beta header and migrate the session/event shapes to the GA interface (session.type, audio.output nesting, response.output_audio.delta event names).",
1064
+ docsUrl: "https://developers.openai.com/api/docs/guides/realtime",
1065
+ severity: "error"
1066
+ },
1067
+ {
1068
+ key: "openai-realtime-no-log-raw-message-payloads",
1069
+ resultRule: "openai-realtime/security/no-log-raw-message-payloads",
1070
+ message: "A raw OpenAI Realtime message is logged verbatim, which can include live call audio or transcript content.",
1071
+ fix: "Log only derived fields (e.g. { type: message.type }) at info level; log full payloads only at trace level behind an explicit opt-in.",
1072
+ docsUrl: "https://developers.openai.com/api/docs/guides/realtime",
1073
+ severity: "error"
1074
+ },
1075
+ {
1076
+ key: "openai-realtime-handle-error-server-event",
1077
+ resultRule: "openai-realtime/reliability/handle-error-server-event",
1078
+ message: 'This Realtime message handler branches on event types but never checks for the API-level "error" event.',
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.",
1080
+ docsUrl: "https://developers.openai.com/api/reference/resources/realtime/server-events",
1081
+ severity: "error"
1082
+ },
1083
+ {
1084
+ key: "openai-realtime-reconnect-on-drop",
1085
+ resultRule: "openai-realtime/reliability/reconnect-on-drop",
1086
+ message: "This Realtime socket's close handler only logs and never attempts to reconnect.",
1087
+ fix: "On close, attempt a bounded number of reconnects with a fresh session.update, and proactively end/flag the call if reconnection ultimately fails.",
1088
+ docsUrl: "https://developers.openai.com/api/docs/guides/realtime",
1089
+ severity: "error"
1090
+ },
1091
+ {
1092
+ key: "openai-realtime-avoid-dated-preview-snapshots",
1093
+ resultRule: "openai-realtime/correctness/avoid-dated-preview-snapshots",
1094
+ message: "The Realtime connection is pinned to a dated preview model snapshot instead of the GA alias.",
1095
+ fix: "Use the GA model id (e.g. 'gpt-realtime') or a current dated snapshot tracked against OpenAI's deprecation notices.",
1096
+ docsUrl: "https://developers.openai.com/api/reference/resources/realtime/subresources/client_secrets",
884
1097
  severity: "warning"
1098
+ },
1099
+ {
1100
+ key: "openai-realtime-verify-deprecated-session-fields",
1101
+ resultRule: "openai-realtime/correctness/verify-deprecated-session-fields",
1102
+ message: "The session config sets 'temperature', a field not documented in the current GA Realtime sessions schema.",
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.",
1104
+ docsUrl: "https://developers.openai.com/api/reference/resources/realtime/subresources/client_secrets",
1105
+ severity: "warning"
1106
+ },
1107
+ {
1108
+ key: "openai-realtime-buffer-audio-until-session-ready",
1109
+ resultRule: "openai-realtime/reliability/buffer-audio-until-session-ready",
1110
+ message: "Audio sent before the Realtime socket reaches the open state is dropped instead of buffered.",
1111
+ fix: "Queue outbound input_audio_buffer.append messages until the open event fires, then flush them in order.",
1112
+ docsUrl: "https://developers.openai.com/api/reference/resources/realtime/client-events",
1113
+ severity: "warning"
1114
+ },
1115
+ {
1116
+ key: "openai-realtime-send-safety-identifier",
1117
+ resultRule: "openai-realtime/security/send-safety-identifier",
1118
+ message: "This Realtime WebSocket connection does not send an OpenAI-Safety-Identifier header.",
1119
+ fix: "Add an OpenAI-Safety-Identifier header with a stable, privacy-preserving value (e.g. a hashed account/call id) to support abuse/safety monitoring.",
1120
+ docsUrl: "https://developers.openai.com/api/docs/guides/realtime",
1121
+ severity: "info"
1122
+ },
1123
+ {
1124
+ key: "openai-realtime-transcription-model-choice",
1125
+ resultRule: "openai-realtime/correctness/transcription-model-choice",
1126
+ message: "The session's input transcription is configured with 'whisper-1', which is not natively streaming and not optimized for realtime sessions.",
1127
+ fix: "Switch to 'gpt-realtime-whisper' if transcription output is consumed, or drop input_audio_transcription entirely if it isn't.",
1128
+ docsUrl: "https://developers.openai.com/api/docs/guides/realtime-transcription",
1129
+ severity: "info"
885
1130
  }
886
1131
  ]
887
1132
  };
@@ -895,7 +1140,10 @@ var providers = [
895
1140
  lovableManifest,
896
1141
  browserbaseManifest,
897
1142
  openaiCuaManifest,
898
- tiptapManifest
1143
+ tiptapManifest,
1144
+ elevenlabsManifest,
1145
+ twilioManifest,
1146
+ openaiRealtimeManifest
899
1147
  ];
900
1148
 
901
1149
  // src/reporter/animate.ts
@@ -968,7 +1216,7 @@ var rule = {
968
1216
  cwe: "CWE-345",
969
1217
  owasp: "API2:2023 Broken Authentication",
970
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.",
971
- docsUrl: "https://resend.com/docs/dashboard/webhooks/introduction#verify-webhook-signatures",
1219
+ docsUrl: "https://resend.com/docs/webhooks/verify-webhooks-requests",
972
1220
  recommended: true
973
1221
  },
974
1222
  messages: {
@@ -1045,6 +1293,15 @@ var rule = {
1045
1293
  const obj = callee.object;
1046
1294
  return obj?.type === "Identifier" && (obj.name === "req" || obj.name === "request");
1047
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
+ }
1048
1305
  function isBodyMember(n) {
1049
1306
  if (n?.type !== "MemberExpression") return false;
1050
1307
  const prop = n.property;
@@ -1069,6 +1326,17 @@ var rule = {
1069
1326
  const obj = callee.object;
1070
1327
  return obj?.type === "Identifier" && svixImports.has(obj.name);
1071
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
+ }
1072
1340
  function recordFirst(posKey, handler, pos) {
1073
1341
  const existing = handler[posKey];
1074
1342
  if (!existing) {
@@ -1083,6 +1351,11 @@ var rule = {
1083
1351
  ImportDeclaration(node) {
1084
1352
  const importSource = node?.source?.value;
1085
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
+ }
1086
1359
  if (importSource === "svix") {
1087
1360
  for (const s of node.specifiers ?? []) {
1088
1361
  if (s?.type === "ImportSpecifier" && s.local?.type === "Identifier") {
@@ -1104,8 +1377,10 @@ var rule = {
1104
1377
  for (const handler of postHandlers) {
1105
1378
  if (!within(handler, node)) continue;
1106
1379
  if (isReqJsonCall(node)) recordFirst("firstBodyPos", handler, pos);
1380
+ if (isReqTextCall(node)) recordFirst("firstRawBodyPos", handler, pos);
1107
1381
  if (isSvixVerifyCall(node)) recordFirst("firstVerifyPos", handler, pos);
1108
1382
  if (isCryptoCreateHmacCall(node)) recordFirst("firstVerifyPos", handler, pos);
1383
+ if (isResendWebhooksVerifyCall(node)) recordFirst("firstVerifyPos", handler, pos);
1109
1384
  }
1110
1385
  },
1111
1386
  MemberExpression(node) {
@@ -1120,6 +1395,8 @@ var rule = {
1120
1395
  "Program:exit"() {
1121
1396
  if (!importsResend) return;
1122
1397
  for (const handler of postHandlers) {
1398
+ const consumesRequest = handler.firstBodyPos || handler.firstRawBodyPos;
1399
+ if (!consumesRequest) continue;
1123
1400
  if (!handler.firstVerifyPos) {
1124
1401
  context.report({ node: handler.node, messageId: "missingVerification" });
1125
1402
  continue;
@@ -1691,7 +1968,7 @@ var rule11 = {
1691
1968
  description: "Resend webhook handlers should deduplicate retried events",
1692
1969
  category: "reliability",
1693
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.",
1694
- docsUrl: "https://resend.com/docs/dashboard/webhooks/introduction",
1971
+ docsUrl: "https://resend.com/docs/webhooks/introduction",
1695
1972
  recommended: true
1696
1973
  },
1697
1974
  messages: {
@@ -1993,12 +2270,12 @@ var rule14 = {
1993
2270
  docs: {
1994
2271
  description: "Supabase queries that select a tenant column must filter by it",
1995
2272
  category: "correctness",
1996
- rationale: "A column like session_id or user_id existing in the schema (and being selected) signals intent to scope rows to one caller, but selecting it is not the same as filtering by it. Without an .eq()/.match()/.filter() on that column, the query returns every row for every tenant, turning a per-user feed into a single shared, cross-user one.",
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.",
1997
2274
  docsUrl: "https://supabase.com/docs/reference/javascript/eq",
1998
2275
  recommended: true
1999
2276
  },
2000
2277
  messages: {
2001
- missingTenantFilter: 'This query selects "{{column}}" but never filters by it. Add .eq("{{column}}", ...) (or .match()/.filter()) to scope results to the caller.'
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.`
2002
2279
  },
2003
2280
  schema: []
2004
2281
  },
@@ -2270,7 +2547,8 @@ function objectHasIdempotencyKey(objectExpression) {
2270
2547
  return (objectExpression.properties ?? []).some((p) => {
2271
2548
  if (p?.type !== "Property") return false;
2272
2549
  const name = p.key?.type === "Identifier" ? p.key.name : p.key?.type === "Literal" ? p.key.value : void 0;
2273
- return typeof name === "string" && /idempot|dedupe/i.test(name);
2550
+ if (typeof name !== "string") return false;
2551
+ return /idempot|dedupe/i.test(name) || /^(id|uuid)$/i.test(name) || /_(key|uuid)$/i.test(name);
2274
2552
  });
2275
2553
  }
2276
2554
  function insertPayloadHasIdempotencyKey(arg) {
@@ -2286,12 +2564,12 @@ var rule18 = {
2286
2564
  docs: {
2287
2565
  description: "Supabase insert calls should be retry-safe via an idempotency key",
2288
2566
  category: "reliability",
2289
- rationale: 'Nothing prevents a duplicate row if the client fetch behind an insert is retried (flaky network, double-click, browser replay) \u2014 there is no unique constraint or dedupe key visible in the payload, and no upsert semantics. Generate a client-side idempotency key per logical action and either include it as a field guarded by a unique constraint, or use .upsert(..., { onConflict: "<key column>" }).',
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.',
2290
2568
  docsUrl: "https://supabase.com/docs/reference/javascript/upsert",
2291
2569
  recommended: true
2292
2570
  },
2293
2571
  messages: {
2294
- missingIdempotencyKey: 'This insert has no idempotency/dedupe key field, so a retried request can create a duplicate row. Add one, or use .upsert(..., { onConflict: "<key column>" }).'
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>" }).'
2295
2573
  },
2296
2574
  schema: []
2297
2575
  },
@@ -2340,7 +2618,7 @@ var rule19 = {
2340
2618
  docs: {
2341
2619
  description: "createClient must fail fast when required env vars are missing",
2342
2620
  category: "reliability",
2343
- rationale: "createClient does not throw on undefined arguments \u2014 a missing env var surfaces later as an opaque error deep in a fetch call rather than a clear message at startup. Checking presence before calling createClient turns a confusing runtime failure (e.g. on a misconfigured second service) into an immediate, actionable one.",
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.`,
2344
2622
  docsUrl: "https://supabase.com/docs/reference/javascript/initializing",
2345
2623
  recommended: true
2346
2624
  },
@@ -2350,7 +2628,11 @@ var rule19 = {
2350
2628
  schema: []
2351
2629
  },
2352
2630
  create(context) {
2353
- let createClientLocalName;
2631
+ const FACTORY_IMPORTS = {
2632
+ "@supabase/supabase-js": ["createClient"],
2633
+ "@supabase/ssr": ["createBrowserClient", "createServerClient"]
2634
+ };
2635
+ const factoryLocalNames = /* @__PURE__ */ new Set();
2354
2636
  const envVarOfVariable = /* @__PURE__ */ new Map();
2355
2637
  const validatedVarNames = /* @__PURE__ */ new Set();
2356
2638
  const validatedEnvNames = /* @__PURE__ */ new Set();
@@ -2384,10 +2666,11 @@ var rule19 = {
2384
2666
  }
2385
2667
  return {
2386
2668
  ImportDeclaration(node) {
2387
- if (node.source?.value !== "@supabase/supabase-js") return;
2669
+ const factories = FACTORY_IMPORTS[node.source?.value];
2670
+ if (!factories) return;
2388
2671
  for (const s of node.specifiers ?? []) {
2389
- if (s?.type === "ImportSpecifier" && s.imported?.type === "Identifier" && s.imported.name === "createClient" && s.local?.type === "Identifier") {
2390
- createClientLocalName = s.local.name;
2672
+ if (s?.type === "ImportSpecifier" && s.imported?.type === "Identifier" && factories.includes(s.imported.name) && s.local?.type === "Identifier") {
2673
+ factoryLocalNames.add(s.local.name);
2391
2674
  }
2392
2675
  }
2393
2676
  },
@@ -2401,8 +2684,8 @@ var rule19 = {
2401
2684
  collectGuardTargets(node.test);
2402
2685
  },
2403
2686
  CallExpression(node) {
2404
- if (!createClientLocalName) return;
2405
- if (node.callee?.type !== "Identifier" || node.callee.name !== createClientLocalName) return;
2687
+ if (factoryLocalNames.size === 0) return;
2688
+ if (node.callee?.type !== "Identifier" || !factoryLocalNames.has(node.callee.name)) return;
2406
2689
  const missing = [];
2407
2690
  for (const rawArg of node.arguments ?? []) {
2408
2691
  const arg = unwrapNonNull(rawArg);
@@ -2470,8 +2753,8 @@ var rule20 = {
2470
2753
  category: "security",
2471
2754
  cwe: "CWE-285",
2472
2755
  owasp: "A01:2021 Broken Access Control",
2473
- rationale: "Supabase documents raw_user_meta_data as client-writable and unsuitable for authorization. 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.",
2474
- docsUrl: "https://supabase.com/docs/guides/database/postgres/row-level-security",
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",
2475
2758
  recommended: true
2476
2759
  },
2477
2760
  messages: {
@@ -2507,7 +2790,7 @@ var rule21 = {
2507
2790
  docs: {
2508
2791
  description: "Supabase .single() calls must inspect the returned error field",
2509
2792
  category: "correctness",
2510
- 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.",
2511
2794
  docsUrl: "https://supabase.com/docs/reference/javascript/single",
2512
2795
  recommended: true
2513
2796
  },
@@ -2517,14 +2800,29 @@ var rule21 = {
2517
2800
  schema: []
2518
2801
  },
2519
2802
  create(context) {
2803
+ const deferredBindings = [];
2804
+ const errorReadNames = /* @__PURE__ */ new Set();
2520
2805
  function checkAwaitBinding(node, pattern, awaitExpr) {
2521
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
+ }
2522
2812
  const names = destructuredNames(pattern);
2523
2813
  if (names.has("error")) return;
2524
2814
  context.report({ node, messageId: "missingErrorCheck" });
2525
2815
  }
2526
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
+ },
2527
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
+ }
2528
2826
  if (node.init?.type !== "AwaitExpression") return;
2529
2827
  checkAwaitBinding(node, node.id, node.init);
2530
2828
  },
@@ -2536,9 +2834,17 @@ var rule21 = {
2536
2834
  const expr = node.expression;
2537
2835
  if (expr?.type !== "AwaitExpression") return;
2538
2836
  if (!isSingleSupabaseQuery(expr.argument)) return;
2837
+ if (chainHasMethod(expr.argument, "throwOnError")) return;
2539
2838
  if (memberPropName(expr.argument) === "single") {
2540
2839
  context.report({ node, messageId: "missingErrorCheck" });
2541
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
+ }
2542
2848
  }
2543
2849
  };
2544
2850
  }
@@ -2647,7 +2953,7 @@ var rule23 = {
2647
2953
  docs: {
2648
2954
  description: "Supabase mutations must check the returned error field",
2649
2955
  category: "correctness",
2650
- 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.",
2651
2957
  docsUrl: "https://supabase.com/docs/reference/javascript/insert",
2652
2958
  recommended: true
2653
2959
  },
@@ -2657,30 +2963,52 @@ var rule23 = {
2657
2963
  schema: []
2658
2964
  },
2659
2965
  create(context) {
2966
+ const deferredBindings = [];
2967
+ const errorReadNames = /* @__PURE__ */ new Set();
2660
2968
  function checkMutationAwait(node, pattern, awaitExpr) {
2661
2969
  if (!isSupabaseMutationCall(awaitExpr.argument)) return;
2970
+ if (chainHasMethod(awaitExpr.argument, "throwOnError")) return;
2662
2971
  if (!pattern) {
2663
2972
  context.report({ node, messageId: "uncheckedMutation" });
2664
2973
  return;
2665
2974
  }
2975
+ if (pattern.type === "Identifier") {
2976
+ deferredBindings.push({ node, name: pattern.name });
2977
+ return;
2978
+ }
2666
2979
  const names = destructuredNames(pattern);
2667
2980
  if (!names.has("error")) {
2668
2981
  context.report({ node, messageId: "uncheckedMutation" });
2669
2982
  }
2670
2983
  }
2671
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
+ },
2672
2990
  ExpressionStatement(node) {
2673
2991
  const expr = node.expression;
2674
2992
  if (expr?.type !== "AwaitExpression") return;
2675
2993
  checkMutationAwait(node, void 0, expr);
2676
2994
  },
2677
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
+ }
2678
2999
  if (node.init?.type !== "AwaitExpression") return;
2679
3000
  checkMutationAwait(node, node.id, node.init);
2680
3001
  },
2681
3002
  AssignmentExpression(node) {
2682
3003
  if (node.right?.type !== "AwaitExpression") return;
2683
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
+ }
2684
3012
  }
2685
3013
  };
2686
3014
  }
@@ -2730,11 +3058,20 @@ function isStorageUploadCall(node) {
2730
3058
  let current = node;
2731
3059
  let sawStorage = false;
2732
3060
  let sawUpload = false;
2733
- while (current?.type === "CallExpression") {
2734
- const prop = memberPropName(current);
2735
- if (prop === "storage") sawStorage = true;
2736
- if (prop === "upload") sawUpload = true;
2737
- current = chainObjectCall(current);
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
+ }
2738
3075
  }
2739
3076
  return sawStorage && sawUpload;
2740
3077
  }
@@ -3315,8 +3652,8 @@ var rule30 = {
3315
3652
  category: "security",
3316
3653
  cwe: "CWE-285",
3317
3654
  owasp: "A04:2021 Insecure Design",
3318
- 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.",
3319
- docsUrl: "https://firebase.google.com/docs/database/web/start",
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",
3320
3657
  recommended: true
3321
3658
  },
3322
3659
  messages: {
@@ -3490,7 +3827,13 @@ function isValidationLikeCall(node) {
3490
3827
  const callee = node.callee;
3491
3828
  const name = callee?.type === "Identifier" ? callee.name : callee?.type === "MemberExpression" && !callee.computed && callee.property?.type === "Identifier" ? callee.property.name : void 0;
3492
3829
  if (!name) return false;
3493
- return /valid|^test$|check|assert|schema/i.test(name);
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;
3494
3837
  }
3495
3838
  var rule33 = {
3496
3839
  meta: {
@@ -3679,18 +4022,24 @@ var firebaseRtdbListenerErrorNotHandledRule = rule35;
3679
4022
  function isUseEffectCall(node) {
3680
4023
  return node?.type === "CallExpression" && node.callee?.type === "Identifier" && node.callee.name === "useEffect";
3681
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
+ }
3682
4031
  var rule36 = {
3683
4032
  meta: {
3684
4033
  type: "suggestion",
3685
4034
  docs: {
3686
4035
  description: "useEffect should depend on user?.uid, not the whole user object",
3687
4036
  category: "reliability",
3688
- rationale: "onAuthStateChanged delivers a new user object reference on every internal token refresh (roughly hourly) 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.",
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.",
3689
4038
  docsUrl: "https://firebase.google.com/docs/reference/js/auth.md#onauthstatechanged",
3690
4039
  recommended: true
3691
4040
  },
3692
4041
  messages: {
3693
- wholeUserObjectDep: "This effect only reads {{name}}.uid but depends on the whole {{name}} object, which gets a new reference on every token refresh. Depend on {{name}}?.uid instead."
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."
3694
4043
  },
3695
4044
  schema: []
3696
4045
  },
@@ -3703,9 +4052,9 @@ var rule36 = {
3703
4052
  for (const el of depsArg.elements ?? []) {
3704
4053
  if (el?.type !== "Identifier") continue;
3705
4054
  const name = el.name;
3706
- if (someDescendant(callback, (n) => isUidMemberAccess(n, name))) {
3707
- context.report({ node, messageId: "wholeUserObjectDep", data: { name } });
3708
- }
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 } });
3709
4058
  }
3710
4059
  }
3711
4060
  };
@@ -3801,13 +4150,13 @@ var rule38 = {
3801
4150
  schema: []
3802
4151
  },
3803
4152
  create(context) {
3804
- const EXPIRED_YEAR = 2025;
3805
4153
  function checkStringForExpiredDate(value, reportNode) {
3806
- const re = /timestamp\.date\(\s*(\d{4})\s*,/g;
4154
+ const re = /timestamp\.date\(\s*(\d{4})\s*,\s*(\d{1,2})\s*,\s*(\d{1,2})\s*\)/g;
3807
4155
  let match;
3808
4156
  while ((match = re.exec(value)) !== null) {
3809
- const year = parseInt(match[1], 10);
3810
- if (year <= EXPIRED_YEAR) {
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()) {
3811
4160
  context.report({ node: reportNode, messageId: "firestoreRulesExpired" });
3812
4161
  return;
3813
4162
  }
@@ -3839,6 +4188,18 @@ function findProp(obj, name) {
3839
4188
  (p) => p?.type === "Property" && (p.key?.type === "Identifier" && p.key.name === name || p.key?.type === "Literal" && p.key.value === name)
3840
4189
  );
3841
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
+ }
3842
4203
  var rule39 = {
3843
4204
  meta: {
3844
4205
  type: "problem",
@@ -3846,110 +4207,71 @@ var rule39 = {
3846
4207
  description: "Firebase ID token stored in a cookie without httpOnly flag",
3847
4208
  category: "security",
3848
4209
  cwe: "CWE-1004",
3849
- owasp: "A02:2021 Cryptographic Failures",
3850
- 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.",
3851
4212
  docsUrl: "https://firebase.google.com/docs/auth/admin/manage-cookies",
3852
4213
  recommended: true
3853
4214
  },
3854
4215
  messages: {
3855
- idTokenCookieMissingHttpOnly: "Firebase ID token stored in cookie without httpOnly: true. Any XSS vulnerability can steal the token. Use the Firebase Admin SDK createSessionCookie flow instead."
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."
3856
4217
  },
3857
4218
  schema: []
3858
4219
  },
3859
4220
  create(context) {
3860
- function isTokenName(val) {
3861
- return /token/i.test(val);
3862
- }
3863
4221
  function hasHttpOnlyTrue(optsNode) {
3864
- if (optsNode?.type !== "ObjectExpression") return false;
3865
4222
  const prop = findProp(optsNode, "httpOnly");
3866
- if (!prop) return false;
3867
- return prop.value?.type === "Literal" && prop.value.value === true;
3868
- }
3869
- return {
3870
- CallExpression(node) {
3871
- const callee = node.callee;
3872
- const isCookieSet = callee?.type === "Identifier" && callee.name === "setCookie";
3873
- if (!isCookieSet) return;
3874
- const args = node.arguments ?? [];
3875
- const nameArg = args[0];
3876
- if (nameArg?.type !== "Literal" || typeof nameArg.value !== "string") return;
3877
- if (!isTokenName(nameArg.value)) return;
3878
- const optsArg = args.length >= 3 ? args[2] : null;
3879
- if (!optsArg || optsArg.type !== "ObjectExpression") {
3880
- context.report({ node, messageId: "idTokenCookieMissingHttpOnly" });
3881
- return;
3882
- }
3883
- 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])) {
3884
4231
  context.report({ node, messageId: "idTokenCookieMissingHttpOnly" });
3885
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" });
3886
4239
  }
3887
- };
3888
- }
3889
- };
3890
- var firebaseIdTokenCookieFlagsRule = rule39;
3891
-
3892
- // src/providers/firebase/rules/middleware-token-not-verified.ts
3893
- var rule40 = {
3894
- meta: {
3895
- type: "problem",
3896
- docs: {
3897
- description: "Next.js middleware reads auth cookie but never verifies it",
3898
- category: "security",
3899
- cwe: "CWE-345",
3900
- owasp: "A07:2021 Identification and Authentication Failures",
3901
- 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.",
3902
- docsUrl: "https://firebase.google.com/docs/auth/admin/manage-cookies#verify_session_cookies_and_check_permissions",
3903
- recommended: true
3904
- },
3905
- messages: {
3906
- 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."
3907
- },
3908
- schema: []
3909
- },
3910
- create(context) {
3911
- const AUTH_COOKIE_NAMES = /token|session/i;
3912
- const VERIFY_METHOD_NAMES = /* @__PURE__ */ new Set(["verifySessionCookie", "verifyIdToken", "verify"]);
3913
- let readsCookieWithAuthName = false;
3914
- let hasVerifyCall = false;
3915
- const cookieReadNodes = [];
3916
- function isAuthCookieGet(node) {
3917
- if (node?.type !== "CallExpression") return false;
3918
- const callee = node.callee;
3919
- if (callee?.type !== "MemberExpression") return false;
3920
- if (callee.property?.type !== "Identifier" || callee.property.name !== "get") return false;
3921
- const arg = node.arguments?.[0];
3922
- if (arg?.type !== "Literal" || typeof arg.value !== "string") return false;
3923
- return AUTH_COOKIE_NAMES.test(arg.value);
3924
4240
  }
3925
4241
  return {
3926
4242
  CallExpression(node) {
3927
- if (isAuthCookieGet(node)) {
3928
- readsCookieWithAuthName = true;
3929
- cookieReadNodes.push(node);
3930
- }
3931
4243
  const callee = node.callee;
3932
- if (callee?.type === "Identifier" && VERIFY_METHOD_NAMES.has(callee.name)) {
3933
- hasVerifyCall = true;
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;
3934
4252
  }
3935
- if (callee?.type === "MemberExpression" && callee.property?.type === "Identifier" && VERIFY_METHOD_NAMES.has(callee.property.name)) {
3936
- hasVerifyCall = true;
4253
+ if (callee.property.name === "set" && isCookieReceiver(callee.object)) {
4254
+ checkNameValueOptsCall(node);
3937
4255
  }
3938
4256
  },
3939
- "Program:exit"() {
3940
- if (readsCookieWithAuthName && !hasVerifyCall) {
3941
- for (const node of cookieReadNodes) {
3942
- context.report({ node, messageId: "tokenNotVerified" });
3943
- }
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" });
3944
4266
  }
3945
4267
  }
3946
4268
  };
3947
4269
  }
3948
4270
  };
3949
- var firebaseMiddlewareTokenNotVerifiedRule = rule40;
4271
+ var firebaseIdTokenCookieFlagsRule = rule39;
3950
4272
 
3951
4273
  // src/providers/firebase/rules/hardcoded-user-id.ts
3952
- var rule41 = {
4274
+ var rule40 = {
3953
4275
  meta: {
3954
4276
  type: "problem",
3955
4277
  docs: {
@@ -3967,6 +4289,9 @@ var rule41 = {
3967
4289
  schema: []
3968
4290
  },
3969
4291
  create(context) {
4292
+ if (isInsideTestFile2(String(context.filename ?? ""))) {
4293
+ return {};
4294
+ }
3970
4295
  const USER_ID_NAMES = /* @__PURE__ */ new Set(["userId", "uid", "userID", "user_id"]);
3971
4296
  return {
3972
4297
  VariableDeclarator(node) {
@@ -3981,10 +4306,10 @@ var rule41 = {
3981
4306
  };
3982
4307
  }
3983
4308
  };
3984
- var firebaseHardcodedUserIdRule = rule41;
4309
+ var firebaseHardcodedUserIdRule = rule40;
3985
4310
 
3986
4311
  // src/providers/firebase/rules/auth-user-not-found-disclosure.ts
3987
- var rule42 = {
4312
+ var rule41 = {
3988
4313
  meta: {
3989
4314
  type: "suggestion",
3990
4315
  docs: {
@@ -4023,10 +4348,10 @@ var rule42 = {
4023
4348
  };
4024
4349
  }
4025
4350
  };
4026
- var firebaseAuthUserNotFoundDisclosureRule = rule42;
4351
+ var firebaseAuthUserNotFoundDisclosureRule = rule41;
4027
4352
 
4028
4353
  // src/providers/firebase/rules/signup-password-confirm.ts
4029
- var rule43 = {
4354
+ var rule42 = {
4030
4355
  meta: {
4031
4356
  type: "suggestion",
4032
4357
  docs: {
@@ -4060,7 +4385,7 @@ var rule43 = {
4060
4385
  },
4061
4386
  BinaryExpression(node) {
4062
4387
  if (node.operator !== "===" && node.operator !== "==" && node.operator !== "!==" && node.operator !== "!=") return;
4063
- 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";
4064
4389
  if (mentionsConfirm(node.left) || mentionsConfirm(node.right)) {
4065
4390
  hasPasswordComparison = true;
4066
4391
  }
@@ -4083,16 +4408,16 @@ var rule43 = {
4083
4408
  };
4084
4409
  }
4085
4410
  };
4086
- var firebaseSignupPasswordConfirmRule = rule43;
4411
+ var firebaseSignupPasswordConfirmRule = rule42;
4087
4412
 
4088
4413
  // src/providers/firebase/rules/use-array-union-remove.ts
4089
- var rule44 = {
4414
+ var rule43 = {
4090
4415
  meta: {
4091
4416
  type: "suggestion",
4092
4417
  docs: {
4093
4418
  description: "Firestore array field updated with read-modify-write spread/filter instead of arrayUnion/arrayRemove",
4094
4419
  category: "correctness",
4095
- 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.",
4096
4421
  docsUrl: "https://firebase.google.com/docs/firestore/manage-data/add-data#update_elements_in_an_array",
4097
4422
  recommended: true
4098
4423
  },
@@ -4132,10 +4457,10 @@ var rule44 = {
4132
4457
  };
4133
4458
  }
4134
4459
  };
4135
- var firebaseUseArrayUnionRemoveRule = rule44;
4460
+ var firebaseUseArrayUnionRemoveRule = rule43;
4136
4461
 
4137
4462
  // src/providers/firebase/rules/duplicate-initialize-app.ts
4138
- var rule45 = {
4463
+ var rule44 = {
4139
4464
  meta: {
4140
4465
  type: "suggestion",
4141
4466
  docs: {
@@ -4168,9 +4493,11 @@ var rule45 = {
4168
4493
  const callee = node.callee;
4169
4494
  if (callee?.type !== "Identifier") return;
4170
4495
  if (initializeAppLocalName && callee.name === initializeAppLocalName) {
4171
- initializeAppCalls.push(node);
4496
+ if ((node.arguments ?? []).length < 2) {
4497
+ initializeAppCalls.push(node);
4498
+ }
4172
4499
  }
4173
- if (callee.name === "getApps") {
4500
+ if (callee.name === "getApps" || callee.name === "getApp") {
4174
4501
  hasGetAppsCall = true;
4175
4502
  }
4176
4503
  },
@@ -4183,21 +4510,21 @@ var rule45 = {
4183
4510
  };
4184
4511
  }
4185
4512
  };
4186
- var firebaseDuplicateInitializeAppRule = rule45;
4513
+ var firebaseDuplicateInitializeAppRule = rule44;
4187
4514
 
4188
4515
  // src/providers/firebase/rules/onSnapshot-async-throw.ts
4189
- var rule46 = {
4516
+ var rule45 = {
4190
4517
  meta: {
4191
4518
  type: "suggestion",
4192
4519
  docs: {
4193
4520
  description: "throw inside async onSnapshot callback creates an unhandled promise rejection",
4194
4521
  category: "reliability",
4195
- rationale: "onSnapshot does not handle Promise rejections from its callback. A throw inside an async callback becomes an unhandled rejection, silently terminating the listener and leaving the UI in a broken state with no error feedback.",
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.",
4196
4523
  docsUrl: "https://firebase.google.com/docs/firestore/query-data/listen#handle_listen_errors",
4197
4524
  recommended: true
4198
4525
  },
4199
4526
  messages: {
4200
- asyncThrowInSnapshot: "throw inside an async onSnapshot callback creates an unhandled promise rejection. The listener silently stops. Use return with error logging or the onSnapshot error callback instead."
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."
4201
4528
  },
4202
4529
  schema: []
4203
4530
  },
@@ -4237,10 +4564,10 @@ var rule46 = {
4237
4564
  };
4238
4565
  }
4239
4566
  };
4240
- var firebaseOnSnapshotAsyncThrowRule = rule46;
4567
+ var firebaseOnSnapshotAsyncThrowRule = rule45;
4241
4568
 
4242
4569
  // src/providers/firebase/rules/onSnapshot-missing-error-callback.ts
4243
- var rule47 = {
4570
+ var rule46 = {
4244
4571
  meta: {
4245
4572
  type: "suggestion",
4246
4573
  docs: {
@@ -4278,21 +4605,21 @@ var rule47 = {
4278
4605
  };
4279
4606
  }
4280
4607
  };
4281
- var firebaseOnSnapshotMissingErrorCallbackRule = rule47;
4608
+ var firebaseOnSnapshotMissingErrorCallbackRule = rule46;
4282
4609
 
4283
4610
  // src/providers/firebase/rules/firestore-document-size-guard.ts
4284
- var rule48 = {
4611
+ var rule47 = {
4285
4612
  meta: {
4286
4613
  type: "suggestion",
4287
4614
  docs: {
4288
4615
  description: "Firestore write includes editor.getJSON() without a document size guard",
4289
4616
  category: "reliability",
4290
- 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 silently fails with INVALID_ARGUMENT. Always check document size before calling updateDoc/setDoc when the payload includes editor JSON.",
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.",
4291
4618
  docsUrl: "https://firebase.google.com/docs/firestore/quotas#limits",
4292
4619
  recommended: true
4293
4620
  },
4294
4621
  messages: {
4295
- missingDocumentSizeGuard: "Firestore write includes editor.getJSON() without a document size check. Base64 images can exceed the 1 MiB Firestore limit, silently failing the write. Add a Blob size guard before calling updateDoc/setDoc."
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."
4296
4623
  },
4297
4624
  schema: []
4298
4625
  },
@@ -4332,21 +4659,21 @@ var rule48 = {
4332
4659
  };
4333
4660
  }
4334
4661
  };
4335
- var firebaseFirestoreDocumentSizeGuardRule = rule48;
4662
+ var firebaseFirestoreDocumentSizeGuardRule = rule47;
4336
4663
 
4337
4664
  // src/providers/firebase/rules/use-timestamp-now.ts
4338
- var rule49 = {
4665
+ var rule48 = {
4339
4666
  meta: {
4340
4667
  type: "suggestion",
4341
4668
  docs: {
4342
- description: "new Date() used for Firestore timestamp instead of Timestamp.now() or serverTimestamp()",
4669
+ description: "new Date() used for Firestore timestamp instead of serverTimestamp()",
4343
4670
  category: "correctness",
4344
- rationale: "While Firestore auto-converts Date objects on write, mixing new Date() with Timestamp.now() creates type inconsistencies. Firestore security rules that compare request.resource.data.createdAt against request.time expect a Timestamp on both sides; a bare Date object can cause silent rule evaluation failures.",
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().",
4345
4672
  docsUrl: "https://firebase.google.com/docs/reference/js/firestore_.timestamp",
4346
4673
  recommended: true
4347
4674
  },
4348
4675
  messages: {
4349
- useTimestampNow: "Use Timestamp.now() or serverTimestamp() instead of new Date() for Firestore timestamp fields. new Date() creates type inconsistencies with security rules that compare against request.time."
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."
4350
4677
  },
4351
4678
  schema: []
4352
4679
  },
@@ -4355,7 +4682,7 @@ var rule49 = {
4355
4682
  return {
4356
4683
  ImportDeclaration(node) {
4357
4684
  const src = node.source?.value;
4358
- if (typeof src === "string" && (src.startsWith("firebase/") || src === "firebase-admin/firestore")) {
4685
+ if (typeof src === "string" && (src.startsWith("firebase/firestore") || src === "firebase-admin/firestore")) {
4359
4686
  importsFromFirestore = true;
4360
4687
  }
4361
4688
  },
@@ -4368,7 +4695,7 @@ var rule49 = {
4368
4695
  };
4369
4696
  }
4370
4697
  };
4371
- var firebaseUseTimestampNowRule = rule49;
4698
+ var firebaseUseTimestampNowRule = rule48;
4372
4699
 
4373
4700
  // src/providers/lovable/utils.ts
4374
4701
  var LLM_HOST_PATTERN = /api\.anthropic\.com|api\.openai\.com/;
@@ -4383,7 +4710,7 @@ function containsKnownLlmHost(node) {
4383
4710
  }
4384
4711
 
4385
4712
  // src/providers/lovable/rules/no-client-side-secret-fetch.ts
4386
- var rule50 = {
4713
+ var rule49 = {
4387
4714
  meta: {
4388
4715
  type: "problem",
4389
4716
  docs: {
@@ -4456,13 +4783,13 @@ var rule50 = {
4456
4783
  };
4457
4784
  }
4458
4785
  };
4459
- var lovableNoClientSideSecretFetchRule = rule50;
4786
+ var lovableNoClientSideSecretFetchRule = rule49;
4460
4787
 
4461
4788
  // src/providers/lovable/rules/paid-flag-without-edge-function.ts
4462
4789
  var PRICE_PATTERN = /\$\s?\d/;
4463
4790
  var FLAG_PROPERTY_PATTERN = /^is_[a-z0-9_]+$/i;
4464
4791
  var FLAG_SUFFIX_PATTERN = /_(unlocked|active|enabled)$/i;
4465
- var rule51 = {
4792
+ var rule50 = {
4466
4793
  meta: {
4467
4794
  type: "problem",
4468
4795
  docs: {
@@ -4471,7 +4798,7 @@ var rule51 = {
4471
4798
  cwe: "CWE-840",
4472
4799
  owasp: "A04:2021 \u2013 Insecure Design",
4473
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.",
4474
- docsUrl: "https://docs.lovable.dev/features/cloud",
4801
+ docsUrl: "https://docs.lovable.dev/integrations/stripe",
4475
4802
  recommended: true
4476
4803
  },
4477
4804
  messages: {
@@ -4604,20 +4931,20 @@ var rule51 = {
4604
4931
  };
4605
4932
  }
4606
4933
  };
4607
- var lovablePaidFlagWithoutEdgeFunctionRule = rule51;
4934
+ var lovablePaidFlagWithoutEdgeFunctionRule = rule50;
4608
4935
 
4609
4936
  // src/providers/lovable/rules/expiry-column-never-checked.ts
4610
4937
  var EXPIRY_COLUMN_PATTERN = /_(until|expires?_at|expiry)$/i;
4611
4938
  var DATE_COMPARISON_OPERATORS = /* @__PURE__ */ new Set([">", "<", ">=", "<="]);
4612
4939
  var FILTER_METHODS = /* @__PURE__ */ new Set(["gt", "gte", "lt", "lte"]);
4613
- var rule52 = {
4940
+ var rule51 = {
4614
4941
  meta: {
4615
4942
  type: "suggestion",
4616
4943
  docs: {
4617
4944
  description: "An expiry column must be checked against the current time somewhere",
4618
4945
  category: "correctness",
4619
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.",
4620
- docsUrl: "https://docs.lovable.dev/features/cloud",
4947
+ docsUrl: "https://docs.lovable.dev/integrations/stripe",
4621
4948
  recommended: true
4622
4949
  },
4623
4950
  messages: {
@@ -4702,18 +5029,18 @@ var rule52 = {
4702
5029
  };
4703
5030
  }
4704
5031
  };
4705
- var lovableExpiryColumnNeverCheckedRule = rule52;
5032
+ var lovableExpiryColumnNeverCheckedRule = rule51;
4706
5033
 
4707
5034
  // src/providers/lovable/rules/silent-catch-on-provider-call.ts
4708
5035
  var LOGGING_CONSOLE_METHODS = /* @__PURE__ */ new Set(["error", "warn", "log", "info"]);
4709
- var rule53 = {
5036
+ var rule52 = {
4710
5037
  meta: {
4711
5038
  type: "suggestion",
4712
5039
  docs: {
4713
5040
  description: "A catch block around an LLM provider call must log the failure",
4714
5041
  category: "correctness",
4715
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.`,
4716
- docsUrl: "https://docs.lovable.dev/features/cloud",
5043
+ docsUrl: "https://docs.lovable.dev/integrations/cloud",
4717
5044
  recommended: true
4718
5045
  },
4719
5046
  messages: {
@@ -4779,7 +5106,7 @@ var rule53 = {
4779
5106
  };
4780
5107
  }
4781
5108
  };
4782
- var lovableSilentCatchOnProviderCallRule = rule53;
5109
+ var lovableSilentCatchOnProviderCallRule = rule52;
4783
5110
 
4784
5111
  // src/providers/browserbase/utils.ts
4785
5112
  function memberPropName3(node) {
@@ -4892,7 +5219,7 @@ function isTruthyGateTest(test) {
4892
5219
  }
4893
5220
  return false;
4894
5221
  }
4895
- var rule54 = {
5222
+ var rule53 = {
4896
5223
  meta: {
4897
5224
  type: "problem",
4898
5225
  docs: {
@@ -4964,10 +5291,10 @@ var rule54 = {
4964
5291
  };
4965
5292
  }
4966
5293
  };
4967
- var browserbaseNoConditionalAuthzOnAnonymousUserRule = rule54;
5294
+ var browserbaseNoConditionalAuthzOnAnonymousUserRule = rule53;
4968
5295
 
4969
5296
  // src/providers/browserbase/rules/no-connect-url-in-api-response.ts
4970
- var rule55 = {
5297
+ var rule54 = {
4971
5298
  meta: {
4972
5299
  type: "problem",
4973
5300
  docs: {
@@ -4997,7 +5324,7 @@ var rule55 = {
4997
5324
  };
4998
5325
  }
4999
5326
  };
5000
- var browserbaseNoConnectUrlInApiResponseRule = rule55;
5327
+ var browserbaseNoConnectUrlInApiResponseRule = rule54;
5001
5328
 
5002
5329
  // src/providers/browserbase/rules/session-id-requires-ownership-check.ts
5003
5330
  function isOwnershipCheckCall(node) {
@@ -5014,7 +5341,7 @@ function isSessionIdLikeArg(node) {
5014
5341
  }
5015
5342
  return false;
5016
5343
  }
5017
- var rule56 = {
5344
+ var rule55 = {
5018
5345
  meta: {
5019
5346
  type: "suggestion",
5020
5347
  docs: {
@@ -5022,7 +5349,7 @@ var rule56 = {
5022
5349
  category: "security",
5023
5350
  cwe: "CWE-862",
5024
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.",
5025
- docsUrl: "https://docs.browserbase.com/features/session-live-view",
5352
+ docsUrl: "https://docs.browserbase.com/platform/browser/observability/session-live-view",
5026
5353
  recommended: true
5027
5354
  },
5028
5355
  messages: {
@@ -5078,7 +5405,7 @@ var rule56 = {
5078
5405
  };
5079
5406
  }
5080
5407
  };
5081
- var browserbaseSessionIdRequiresOwnershipCheckRule = rule56;
5408
+ var browserbaseSessionIdRequiresOwnershipCheckRule = rule55;
5082
5409
 
5083
5410
  // src/providers/browserbase/rules/no-concurrent-shared-context.ts
5084
5411
  function isPromiseAllCall2(node) {
@@ -5135,14 +5462,14 @@ function findSharedContextCreateCall(callback) {
5135
5462
  visit(body);
5136
5463
  return found;
5137
5464
  }
5138
- var rule57 = {
5465
+ var rule56 = {
5139
5466
  meta: {
5140
5467
  type: "problem",
5141
5468
  docs: {
5142
5469
  description: "A Browserbase Context must not be attached to concurrent sessions",
5143
5470
  category: "correctness",
5144
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.`,
5145
- docsUrl: "https://docs.browserbase.com/features/contexts",
5472
+ docsUrl: "https://docs.browserbase.com/platform/browser/core-features/contexts",
5146
5473
  recommended: true
5147
5474
  },
5148
5475
  messages: {
@@ -5182,7 +5509,7 @@ var rule57 = {
5182
5509
  };
5183
5510
  }
5184
5511
  };
5185
- var browserbaseNoConcurrentSharedContextRule = rule57;
5512
+ var browserbaseNoConcurrentSharedContextRule = rule56;
5186
5513
 
5187
5514
  // src/providers/browserbase/rules/mobile-device-requires-os-setting.ts
5188
5515
  function isMobileLiteral(node) {
@@ -5215,14 +5542,14 @@ function isOsMobileSetting(node) {
5215
5542
  }
5216
5543
  return false;
5217
5544
  }
5218
- var rule58 = {
5545
+ var rule57 = {
5219
5546
  meta: {
5220
5547
  type: "problem",
5221
5548
  docs: {
5222
5549
  description: "Mobile device combos must set browserSettings.os, not just resize the viewport",
5223
5550
  category: "correctness",
5224
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.`,
5225
- docsUrl: "https://docs.browserbase.com/features/stealth-mode",
5552
+ docsUrl: "https://docs.browserbase.com/platform/identity/verified-customization",
5226
5553
  recommended: true
5227
5554
  },
5228
5555
  messages: {
@@ -5254,7 +5581,7 @@ var rule58 = {
5254
5581
  };
5255
5582
  }
5256
5583
  };
5257
- var browserbaseMobileDeviceRequiresOsSettingRule = rule58;
5584
+ var browserbaseMobileDeviceRequiresOsSettingRule = rule57;
5258
5585
 
5259
5586
  // src/providers/browserbase/rules/use-typed-exception-status-not-substring.ts
5260
5587
  function isStringifiedErrorSource(node, errName) {
@@ -5316,7 +5643,7 @@ function collectStringifiedVars(body, errName) {
5316
5643
  });
5317
5644
  return names;
5318
5645
  }
5319
- var rule59 = {
5646
+ var rule58 = {
5320
5647
  meta: {
5321
5648
  type: "suggestion",
5322
5649
  docs: {
@@ -5346,7 +5673,7 @@ var rule59 = {
5346
5673
  };
5347
5674
  }
5348
5675
  };
5349
- var browserbaseUseTypedExceptionStatusNotSubstringRule = rule59;
5676
+ var browserbaseUseTypedExceptionStatusNotSubstringRule = rule58;
5350
5677
 
5351
5678
  // src/providers/browserbase/rules/release-session-on-connect-failure.ts
5352
5679
  function isSessionsCreateAwait(node) {
@@ -5369,7 +5696,7 @@ function isReleaseCall(node) {
5369
5696
  const name = callee?.type === "Identifier" ? callee.name : memberPropName3(node);
5370
5697
  return !!name && /requeststop|releasesession|release_session/i.test(name);
5371
5698
  }
5372
- var rule60 = {
5699
+ var rule59 = {
5373
5700
  meta: {
5374
5701
  type: "problem",
5375
5702
  docs: {
@@ -5411,10 +5738,10 @@ var rule60 = {
5411
5738
  };
5412
5739
  }
5413
5740
  };
5414
- var browserbaseReleaseSessionOnConnectFailureRule = rule60;
5741
+ var browserbaseReleaseSessionOnConnectFailureRule = rule59;
5415
5742
 
5416
5743
  // src/providers/browserbase/rules/dont-stack-custom-retry-on-sdk-retry.ts
5417
- var rule61 = {
5744
+ var rule60 = {
5418
5745
  meta: {
5419
5746
  type: "suggestion",
5420
5747
  docs: {
@@ -5459,7 +5786,7 @@ var rule61 = {
5459
5786
  };
5460
5787
  }
5461
5788
  };
5462
- var browserbaseDontStackCustomRetryOnSdkRetryRule = rule61;
5789
+ var browserbaseDontStackCustomRetryOnSdkRetryRule = rule60;
5463
5790
 
5464
5791
  // src/providers/browserbase/rules/no-overbroad-error-substring-match.ts
5465
5792
  var OVERBROAD_TERMS = /* @__PURE__ */ new Set(["session", "context", "browser", "browserbase"]);
@@ -5481,7 +5808,7 @@ function isCleanupCall(node) {
5481
5808
  const name = callee?.type === "Identifier" ? callee.name : memberPropName3(node);
5482
5809
  return !!name && /release|remove|cleanup|stop|teardown/i.test(name);
5483
5810
  }
5484
- var rule62 = {
5811
+ var rule61 = {
5485
5812
  meta: {
5486
5813
  type: "problem",
5487
5814
  docs: {
@@ -5511,7 +5838,7 @@ var rule62 = {
5511
5838
  };
5512
5839
  }
5513
5840
  };
5514
- var browserbaseNoOverbroadErrorSubstringMatchRule = rule62;
5841
+ var browserbaseNoOverbroadErrorSubstringMatchRule = rule61;
5515
5842
 
5516
5843
  // src/providers/browserbase/rules/use-sdk-not-raw-requests.ts
5517
5844
  var BROWSERBASE_HOST = "api.browserbase.com";
@@ -5551,7 +5878,7 @@ function isRawHttpCall(node) {
5551
5878
  }
5552
5879
  return { isCall: false, urlArg: void 0 };
5553
5880
  }
5554
- var rule63 = {
5881
+ var rule62 = {
5555
5882
  meta: {
5556
5883
  type: "suggestion",
5557
5884
  docs: {
@@ -5585,10 +5912,10 @@ var rule63 = {
5585
5912
  };
5586
5913
  }
5587
5914
  };
5588
- var browserbaseUseSdkNotRawRequestsRule = rule63;
5915
+ var browserbaseUseSdkNotRawRequestsRule = rule62;
5589
5916
 
5590
5917
  // src/providers/browserbase/rules/centralize-request-release.ts
5591
- var rule64 = {
5918
+ var rule63 = {
5592
5919
  meta: {
5593
5920
  type: "suggestion",
5594
5921
  docs: {
@@ -5621,11 +5948,11 @@ var rule64 = {
5621
5948
  };
5622
5949
  }
5623
5950
  };
5624
- var browserbaseCentralizeRequestReleaseRule = rule64;
5951
+ var browserbaseCentralizeRequestReleaseRule = rule63;
5625
5952
 
5626
5953
  // src/providers/openai-cua/rules/no-domain-allowlist.ts
5627
5954
  var ACTION_METHOD_NAMES = /* @__PURE__ */ new Set(["click", "type", "press", "move", "goto", "fill", "dblclick", "dragAndDrop"]);
5628
- var rule65 = {
5955
+ var rule64 = {
5629
5956
  meta: {
5630
5957
  type: "problem",
5631
5958
  docs: {
@@ -5751,11 +6078,11 @@ var rule65 = {
5751
6078
  };
5752
6079
  }
5753
6080
  };
5754
- var openaiCuaNoDomainAllowlistRule = rule65;
6081
+ var openaiCuaNoDomainAllowlistRule = rule64;
5755
6082
 
5756
6083
  // src/providers/openai-cua/rules/scroll-delta-default-zero.ts
5757
6084
  var VERTICAL_DELTA_NAME_PATTERN = /^(dy|delta_?y|scroll_?y)$/i;
5758
- var rule66 = {
6085
+ var rule65 = {
5759
6086
  meta: {
5760
6087
  type: "problem",
5761
6088
  docs: {
@@ -5831,12 +6158,12 @@ var rule66 = {
5831
6158
  };
5832
6159
  }
5833
6160
  };
5834
- var openaiCuaScrollDeltaDefaultZeroRule = rule66;
6161
+ var openaiCuaScrollDeltaDefaultZeroRule = rule65;
5835
6162
 
5836
6163
  // src/providers/openai-cua/rules/structured-step-metadata-not-text-json.ts
5837
6164
  var BRACE_SEARCH_METHODS = /* @__PURE__ */ new Set(["indexOf", "lastIndexOf"]);
5838
6165
  var SLICE_METHODS = /* @__PURE__ */ new Set(["slice", "substring", "substr"]);
5839
- var rule67 = {
6166
+ var rule66 = {
5840
6167
  meta: {
5841
6168
  type: "suggestion",
5842
6169
  docs: {
@@ -5935,10 +6262,10 @@ var rule67 = {
5935
6262
  };
5936
6263
  }
5937
6264
  };
5938
- var openaiCuaStructuredStepMetadataNotTextJsonRule = rule67;
6265
+ var openaiCuaStructuredStepMetadataNotTextJsonRule = rule66;
5939
6266
 
5940
6267
  // src/providers/openai-cua/rules/no-blind-safety-check-ack.ts
5941
- var rule68 = {
6268
+ var rule67 = {
5942
6269
  meta: {
5943
6270
  type: "suggestion",
5944
6271
  docs: {
@@ -6001,7 +6328,7 @@ var rule68 = {
6001
6328
  };
6002
6329
  }
6003
6330
  };
6004
- var openaiCuaNoBlindSafetyCheckAckRule = rule68;
6331
+ var openaiCuaNoBlindSafetyCheckAckRule = rule67;
6005
6332
 
6006
6333
  // src/providers/openai-cua/utils.ts
6007
6334
  function propName(node) {
@@ -6047,7 +6374,7 @@ function findResponsesCreateCall(node, depth = 0) {
6047
6374
  }
6048
6375
 
6049
6376
  // src/providers/openai-cua/rules/retry-transient-turn-errors.ts
6050
- var rule69 = {
6377
+ var rule68 = {
6051
6378
  meta: {
6052
6379
  type: "problem",
6053
6380
  docs: {
@@ -6129,10 +6456,10 @@ var rule69 = {
6129
6456
  };
6130
6457
  }
6131
6458
  };
6132
- var openaiCuaRetryTransientTurnErrorsRule = rule69;
6459
+ var openaiCuaRetryTransientTurnErrorsRule = rule68;
6133
6460
 
6134
6461
  // src/providers/openai-cua/rules/check-response-status-incomplete.ts
6135
- var rule70 = {
6462
+ var rule69 = {
6136
6463
  meta: {
6137
6464
  type: "problem",
6138
6465
  docs: {
@@ -6241,17 +6568,17 @@ var rule70 = {
6241
6568
  };
6242
6569
  }
6243
6570
  };
6244
- var openaiCuaCheckResponseStatusIncompleteRule = rule70;
6571
+ var openaiCuaCheckResponseStatusIncompleteRule = rule69;
6245
6572
 
6246
6573
  // src/providers/openai-cua/rules/set-safety-identifier.ts
6247
- var rule71 = {
6574
+ var rule70 = {
6248
6575
  meta: {
6249
6576
  type: "problem",
6250
6577
  docs: {
6251
6578
  description: "responses.create() must set safety_identifier for per-end-user policy attribution",
6252
6579
  category: "integration",
6253
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.",
6254
- docsUrl: "https://help.openai.com/en/articles/5428082-how-to-incorporate-a-safety-identifier",
6581
+ docsUrl: "https://developers.openai.com/api/docs/guides/safety-best-practices",
6255
6582
  recommended: true
6256
6583
  },
6257
6584
  messages: {
@@ -6282,10 +6609,10 @@ var rule71 = {
6282
6609
  };
6283
6610
  }
6284
6611
  };
6285
- var openaiCuaSetSafetyIdentifierRule = rule71;
6612
+ var openaiCuaSetSafetyIdentifierRule = rule70;
6286
6613
 
6287
6614
  // src/providers/tiptap/rules/upload-validate-fn-void.ts
6288
- var rule72 = {
6615
+ var rule71 = {
6289
6616
  meta: {
6290
6617
  type: "problem",
6291
6618
  docs: {
@@ -6349,10 +6676,10 @@ var rule72 = {
6349
6676
  };
6350
6677
  }
6351
6678
  };
6352
- var tiptapUploadValidateFnVoidRule = rule72;
6679
+ var tiptapUploadValidateFnVoidRule = rule71;
6353
6680
 
6354
6681
  // src/providers/tiptap/rules/script-src-hardcoded-api-key.ts
6355
- var rule73 = {
6682
+ var rule72 = {
6356
6683
  meta: {
6357
6684
  type: "problem",
6358
6685
  docs: {
@@ -6410,10 +6737,10 @@ var rule73 = {
6410
6737
  };
6411
6738
  }
6412
6739
  };
6413
- var tiptapScriptSrcHardcodedApiKeyRule = rule73;
6740
+ var tiptapScriptSrcHardcodedApiKeyRule = rule72;
6414
6741
 
6415
6742
  // src/providers/tiptap/rules/dynamic-script-no-sri.ts
6416
- var rule74 = {
6743
+ var rule73 = {
6417
6744
  meta: {
6418
6745
  type: "problem",
6419
6746
  docs: {
@@ -6422,7 +6749,7 @@ var rule74 = {
6422
6749
  cwe: "CWE-829",
6423
6750
  owasp: "API8:2023 Security Misconfiguration",
6424
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.',
6425
- 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",
6426
6753
  recommended: true
6427
6754
  },
6428
6755
  messages: {
@@ -6509,7 +6836,7 @@ var rule74 = {
6509
6836
  };
6510
6837
  }
6511
6838
  };
6512
- var tiptapDynamicScriptNoSriRule = rule74;
6839
+ var tiptapDynamicScriptNoSriRule = rule73;
6513
6840
 
6514
6841
  // src/providers/tiptap/utils.ts
6515
6842
  function findProperty3(objectExpression, name) {
@@ -6534,7 +6861,7 @@ function walk(node, visit) {
6534
6861
  }
6535
6862
 
6536
6863
  // src/providers/tiptap/rules/addAttributes-missing-renderHTML.ts
6537
- var rule75 = {
6864
+ var rule74 = {
6538
6865
  meta: {
6539
6866
  type: "problem",
6540
6867
  docs: {
@@ -6587,10 +6914,10 @@ var rule75 = {
6587
6914
  };
6588
6915
  }
6589
6916
  };
6590
- var tiptapAddAttributesMissingRenderHTMLRule = rule75;
6917
+ var tiptapAddAttributesMissingRenderHTMLRule = rule74;
6591
6918
 
6592
6919
  // src/providers/tiptap/rules/appendTransaction-add-to-history.ts
6593
- var rule76 = {
6920
+ var rule75 = {
6594
6921
  meta: {
6595
6922
  type: "suggestion",
6596
6923
  docs: {
@@ -6669,10 +6996,10 @@ var rule76 = {
6669
6996
  };
6670
6997
  }
6671
6998
  };
6672
- var tiptapAppendTransactionAddToHistoryRule = rule76;
6999
+ var tiptapAppendTransactionAddToHistoryRule = rule75;
6673
7000
 
6674
7001
  // src/providers/tiptap/rules/appendTransaction-full-scan.ts
6675
- var rule77 = {
7002
+ var rule76 = {
6676
7003
  meta: {
6677
7004
  type: "suggestion",
6678
7005
  docs: {
@@ -6732,10 +7059,10 @@ var rule77 = {
6732
7059
  };
6733
7060
  }
6734
7061
  };
6735
- var tiptapAppendTransactionFullScanRule = rule77;
7062
+ var tiptapAppendTransactionFullScanRule = rule76;
6736
7063
 
6737
7064
  // src/providers/tiptap/rules/atom-node-wrap-in.ts
6738
- var rule78 = {
7065
+ var rule77 = {
6739
7066
  meta: {
6740
7067
  type: "problem",
6741
7068
  docs: {
@@ -6798,57 +7125,16 @@ var rule78 = {
6798
7125
  };
6799
7126
  }
6800
7127
  };
6801
- var tiptapAtomNodeWrapInRule = rule78;
6802
-
6803
- // src/providers/tiptap/rules/twitter-url-regex.ts
6804
- var rule79 = {
6805
- meta: {
6806
- type: "suggestion",
6807
- docs: {
6808
- description: "Twitter/X URL regex must match both x.com and twitter.com",
6809
- category: "integration",
6810
- 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.",
6811
- docsUrl: "https://tiptap.dev/docs/editor/extensions/nodes",
6812
- recommended: true
6813
- },
6814
- messages: {
6815
- 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."
6816
- },
6817
- schema: []
6818
- },
6819
- create(context) {
6820
- function checkPattern(pattern, node) {
6821
- if (!pattern.includes("x\\.com") && !pattern.includes("x.com")) return;
6822
- if (pattern.includes("twitter\\.com") || pattern.includes("twitter.com")) return;
6823
- context.report({ node, messageId: "twitterRegexMissingLegacyDomain" });
6824
- }
6825
- return {
6826
- Literal(node) {
6827
- if (node.regex?.pattern) {
6828
- checkPattern(node.regex.pattern, node);
6829
- }
6830
- },
6831
- NewExpression(node) {
6832
- if (node.callee?.type === "Identifier" && node.callee.name === "RegExp") {
6833
- const firstArg = node.arguments?.[0];
6834
- if (firstArg?.type === "Literal" && typeof firstArg.value === "string") {
6835
- checkPattern(firstArg.value, node);
6836
- }
6837
- }
6838
- }
6839
- };
6840
- }
6841
- };
6842
- var tiptapTwitterUrlRegexRule = rule79;
7128
+ var tiptapAtomNodeWrapInRule = rule77;
6843
7129
 
6844
7130
  // src/providers/tiptap/rules/drop-handler-pos-precedence.ts
6845
- var rule80 = {
7131
+ var rule78 = {
6846
7132
  meta: {
6847
7133
  type: "problem",
6848
7134
  docs: {
6849
7135
  description: "x ?? 0 - 1 is parsed as x ?? (0 - 1) = x ?? -1 due to operator precedence",
6850
7136
  category: "correctness",
6851
- rationale: "The nullish coalescing operator (??) has lower precedence than arithmetic operators. Writing `pos ?? 0 - 1` evaluates as `pos ?? -1`, not `(pos ?? 0) - 1`. When pos is null or undefined, the fallback is -1 instead of the intended -1 offset from 0. In ProseMirror drop handlers this produces a decoration at position -1, which corrupts the placeholder position map and can insert content at the document start.",
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.",
6852
7138
  docsUrl: "https://prosemirror.net/docs/ref/#view.EditorView.posAtCoords",
6853
7139
  recommended: true
6854
7140
  },
@@ -6869,7 +7155,7 @@ var rule80 = {
6869
7155
  };
6870
7156
  }
6871
7157
  };
6872
- var tiptapDropHandlerPosPrecedenceRule = rule80;
7158
+ var tiptapDropHandlerPosPrecedenceRule = rule78;
6873
7159
 
6874
7160
  // src/providers/tiptap/rules/prefer-table-kit.ts
6875
7161
  var INDIVIDUAL_TABLE_PACKAGES = /* @__PURE__ */ new Set([
@@ -6877,18 +7163,18 @@ var INDIVIDUAL_TABLE_PACKAGES = /* @__PURE__ */ new Set([
6877
7163
  "@tiptap/extension-table-cell",
6878
7164
  "@tiptap/extension-table-header"
6879
7165
  ]);
6880
- var rule81 = {
7166
+ var rule79 = {
6881
7167
  meta: {
6882
7168
  type: "suggestion",
6883
7169
  docs: {
6884
7170
  description: "Use TableKit from @tiptap/extension-table instead of individual table packages",
6885
7171
  category: "integration",
6886
- rationale: "Importing table extensions individually from their separate packages bypasses the coordinated wiring that TableKit provides: shared configuration, the mergeOrSplit command, and consistent HTMLAttributes across all table elements. Individual imports also leave TableCell and TableHeader unconfigured by default.",
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.",
6887
7173
  docsUrl: "https://tiptap.dev/docs/editor/extensions/functionality/table-kit",
6888
7174
  recommended: true
6889
7175
  },
6890
7176
  messages: {
6891
- preferTableKit: "Individual TipTap table extension imported. Use TableKit from @tiptap/extension-table for coordinated configuration and the mergeOrSplit command."
7177
+ preferTableKit: "Individual TipTap table extension imported. Use TableKit from @tiptap/extension-table to configure all table elements together."
6892
7178
  },
6893
7179
  schema: []
6894
7180
  },
@@ -6911,21 +7197,21 @@ var rule81 = {
6911
7197
  };
6912
7198
  }
6913
7199
  };
6914
- var tiptapPreferTableKitRule = rule81;
7200
+ var tiptapPreferTableKitRule = rule79;
6915
7201
 
6916
7202
  // src/providers/tiptap/rules/tiptap-markdown-missing-node-spec.ts
6917
- var rule82 = {
7203
+ var rule80 = {
6918
7204
  meta: {
6919
7205
  type: "suggestion",
6920
7206
  docs: {
6921
7207
  description: "TipTap nodes used with tiptap-markdown must define a markdown serialization spec",
6922
7208
  category: "integration",
6923
- 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 via MarkdownNodeSpec (in addStorage or addOptions) so content survives markdown export/import cycles.",
6924
- docsUrl: "https://github.com/ueberdosis/tiptap-markdown",
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",
6925
7211
  recommended: true
6926
7212
  },
6927
7213
  messages: {
6928
- missingMarkdownNodeSpec: "This TipTap node is used alongside tiptap-markdown but defines no markdown serialization. Nodes without a markdown spec are silently dropped on export. Add a MarkdownNodeSpec to addStorage or addOptions."
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."
6929
7215
  },
6930
7216
  schema: []
6931
7217
  },
@@ -6980,182 +7266,2294 @@ var rule82 = {
6980
7266
  };
6981
7267
  }
6982
7268
  };
6983
- var tiptapTiptapMarkdownMissingNodeSpecRule = rule82;
7269
+ var tiptapTiptapMarkdownMissingNodeSpecRule = rule80;
6984
7270
 
6985
- // src/plugin/index.ts
6986
- var plugin = {
6987
- meta: { name: PLUGIN_NAME, version: "0.0.1" },
6988
- rules: {
6989
- "resend-webhook-signature": resendWebhookSignatureRule,
6990
- "resend-api-key-hardcoded": resendApiKeyHardcodedRule,
6991
- "resend-api-key-in-client-bundle": resendApiKeyInClientBundleRule,
6992
- "resend-marketing-via-batch-send": resendMarketingViaBatchSendRule,
6993
- "resend-marketing-missing-unsubscribe": resendMarketingMissingUnsubscribeRule,
6994
- "resend-test-domain-in-production-path": resendTestDomainInProductionPathRule,
6995
- "resend-from-address-not-friendly-format": resendFromAddressNotFriendlyFormatRule,
6996
- "resend-batch-size-not-enforced": resendBatchSizeNotEnforcedRule,
6997
- "resend-missing-idempotency-key": resendMissingIdempotencyKeyRule,
6998
- "resend-no-error-code-mapping": resendNoErrorCodeMappingRule,
6999
- "resend-webhook-no-idempotency": resendWebhookNoIdempotencyRule,
7000
- "resend-missing-tags": resendMissingTagsRule,
7001
- "resend-request-id-not-logged": resendRequestIdNotLoggedRule,
7002
- "supabase-scope-queries-by-tenant-column": supabaseScopeQueriesByTenantColumnRule,
7003
- "supabase-validate-uuid-columns": supabaseValidateUuidColumnsRule,
7004
- "supabase-order-by-timestamp-not-identity": supabaseOrderByTimestampNotIdentityRule,
7005
- "supabase-consistent-input-length-limits": supabaseConsistentInputLengthLimitsRule,
7006
- "supabase-idempotent-mutations": supabaseIdempotentMutationsRule,
7007
- "supabase-fail-fast-env-validation": supabaseFailFastEnvValidationRule,
7008
- "supabase-no-user-metadata-authz": supabaseNoUserMetadataAuthzRule,
7009
- "supabase-single-without-error-check": supabaseSingleWithoutErrorCheckRule,
7010
- "supabase-non-atomic-replace-pattern": supabaseNonAtomicReplacePatternRule,
7011
- "supabase-unchecked-mutation-error": supabaseUncheckedMutationErrorRule,
7012
- "supabase-realtime-missing-filter": supabaseRealtimeMissingFilterRule,
7013
- "supabase-storage-error-not-surfaced": supabaseStorageErrorNotSurfacedRule,
7014
- "auth0-required-audience-validation": auth0RequiredAudienceValidationRule,
7015
- "auth0-no-account-link-without-verified-email": auth0NoAccountLinkWithoutVerifiedEmailRule,
7016
- "auth0-dead-claim-verification-check": auth0DeadClaimVerificationCheckRule,
7017
- "auth0-jwks-refresh-on-unknown-kid": auth0JwksRefreshOnUnknownKidRule,
7018
- "firebase-missing-app-check": firebaseMissingAppCheckRule,
7019
- "firebase-unhandled-auth-popup-rejection": firebaseUnhandledAuthPopupRejectionRule,
7020
- "firebase-rtdb-list-read-for-single-item": firebaseRtdbListReadForSingleItemRule,
7021
- "firebase-unvalidated-external-data-to-rtdb": firebaseUnvalidatedExternalDataToRtdbRule,
7022
- "firebase-rtdb-batch-write-not-atomic": firebaseRtdbBatchWriteNotAtomicRule,
7023
- "firebase-rtdb-listener-error-not-handled": firebaseRtdbListenerErrorNotHandledRule,
7024
- "firebase-effect-deps-whole-user-object": firebaseEffectDepsWholeUserObjectRule,
7025
- "firebase-rtdb-write-promise-not-handled": firebaseRtdbWritePromiseNotHandledRule,
7026
- "firebase-firestore-rules-expired": firebaseFirestoreRulesExpiredRule,
7027
- "firebase-id-token-cookie-flags": firebaseIdTokenCookieFlagsRule,
7028
- "firebase-middleware-token-not-verified": firebaseMiddlewareTokenNotVerifiedRule,
7029
- "firebase-hardcoded-user-id": firebaseHardcodedUserIdRule,
7030
- "firebase-auth-user-not-found-disclosure": firebaseAuthUserNotFoundDisclosureRule,
7031
- "firebase-signup-password-confirm": firebaseSignupPasswordConfirmRule,
7032
- "firebase-use-array-union-remove": firebaseUseArrayUnionRemoveRule,
7033
- "firebase-duplicate-initialize-app": firebaseDuplicateInitializeAppRule,
7034
- "firebase-onSnapshot-async-throw": firebaseOnSnapshotAsyncThrowRule,
7035
- "firebase-onSnapshot-missing-error-callback": firebaseOnSnapshotMissingErrorCallbackRule,
7036
- "firebase-firestore-document-size-guard": firebaseFirestoreDocumentSizeGuardRule,
7037
- "firebase-use-timestamp-now": firebaseUseTimestampNowRule,
7038
- "lovable-no-client-side-secret-fetch": lovableNoClientSideSecretFetchRule,
7039
- "lovable-paid-flag-without-edge-function": lovablePaidFlagWithoutEdgeFunctionRule,
7040
- "lovable-expiry-column-never-checked": lovableExpiryColumnNeverCheckedRule,
7041
- "lovable-silent-catch-on-provider-call": lovableSilentCatchOnProviderCallRule,
7042
- "browserbase-no-conditional-authz-on-anonymous-user": browserbaseNoConditionalAuthzOnAnonymousUserRule,
7043
- "browserbase-no-connect-url-in-api-response": browserbaseNoConnectUrlInApiResponseRule,
7044
- "browserbase-session-id-requires-ownership-check": browserbaseSessionIdRequiresOwnershipCheckRule,
7045
- "browserbase-no-concurrent-shared-context": browserbaseNoConcurrentSharedContextRule,
7046
- "browserbase-mobile-device-requires-os-setting": browserbaseMobileDeviceRequiresOsSettingRule,
7047
- "browserbase-use-typed-exception-status-not-substring": browserbaseUseTypedExceptionStatusNotSubstringRule,
7048
- "browserbase-release-session-on-connect-failure": browserbaseReleaseSessionOnConnectFailureRule,
7049
- "browserbase-dont-stack-custom-retry-on-sdk-retry": browserbaseDontStackCustomRetryOnSdkRetryRule,
7050
- "browserbase-no-overbroad-error-substring-match": browserbaseNoOverbroadErrorSubstringMatchRule,
7051
- "browserbase-use-sdk-not-raw-requests": browserbaseUseSdkNotRawRequestsRule,
7052
- "browserbase-centralize-request-release": browserbaseCentralizeRequestReleaseRule,
7053
- "openai-cua-no-domain-allowlist": openaiCuaNoDomainAllowlistRule,
7054
- "openai-cua-scroll-delta-default-zero": openaiCuaScrollDeltaDefaultZeroRule,
7055
- "openai-cua-structured-step-metadata-not-text-json": openaiCuaStructuredStepMetadataNotTextJsonRule,
7056
- "openai-cua-no-blind-safety-check-ack": openaiCuaNoBlindSafetyCheckAckRule,
7057
- "openai-cua-retry-transient-turn-errors": openaiCuaRetryTransientTurnErrorsRule,
7058
- "openai-cua-check-response-status-incomplete": openaiCuaCheckResponseStatusIncompleteRule,
7059
- "openai-cua-set-safety-identifier": openaiCuaSetSafetyIdentifierRule,
7060
- "tiptap-upload-validate-fn-void": tiptapUploadValidateFnVoidRule,
7061
- "tiptap-script-src-hardcoded-api-key": tiptapScriptSrcHardcodedApiKeyRule,
7062
- "tiptap-dynamic-script-no-sri": tiptapDynamicScriptNoSriRule,
7063
- "tiptap-addAttributes-missing-renderHTML": tiptapAddAttributesMissingRenderHTMLRule,
7064
- "tiptap-appendTransaction-add-to-history": tiptapAppendTransactionAddToHistoryRule,
7065
- "tiptap-appendTransaction-full-scan": tiptapAppendTransactionFullScanRule,
7066
- "tiptap-atom-node-wrap-in": tiptapAtomNodeWrapInRule,
7067
- "tiptap-twitter-url-regex": tiptapTwitterUrlRegexRule,
7068
- "tiptap-drop-handler-pos-precedence": tiptapDropHandlerPosPrecedenceRule,
7069
- "tiptap-prefer-table-kit": tiptapPreferTableKitRule,
7070
- "tiptap-tiptap-markdown-missing-node-spec": tiptapTiptapMarkdownMissingNodeSpecRule
7271
+ // src/providers/elevenlabs/utils.ts
7272
+ function isElevenLabsUrlArg(node) {
7273
+ if (!node) return false;
7274
+ if (node.type === "Literal" && typeof node.value === "string") {
7275
+ return node.value.includes("elevenlabs.io");
7071
7276
  }
7072
- };
7073
-
7074
- // src/plugin/rule-registry.ts
7075
- function buildRegistry() {
7076
- const registry2 = /* @__PURE__ */ new Map();
7077
- for (const [key, rule83] of Object.entries(plugin.rules)) {
7078
- const docs = rule83?.meta?.docs ?? {};
7079
- registry2.set(key, {
7080
- category: docs.category,
7081
- description: docs.description ?? "",
7082
- rationale: docs.rationale ?? "",
7083
- docsUrl: docs.docsUrl,
7084
- cwe: docs.cwe,
7085
- owasp: docs.owasp
7086
- });
7277
+ if (node.type === "TemplateLiteral") {
7278
+ return (node.quasis ?? []).some(
7279
+ (q) => typeof q?.value?.raw === "string" && q.value.raw.includes("elevenlabs.io")
7280
+ );
7087
7281
  }
7088
- return registry2;
7089
- }
7090
- var registry = buildRegistry();
7091
- function getRuleDocsMeta(ruleKey) {
7092
- return registry.get(ruleKey);
7282
+ return false;
7093
7283
  }
7094
-
7095
- // src/reporter/snippet.ts
7096
- function extractCodeSnippet(content, line, contextLines = 2) {
7097
- const allLines = content.split(/\r?\n/);
7098
- const highlighted = Math.min(Math.max(line, 1), allLines.length || 1);
7099
- const start = Math.max(1, highlighted - contextLines);
7100
- const end = Math.min(allLines.length, highlighted + contextLines);
7101
- const lines = [];
7102
- for (let n = start; n <= end; n++) {
7103
- lines.push({ number: n, text: allLines[n - 1] ?? "" });
7284
+ function findElevenLabsFetchCall(node, depth = 0) {
7285
+ if (!node || typeof node !== "object" || depth > 20) return null;
7286
+ if (Array.isArray(node)) {
7287
+ for (const n of node) {
7288
+ const found = findElevenLabsFetchCall(n, depth + 1);
7289
+ if (found) return found;
7290
+ }
7291
+ return null;
7104
7292
  }
7105
- return { lines, highlightedLine: highlighted };
7106
- }
7107
-
7108
- // src/types.ts
7109
- var SEVERITY_ORDER = {
7110
- error: 0,
7111
- warning: 1,
7112
- info: 2
7113
- };
7114
- function scoreToSeverityLabel(score) {
7115
- if (score >= 80) return "excellent";
7116
- if (score >= 60) return "good";
7117
- if (score >= 40) return "needs-work";
7118
- return "critical";
7293
+ if (node.type === "CallExpression" && node.callee?.type === "Identifier" && node.callee.name === "fetch") {
7294
+ if (isElevenLabsUrlArg(node.arguments?.[0])) return node;
7295
+ }
7296
+ for (const key of Object.keys(node)) {
7297
+ if (key === "parent" || key === "loc" || key === "range") continue;
7298
+ const val = node[key];
7299
+ if (val && typeof val === "object") {
7300
+ const found = findElevenLabsFetchCall(val, depth + 1);
7301
+ if (found) return found;
7302
+ }
7303
+ }
7304
+ return null;
7119
7305
  }
7120
-
7121
- // src/reporter/report-builder.ts
7122
- function computeScore(errors, warnings) {
7123
- return Math.max(0, 100 - errors * 15 - warnings * 5);
7306
+ function unwrapAwait(node) {
7307
+ return node?.type === "AwaitExpression" ? node.argument : node;
7124
7308
  }
7125
- function buildSummary(results) {
7126
- const errors = results.filter((r) => r.severity === "error").length;
7127
- const warnings = results.filter((r) => r.severity === "warning").length;
7128
- const info = results.filter((r) => r.severity === "info").length;
7129
- const score = computeScore(errors, warnings);
7130
- return {
7131
- score,
7132
- severity: scoreToSeverityLabel(score),
7133
- errors,
7134
- warnings,
7135
- info,
7136
- totalIssues: results.length
7137
- };
7309
+ function collectVarDeclarators(node, out, depth = 0) {
7310
+ if (!node || typeof node !== "object" || depth > 24) return;
7311
+ if (Array.isArray(node)) {
7312
+ for (const n of node) collectVarDeclarators(n, out, depth + 1);
7313
+ return;
7314
+ }
7315
+ if (node.type === "VariableDeclarator") out.push(node);
7316
+ for (const key of Object.keys(node)) {
7317
+ if (key === "parent" || key === "loc" || key === "range") continue;
7318
+ const val = node[key];
7319
+ if (val && typeof val === "object") collectVarDeclarators(val, out, depth + 1);
7320
+ }
7138
7321
  }
7139
- function sortResults(results) {
7140
- return [...results].sort((a, b) => {
7141
- const bySeverity = SEVERITY_ORDER[a.severity] - SEVERITY_ORDER[b.severity];
7142
- if (bySeverity !== 0) return bySeverity;
7143
- const byFile = a.file.localeCompare(b.file);
7144
- if (byFile !== 0) return byFile;
7145
- return a.line - b.line;
7146
- });
7322
+ function posOf(n) {
7323
+ if (typeof n?.range?.[0] === "number") return n.range[0];
7324
+ const line = n?.loc?.start?.line ?? 0;
7325
+ const column = n?.loc?.start?.column ?? 0;
7326
+ return line * 1e6 + column;
7147
7327
  }
7148
- function toFinding(result, sequence, content) {
7149
- const docs = getRuleDocsMeta(result.ruleKey);
7150
- return {
7151
- id: `${result.ruleKey}-${sequence}`,
7152
- rule: result.rule,
7153
- category: docs?.category ?? "correctness",
7154
- severity: result.severity,
7155
- message: result.message,
7156
- fix: result.fix,
7157
- docsUrl: result.docsUrl ?? docs?.docsUrl,
7158
- cwe: docs?.cwe,
7328
+
7329
+ // src/providers/elevenlabs/rules/validate-signed-url-response.ts
7330
+ var rule81 = {
7331
+ meta: {
7332
+ type: "problem",
7333
+ docs: {
7334
+ description: "ElevenLabs signed URL responses must be validated before the signed_url field is used",
7335
+ category: "correctness",
7336
+ cwe: "CWE-252",
7337
+ rationale: "The signed-url endpoint assumes the ElevenLabs API always returns a well-formed body. If the API ever returns an unexpected shape (error payload, empty body, schema change), `data.signed_url` is silently `undefined` and the failure only surfaces downstream when the client tries to connect with an invalid URL.",
7338
+ docsUrl: "https://elevenlabs.io/docs/eleven-agents/api-reference/conversations/get-signed-url",
7339
+ recommended: true
7340
+ },
7341
+ messages: {
7342
+ missingValidation: "This code reads signed_url from the ElevenLabs API response without checking that the field exists. A malformed or unexpected response will silently produce an undefined signed URL."
7343
+ }
7344
+ },
7345
+ create(context) {
7346
+ function analyzeFunction(fnNode) {
7347
+ const declarators = [];
7348
+ collectVarDeclarators(fnNode.body, declarators);
7349
+ const responseVarNames = [];
7350
+ for (const d of declarators) {
7351
+ const init = unwrapAwait(d.init);
7352
+ if (init?.type === "CallExpression" && init.callee?.type === "Identifier" && init.callee.name === "fetch" && isElevenLabsUrlArg(init.arguments?.[0]) && d.id?.type === "Identifier") {
7353
+ responseVarNames.push(d.id.name);
7354
+ }
7355
+ }
7356
+ if (responseVarNames.length === 0) return;
7357
+ for (const responseVarName of responseVarNames) {
7358
+ for (const d of declarators) {
7359
+ const init = unwrapAwait(d.init);
7360
+ if (init?.type === "CallExpression" && init.callee?.type === "MemberExpression" && init.callee.property?.type === "Identifier" && init.callee.property.name === "json" && init.callee.object?.type === "Identifier" && init.callee.object.name === responseVarName && d.id?.type === "Identifier") {
7361
+ analyzeDataVar(fnNode, d.id.name, d);
7362
+ }
7363
+ }
7364
+ }
7365
+ }
7366
+ function analyzeDataVar(fnNode, dataVarName, dataDeclaratorNode) {
7367
+ function isSignedUrlMember(n) {
7368
+ if (n?.type !== "MemberExpression") return false;
7369
+ if (n.object?.type !== "Identifier" || n.object.name !== dataVarName) return false;
7370
+ if (!n.computed) return n.property?.type === "Identifier" && n.property.name === "signed_url";
7371
+ return n.property?.type === "Literal" && n.property.value === "signed_url";
7372
+ }
7373
+ function isNullishLiteral(n) {
7374
+ return n?.type === "Identifier" && n.name === "undefined" || n?.type === "Literal" && n.value === null;
7375
+ }
7376
+ function isFalsyGuardOnSignedUrl(test) {
7377
+ if (!test) return false;
7378
+ if (test.type === "UnaryExpression" && test.operator === "!") {
7379
+ return isSignedUrlMember(test.argument);
7380
+ }
7381
+ if (test.type === "BinaryExpression" && (test.operator === "==" || test.operator === "===")) {
7382
+ return isSignedUrlMember(test.left) && isNullishLiteral(test.right) || isSignedUrlMember(test.right) && isNullishLiteral(test.left);
7383
+ }
7384
+ if (test.type === "LogicalExpression") {
7385
+ return isFalsyGuardOnSignedUrl(test.left) || isFalsyGuardOnSignedUrl(test.right);
7386
+ }
7387
+ return false;
7388
+ }
7389
+ let guardPos = null;
7390
+ let usagePos = null;
7391
+ function walk3(n, depth = 0) {
7392
+ if (!n || typeof n !== "object" || depth > 40) return;
7393
+ if (Array.isArray(n)) {
7394
+ for (const item of n) walk3(item, depth + 1);
7395
+ return;
7396
+ }
7397
+ if (n.type === "IfStatement" && isFalsyGuardOnSignedUrl(n.test)) {
7398
+ const p = posOf(n);
7399
+ if (guardPos === null || p < guardPos) guardPos = p;
7400
+ }
7401
+ if (isSignedUrlMember(n) && n !== dataDeclaratorNode) {
7402
+ const p = posOf(n);
7403
+ if (usagePos === null || p < usagePos) usagePos = p;
7404
+ }
7405
+ for (const key of Object.keys(n)) {
7406
+ if (key === "parent" || key === "loc" || key === "range") continue;
7407
+ const val = n[key];
7408
+ if (val && typeof val === "object") walk3(val, depth + 1);
7409
+ }
7410
+ }
7411
+ walk3(fnNode.body);
7412
+ if (usagePos === null) return;
7413
+ if (guardPos !== null && guardPos < usagePos) return;
7414
+ context.report({ node: dataDeclaratorNode, messageId: "missingValidation" });
7415
+ }
7416
+ return {
7417
+ FunctionDeclaration(node) {
7418
+ analyzeFunction(node);
7419
+ },
7420
+ FunctionExpression(node) {
7421
+ analyzeFunction(node);
7422
+ },
7423
+ ArrowFunctionExpression(node) {
7424
+ analyzeFunction(node);
7425
+ }
7426
+ };
7427
+ }
7428
+ };
7429
+ var elevenlabsValidateSignedUrlResponseRule = rule81;
7430
+
7431
+ // src/providers/elevenlabs/rules/no-error-object-logging.ts
7432
+ var LOGGING_METHODS = /* @__PURE__ */ new Set(["error", "warn", "log"]);
7433
+ var rule82 = {
7434
+ meta: {
7435
+ type: "problem",
7436
+ docs: {
7437
+ description: "Catch blocks must not log the raw caught error object",
7438
+ category: "security",
7439
+ cwe: "CWE-532",
7440
+ rationale: "Error objects thrown by fetch/SDK calls can carry response bodies, headers, or internal state. Logging them directly with console.error(error) writes that data verbatim into server logs, which may be shipped to third-party log aggregators or be readable by anyone with log access.",
7441
+ docsUrl: "https://elevenlabs.io/docs/api-reference/authentication",
7442
+ recommended: true
7443
+ },
7444
+ messages: {
7445
+ rawErrorLogged: "This catch block logs the raw error object instead of a sanitized message \u2014 error responses, headers, or internal state may leak into server logs."
7446
+ }
7447
+ },
7448
+ create(context) {
7449
+ function isConsoleLogCall(node) {
7450
+ if (node?.type !== "CallExpression") return false;
7451
+ const callee = node.callee;
7452
+ if (callee?.type !== "MemberExpression") return false;
7453
+ if (callee.object?.type !== "Identifier" || callee.object.name !== "console") return false;
7454
+ return callee.property?.type === "Identifier" && LOGGING_METHODS.has(callee.property.name);
7455
+ }
7456
+ function referencesRawIdentifier(node, paramName, depth = 0) {
7457
+ if (!node || typeof node !== "object" || depth > 10) return false;
7458
+ if (Array.isArray(node)) return node.some((n) => referencesRawIdentifier(n, paramName, depth + 1));
7459
+ if (node.type === "Identifier" && node.name === paramName) return true;
7460
+ if (node.type === "ObjectExpression") {
7461
+ return (node.properties ?? []).some((p) => {
7462
+ if (p.type !== "Property") return false;
7463
+ return referencesRawIdentifier(p.value, paramName, depth + 1);
7464
+ });
7465
+ }
7466
+ if (node.type === "ArrayExpression") {
7467
+ return (node.elements ?? []).some((el) => referencesRawIdentifier(el, paramName, depth + 1));
7468
+ }
7469
+ return false;
7470
+ }
7471
+ return {
7472
+ TryStatement(node) {
7473
+ if (!findElevenLabsFetchCall(node.block)) return;
7474
+ const handler = node.handler;
7475
+ const paramName = handler?.param?.type === "Identifier" ? handler.param.name : null;
7476
+ if (!paramName) return;
7477
+ function walk3(n, depth = 0) {
7478
+ if (!n || typeof n !== "object" || depth > 30) return;
7479
+ if (Array.isArray(n)) {
7480
+ for (const item of n) walk3(item, depth + 1);
7481
+ return;
7482
+ }
7483
+ if (isConsoleLogCall(n)) {
7484
+ const args = n.arguments ?? [];
7485
+ const loggedRaw = args.some((a) => referencesRawIdentifier(a, paramName));
7486
+ if (loggedRaw) {
7487
+ context.report({ node: n, messageId: "rawErrorLogged" });
7488
+ }
7489
+ }
7490
+ for (const key of Object.keys(n)) {
7491
+ if (key === "parent" || key === "loc" || key === "range") continue;
7492
+ const val = n[key];
7493
+ if (val && typeof val === "object") walk3(val, depth + 1);
7494
+ }
7495
+ }
7496
+ walk3(handler.body);
7497
+ }
7498
+ };
7499
+ }
7500
+ };
7501
+ var elevenlabsNoErrorObjectLoggingRule = rule82;
7502
+
7503
+ // src/providers/elevenlabs/rules/fetch-timeout-required.ts
7504
+ var rule83 = {
7505
+ meta: {
7506
+ type: "suggestion",
7507
+ docs: {
7508
+ description: "Fetch calls to the ElevenLabs API must set an abort timeout",
7509
+ category: "reliability",
7510
+ rationale: "Native fetch has no default timeout. If the ElevenLabs API becomes slow or hangs, a request with no AbortController/signal will wait indefinitely, tying up the request handler and starving the server of available connections.",
7511
+ docsUrl: "https://elevenlabs.io/docs/eleven-agents/api-reference/conversations/get-signed-url",
7512
+ recommended: true
7513
+ },
7514
+ messages: {
7515
+ missingTimeout: "This fetch call to the ElevenLabs API has no abort signal/timeout \u2014 a slow or unresponsive API will hang the request indefinitely."
7516
+ }
7517
+ },
7518
+ create(context) {
7519
+ function hasSignalOption(optionsArg) {
7520
+ if (optionsArg?.type !== "ObjectExpression") return false;
7521
+ return (optionsArg.properties ?? []).some((p) => {
7522
+ if (p.type === "SpreadElement") return true;
7523
+ return p.type === "Property" && p.key?.type === "Identifier" && p.key.name === "signal";
7524
+ });
7525
+ }
7526
+ return {
7527
+ CallExpression(node) {
7528
+ if (node.callee?.type !== "Identifier" || node.callee.name !== "fetch") return;
7529
+ if (!isElevenLabsUrlArg(node.arguments?.[0])) return;
7530
+ const optionsArg = node.arguments?.[1];
7531
+ if (hasSignalOption(optionsArg)) return;
7532
+ context.report({ node, messageId: "missingTimeout" });
7533
+ }
7534
+ };
7535
+ }
7536
+ };
7537
+ var elevenlabsFetchTimeoutRequiredRule = rule83;
7538
+
7539
+ // src/providers/elevenlabs/rules/validate-agent-id-format.ts
7540
+ var rule84 = {
7541
+ meta: {
7542
+ type: "problem",
7543
+ docs: {
7544
+ description: "agentId must be format-validated, not just checked for existence, before use",
7545
+ category: "correctness",
7546
+ cwe: "CWE-20",
7547
+ rationale: "A query-param agentId that is only checked with `if (!agentId)` accepts any non-empty string. An attacker can pass arbitrary values \u2014 oversized strings, path-traversal-like sequences, or characters the ElevenLabs API was never designed to receive \u2014 directly into the request URL, producing undefined behavior instead of a clean 400.",
7548
+ docsUrl: "https://elevenlabs.io/docs/eleven-agents/customization/authentication",
7549
+ recommended: true
7550
+ },
7551
+ messages: {
7552
+ missingFormatValidation: "agentId is checked for existence but never validated against an expected format before being used in an ElevenLabs API request."
7553
+ }
7554
+ },
7555
+ create(context) {
7556
+ function isSearchParamsGetAgentId(node) {
7557
+ if (node?.type !== "CallExpression") return false;
7558
+ const callee = node.callee;
7559
+ if (callee?.type !== "MemberExpression") return false;
7560
+ if (callee.property?.type !== "Identifier" || callee.property.name !== "get") return false;
7561
+ const arg = node.arguments?.[0];
7562
+ return arg?.type === "Literal" && arg.value === "agentId";
7563
+ }
7564
+ function analyzeFunction(fnNode) {
7565
+ const declarators = [];
7566
+ collectVarDeclarators(fnNode.body, declarators);
7567
+ let agentIdVarName = null;
7568
+ let agentIdDeclaratorNode = null;
7569
+ for (const d of declarators) {
7570
+ const init = unwrapAwait(d.init);
7571
+ if (isSearchParamsGetAgentId(init) && d.id?.type === "Identifier") {
7572
+ agentIdVarName = d.id.name;
7573
+ agentIdDeclaratorNode = d;
7574
+ }
7575
+ }
7576
+ if (!agentIdVarName) {
7577
+ for (const param of fnNode.params ?? []) {
7578
+ if (param?.type === "Identifier" && param.name === "agentId") {
7579
+ agentIdVarName = param.name;
7580
+ agentIdDeclaratorNode = param;
7581
+ }
7582
+ }
7583
+ }
7584
+ if (!agentIdVarName) return;
7585
+ function isAgentIdMember(n) {
7586
+ return n?.type === "Identifier" && n.name === agentIdVarName;
7587
+ }
7588
+ function isFormatCheck(n, depth = 0) {
7589
+ if (!n || typeof n !== "object" || depth > 10) return false;
7590
+ if (Array.isArray(n)) return n.some((x) => isFormatCheck(x, depth + 1));
7591
+ if (n.type === "CallExpression" && n.callee?.type === "MemberExpression") {
7592
+ const propName2 = n.callee.property?.type === "Identifier" ? n.callee.property.name : null;
7593
+ if (propName2 === "test" && isAgentIdMember(n.arguments?.[0])) return true;
7594
+ if ((propName2 === "match" || propName2 === "matchAll") && isAgentIdMember(n.callee.object)) return true;
7595
+ }
7596
+ if (n.type === "LogicalExpression") {
7597
+ return isFormatCheck(n.left, depth + 1) || isFormatCheck(n.right, depth + 1);
7598
+ }
7599
+ if (n.type === "UnaryExpression") return isFormatCheck(n.argument, depth + 1);
7600
+ return false;
7601
+ }
7602
+ let formatGuardPos = null;
7603
+ let usagePos = null;
7604
+ function walk3(n, depth = 0) {
7605
+ if (!n || typeof n !== "object" || depth > 40) return;
7606
+ if (Array.isArray(n)) {
7607
+ for (const item of n) walk3(item, depth + 1);
7608
+ return;
7609
+ }
7610
+ if (n.type === "IfStatement" && isFormatCheck(n.test)) {
7611
+ const p = posOf(n);
7612
+ if (formatGuardPos === null || p < formatGuardPos) formatGuardPos = p;
7613
+ }
7614
+ if (n.type === "CallExpression" && n.callee?.type === "Identifier" && n.callee.name === "fetch" && isElevenLabsUrlArg(n.arguments?.[0])) {
7615
+ const urlArg = n.arguments[0];
7616
+ const containsAgentId = urlArg.type === "TemplateLiteral" && (urlArg.expressions ?? []).some((e) => isAgentIdMember(e));
7617
+ if (containsAgentId) {
7618
+ const p = posOf(n);
7619
+ if (usagePos === null || p < usagePos) usagePos = p;
7620
+ }
7621
+ }
7622
+ for (const key of Object.keys(n)) {
7623
+ if (key === "parent" || key === "loc" || key === "range") continue;
7624
+ const val = n[key];
7625
+ if (val && typeof val === "object") walk3(val, depth + 1);
7626
+ }
7627
+ }
7628
+ walk3(fnNode.body);
7629
+ if (usagePos === null) return;
7630
+ if (formatGuardPos !== null && formatGuardPos < usagePos) return;
7631
+ context.report({ node: agentIdDeclaratorNode, messageId: "missingFormatValidation" });
7632
+ }
7633
+ return {
7634
+ FunctionDeclaration(node) {
7635
+ analyzeFunction(node);
7636
+ },
7637
+ FunctionExpression(node) {
7638
+ analyzeFunction(node);
7639
+ },
7640
+ ArrowFunctionExpression(node) {
7641
+ analyzeFunction(node);
7642
+ }
7643
+ };
7644
+ }
7645
+ };
7646
+ var elevenlabsValidateAgentIdFormatRule = rule84;
7647
+
7648
+ // src/providers/elevenlabs/rules/secure-session-id-generation.ts
7649
+ var rule85 = {
7650
+ meta: {
7651
+ type: "problem",
7652
+ docs: {
7653
+ description: "Session ids must be generated with a cryptographically secure RNG",
7654
+ category: "security",
7655
+ cwe: "CWE-338",
7656
+ rationale: 'Math.random() is a non-cryptographic PRNG \u2014 its output can be predicted by an attacker who observes enough samples or knows the engine implementation. If a "session_*" value derived from it is later trusted for routing, deduplication, or any access-control-adjacent decision, that predictability can be exploited.',
7657
+ docsUrl: "https://elevenlabs.io/docs/eleven-api/guides/how-to/best-practices/security",
7658
+ recommended: true
7659
+ },
7660
+ messages: {
7661
+ insecureSessionId: "This session id is generated with Math.random(), which is not cryptographically secure and can be predicted. Use crypto.getRandomValues() instead."
7662
+ }
7663
+ },
7664
+ create(context) {
7665
+ function containsMathRandomCall(node, depth = 0) {
7666
+ if (!node || typeof node !== "object" || depth > 20) return false;
7667
+ if (Array.isArray(node)) return node.some((n) => containsMathRandomCall(n, depth + 1));
7668
+ if (node.type === "CallExpression" && node.callee?.type === "MemberExpression" && node.callee.object?.type === "Identifier" && node.callee.object.name === "Math" && node.callee.property?.type === "Identifier" && node.callee.property.name === "random") {
7669
+ return true;
7670
+ }
7671
+ for (const key of Object.keys(node)) {
7672
+ if (key === "parent" || key === "loc" || key === "range") continue;
7673
+ const val = node[key];
7674
+ if (val && typeof val === "object" && containsMathRandomCall(val, depth + 1)) return true;
7675
+ }
7676
+ return false;
7677
+ }
7678
+ function looksLikeSessionId(name) {
7679
+ return typeof name === "string" && /session/i.test(name);
7680
+ }
7681
+ function isSessionIdValue(init) {
7682
+ if (init?.type === "TemplateLiteral") {
7683
+ const firstQuasi = init.quasis?.[0]?.value?.raw ?? "";
7684
+ if (/session[-_]?/i.test(firstQuasi) && containsMathRandomCall(init)) return true;
7685
+ }
7686
+ if (init?.type === "BinaryExpression" && init.operator === "+") {
7687
+ const leftLiteral = init.left?.type === "Literal" && typeof init.left.value === "string";
7688
+ if (leftLiteral && /session[-_]?/i.test(init.left.value) && containsMathRandomCall(init)) return true;
7689
+ }
7690
+ return false;
7691
+ }
7692
+ return {
7693
+ VariableDeclarator(node) {
7694
+ const init = node.init;
7695
+ if (!init) return;
7696
+ const declaredName = node.id?.type === "Identifier" ? node.id.name : node.id?.type === "ArrayPattern" && node.id.elements?.[0]?.type === "Identifier" ? node.id.elements[0].name : void 0;
7697
+ let valueNode = init;
7698
+ if (init.type === "CallExpression" && init.callee?.type === "Identifier" && init.callee.name === "useState" && init.arguments?.[0]?.type === "ArrowFunctionExpression") {
7699
+ const body = init.arguments[0].body;
7700
+ valueNode = body?.type === "BlockStatement" ? null : body;
7701
+ if (body?.type === "BlockStatement") {
7702
+ const returnStmt = (body.body ?? []).find((s) => s.type === "ReturnStatement");
7703
+ valueNode = returnStmt?.argument ?? null;
7704
+ }
7705
+ }
7706
+ if (!valueNode) return;
7707
+ if (!looksLikeSessionId(declaredName) && !isSessionIdValue(valueNode)) return;
7708
+ if (!containsMathRandomCall(valueNode)) return;
7709
+ context.report({ node, messageId: "insecureSessionId" });
7710
+ }
7711
+ };
7712
+ }
7713
+ };
7714
+ var elevenlabsSecureSessionIdGenerationRule = rule85;
7715
+
7716
+ // src/providers/elevenlabs/rules/conversation-error-recovery.ts
7717
+ var rule86 = {
7718
+ meta: {
7719
+ type: "suggestion",
7720
+ docs: {
7721
+ description: "A loading flag set before starting a conversation must be reset on failure",
7722
+ category: "reliability",
7723
+ rationale: 'If startConversation() sets isLoading true and getSignedUrl()/Conversation.startSession() throws, the loading flag must be reset in the catch or finally block. Otherwise the UI is stuck "loading" forever and the user can trigger overlapping conversation attempts by clicking again.',
7724
+ docsUrl: "https://elevenlabs.io/docs/eleven-agents/libraries/java-script",
7725
+ recommended: true
7726
+ },
7727
+ messages: {
7728
+ missingLoadingReset: "This function sets a loading flag true before a try block, but neither the catch nor a finally block resets it to false on failure."
7729
+ }
7730
+ },
7731
+ create(context) {
7732
+ function isSetterCallWithBoolean(n, setterName, expected) {
7733
+ if (n?.type !== "CallExpression") return false;
7734
+ if (n.callee?.type !== "Identifier" || n.callee.name !== setterName) return false;
7735
+ const arg = n.arguments?.[0];
7736
+ return arg?.type === "Literal" && arg.value === expected;
7737
+ }
7738
+ function containsCall(node, predicate, depth = 0) {
7739
+ if (!node || typeof node !== "object" || depth > 20) return false;
7740
+ if (Array.isArray(node)) return node.some((n) => containsCall(n, predicate, depth + 1));
7741
+ if (predicate(node)) return true;
7742
+ for (const key of Object.keys(node)) {
7743
+ if (key === "parent" || key === "loc" || key === "range") continue;
7744
+ const val = node[key];
7745
+ if (val && typeof val === "object" && containsCall(val, predicate, depth + 1)) return true;
7746
+ }
7747
+ return false;
7748
+ }
7749
+ function findLoadingSetterFromUseState(fnNode) {
7750
+ let setterName = null;
7751
+ function collect(n, depth = 0) {
7752
+ if (setterName || !n || typeof n !== "object" || depth > 20) return;
7753
+ if (Array.isArray(n)) {
7754
+ for (const item of n) collect(item, depth + 1);
7755
+ return;
7756
+ }
7757
+ if (n.type === "VariableDeclarator" && n.id?.type === "ArrayPattern" && n.id.elements?.length === 2 && n.id.elements[0]?.type === "Identifier" && /loading/i.test(n.id.elements[0].name) && n.id.elements[1]?.type === "Identifier" && n.init?.type === "CallExpression" && n.init.callee?.type === "Identifier" && n.init.callee.name === "useState") {
7758
+ setterName = n.id.elements[1].name;
7759
+ return;
7760
+ }
7761
+ for (const key of Object.keys(n)) {
7762
+ if (key === "parent" || key === "loc" || key === "range") continue;
7763
+ const val = n[key];
7764
+ if (val && typeof val === "object") collect(val, depth + 1);
7765
+ }
7766
+ }
7767
+ collect(fnNode);
7768
+ return setterName;
7769
+ }
7770
+ function isLoadingTrueStatement(stmt, setterName) {
7771
+ return stmt?.type === "ExpressionStatement" && isSetterCallWithBoolean(stmt.expression, setterName, true);
7772
+ }
7773
+ function analyzeFunction(fnNode) {
7774
+ const setterName = findLoadingSetterFromUseState(fnNode);
7775
+ if (!setterName) return;
7776
+ function walk3(n, depth = 0) {
7777
+ if (!n || typeof n !== "object" || depth > 30) return;
7778
+ if (Array.isArray(n)) {
7779
+ for (const item of n) walk3(item, depth + 1);
7780
+ return;
7781
+ }
7782
+ if (n.type === "BlockStatement" && Array.isArray(n.body)) {
7783
+ for (let i = 0; i < n.body.length - 1; i++) {
7784
+ if (!isLoadingTrueStatement(n.body[i], setterName)) continue;
7785
+ const tryNode = n.body[i + 1];
7786
+ if (tryNode?.type !== "TryStatement") continue;
7787
+ const handler = tryNode.handler;
7788
+ const finalizer = tryNode.finalizer;
7789
+ const finallyResets = finalizer && containsCall(finalizer, (x) => isSetterCallWithBoolean(x, setterName, false));
7790
+ const catchResets = handler && containsCall(handler.body, (x) => isSetterCallWithBoolean(x, setterName, false));
7791
+ if (!finallyResets && !catchResets) {
7792
+ context.report({ node: tryNode, messageId: "missingLoadingReset" });
7793
+ }
7794
+ }
7795
+ }
7796
+ for (const key of Object.keys(n)) {
7797
+ if (key === "parent" || key === "loc" || key === "range") continue;
7798
+ const val = n[key];
7799
+ if (val && typeof val === "object") walk3(val, depth + 1);
7800
+ }
7801
+ }
7802
+ walk3(fnNode.body);
7803
+ }
7804
+ return {
7805
+ FunctionDeclaration(node) {
7806
+ analyzeFunction(node);
7807
+ },
7808
+ FunctionExpression(node) {
7809
+ analyzeFunction(node);
7810
+ },
7811
+ ArrowFunctionExpression(node) {
7812
+ analyzeFunction(node);
7813
+ }
7814
+ };
7815
+ }
7816
+ };
7817
+ var elevenlabsConversationErrorRecoveryRule = rule86;
7818
+
7819
+ // src/providers/elevenlabs/rules/api-version-pinning.ts
7820
+ var rule87 = {
7821
+ meta: {
7822
+ type: "suggestion",
7823
+ docs: {
7824
+ description: "ElevenLabs API requests should pin an explicit API version header",
7825
+ category: "reliability",
7826
+ rationale: "The endpoint is hardcoded to /v1 in the URL path with no explicit version header. If ElevenLabs ships a v2 API and changes default response behavior for unversioned clients, requests with no version header silently pick up the new behavior instead of failing loudly or staying pinned.",
7827
+ docsUrl: "https://elevenlabs.io/docs/eleven-api/resources/breaking-changes-policy",
7828
+ recommended: true
7829
+ },
7830
+ messages: {
7831
+ missingVersionHeader: "This fetch call to the ElevenLabs API has no explicit API version header \u2014 a future API change could silently alter behavior."
7832
+ }
7833
+ },
7834
+ create(context) {
7835
+ function hasVersionHeader(optionsArg) {
7836
+ if (optionsArg?.type !== "ObjectExpression") return false;
7837
+ const headersProp = (optionsArg.properties ?? []).find(
7838
+ (p) => p.type === "Property" && p.key?.type === "Identifier" && p.key.name === "headers"
7839
+ );
7840
+ if (!headersProp) return false;
7841
+ if (headersProp.value?.type !== "ObjectExpression") return true;
7842
+ return (headersProp.value.properties ?? []).some((p) => {
7843
+ if (p.type === "SpreadElement") return true;
7844
+ if (p.type !== "Property") return false;
7845
+ const keyName = p.key?.type === "Identifier" ? p.key.name : p.key?.type === "Literal" ? p.key.value : null;
7846
+ return typeof keyName === "string" && /version/i.test(keyName);
7847
+ });
7848
+ }
7849
+ return {
7850
+ CallExpression(node) {
7851
+ if (node.callee?.type !== "Identifier" || node.callee.name !== "fetch") return;
7852
+ if (!isElevenLabsUrlArg(node.arguments?.[0])) return;
7853
+ const optionsArg = node.arguments?.[1];
7854
+ if (hasVersionHeader(optionsArg)) return;
7855
+ context.report({ node, messageId: "missingVersionHeader" });
7856
+ }
7857
+ };
7858
+ }
7859
+ };
7860
+ var elevenlabsApiVersionPinningRule = rule87;
7861
+
7862
+ // src/providers/elevenlabs/rules/check-http-status-before-json.ts
7863
+ var rule88 = {
7864
+ meta: {
7865
+ type: "problem",
7866
+ docs: {
7867
+ description: "response.json() must not be called before checking the HTTP status of an ElevenLabs response",
7868
+ category: "correctness",
7869
+ rationale: "Calling response.json() unconditionally parses whatever body the server returned, including error payloads, as if it were a successful response. Without checking response.ok (or response.status) first, a 4xx/5xx error from the ElevenLabs API is silently treated as valid data.",
7870
+ docsUrl: "https://elevenlabs.io/docs/eleven-api/resources/errors",
7871
+ recommended: true
7872
+ },
7873
+ messages: {
7874
+ missingStatusCheck: "response.json() is called on this ElevenLabs API response without first checking response.ok/status \u2014 an error response will be parsed as if it were valid data."
7875
+ }
7876
+ },
7877
+ create(context) {
7878
+ function analyzeFunction(fnNode) {
7879
+ const declarators = [];
7880
+ collectVarDeclarators(fnNode.body, declarators);
7881
+ const responseVarNames = [];
7882
+ for (const d of declarators) {
7883
+ const init = unwrapAwait(d.init);
7884
+ if (init?.type === "CallExpression" && init.callee?.type === "Identifier" && init.callee.name === "fetch" && isElevenLabsUrlArg(init.arguments?.[0]) && d.id?.type === "Identifier") {
7885
+ responseVarNames.push(d.id.name);
7886
+ }
7887
+ }
7888
+ for (const responseVarName of responseVarNames) {
7889
+ analyzeResponseVar(fnNode, responseVarName);
7890
+ }
7891
+ }
7892
+ function analyzeResponseVar(fnNode, responseVarName) {
7893
+ function isResponseStatusCheck(test) {
7894
+ if (!test) return false;
7895
+ if (test.type === "UnaryExpression" && test.operator === "!") return isResponseStatusCheck(test.argument);
7896
+ if (test.type === "MemberExpression" && test.object?.type === "Identifier" && test.object.name === responseVarName && test.property?.type === "Identifier" && test.property.name === "ok") {
7897
+ return true;
7898
+ }
7899
+ if (test.type === "BinaryExpression") {
7900
+ const sideIsStatus = (n) => n?.type === "MemberExpression" && n.object?.type === "Identifier" && n.object.name === responseVarName && n.property?.type === "Identifier" && n.property.name === "status";
7901
+ if (sideIsStatus(test.left) || sideIsStatus(test.right)) return true;
7902
+ }
7903
+ if (test.type === "LogicalExpression") {
7904
+ return isResponseStatusCheck(test.left) || isResponseStatusCheck(test.right);
7905
+ }
7906
+ return false;
7907
+ }
7908
+ function isResponseJsonCall2(n) {
7909
+ if (n?.type !== "CallExpression") return false;
7910
+ const callee = n.callee;
7911
+ if (callee?.type !== "MemberExpression") return false;
7912
+ if (callee.property?.type !== "Identifier" || callee.property.name !== "json") return false;
7913
+ return callee.object?.type === "Identifier" && callee.object.name === responseVarName;
7914
+ }
7915
+ let guardPos = null;
7916
+ let jsonCallNode = null;
7917
+ let jsonCallPos = null;
7918
+ function walk3(n, depth = 0) {
7919
+ if (!n || typeof n !== "object" || depth > 40) return;
7920
+ if (Array.isArray(n)) {
7921
+ for (const item of n) walk3(item, depth + 1);
7922
+ return;
7923
+ }
7924
+ if (n.type === "IfStatement" && isResponseStatusCheck(n.test)) {
7925
+ const p = posOf(n);
7926
+ if (guardPos === null || p < guardPos) guardPos = p;
7927
+ }
7928
+ if (isResponseJsonCall2(n)) {
7929
+ const p = posOf(n);
7930
+ if (jsonCallPos === null || p < jsonCallPos) {
7931
+ jsonCallPos = p;
7932
+ jsonCallNode = n;
7933
+ }
7934
+ }
7935
+ for (const key of Object.keys(n)) {
7936
+ if (key === "parent" || key === "loc" || key === "range") continue;
7937
+ const val = n[key];
7938
+ if (val && typeof val === "object") walk3(val, depth + 1);
7939
+ }
7940
+ }
7941
+ walk3(fnNode.body);
7942
+ if (jsonCallPos === null) return;
7943
+ if (guardPos !== null && guardPos < jsonCallPos) return;
7944
+ context.report({ node: jsonCallNode, messageId: "missingStatusCheck" });
7945
+ }
7946
+ return {
7947
+ FunctionDeclaration(node) {
7948
+ analyzeFunction(node);
7949
+ },
7950
+ FunctionExpression(node) {
7951
+ analyzeFunction(node);
7952
+ },
7953
+ ArrowFunctionExpression(node) {
7954
+ analyzeFunction(node);
7955
+ }
7956
+ };
7957
+ }
7958
+ };
7959
+ var elevenlabsCheckHttpStatusBeforeJsonRule = rule88;
7960
+
7961
+ // src/providers/elevenlabs/rules/conversation-cleanup-on-error.ts
7962
+ var END_METHODS = /* @__PURE__ */ new Set(["endSession", "endConversation"]);
7963
+ var rule89 = {
7964
+ meta: {
7965
+ type: "suggestion",
7966
+ docs: {
7967
+ description: "Ending a conversation must be wrapped in try/catch so a rejected call is handled",
7968
+ category: "reliability",
7969
+ rationale: "If conversation.endSession()/endConversation() rejects (e.g. during a GL-mode transition) and the call site has no try/catch, the rejection becomes an unhandled promise rejection and the conversation state is never cleaned up, leaving stale connections in memory.",
7970
+ docsUrl: "https://elevenlabs.io/docs/eleven-agents/libraries/java-script",
7971
+ recommended: true
7972
+ },
7973
+ messages: {
7974
+ missingTryCatch: "This call to end the conversation has no surrounding try/catch \u2014 a rejection will go unhandled and the conversation state may never be cleaned up."
7975
+ }
7976
+ },
7977
+ create(context) {
7978
+ function posEnd(n) {
7979
+ if (typeof n?.range?.[1] === "number") return n.range[1];
7980
+ const line = n?.loc?.end?.line ?? n?.loc?.start?.line ?? 0;
7981
+ const column = n?.loc?.end?.column ?? n?.loc?.start?.column ?? 0;
7982
+ return line * 1e6 + column;
7983
+ }
7984
+ function isConversationEndCall(node) {
7985
+ if (node?.type !== "CallExpression") return false;
7986
+ const callee = node.callee;
7987
+ if (callee?.type === "MemberExpression") {
7988
+ return callee.property?.type === "Identifier" && END_METHODS.has(callee.property.name);
7989
+ }
7990
+ return callee?.type === "Identifier" && END_METHODS.has(callee.name);
7991
+ }
7992
+ return {
7993
+ "Program:exit"(program2) {
7994
+ const tryRanges = [];
7995
+ const endCalls = [];
7996
+ function walk3(n, depth = 0) {
7997
+ if (!n || typeof n !== "object" || depth > 60) return;
7998
+ if (Array.isArray(n)) {
7999
+ for (const item of n) walk3(item, depth + 1);
8000
+ return;
8001
+ }
8002
+ if (n.type === "TryStatement" && n.block) {
8003
+ tryRanges.push([posOf(n.block), posEnd(n.block)]);
8004
+ }
8005
+ if (isConversationEndCall(n)) endCalls.push(n);
8006
+ for (const key of Object.keys(n)) {
8007
+ if (key === "parent" || key === "loc" || key === "range") continue;
8008
+ const val = n[key];
8009
+ if (val && typeof val === "object") walk3(val, depth + 1);
8010
+ }
8011
+ }
8012
+ walk3(program2);
8013
+ for (const call of endCalls) {
8014
+ const p = posOf(call);
8015
+ const covered = tryRanges.some(([start, end]) => p >= start && p <= end);
8016
+ if (!covered) {
8017
+ context.report({ node: call, messageId: "missingTryCatch" });
8018
+ }
8019
+ }
8020
+ }
8021
+ };
8022
+ }
8023
+ };
8024
+ var elevenlabsConversationCleanupOnErrorRule = rule89;
8025
+
8026
+ // src/providers/elevenlabs/rules/env-var-validation.ts
8027
+ var ELEVENLABS_ENV_VAR_PATTERN = /^(XI_API_KEY|ELEVENLABS_API_KEY)$/;
8028
+ var rule90 = {
8029
+ meta: {
8030
+ type: "suggestion",
8031
+ docs: {
8032
+ description: "ElevenLabs API key environment variables should be validated at module load, not only at request time",
8033
+ category: "correctness",
8034
+ rationale: "Checking `if (!apiKey)` inside a request handler means a misconfigured deployment only fails the first time a user exercises the feature, instead of failing fast at startup where it would show up in deploy logs or a health check.",
8035
+ docsUrl: "https://elevenlabs.io/docs/api-reference/authentication",
8036
+ recommended: true
8037
+ },
8038
+ messages: {
8039
+ missingStartupValidation: "This module reads an ElevenLabs API key from process.env but only validates it inside a request handler \u2014 validate it at module scope so a missing key fails at startup, not at first use."
8040
+ }
8041
+ },
8042
+ create(context) {
8043
+ function isElevenLabsEnvAccess(node) {
8044
+ if (node?.type !== "MemberExpression") return false;
8045
+ if (node.object?.type !== "MemberExpression") return false;
8046
+ const inner = node.object;
8047
+ if (inner.object?.type !== "Identifier" || inner.object.name !== "process") return false;
8048
+ if (inner.property?.type !== "Identifier" || inner.property.name !== "env") return false;
8049
+ const propName2 = node.property?.type === "Identifier" ? node.property.name : node.property?.value;
8050
+ return typeof propName2 === "string" && ELEVENLABS_ENV_VAR_PATTERN.test(propName2);
8051
+ }
8052
+ function isInsideFunction(node, ancestors) {
8053
+ return ancestors.some(
8054
+ (a) => a?.type === "FunctionDeclaration" || a?.type === "FunctionExpression" || a?.type === "ArrowFunctionExpression"
8055
+ );
8056
+ }
8057
+ return {
8058
+ "Program:exit"(program2) {
8059
+ let moduleScopeAccess = false;
8060
+ let handlerScopeAccess = false;
8061
+ function walk3(n, ancestors, depth = 0) {
8062
+ if (!n || typeof n !== "object" || depth > 60) return;
8063
+ if (Array.isArray(n)) {
8064
+ for (const item of n) walk3(item, ancestors, depth + 1);
8065
+ return;
8066
+ }
8067
+ if (isElevenLabsEnvAccess(n)) {
8068
+ if (isInsideFunction(n, ancestors)) {
8069
+ handlerScopeAccess = true;
8070
+ } else {
8071
+ moduleScopeAccess = true;
8072
+ }
8073
+ }
8074
+ const nextAncestors = n.type === "FunctionDeclaration" || n.type === "FunctionExpression" || n.type === "ArrowFunctionExpression" ? [...ancestors, n] : ancestors;
8075
+ for (const key of Object.keys(n)) {
8076
+ if (key === "parent" || key === "loc" || key === "range") continue;
8077
+ const val = n[key];
8078
+ if (val && typeof val === "object") walk3(val, nextAncestors, depth + 1);
8079
+ }
8080
+ }
8081
+ walk3(program2, []);
8082
+ if (handlerScopeAccess && !moduleScopeAccess) {
8083
+ context.report({ node: program2, messageId: "missingStartupValidation" });
8084
+ }
8085
+ }
8086
+ };
8087
+ }
8088
+ };
8089
+ var elevenlabsEnvVarValidationRule = rule90;
8090
+
8091
+ // src/providers/twilio/utils.ts
8092
+ var POST_METHOD_NAMES = /* @__PURE__ */ new Set(["post"]);
8093
+ var ROUTER_OBJECT_NAMES = /* @__PURE__ */ new Set(["server", "app", "router", "fastify"]);
8094
+ function isPostRouteRegistration(node) {
8095
+ if (node?.type !== "CallExpression") return false;
8096
+ const callee = node.callee;
8097
+ if (callee?.type !== "MemberExpression") return false;
8098
+ if (callee.property?.type !== "Identifier" || !POST_METHOD_NAMES.has(callee.property.name)) return false;
8099
+ return callee.object?.type === "Identifier" && ROUTER_OBJECT_NAMES.has(callee.object.name);
8100
+ }
8101
+ function findInSubtree(node, predicate, depth = 0) {
8102
+ if (!node || typeof node !== "object" || depth > 40) return null;
8103
+ if (Array.isArray(node)) {
8104
+ for (const n of node) {
8105
+ const found = findInSubtree(n, predicate, depth + 1);
8106
+ if (found) return found;
8107
+ }
8108
+ return null;
8109
+ }
8110
+ if (predicate(node)) return node;
8111
+ for (const key of Object.keys(node)) {
8112
+ if (key === "parent" || key === "loc" || key === "range") continue;
8113
+ const val = node[key];
8114
+ if (val && typeof val === "object") {
8115
+ const found = findInSubtree(val, predicate, depth + 1);
8116
+ if (found) return found;
8117
+ }
8118
+ }
8119
+ return null;
8120
+ }
8121
+ function referencesRequestBody(node) {
8122
+ return !!findInSubtree(node, (n) => {
8123
+ if (n?.type !== "MemberExpression") return false;
8124
+ if (n.property?.type !== "Identifier" || n.property.name !== "body") return false;
8125
+ return n.object?.type === "Identifier" && (n.object.name === "req" || n.object.name === "request");
8126
+ });
8127
+ }
8128
+ function collectVarDeclarators2(node, out, depth = 0) {
8129
+ if (!node || typeof node !== "object" || depth > 24) return;
8130
+ if (Array.isArray(node)) {
8131
+ for (const n of node) collectVarDeclarators2(n, out, depth + 1);
8132
+ return;
8133
+ }
8134
+ if (node.type === "VariableDeclarator") out.push(node);
8135
+ for (const key of Object.keys(node)) {
8136
+ if (key === "parent" || key === "loc" || key === "range") continue;
8137
+ const val = node[key];
8138
+ if (val && typeof val === "object") collectVarDeclarators2(val, out, depth + 1);
8139
+ }
8140
+ }
8141
+
8142
+ // src/providers/twilio/rules/validate-webhook-signature.ts
8143
+ var rule91 = {
8144
+ meta: {
8145
+ type: "problem",
8146
+ docs: {
8147
+ description: "Twilio webhook routes must validate the X-Twilio-Signature header before trusting the body",
8148
+ category: "security",
8149
+ cwe: "CWE-345",
8150
+ owasp: "A07:2021 Identification and Authentication Failures",
8151
+ rationale: "Webhook URLs are public. Anyone who learns or guesses the path can POST a forged body \u2014 fake CallSid/From/TaskAttributes values \u2014 and the handler has no way to tell it apart from a real Twilio request. Without validating the signature first, an attacker can trigger outbound calls (toll/billing impact) or falsely invoke reservation/task callbacks to manipulate call state for an arbitrary caller.",
8152
+ docsUrl: "https://www.twilio.com/docs/usage/webhooks/webhooks-security",
8153
+ recommended: true
8154
+ },
8155
+ messages: {
8156
+ missingSignatureValidation: "This POST webhook route reads req.body but the file never validates the X-Twilio-Signature header via twilio.validateRequest()/RequestValidator \u2014 a forged request body is indistinguishable from a real Twilio callback."
8157
+ }
8158
+ },
8159
+ create(context) {
8160
+ function isValidateRequestCall(n) {
8161
+ if (n?.type !== "CallExpression") return false;
8162
+ const callee = n.callee;
8163
+ if (callee?.type !== "MemberExpression") return false;
8164
+ return callee.property?.type === "Identifier" && callee.property.name === "validateRequest";
8165
+ }
8166
+ function isRequestValidatorConstruction(n) {
8167
+ return n?.type === "NewExpression" && n.callee?.type === "Identifier" && n.callee.name === "RequestValidator";
8168
+ }
8169
+ return {
8170
+ "Program:exit"(program2) {
8171
+ const hasValidation = !!findInSubtree(program2, isValidateRequestCall) || !!findInSubtree(program2, isRequestValidatorConstruction);
8172
+ if (hasValidation) return;
8173
+ const postRoutes = [];
8174
+ findInSubtree(program2, (n) => {
8175
+ if (isPostRouteRegistration(n) && referencesRequestBody(n)) postRoutes.push(n);
8176
+ return false;
8177
+ });
8178
+ for (const route of postRoutes) {
8179
+ context.report({ node: route, messageId: "missingSignatureValidation" });
8180
+ }
8181
+ }
8182
+ };
8183
+ }
8184
+ };
8185
+ var twilioValidateWebhookSignatureRule = rule91;
8186
+
8187
+ // src/providers/twilio/rules/taskrouter-attributes-match-consumer.ts
8188
+ var rule92 = {
8189
+ meta: {
8190
+ type: "problem",
8191
+ docs: {
8192
+ description: "TaskRouter Task attributes must include every field the reservation handler reads back out",
8193
+ category: "correctness",
8194
+ rationale: "A Task is created with attributes like `{ name, type }`, and later a reservation-accepted handler does `const { from } = JSON.parse(req.body.TaskAttributes)` to look up state by `from`. If the producer never set `from` on the Task attributes, that destructure is always `undefined`, the lookup always misses, and the handler returns 404 \u2014 even though every other part of the flow worked correctly.",
8195
+ docsUrl: "https://www.twilio.com/docs/taskrouter/twiml-queue-calls",
8196
+ recommended: true
8197
+ },
8198
+ messages: {
8199
+ attributeMismatch: 'This .task() attributes object does not set "{{field}}", but a reservation handler in this codebase destructures "{{field}}" from JSON.parse(TaskAttributes) \u2014 that lookup will always be undefined.'
8200
+ }
8201
+ },
8202
+ create(context) {
8203
+ function isTaskCall(n) {
8204
+ if (n?.type !== "CallExpression") return false;
8205
+ const callee = n.callee;
8206
+ if (callee?.type !== "MemberExpression") return false;
8207
+ return callee.property?.type === "Identifier" && callee.property.name === "task";
8208
+ }
8209
+ function extractAttributeKeys(taskCallNode) {
8210
+ const arg = taskCallNode.arguments?.[0];
8211
+ if (!arg) return null;
8212
+ if (arg.type === "CallExpression" && arg.callee?.type === "MemberExpression" && arg.callee.object?.type === "Identifier" && arg.callee.object.name === "JSON" && arg.callee.property?.type === "Identifier" && arg.callee.property.name === "stringify" && arg.arguments?.[0]?.type === "ObjectExpression") {
8213
+ return objectKeys(arg.arguments[0]);
8214
+ }
8215
+ if (arg.type === "TemplateLiteral") {
8216
+ const raw = arg.quasis.map((q) => q.value?.raw ?? "").join("");
8217
+ const keys = /* @__PURE__ */ new Set();
8218
+ for (const m of raw.matchAll(/"([a-zA-Z0-9_]+)"\s*:/g)) keys.add(m[1]);
8219
+ return keys;
8220
+ }
8221
+ if (arg.type === "Literal" && typeof arg.value === "string") {
8222
+ const keys = /* @__PURE__ */ new Set();
8223
+ for (const m of arg.value.matchAll(/"([a-zA-Z0-9_]+)"\s*:/g)) keys.add(m[1]);
8224
+ return keys;
8225
+ }
8226
+ return null;
8227
+ }
8228
+ function objectKeys(objExpr) {
8229
+ const keys = /* @__PURE__ */ new Set();
8230
+ for (const p of objExpr.properties ?? []) {
8231
+ if (p.type !== "Property") continue;
8232
+ const name = p.key?.type === "Identifier" ? p.key.name : p.key?.type === "Literal" ? p.key.value : null;
8233
+ if (typeof name === "string") keys.add(name);
8234
+ }
8235
+ return keys;
8236
+ }
8237
+ function isTaskAttributesJsonParse(n) {
8238
+ if (n?.type !== "CallExpression") return false;
8239
+ const callee = n.callee;
8240
+ if (callee?.type !== "MemberExpression") return false;
8241
+ if (callee.object?.type !== "Identifier" || callee.object.name !== "JSON") return false;
8242
+ if (callee.property?.type !== "Identifier" || callee.property.name !== "parse") return false;
8243
+ const arg = n.arguments?.[0];
8244
+ if (arg?.type !== "MemberExpression") return false;
8245
+ return arg.property?.type === "Identifier" && arg.property.name === "TaskAttributes";
8246
+ }
8247
+ function findConsumedFields(program2) {
8248
+ const fields = /* @__PURE__ */ new Set();
8249
+ const declarators = [];
8250
+ collectVarDeclarators2(program2, declarators);
8251
+ for (const d of declarators) {
8252
+ if (!isTaskAttributesJsonParse(d.init)) continue;
8253
+ if (d.id?.type === "ObjectPattern") {
8254
+ for (const p of d.id.properties ?? []) {
8255
+ if (p.type !== "Property") continue;
8256
+ const name = p.key?.type === "Identifier" ? p.key.name : null;
8257
+ if (name) fields.add(name);
8258
+ }
8259
+ }
8260
+ }
8261
+ return fields;
8262
+ }
8263
+ return {
8264
+ "Program:exit"(program2) {
8265
+ const consumedFields = findConsumedFields(program2);
8266
+ if (consumedFields.size === 0) return;
8267
+ const taskCalls = [];
8268
+ findInSubtree(program2, (n) => {
8269
+ if (isTaskCall(n)) taskCalls.push(n);
8270
+ return false;
8271
+ });
8272
+ for (const taskCall of taskCalls) {
8273
+ const producedKeys = extractAttributeKeys(taskCall);
8274
+ if (!producedKeys) continue;
8275
+ for (const field of consumedFields) {
8276
+ if (!producedKeys.has(field)) {
8277
+ context.report({ node: taskCall, messageId: "attributeMismatch", data: { field } });
8278
+ }
8279
+ }
8280
+ }
8281
+ }
8282
+ };
8283
+ }
8284
+ };
8285
+ var twilioTaskrouterAttributesMatchConsumerRule = rule92;
8286
+
8287
+ // src/providers/twilio/rules/enqueue-task-json-stringify.ts
8288
+ var rule93 = {
8289
+ meta: {
8290
+ type: "problem",
8291
+ docs: {
8292
+ description: "TaskRouter .task() attributes must be built with JSON.stringify(), not raw string interpolation",
8293
+ category: "security",
8294
+ cwe: "CWE-116",
8295
+ owasp: "CWE-91 XML/JSON Injection",
8296
+ rationale: 'Hand-building the Task attributes JSON by interpolating an unvalidated request field directly into a string literal breaks the moment that field contains a `"` or `\\`. The resulting body is invalid JSON, and Twilio rejects the Enqueue with error 14221 ("Provided Attributes JSON was not valid") \u2014 turning a single unexpected character in a caller-controlled field (like a name with a quote in it) into a hard failure of the call flow.',
8297
+ docsUrl: "https://www.twilio.com/docs/api/errors/14221",
8298
+ recommended: true
8299
+ },
8300
+ messages: {
8301
+ rawJsonInterpolation: 'This .task() call builds JSON attributes via raw string interpolation instead of JSON.stringify() \u2014 a "/\\\\ character in the interpolated value produces invalid JSON and Twilio error 14221.'
8302
+ }
8303
+ },
8304
+ create(context) {
8305
+ function isTaskCall(n) {
8306
+ if (n?.type !== "CallExpression") return false;
8307
+ const callee = n.callee;
8308
+ if (callee?.type !== "MemberExpression") return false;
8309
+ return callee.property?.type === "Identifier" && callee.property.name === "task";
8310
+ }
8311
+ function hasInterpolatedExpression(templateLiteral) {
8312
+ return (templateLiteral.expressions ?? []).length > 0;
8313
+ }
8314
+ return {
8315
+ CallExpression(node) {
8316
+ if (!isTaskCall(node)) return;
8317
+ const arg = node.arguments?.[0];
8318
+ if (!arg) return;
8319
+ if (arg.type === "TemplateLiteral" && hasInterpolatedExpression(arg)) {
8320
+ context.report({ node, messageId: "rawJsonInterpolation" });
8321
+ return;
8322
+ }
8323
+ if (arg.type === "BinaryExpression" && arg.operator === "+") {
8324
+ context.report({ node, messageId: "rawJsonInterpolation" });
8325
+ }
8326
+ }
8327
+ };
8328
+ }
8329
+ };
8330
+ var twilioEnqueueTaskJsonStringifyRule = rule93;
8331
+
8332
+ // src/providers/twilio/rules/media-streams-key-by-call-sid.ts
8333
+ var rule94 = {
8334
+ meta: {
8335
+ type: "problem",
8336
+ docs: {
8337
+ description: "Media Streams session maps must be keyed by callSid, not phone number",
8338
+ category: "reliability",
8339
+ rationale: "A Map keyed on the caller's phone number breaks the moment the same number places two concurrent calls \u2014 a retry after a dropped call, two family members sharing a line, a misdial-and-redial. The second map.set() silently overwrites the first call's entry, and that first call's downstream events (reservation-accepted, outbound-leg wiring) end up pointed at the wrong session, crossing audio streams between two unrelated calls. Twilio provides a unique callSid on every start message specifically to avoid this collision.",
8340
+ docsUrl: "https://www.twilio.com/docs/voice/media-streams/websocket-messages",
8341
+ recommended: true
8342
+ },
8343
+ messages: {
8344
+ keyedByPhoneNumber: `This Map is keyed by "{{key}}" (a phone-number-like field) instead of callSid \u2014 two concurrent calls from the same number will silently overwrite each other's entry.`
8345
+ }
8346
+ },
8347
+ create(context) {
8348
+ function isFromLikeIdentifier(n) {
8349
+ if (n?.type === "Identifier" && n.name === "from") return "from";
8350
+ if (n?.type === "MemberExpression" && n.property?.type === "Identifier" && n.property.name === "from" && !n.computed) {
8351
+ return "from";
8352
+ }
8353
+ return null;
8354
+ }
8355
+ function isMapMutationCall(n, methodNames) {
8356
+ if (n?.type !== "CallExpression") return false;
8357
+ const callee = n.callee;
8358
+ if (callee?.type !== "MemberExpression") return false;
8359
+ return callee.property?.type === "Identifier" && methodNames.has(callee.property.name);
8360
+ }
8361
+ const SET_OR_GET = /* @__PURE__ */ new Set(["set", "get"]);
8362
+ return {
8363
+ "Program:exit"(program2) {
8364
+ let callSidAvailable = false;
8365
+ function scanForCallSid(n, depth = 0) {
8366
+ if (callSidAvailable || !n || typeof n !== "object" || depth > 60) return;
8367
+ if (Array.isArray(n)) {
8368
+ for (const item of n) scanForCallSid(item, depth + 1);
8369
+ return;
8370
+ }
8371
+ if (n.type === "Identifier" && n.name === "callSid") {
8372
+ callSidAvailable = true;
8373
+ return;
8374
+ }
8375
+ if (n.type === "MemberExpression" && !n.computed && n.property?.type === "Identifier" && n.property.name === "callSid") {
8376
+ callSidAvailable = true;
8377
+ return;
8378
+ }
8379
+ for (const key of Object.keys(n)) {
8380
+ if (key === "parent" || key === "loc" || key === "range") continue;
8381
+ const val = n[key];
8382
+ if (val && typeof val === "object") scanForCallSid(val, depth + 1);
8383
+ }
8384
+ }
8385
+ scanForCallSid(program2);
8386
+ if (!callSidAvailable) return;
8387
+ function walk3(n, depth = 0) {
8388
+ if (!n || typeof n !== "object" || depth > 60) return;
8389
+ if (Array.isArray(n)) {
8390
+ for (const item of n) walk3(item, depth + 1);
8391
+ return;
8392
+ }
8393
+ if (isMapMutationCall(n, SET_OR_GET)) {
8394
+ const firstArg = n.arguments?.[0];
8395
+ const key = isFromLikeIdentifier(firstArg);
8396
+ if (key) context.report({ node: n, messageId: "keyedByPhoneNumber", data: { key } });
8397
+ }
8398
+ for (const k of Object.keys(n)) {
8399
+ if (k === "parent" || k === "loc" || k === "range") continue;
8400
+ const val = n[k];
8401
+ if (val && typeof val === "object") walk3(val, depth + 1);
8402
+ }
8403
+ }
8404
+ walk3(program2);
8405
+ }
8406
+ };
8407
+ }
8408
+ };
8409
+ var twilioMediaStreamsKeyByCallSidRule = rule94;
8410
+
8411
+ // src/providers/twilio/rules/await-or-catch-rest-calls-in-event-handlers.ts
8412
+ var REST_RESOURCE_METHODS = /* @__PURE__ */ new Set(["create", "update", "remove", "fetch"]);
8413
+ var EVENT_REGISTRATION_METHODS = /* @__PURE__ */ new Set(["onStart", "onStop", "onMedia", "onConnected", "on"]);
8414
+ var rule95 = {
8415
+ meta: {
8416
+ type: "problem",
8417
+ docs: {
8418
+ description: "Twilio REST calls inside event-handler callbacks must be wrapped in try/catch",
8419
+ category: "reliability",
8420
+ rationale: 'Event-emitter style callbacks (onStart, onMessage, on("message", ...)) are typically invoked via something like `callbacks.map((cb) => cb(event))`, which discards the returned promise. If the callback is `async` and calls `await twilio.calls.create(...)` with no try/catch, any Twilio REST error (invalid number, suspended account, rate limit) becomes an unhandled promise rejection. If the process has a global `unhandledRejection` handler that calls `process.exit()` (a common pattern), one failed outbound call takes down the entire server and every other in-progress call, not just the failing one.',
8421
+ docsUrl: "https://www.twilio.com/docs/usage/webhooks/webhooks-faq",
8422
+ recommended: true
8423
+ },
8424
+ messages: {
8425
+ missingTryCatch: "This await twilio.{{resource}}.{{method}}() call is inside an event-handler callback with no surrounding try/catch \u2014 a REST API error here becomes an unhandled promise rejection that can crash the whole process."
8426
+ }
8427
+ },
8428
+ create(context) {
8429
+ function isAwaitedTwilioRestCall(n) {
8430
+ if (n?.type !== "AwaitExpression") return null;
8431
+ const call = n.argument;
8432
+ if (call?.type !== "CallExpression") return null;
8433
+ const callee = call.callee;
8434
+ if (callee?.type !== "MemberExpression") return null;
8435
+ const method = callee.property?.type === "Identifier" ? callee.property.name : null;
8436
+ if (!method || !REST_RESOURCE_METHODS.has(method)) return null;
8437
+ const obj = callee.object;
8438
+ if (obj?.type !== "MemberExpression") return null;
8439
+ const resource = obj.property?.type === "Identifier" ? obj.property.name : null;
8440
+ if (!resource) return null;
8441
+ const base = obj.object;
8442
+ const baseName = base?.type === "Identifier" ? base.name : null;
8443
+ if (!baseName || !/^(twilio\w*|client)$/i.test(baseName)) return null;
8444
+ return { resource, method };
8445
+ }
8446
+ function isEventHandlerRegistration(n) {
8447
+ if (n?.type !== "CallExpression") return false;
8448
+ const callee = n.callee;
8449
+ if (callee?.type !== "MemberExpression") return false;
8450
+ return callee.property?.type === "Identifier" && EVENT_REGISTRATION_METHODS.has(callee.property.name);
8451
+ }
8452
+ function isAsyncCallback(n) {
8453
+ return (n?.type === "ArrowFunctionExpression" || n?.type === "FunctionExpression") && n.async === true;
8454
+ }
8455
+ function posOf2(n) {
8456
+ if (typeof n?.range?.[0] === "number") return n.range[0];
8457
+ const line = n?.loc?.start?.line ?? 0;
8458
+ const column = n?.loc?.start?.column ?? 0;
8459
+ return line * 1e6 + column;
8460
+ }
8461
+ function posEnd(n) {
8462
+ if (typeof n?.range?.[1] === "number") return n.range[1];
8463
+ const line = n?.loc?.end?.line ?? n?.loc?.start?.line ?? 0;
8464
+ const column = n?.loc?.end?.column ?? n?.loc?.start?.column ?? 0;
8465
+ return line * 1e6 + column;
8466
+ }
8467
+ function isWithinTryBlock(node, root) {
8468
+ const tryRanges = [];
8469
+ function collectTryRanges(n, depth = 0) {
8470
+ if (!n || typeof n !== "object" || depth > 60) return;
8471
+ if (Array.isArray(n)) {
8472
+ for (const item of n) collectTryRanges(item, depth + 1);
8473
+ return;
8474
+ }
8475
+ if (n.type === "TryStatement" && n.block) {
8476
+ tryRanges.push([posOf2(n.block), posEnd(n.block)]);
8477
+ }
8478
+ for (const key of Object.keys(n)) {
8479
+ if (key === "parent" || key === "loc" || key === "range") continue;
8480
+ const val = n[key];
8481
+ if (val && typeof val === "object") collectTryRanges(val, depth + 1);
8482
+ }
8483
+ }
8484
+ collectTryRanges(root);
8485
+ const p = posOf2(node);
8486
+ return tryRanges.some(([start, end]) => p >= start && p <= end);
8487
+ }
8488
+ return {
8489
+ CallExpression(node) {
8490
+ if (!isEventHandlerRegistration(node)) return;
8491
+ const callback = node.arguments?.find((a) => isAsyncCallback(a));
8492
+ if (!callback) return;
8493
+ const restCalls = [];
8494
+ function collect(n, depth = 0) {
8495
+ if (!n || typeof n !== "object" || depth > 40) return;
8496
+ if (Array.isArray(n)) {
8497
+ for (const item of n) collect(item, depth + 1);
8498
+ return;
8499
+ }
8500
+ const info = isAwaitedTwilioRestCall(n);
8501
+ if (info) restCalls.push({ node: n, ...info });
8502
+ for (const key of Object.keys(n)) {
8503
+ if (key === "parent" || key === "loc" || key === "range") continue;
8504
+ const val = n[key];
8505
+ if (val && typeof val === "object") collect(val, depth + 1);
8506
+ }
8507
+ }
8508
+ collect(callback.body);
8509
+ for (const call of restCalls) {
8510
+ if (!isWithinTryBlock(call.node, callback.body)) {
8511
+ context.report({
8512
+ node: call.node,
8513
+ messageId: "missingTryCatch",
8514
+ data: { resource: call.resource, method: call.method }
8515
+ });
8516
+ }
8517
+ }
8518
+ }
8519
+ };
8520
+ }
8521
+ };
8522
+ var twilioAwaitOrCatchRestCallsInEventHandlersRule = rule95;
8523
+
8524
+ // src/providers/twilio/rules/use-twiml-builder-not-string-templates.ts
8525
+ var rule96 = {
8526
+ meta: {
8527
+ type: "suggestion",
8528
+ docs: {
8529
+ description: "TwiML responses must be built with the VoiceResponse builder, not raw XML template strings",
8530
+ category: "security",
8531
+ cwe: "CWE-91",
8532
+ rationale: "The VoiceResponse builder XML-escapes every interpolated value automatically. A raw template literal that interpolates values directly into XML attribute or text positions has no such protection \u2014 if either value's source ever changes to something attacker-influenced (e.g. a SIP header instead of a Twilio-controlled CallSid), unescaped `<`/`\"`/`&` characters can break out of the intended XML structure.",
8533
+ docsUrl: "https://www.twilio.com/docs/voice/twiml/stream",
8534
+ recommended: true
8535
+ },
8536
+ messages: {
8537
+ rawTwimlTemplate: "TwiML is built here as a raw template literal with interpolated values instead of the VoiceResponse builder \u2014 interpolated values are not XML-escaped, unlike new VoiceResponse()."
8538
+ }
8539
+ },
8540
+ create(context) {
8541
+ function looksLikeTwimlXml(raw) {
8542
+ return /<Response>|<Connect>|<Say>|<Stream\b|<Enqueue\b|<Dial\b/.test(raw);
8543
+ }
8544
+ return {
8545
+ TemplateLiteral(node) {
8546
+ if ((node.expressions ?? []).length === 0) return;
8547
+ const raw = (node.quasis ?? []).map((q) => q.value?.raw ?? "").join("");
8548
+ if (!looksLikeTwimlXml(raw)) return;
8549
+ context.report({ node, messageId: "rawTwimlTemplate" });
8550
+ }
8551
+ };
8552
+ }
8553
+ };
8554
+ var twilioUseTwimlBuilderNotStringTemplatesRule = rule96;
8555
+
8556
+ // src/providers/twilio/rules/media-streams-mark-pacing.ts
8557
+ var rule97 = {
8558
+ meta: {
8559
+ type: "suggestion",
8560
+ docs: {
8561
+ description: "Media Streams audio forwarding should use mark-based pacing to avoid buffer overflow",
8562
+ category: "reliability",
8563
+ rationale: 'Twilio buffers at most 10 minutes of audio per bidirectional Stream. If audio chunks are forwarded with no mark events and no pacing, and the sender produces media faster than real-time (or playback stalls), Twilio stops accepting further audio for that Stream and emits warning 31931 ("Media Discarded") \u2014 silently dropping audio rather than erroring loudly.',
8564
+ docsUrl: "https://www.twilio.com/docs/api/errors/31931",
8565
+ recommended: true
8566
+ },
8567
+ messages: {
8568
+ noMarkPacing: "This file forwards media payloads via send() but never passes isLast/a mark argument anywhere \u2014 sustained high-throughput forwarding risks Twilio silently discarding buffered audio (warning 31931)."
8569
+ }
8570
+ },
8571
+ create(context) {
8572
+ function isMediaSendCall(n) {
8573
+ if (n?.type !== "CallExpression") return false;
8574
+ const callee = n.callee;
8575
+ if (callee?.type !== "MemberExpression") return false;
8576
+ if (callee.property?.type !== "Identifier" || callee.property.name !== "send") return false;
8577
+ const arg = n.arguments?.[0];
8578
+ const argText = arg?.type === "ArrayExpression" && (arg.elements ?? []).some(
8579
+ (el) => el?.type === "MemberExpression" && el.property?.type === "Identifier" && el.property.name === "delta"
8580
+ ) || arg?.type === "Identifier" && /delta|payload|media/i.test(arg.name);
8581
+ return !!argText;
8582
+ }
8583
+ function hasIsLastOrMarkSignal(program2) {
8584
+ let found = false;
8585
+ function walk3(n, depth = 0) {
8586
+ if (found || !n || typeof n !== "object" || depth > 60) return;
8587
+ if (Array.isArray(n)) {
8588
+ for (const item of n) walk3(item, depth + 1);
8589
+ return;
8590
+ }
8591
+ if (typeof n.type === "string" && n.type.startsWith("TS")) return;
8592
+ if (n.type === "CallExpression") {
8593
+ const callee = n.callee;
8594
+ if (callee?.type === "MemberExpression" && callee.property?.type === "Identifier" && callee.property.name === "send") {
8595
+ const second = n.arguments?.[1];
8596
+ if (second?.type === "Literal" && second.value === true) {
8597
+ found = true;
8598
+ return;
8599
+ }
8600
+ if (second?.type === "Identifier" && /isLast/i.test(second.name)) {
8601
+ found = true;
8602
+ return;
8603
+ }
8604
+ }
8605
+ }
8606
+ if (n.type === "Identifier" && n.name === "isLast") {
8607
+ found = true;
8608
+ return;
8609
+ }
8610
+ for (const key of Object.keys(n)) {
8611
+ if (key === "parent" || key === "loc" || key === "range" || key === "typeAnnotation" || key === "returnType") continue;
8612
+ const val = n[key];
8613
+ if (val && typeof val === "object") walk3(val, depth + 1);
8614
+ }
8615
+ }
8616
+ walk3(program2);
8617
+ return found;
8618
+ }
8619
+ return {
8620
+ "Program:exit"(program2) {
8621
+ const sendCalls = [];
8622
+ findInSubtree(program2, (n) => {
8623
+ if (isMediaSendCall(n)) sendCalls.push(n);
8624
+ return false;
8625
+ });
8626
+ if (sendCalls.length === 0) return;
8627
+ if (hasIsLastOrMarkSignal(program2)) return;
8628
+ context.report({ node: sendCalls[0], messageId: "noMarkPacing" });
8629
+ }
8630
+ };
8631
+ }
8632
+ };
8633
+ var twilioMediaStreamsMarkPacingRule = rule97;
8634
+
8635
+ // src/providers/twilio/rules/validate-all-request-inputs.ts
8636
+ var rule98 = {
8637
+ meta: {
8638
+ type: "suggestion",
8639
+ docs: {
8640
+ description: "Fastify Twilio webhook routes must declare a schema for every request input they read",
8641
+ category: "correctness",
8642
+ rationale: "A route that validates `body` but not `querystring` (or declares no schema at all) lets unvalidated, possibly-undefined fields flow downstream. If a later call does `value.toString()` or similar on a field that never arrived, that throws instead of failing the request cleanly with a 400 \u2014 the failure surfaces far from the actual missing-validation root cause.",
8643
+ docsUrl: "https://www.twilio.com/docs/voice/twiml/stream",
8644
+ recommended: true
8645
+ },
8646
+ messages: {
8647
+ missingQuerystringSchema: "This route reads req.query.{{field}} but the route schema validates body without a matching querystring schema \u2014 a missing query param flows downstream as undefined instead of failing with a 400.",
8648
+ missingSchemaEntirely: "This route reads req.body.{{field}} but declares no request schema at all \u2014 req.body is untyped and unvalidated before use."
8649
+ }
8650
+ },
8651
+ create(context) {
8652
+ function getOptionsArg(routeCall) {
8653
+ return routeCall.arguments?.length === 3 ? routeCall.arguments[1] : null;
8654
+ }
8655
+ function getHandlerArg(routeCall) {
8656
+ const args = routeCall.arguments ?? [];
8657
+ return args[args.length - 1];
8658
+ }
8659
+ function hasSchemaProperty(optionsArg, propName2) {
8660
+ if (optionsArg?.type !== "ObjectExpression") return false;
8661
+ const schemaProp = (optionsArg.properties ?? []).find(
8662
+ (p) => p.type === "Property" && p.key?.type === "Identifier" && p.key.name === "schema"
8663
+ );
8664
+ if (!schemaProp || schemaProp.value?.type !== "ObjectExpression") return false;
8665
+ return (schemaProp.value.properties ?? []).some(
8666
+ (p) => p.type === "Property" && p.key?.type === "Identifier" && p.key.name === propName2
8667
+ );
8668
+ }
8669
+ function hasAnySchema(optionsArg) {
8670
+ if (optionsArg?.type !== "ObjectExpression") return false;
8671
+ return (optionsArg.properties ?? []).some(
8672
+ (p) => p.type === "Property" && p.key?.type === "Identifier" && p.key.name === "schema"
8673
+ );
8674
+ }
8675
+ function findReadFields(handlerBody, objectNames) {
8676
+ const fields = /* @__PURE__ */ new Set();
8677
+ findInSubtree(handlerBody, (n) => {
8678
+ if (n?.type !== "MemberExpression") return false;
8679
+ const obj = n.object;
8680
+ if (obj?.type !== "MemberExpression") return false;
8681
+ if (obj.object?.type !== "Identifier" || obj.object.name !== "req" && obj.object.name !== "request") return false;
8682
+ if (obj.property?.type !== "Identifier" || !objectNames.has(obj.property.name)) return false;
8683
+ const field = n.property?.type === "Identifier" ? n.property.name : null;
8684
+ if (field) fields.add(field);
8685
+ return false;
8686
+ });
8687
+ return fields;
8688
+ }
8689
+ return {
8690
+ CallExpression(node) {
8691
+ if (!isPostRouteRegistration(node)) return;
8692
+ const optionsArg = getOptionsArg(node);
8693
+ const handler = getHandlerArg(node);
8694
+ if (!handler || handler.type !== "ArrowFunctionExpression" && handler.type !== "FunctionExpression") return;
8695
+ const queryFields = findReadFields(handler.body, /* @__PURE__ */ new Set(["query"]));
8696
+ const bodyFields = findReadFields(handler.body, /* @__PURE__ */ new Set(["body"]));
8697
+ const hasBodySchema = hasSchemaProperty(optionsArg, "body");
8698
+ const hasQuerystringSchema = hasSchemaProperty(optionsArg, "querystring");
8699
+ if (queryFields.size > 0 && hasBodySchema && !hasQuerystringSchema) {
8700
+ const field = [...queryFields][0];
8701
+ context.report({ node, messageId: "missingQuerystringSchema", data: { field } });
8702
+ }
8703
+ if (bodyFields.size > 0 && !hasAnySchema(optionsArg)) {
8704
+ const field = [...bodyFields][0];
8705
+ context.report({ node, messageId: "missingSchemaEntirely", data: { field } });
8706
+ }
8707
+ }
8708
+ };
8709
+ }
8710
+ };
8711
+ var twilioValidateAllRequestInputsRule = rule98;
8712
+
8713
+ // src/providers/twilio/rules/media-streams-mark-name-string.ts
8714
+ var rule99 = {
8715
+ meta: {
8716
+ type: "suggestion",
8717
+ docs: {
8718
+ description: "Media Streams mark.name must be a string, not a number",
8719
+ category: "correctness",
8720
+ rationale: 'Twilio\'s documented Media Streams `mark` message schema and every example show `mark.name` as a string (e.g. "my label"). Setting it to a bare number \u2014 `Date.now()` is a common culprit \u2014 means JSON.stringify() serializes it as a numeric literal, not the documented string type, a latent type mismatch that surfaces once mark-based pacing actually depends on matching mark names.',
8721
+ docsUrl: "https://www.twilio.com/docs/voice/media-streams/websocket-messages",
8722
+ recommended: true
8723
+ },
8724
+ messages: {
8725
+ markNameNotString: "mark.name is set to a non-string value here \u2014 Twilio's documented schema requires mark.name to be a string (e.g. String(Date.now()))."
8726
+ }
8727
+ },
8728
+ create(context) {
8729
+ const NUMERIC_CALLS = /* @__PURE__ */ new Set(["Date.now", "performance.now", "Math.random", "Math.floor", "Math.round"]);
8730
+ function isNumericLooking(n) {
8731
+ if (!n) return false;
8732
+ if (n.type === "Literal" && typeof n.value === "number") return true;
8733
+ if (n.type === "CallExpression") {
8734
+ const callee = n.callee;
8735
+ if (callee?.type === "Identifier") return false;
8736
+ if (callee?.type === "MemberExpression" && callee.object?.type === "Identifier" && callee.property?.type === "Identifier") {
8737
+ return NUMERIC_CALLS.has(`${callee.object.name}.${callee.property.name}`);
8738
+ }
8739
+ return false;
8740
+ }
8741
+ return false;
8742
+ }
8743
+ function findMarkNameProperty(markObjExpr) {
8744
+ for (const p of markObjExpr.properties ?? []) {
8745
+ if (p.type !== "Property") continue;
8746
+ const keyName = p.key?.type === "Identifier" ? p.key.name : p.key?.type === "Literal" ? p.key.value : null;
8747
+ if (keyName === "name") return p;
8748
+ }
8749
+ return null;
8750
+ }
8751
+ return {
8752
+ Property(node) {
8753
+ const keyName = node.key?.type === "Identifier" ? node.key.name : node.key?.type === "Literal" ? node.key.value : null;
8754
+ if (keyName !== "mark") return;
8755
+ if (node.value?.type !== "ObjectExpression") return;
8756
+ const nameProp = findMarkNameProperty(node.value);
8757
+ if (!nameProp) return;
8758
+ if (!isNumericLooking(nameProp.value)) return;
8759
+ context.report({ node: nameProp, messageId: "markNameNotString" });
8760
+ }
8761
+ };
8762
+ }
8763
+ };
8764
+ var twilioMediaStreamsMarkNameStringRule = rule99;
8765
+
8766
+ // src/providers/openai-realtime/utils.ts
8767
+ function isOpenAIRealtimeUrlArg(node) {
8768
+ if (!node) return false;
8769
+ if (node.type === "Literal" && typeof node.value === "string") {
8770
+ return node.value.includes("api.openai.com/v1/realtime");
8771
+ }
8772
+ if (node.type === "TemplateLiteral") {
8773
+ return (node.quasis ?? []).some(
8774
+ (q) => typeof q?.value?.raw === "string" && q.value.raw.includes("api.openai.com/v1/realtime")
8775
+ );
8776
+ }
8777
+ return false;
8778
+ }
8779
+ function collectOpenAIRealtimeUrlVarNames(program2) {
8780
+ const names = /* @__PURE__ */ new Set();
8781
+ visit(program2);
8782
+ return names;
8783
+ function visit(node, depth = 0) {
8784
+ if (!node || typeof node !== "object" || depth > 40) return;
8785
+ if (Array.isArray(node)) {
8786
+ for (const n of node) visit(n, depth + 1);
8787
+ return;
8788
+ }
8789
+ if (node.type === "VariableDeclarator" && node.id?.type === "Identifier" && isOpenAIRealtimeUrlArg(node.init)) {
8790
+ names.add(node.id.name);
8791
+ }
8792
+ for (const key of Object.keys(node)) {
8793
+ if (key === "parent" || key === "loc" || key === "range") continue;
8794
+ const val = node[key];
8795
+ if (val && typeof val === "object") visit(val, depth + 1);
8796
+ }
8797
+ }
8798
+ }
8799
+ function isOpenAIRealtimeUrlNode(urlArgNode, urlVarNames) {
8800
+ if (isOpenAIRealtimeUrlArg(urlArgNode)) return true;
8801
+ return urlArgNode?.type === "Identifier" && urlVarNames.has(urlArgNode.name);
8802
+ }
8803
+ function isOpenAIRealtimeNewWebSocket(node, urlVarNames = /* @__PURE__ */ new Set()) {
8804
+ if (node?.type !== "NewExpression") return false;
8805
+ if (node.callee?.type !== "Identifier" || node.callee.name !== "WebSocket") return false;
8806
+ return isOpenAIRealtimeUrlNode(node.arguments?.[0], urlVarNames);
8807
+ }
8808
+ function findProperty4(objectExpression, propertyName) {
8809
+ if (objectExpression?.type !== "ObjectExpression") return null;
8810
+ for (const prop of objectExpression.properties ?? []) {
8811
+ if (prop?.type !== "Property") continue;
8812
+ const key = prop.key;
8813
+ const name = key?.type === "Identifier" ? key.name : key?.type === "Literal" ? key.value : null;
8814
+ if (name === propertyName) return prop;
8815
+ }
8816
+ return null;
8817
+ }
8818
+ function collectOpenAIRealtimeSocketVarNames(program2) {
8819
+ const urlVarNames = collectOpenAIRealtimeUrlVarNames(program2);
8820
+ const names = /* @__PURE__ */ new Set();
8821
+ visit(program2);
8822
+ return names;
8823
+ function visit(node, depth = 0) {
8824
+ if (!node || typeof node !== "object" || depth > 40) return;
8825
+ if (Array.isArray(node)) {
8826
+ for (const n of node) visit(n, depth + 1);
8827
+ return;
8828
+ }
8829
+ if (node.type === "VariableDeclarator" && node.id?.type === "Identifier" && isOpenAIRealtimeNewWebSocket(node.init, urlVarNames)) {
8830
+ names.add(node.id.name);
8831
+ }
8832
+ if (node.type === "AssignmentExpression" && node.operator === "=" && isOpenAIRealtimeNewWebSocket(node.right, urlVarNames)) {
8833
+ const left = node.left;
8834
+ if (left?.type === "Identifier") {
8835
+ names.add(left.name);
8836
+ } else if (left?.type === "MemberExpression" && left.property) {
8837
+ const propName2 = left.property.name;
8838
+ if (typeof propName2 === "string") names.add(propName2);
8839
+ }
8840
+ }
8841
+ for (const key of Object.keys(node)) {
8842
+ if (key === "parent" || key === "loc" || key === "range") continue;
8843
+ const val = node[key];
8844
+ if (val && typeof val === "object") visit(val, depth + 1);
8845
+ }
8846
+ }
8847
+ }
8848
+ function isTrackedSocketRef(objNode, socketVarNames) {
8849
+ if (objNode?.type === "Identifier") return socketVarNames.has(objNode.name);
8850
+ if (objNode?.type === "MemberExpression" && objNode.property) {
8851
+ const propName2 = objNode.property.name;
8852
+ return typeof propName2 === "string" && socketVarNames.has(propName2);
8853
+ }
8854
+ return false;
8855
+ }
8856
+ function isSocketOnCall(node, socketVarNames, eventName) {
8857
+ if (node?.type !== "CallExpression") return false;
8858
+ const callee = node.callee;
8859
+ if (callee?.type !== "MemberExpression") return false;
8860
+ const prop = callee.property;
8861
+ if (prop?.type !== "Identifier" || prop.name !== "on") return false;
8862
+ if (!isTrackedSocketRef(callee.object, socketVarNames)) return false;
8863
+ const firstArg = node.arguments?.[0];
8864
+ return firstArg?.type === "Literal" && firstArg.value === eventName;
8865
+ }
8866
+ function collectStringVarValues(program2) {
8867
+ const values = /* @__PURE__ */ new Map();
8868
+ visit(program2);
8869
+ return values;
8870
+ function visit(node, depth = 0) {
8871
+ if (!node || typeof node !== "object" || depth > 40) return;
8872
+ if (Array.isArray(node)) {
8873
+ for (const n of node) visit(n, depth + 1);
8874
+ return;
8875
+ }
8876
+ if (node.type === "VariableDeclarator" && node.id?.type === "Identifier") {
8877
+ const init = node.init;
8878
+ if (init?.type === "Literal" && typeof init.value === "string") {
8879
+ values.set(node.id.name, init.value);
8880
+ } else if (init?.type === "TemplateLiteral" && (init.expressions ?? []).length === 0) {
8881
+ values.set(node.id.name, (init.quasis ?? []).map((q) => q.value?.raw ?? "").join(""));
8882
+ }
8883
+ }
8884
+ for (const key of Object.keys(node)) {
8885
+ if (key === "parent" || key === "loc" || key === "range") continue;
8886
+ const val = node[key];
8887
+ if (val && typeof val === "object") visit(val, depth + 1);
8888
+ }
8889
+ }
8890
+ }
8891
+ function resolveStringValue(node, stringVarValues) {
8892
+ if (!node) return null;
8893
+ if (node.type === "Literal" && typeof node.value === "string") return node.value;
8894
+ if (node.type === "TemplateLiteral" && (node.expressions ?? []).length === 0) {
8895
+ return (node.quasis ?? []).map((q) => q.value?.raw ?? "").join("");
8896
+ }
8897
+ if (node.type === "Identifier" && stringVarValues.has(node.name)) {
8898
+ return stringVarValues.get(node.name) ?? null;
8899
+ }
8900
+ return null;
8901
+ }
8902
+
8903
+ // src/providers/openai-realtime/rules/migrate-beta-to-ga.ts
8904
+ var rule100 = {
8905
+ meta: {
8906
+ type: "problem",
8907
+ docs: {
8908
+ description: "OpenAI Realtime connections must not pin to the deprecated beta interface",
8909
+ category: "correctness",
8910
+ docsUrl: "https://developers.openai.com/api/docs/guides/realtime",
8911
+ rationale: "OpenAI's current Realtime guide states that beta integrations must migrate to the GA interface before new work proceeds, and explicitly calls out removing the OpenAI-Beta: realtime=v1 header as a required step. Staying on the beta header keeps a connection locked to the legacy session shape and event names with no confirmed sunset date \u2014 a ticking liability rather than an active failure.",
8912
+ recommended: true
8913
+ },
8914
+ messages: {
8915
+ betaHeaderPresent: "This OpenAI Realtime connection sends the deprecated 'OpenAI-Beta: realtime=v1' header instead of using the GA interface."
8916
+ },
8917
+ schema: []
8918
+ },
8919
+ create(context) {
8920
+ let urlVarNames = /* @__PURE__ */ new Set();
8921
+ return {
8922
+ Program(node) {
8923
+ urlVarNames = collectOpenAIRealtimeUrlVarNames(node);
8924
+ },
8925
+ NewExpression(node) {
8926
+ if (!isOpenAIRealtimeNewWebSocket(node, urlVarNames)) return;
8927
+ const optionsArg = node.arguments?.[1];
8928
+ if (optionsArg?.type !== "ObjectExpression") return;
8929
+ const headersProp = findProperty4(optionsArg, "headers");
8930
+ if (headersProp?.value?.type !== "ObjectExpression") return;
8931
+ const betaProp = findProperty4(headersProp.value, "OpenAI-Beta");
8932
+ if (!betaProp) return;
8933
+ const value = betaProp.value;
8934
+ const isRealtimeV1 = value?.type === "Literal" && typeof value.value === "string" && value.value.includes("realtime=v1");
8935
+ if (!isRealtimeV1) return;
8936
+ context.report({ node: betaProp, messageId: "betaHeaderPresent" });
8937
+ }
8938
+ };
8939
+ }
8940
+ };
8941
+ var openaiRealtimeMigrateBetaToGaRule = rule100;
8942
+
8943
+ // src/providers/openai-realtime/rules/no-log-raw-message-payloads.ts
8944
+ var LOG_METHOD_NAMES = /* @__PURE__ */ new Set(["info", "warn", "error", "log"]);
8945
+ function isLogCall(node) {
8946
+ if (node?.type !== "CallExpression") return { matches: false };
8947
+ const callee = node.callee;
8948
+ if (callee?.type !== "MemberExpression") return { matches: false };
8949
+ const prop = callee.property;
8950
+ if (prop?.type !== "Identifier" || !LOG_METHOD_NAMES.has(prop.name)) return { matches: false };
8951
+ return { matches: true, calleeProp: prop };
8952
+ }
8953
+ function referencesRawParam(argNode, paramName) {
8954
+ if (!argNode) return false;
8955
+ if (argNode.type === "Identifier" && argNode.name === paramName) return true;
8956
+ if (argNode.type === "CallExpression" && argNode.callee?.type === "MemberExpression" && argNode.callee.object?.type === "Identifier" && argNode.callee.object.name === paramName && argNode.callee.property?.type === "Identifier" && argNode.callee.property.name === "toString") {
8957
+ return true;
8958
+ }
8959
+ if (argNode.type === "TemplateLiteral") {
8960
+ return (argNode.expressions ?? []).some((expr) => referencesRawParam(expr, paramName));
8961
+ }
8962
+ return false;
8963
+ }
8964
+ function findCallExpressions(node, out, depth = 0) {
8965
+ if (!node || typeof node !== "object" || depth > 40) return;
8966
+ if (Array.isArray(node)) {
8967
+ for (const n of node) findCallExpressions(n, out, depth + 1);
8968
+ return;
8969
+ }
8970
+ if (node.type === "CallExpression") out.push(node);
8971
+ for (const key of Object.keys(node)) {
8972
+ if (key === "parent" || key === "loc" || key === "range") continue;
8973
+ const val = node[key];
8974
+ if (val && typeof val === "object") findCallExpressions(val, out, depth + 1);
8975
+ }
8976
+ }
8977
+ var rule101 = {
8978
+ meta: {
8979
+ type: "problem",
8980
+ docs: {
8981
+ description: "Raw OpenAI Realtime message payloads must not be logged verbatim",
8982
+ category: "security",
8983
+ cwe: "CWE-532",
8984
+ docsUrl: "https://developers.openai.com/api/docs/guides/realtime",
8985
+ rationale: "Every inbound Realtime WebSocket message can include response.audio.delta payloads (base64-encoded live call audio) and, once transcription is wired up, transcript text. Logging the raw message object or string verbatim writes a durable, unredacted record of live conversation content into whatever log sink the application ships to.",
8986
+ recommended: true
8987
+ },
8988
+ messages: {
8989
+ rawPayloadLogged: "A raw OpenAI Realtime message is logged verbatim here, which can include live call audio or transcript content."
8990
+ },
8991
+ schema: []
8992
+ },
8993
+ create(context) {
8994
+ let socketVarNames = /* @__PURE__ */ new Set();
8995
+ return {
8996
+ Program(node) {
8997
+ socketVarNames = collectOpenAIRealtimeSocketVarNames(node);
8998
+ },
8999
+ CallExpression(node) {
9000
+ if (socketVarNames.size === 0) return;
9001
+ if (!isSocketOnCall(node, socketVarNames, "message")) return;
9002
+ const handler = node.arguments?.[1];
9003
+ if (handler?.type !== "ArrowFunctionExpression" && handler?.type !== "FunctionExpression") return;
9004
+ const param = handler.params?.[0];
9005
+ if (param?.type !== "Identifier") return;
9006
+ const paramName = param.name;
9007
+ const calls = [];
9008
+ findCallExpressions(handler.body, calls);
9009
+ for (const call of calls) {
9010
+ const { matches } = isLogCall(call);
9011
+ if (!matches) continue;
9012
+ const hasRawArg = (call.arguments ?? []).some((arg) => referencesRawParam(arg, paramName));
9013
+ if (hasRawArg) {
9014
+ context.report({ node: call, messageId: "rawPayloadLogged" });
9015
+ }
9016
+ }
9017
+ }
9018
+ };
9019
+ }
9020
+ };
9021
+ var openaiRealtimeNoLogRawMessagePayloadsRule = rule101;
9022
+
9023
+ // src/providers/openai-realtime/rules/handle-error-server-event.ts
9024
+ function isTypeMemberExpression(node) {
9025
+ return node?.type === "MemberExpression" && node.property?.type === "Identifier" && node.property.name === "type";
9026
+ }
9027
+ function collectTypeComparisonLiterals(node, out, depth = 0) {
9028
+ if (!node || typeof node !== "object" || depth > 60) return;
9029
+ if (Array.isArray(node)) {
9030
+ for (const n of node) collectTypeComparisonLiterals(n, out, depth + 1);
9031
+ return;
9032
+ }
9033
+ if (node.type === "BinaryExpression" && (node.operator === "===" || node.operator === "==")) {
9034
+ if (isTypeMemberExpression(node.left) && node.right?.type === "Literal" && typeof node.right.value === "string") {
9035
+ out.add(node.right.value);
9036
+ }
9037
+ if (isTypeMemberExpression(node.right) && node.left?.type === "Literal" && typeof node.left.value === "string") {
9038
+ out.add(node.left.value);
9039
+ }
9040
+ }
9041
+ if (node.type === "SwitchStatement" && isTypeMemberExpression(node.discriminant)) {
9042
+ for (const c of node.cases ?? []) {
9043
+ if (c?.test?.type === "Literal" && typeof c.test.value === "string") out.add(c.test.value);
9044
+ }
9045
+ }
9046
+ for (const key of Object.keys(node)) {
9047
+ if (key === "parent" || key === "loc" || key === "range") continue;
9048
+ const val = node[key];
9049
+ if (val && typeof val === "object") collectTypeComparisonLiterals(val, out, depth + 1);
9050
+ }
9051
+ }
9052
+ var rule102 = {
9053
+ meta: {
9054
+ type: "problem",
9055
+ docs: {
9056
+ description: 'OpenAI Realtime message handlers must check for the API-level "error" server event',
9057
+ category: "reliability",
9058
+ docsUrl: "https://developers.openai.com/api/reference/resources/realtime/server-events",
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.",
9060
+ recommended: true
9061
+ },
9062
+ messages: {
9063
+ missingErrorBranch: "This Realtime message handler branches on event types but never checks for message.type === 'error'."
9064
+ },
9065
+ schema: []
9066
+ },
9067
+ create(context) {
9068
+ let socketVarNames = /* @__PURE__ */ new Set();
9069
+ return {
9070
+ Program(node) {
9071
+ socketVarNames = collectOpenAIRealtimeSocketVarNames(node);
9072
+ },
9073
+ CallExpression(node) {
9074
+ if (socketVarNames.size === 0) return;
9075
+ if (!isSocketOnCall(node, socketVarNames, "message")) return;
9076
+ const handler = node.arguments?.[1];
9077
+ if (handler?.type !== "ArrowFunctionExpression" && handler?.type !== "FunctionExpression") return;
9078
+ const literals = /* @__PURE__ */ new Set();
9079
+ collectTypeComparisonLiterals(handler.body, literals);
9080
+ if (literals.size === 0) return;
9081
+ if (!literals.has("error")) {
9082
+ context.report({ node, messageId: "missingErrorBranch" });
9083
+ }
9084
+ }
9085
+ };
9086
+ }
9087
+ };
9088
+ var openaiRealtimeHandleErrorServerEventRule = rule102;
9089
+
9090
+ // src/providers/openai-realtime/rules/reconnect-on-drop.ts
9091
+ var RECONNECT_NAME_PATTERN = /reconnect/i;
9092
+ function hasReconnectAttempt(node, depth = 0) {
9093
+ if (!node || typeof node !== "object" || depth > 60) return false;
9094
+ if (Array.isArray(node)) {
9095
+ return node.some((n) => hasReconnectAttempt(n, depth + 1));
9096
+ }
9097
+ if (node.type === "NewExpression" && node.callee?.type === "Identifier" && node.callee.name === "WebSocket") {
9098
+ return true;
9099
+ }
9100
+ if (node.type === "CallExpression") {
9101
+ const callee = node.callee;
9102
+ const calleeName = callee?.type === "Identifier" ? callee.name : callee?.type === "MemberExpression" && callee.property?.type === "Identifier" ? callee.property.name : null;
9103
+ if (typeof calleeName === "string" && RECONNECT_NAME_PATTERN.test(calleeName)) return true;
9104
+ }
9105
+ for (const key of Object.keys(node)) {
9106
+ if (key === "parent" || key === "loc" || key === "range") continue;
9107
+ const val = node[key];
9108
+ if (val && typeof val === "object" && hasReconnectAttempt(val, depth + 1)) return true;
9109
+ }
9110
+ return false;
9111
+ }
9112
+ var rule103 = {
9113
+ meta: {
9114
+ type: "problem",
9115
+ docs: {
9116
+ description: "A dropped OpenAI Realtime connection must be retried, not just logged",
9117
+ category: "reliability",
9118
+ docsUrl: "https://developers.openai.com/api/docs/guides/realtime",
9119
+ rationale: "For a multi-minute phone call, a single transient OpenAI-side disconnect that is only logged on 'close' permanently kills translation/processing for the remainder of the call in that direction \u2014 the call itself stays connected and silently degraded, with neither party getting a signal that something stopped working.",
9120
+ recommended: true
9121
+ },
9122
+ messages: {
9123
+ noReconnectAttempt: "This Realtime socket's close handler only logs and never attempts to reconnect."
9124
+ },
9125
+ schema: []
9126
+ },
9127
+ create(context) {
9128
+ let socketVarNames = /* @__PURE__ */ new Set();
9129
+ return {
9130
+ Program(node) {
9131
+ socketVarNames = collectOpenAIRealtimeSocketVarNames(node);
9132
+ },
9133
+ CallExpression(node) {
9134
+ if (socketVarNames.size === 0) return;
9135
+ if (!isSocketOnCall(node, socketVarNames, "close")) return;
9136
+ const handler = node.arguments?.[1];
9137
+ if (handler?.type !== "ArrowFunctionExpression" && handler?.type !== "FunctionExpression") return;
9138
+ if (!hasReconnectAttempt(handler.body)) {
9139
+ context.report({ node, messageId: "noReconnectAttempt" });
9140
+ }
9141
+ }
9142
+ };
9143
+ }
9144
+ };
9145
+ var openaiRealtimeReconnectOnDropRule = rule103;
9146
+
9147
+ // src/providers/openai-realtime/rules/avoid-dated-preview-snapshots.ts
9148
+ var DATED_PREVIEW_MODEL_PATTERN = /model=[^&'"`]*-preview-\d{4}-\d{2}-\d{2}/;
9149
+ var rule104 = {
9150
+ meta: {
9151
+ type: "suggestion",
9152
+ docs: {
9153
+ description: "OpenAI Realtime connections should not pin to a dated preview model snapshot",
9154
+ category: "correctness",
9155
+ docsUrl: "https://developers.openai.com/api/reference/resources/realtime/subresources/client_secrets",
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.",
9157
+ recommended: true
9158
+ },
9159
+ messages: {
9160
+ datedPreviewSnapshot: "This Realtime connection is pinned to a dated preview model snapshot instead of the GA alias."
9161
+ },
9162
+ schema: []
9163
+ },
9164
+ create(context) {
9165
+ let stringVarValues = /* @__PURE__ */ new Map();
9166
+ let urlVarNames = /* @__PURE__ */ new Set();
9167
+ return {
9168
+ Program(node) {
9169
+ stringVarValues = collectStringVarValues(node);
9170
+ urlVarNames = collectOpenAIRealtimeUrlVarNames(node);
9171
+ },
9172
+ NewExpression(node) {
9173
+ if (!isOpenAIRealtimeNewWebSocket(node, urlVarNames)) return;
9174
+ const urlArg = node.arguments?.[0];
9175
+ const urlString = resolveStringValue(urlArg, stringVarValues);
9176
+ if (!urlString) return;
9177
+ if (DATED_PREVIEW_MODEL_PATTERN.test(urlString)) {
9178
+ context.report({ node: urlArg, messageId: "datedPreviewSnapshot" });
9179
+ }
9180
+ }
9181
+ };
9182
+ }
9183
+ };
9184
+ var openaiRealtimeAvoidDatedPreviewSnapshotsRule = rule104;
9185
+
9186
+ // src/providers/openai-realtime/rules/verify-deprecated-session-fields.ts
9187
+ var rule105 = {
9188
+ meta: {
9189
+ type: "suggestion",
9190
+ docs: {
9191
+ description: "OpenAI Realtime session.update payloads should not rely on an unverified 'temperature' field",
9192
+ category: "correctness",
9193
+ docsUrl: "https://developers.openai.com/api/reference/resources/realtime/subresources/client_secrets",
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.",
9195
+ recommended: true
9196
+ },
9197
+ messages: {
9198
+ unverifiedTemperatureField: "This session.update payload sets 'temperature', a field not documented in the current GA Realtime sessions schema."
9199
+ },
9200
+ schema: []
9201
+ },
9202
+ create(context) {
9203
+ return {
9204
+ ObjectExpression(node) {
9205
+ const typeProp = findProperty4(node, "type");
9206
+ const typeValue = typeProp?.value;
9207
+ const isSessionUpdate = typeValue?.type === "Literal" && typeof typeValue.value === "string" && typeValue.value === "session.update";
9208
+ if (!isSessionUpdate) return;
9209
+ const sessionProp = findProperty4(node, "session");
9210
+ if (sessionProp?.value?.type !== "ObjectExpression") return;
9211
+ const temperatureProp = findProperty4(sessionProp.value, "temperature");
9212
+ if (!temperatureProp) return;
9213
+ context.report({ node: temperatureProp, messageId: "unverifiedTemperatureField" });
9214
+ }
9215
+ };
9216
+ }
9217
+ };
9218
+ var openaiRealtimeVerifyDeprecatedSessionFieldsRule = rule105;
9219
+
9220
+ // src/providers/openai-realtime/rules/buffer-audio-until-session-ready.ts
9221
+ var QUEUE_CALL_NAME_PATTERN = /^(push|unshift|enqueue|queue)$/i;
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;
9227
+ const isReadyStateMember = (n) => n?.type === "MemberExpression" && n.property?.type === "Identifier" && n.property.name === "readyState";
9228
+ const isOpenValue = (n) => n?.type === "MemberExpression" && n.property?.type === "Identifier" && n.property.name === "OPEN" || n?.type === "Literal" && n.value === 1;
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";
9232
+ }
9233
+ function hasQueueCall(node, depth = 0) {
9234
+ if (!node || typeof node !== "object" || depth > 40) return false;
9235
+ if (Array.isArray(node)) return node.some((n) => hasQueueCall(n, depth + 1));
9236
+ if (node.type === "CallExpression") {
9237
+ const callee = node.callee;
9238
+ const calleeName = callee?.type === "MemberExpression" && callee.property?.type === "Identifier" ? callee.property.name : callee?.type === "Identifier" ? callee.name : null;
9239
+ if (typeof calleeName === "string" && QUEUE_CALL_NAME_PATTERN.test(calleeName)) return true;
9240
+ }
9241
+ for (const key of Object.keys(node)) {
9242
+ if (key === "parent" || key === "loc" || key === "range") continue;
9243
+ const val = node[key];
9244
+ if (val && typeof val === "object" && hasQueueCall(val, depth + 1)) return true;
9245
+ }
9246
+ return false;
9247
+ }
9248
+ var rule106 = {
9249
+ meta: {
9250
+ type: "suggestion",
9251
+ docs: {
9252
+ description: "Audio sent before an OpenAI Realtime socket is open must be buffered, not dropped",
9253
+ category: "reliability",
9254
+ docsUrl: "https://developers.openai.com/api/reference/resources/realtime/client-events",
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.",
9256
+ recommended: true
9257
+ },
9258
+ messages: {
9259
+ audioDroppedNotBuffered: "Audio sent while this Realtime socket is not yet open is dropped here instead of being buffered and flushed once open."
9260
+ },
9261
+ schema: []
9262
+ },
9263
+ create(context) {
9264
+ return {
9265
+ IfStatement(node) {
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" });
9272
+ }
9273
+ };
9274
+ }
9275
+ };
9276
+ var openaiRealtimeBufferAudioUntilSessionReadyRule = rule106;
9277
+
9278
+ // src/providers/openai-realtime/rules/send-safety-identifier.ts
9279
+ var rule107 = {
9280
+ meta: {
9281
+ type: "suggestion",
9282
+ docs: {
9283
+ description: "OpenAI Realtime connections should send an OpenAI-Safety-Identifier header",
9284
+ category: "security",
9285
+ docsUrl: "https://developers.openai.com/api/docs/guides/realtime",
9286
+ rationale: "OpenAI's Realtime guidance recommends including an OpenAI-Safety-Identifier header with a stable, privacy-preserving value (e.g. a hashed user/account id) on Realtime connections, to support OpenAI's abuse/safety monitoring for voice content. A connection that omits it gives OpenAI no way to correlate abusive sessions back to an account without per-connection metadata.",
9287
+ recommended: true
9288
+ },
9289
+ messages: {
9290
+ missingSafetyIdentifier: "This OpenAI Realtime WebSocket connection does not send an OpenAI-Safety-Identifier header."
9291
+ },
9292
+ schema: []
9293
+ },
9294
+ create(context) {
9295
+ let urlVarNames = /* @__PURE__ */ new Set();
9296
+ return {
9297
+ Program(node) {
9298
+ urlVarNames = collectOpenAIRealtimeUrlVarNames(node);
9299
+ },
9300
+ NewExpression(node) {
9301
+ if (!isOpenAIRealtimeNewWebSocket(node, urlVarNames)) return;
9302
+ const optionsArg = node.arguments?.[1];
9303
+ const headersProp = optionsArg?.type === "ObjectExpression" ? findProperty4(optionsArg, "headers") : null;
9304
+ const headersObj = headersProp?.value?.type === "ObjectExpression" ? headersProp.value : null;
9305
+ const safetyIdProp = headersObj ? findProperty4(headersObj, "OpenAI-Safety-Identifier") : null;
9306
+ if (safetyIdProp) return;
9307
+ context.report({ node: headersObj ?? optionsArg ?? node, messageId: "missingSafetyIdentifier" });
9308
+ }
9309
+ };
9310
+ }
9311
+ };
9312
+ var openaiRealtimeSendSafetyIdentifierRule = rule107;
9313
+
9314
+ // src/providers/openai-realtime/rules/transcription-model-choice.ts
9315
+ var rule108 = {
9316
+ meta: {
9317
+ type: "suggestion",
9318
+ docs: {
9319
+ description: "OpenAI Realtime sessions should not configure 'whisper-1' for input transcription",
9320
+ category: "correctness",
9321
+ docsUrl: "https://developers.openai.com/api/docs/guides/realtime-transcription",
9322
+ rationale: "OpenAI's transcription guidance describes 'whisper-1' as for existing Whisper integrations that are not natively streaming, while 'gpt-realtime-whisper' is the natively-streaming option designed for realtime sessions. Configuring input_audio_transcription with 'whisper-1' is the wrong tool for a realtime session, adding latency/cost to a streaming pipeline.",
9323
+ recommended: true
9324
+ },
9325
+ messages: {
9326
+ nonStreamingTranscriptionModel: "This session's input transcription is configured with 'whisper-1', which is not natively streaming and not optimized for realtime sessions."
9327
+ },
9328
+ schema: []
9329
+ },
9330
+ create(context) {
9331
+ return {
9332
+ ObjectExpression(node) {
9333
+ const typeProp = findProperty4(node, "type");
9334
+ const typeValue = typeProp?.value;
9335
+ const isSessionUpdate = typeValue?.type === "Literal" && typeof typeValue.value === "string" && typeValue.value === "session.update";
9336
+ if (!isSessionUpdate) return;
9337
+ const sessionProp = findProperty4(node, "session");
9338
+ if (sessionProp?.value?.type !== "ObjectExpression") return;
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
+ }
9345
+ if (transcriptionProp?.value?.type !== "ObjectExpression") return;
9346
+ const modelProp = findProperty4(transcriptionProp.value, "model");
9347
+ const modelValue = modelProp?.value;
9348
+ const isWhisper1 = modelValue?.type === "Literal" && typeof modelValue.value === "string" && modelValue.value === "whisper-1";
9349
+ if (!isWhisper1) return;
9350
+ context.report({ node: modelProp, messageId: "nonStreamingTranscriptionModel" });
9351
+ }
9352
+ };
9353
+ }
9354
+ };
9355
+ var openaiRealtimeTranscriptionModelChoiceRule = rule108;
9356
+
9357
+ // src/plugin/index.ts
9358
+ var plugin = {
9359
+ meta: { name: PLUGIN_NAME, version: "0.0.1" },
9360
+ rules: {
9361
+ "resend-webhook-signature": resendWebhookSignatureRule,
9362
+ "resend-api-key-hardcoded": resendApiKeyHardcodedRule,
9363
+ "resend-api-key-in-client-bundle": resendApiKeyInClientBundleRule,
9364
+ "resend-marketing-via-batch-send": resendMarketingViaBatchSendRule,
9365
+ "resend-marketing-missing-unsubscribe": resendMarketingMissingUnsubscribeRule,
9366
+ "resend-test-domain-in-production-path": resendTestDomainInProductionPathRule,
9367
+ "resend-from-address-not-friendly-format": resendFromAddressNotFriendlyFormatRule,
9368
+ "resend-batch-size-not-enforced": resendBatchSizeNotEnforcedRule,
9369
+ "resend-missing-idempotency-key": resendMissingIdempotencyKeyRule,
9370
+ "resend-no-error-code-mapping": resendNoErrorCodeMappingRule,
9371
+ "resend-webhook-no-idempotency": resendWebhookNoIdempotencyRule,
9372
+ "resend-missing-tags": resendMissingTagsRule,
9373
+ "resend-request-id-not-logged": resendRequestIdNotLoggedRule,
9374
+ "supabase-scope-queries-by-tenant-column": supabaseScopeQueriesByTenantColumnRule,
9375
+ "supabase-validate-uuid-columns": supabaseValidateUuidColumnsRule,
9376
+ "supabase-order-by-timestamp-not-identity": supabaseOrderByTimestampNotIdentityRule,
9377
+ "supabase-consistent-input-length-limits": supabaseConsistentInputLengthLimitsRule,
9378
+ "supabase-idempotent-mutations": supabaseIdempotentMutationsRule,
9379
+ "supabase-fail-fast-env-validation": supabaseFailFastEnvValidationRule,
9380
+ "supabase-no-user-metadata-authz": supabaseNoUserMetadataAuthzRule,
9381
+ "supabase-single-without-error-check": supabaseSingleWithoutErrorCheckRule,
9382
+ "supabase-non-atomic-replace-pattern": supabaseNonAtomicReplacePatternRule,
9383
+ "supabase-unchecked-mutation-error": supabaseUncheckedMutationErrorRule,
9384
+ "supabase-realtime-missing-filter": supabaseRealtimeMissingFilterRule,
9385
+ "supabase-storage-error-not-surfaced": supabaseStorageErrorNotSurfacedRule,
9386
+ "auth0-required-audience-validation": auth0RequiredAudienceValidationRule,
9387
+ "auth0-no-account-link-without-verified-email": auth0NoAccountLinkWithoutVerifiedEmailRule,
9388
+ "auth0-dead-claim-verification-check": auth0DeadClaimVerificationCheckRule,
9389
+ "auth0-jwks-refresh-on-unknown-kid": auth0JwksRefreshOnUnknownKidRule,
9390
+ "firebase-missing-app-check": firebaseMissingAppCheckRule,
9391
+ "firebase-unhandled-auth-popup-rejection": firebaseUnhandledAuthPopupRejectionRule,
9392
+ "firebase-rtdb-list-read-for-single-item": firebaseRtdbListReadForSingleItemRule,
9393
+ "firebase-unvalidated-external-data-to-rtdb": firebaseUnvalidatedExternalDataToRtdbRule,
9394
+ "firebase-rtdb-batch-write-not-atomic": firebaseRtdbBatchWriteNotAtomicRule,
9395
+ "firebase-rtdb-listener-error-not-handled": firebaseRtdbListenerErrorNotHandledRule,
9396
+ "firebase-effect-deps-whole-user-object": firebaseEffectDepsWholeUserObjectRule,
9397
+ "firebase-rtdb-write-promise-not-handled": firebaseRtdbWritePromiseNotHandledRule,
9398
+ "firebase-firestore-rules-expired": firebaseFirestoreRulesExpiredRule,
9399
+ "firebase-id-token-cookie-flags": firebaseIdTokenCookieFlagsRule,
9400
+ "firebase-hardcoded-user-id": firebaseHardcodedUserIdRule,
9401
+ "firebase-auth-user-not-found-disclosure": firebaseAuthUserNotFoundDisclosureRule,
9402
+ "firebase-signup-password-confirm": firebaseSignupPasswordConfirmRule,
9403
+ "firebase-use-array-union-remove": firebaseUseArrayUnionRemoveRule,
9404
+ "firebase-duplicate-initialize-app": firebaseDuplicateInitializeAppRule,
9405
+ "firebase-onSnapshot-async-throw": firebaseOnSnapshotAsyncThrowRule,
9406
+ "firebase-onSnapshot-missing-error-callback": firebaseOnSnapshotMissingErrorCallbackRule,
9407
+ "firebase-firestore-document-size-guard": firebaseFirestoreDocumentSizeGuardRule,
9408
+ "firebase-use-timestamp-now": firebaseUseTimestampNowRule,
9409
+ "lovable-no-client-side-secret-fetch": lovableNoClientSideSecretFetchRule,
9410
+ "lovable-paid-flag-without-edge-function": lovablePaidFlagWithoutEdgeFunctionRule,
9411
+ "lovable-expiry-column-never-checked": lovableExpiryColumnNeverCheckedRule,
9412
+ "lovable-silent-catch-on-provider-call": lovableSilentCatchOnProviderCallRule,
9413
+ "browserbase-no-conditional-authz-on-anonymous-user": browserbaseNoConditionalAuthzOnAnonymousUserRule,
9414
+ "browserbase-no-connect-url-in-api-response": browserbaseNoConnectUrlInApiResponseRule,
9415
+ "browserbase-session-id-requires-ownership-check": browserbaseSessionIdRequiresOwnershipCheckRule,
9416
+ "browserbase-no-concurrent-shared-context": browserbaseNoConcurrentSharedContextRule,
9417
+ "browserbase-mobile-device-requires-os-setting": browserbaseMobileDeviceRequiresOsSettingRule,
9418
+ "browserbase-use-typed-exception-status-not-substring": browserbaseUseTypedExceptionStatusNotSubstringRule,
9419
+ "browserbase-release-session-on-connect-failure": browserbaseReleaseSessionOnConnectFailureRule,
9420
+ "browserbase-dont-stack-custom-retry-on-sdk-retry": browserbaseDontStackCustomRetryOnSdkRetryRule,
9421
+ "browserbase-no-overbroad-error-substring-match": browserbaseNoOverbroadErrorSubstringMatchRule,
9422
+ "browserbase-use-sdk-not-raw-requests": browserbaseUseSdkNotRawRequestsRule,
9423
+ "browserbase-centralize-request-release": browserbaseCentralizeRequestReleaseRule,
9424
+ "openai-cua-no-domain-allowlist": openaiCuaNoDomainAllowlistRule,
9425
+ "openai-cua-scroll-delta-default-zero": openaiCuaScrollDeltaDefaultZeroRule,
9426
+ "openai-cua-structured-step-metadata-not-text-json": openaiCuaStructuredStepMetadataNotTextJsonRule,
9427
+ "openai-cua-no-blind-safety-check-ack": openaiCuaNoBlindSafetyCheckAckRule,
9428
+ "openai-cua-retry-transient-turn-errors": openaiCuaRetryTransientTurnErrorsRule,
9429
+ "openai-cua-check-response-status-incomplete": openaiCuaCheckResponseStatusIncompleteRule,
9430
+ "openai-cua-set-safety-identifier": openaiCuaSetSafetyIdentifierRule,
9431
+ "tiptap-upload-validate-fn-void": tiptapUploadValidateFnVoidRule,
9432
+ "tiptap-script-src-hardcoded-api-key": tiptapScriptSrcHardcodedApiKeyRule,
9433
+ "tiptap-dynamic-script-no-sri": tiptapDynamicScriptNoSriRule,
9434
+ "tiptap-addAttributes-missing-renderHTML": tiptapAddAttributesMissingRenderHTMLRule,
9435
+ "tiptap-appendTransaction-add-to-history": tiptapAppendTransactionAddToHistoryRule,
9436
+ "tiptap-appendTransaction-full-scan": tiptapAppendTransactionFullScanRule,
9437
+ "tiptap-atom-node-wrap-in": tiptapAtomNodeWrapInRule,
9438
+ "tiptap-drop-handler-pos-precedence": tiptapDropHandlerPosPrecedenceRule,
9439
+ "tiptap-prefer-table-kit": tiptapPreferTableKitRule,
9440
+ "tiptap-tiptap-markdown-missing-node-spec": tiptapTiptapMarkdownMissingNodeSpecRule,
9441
+ "elevenlabs-validate-signed-url-response": elevenlabsValidateSignedUrlResponseRule,
9442
+ "elevenlabs-no-error-object-logging": elevenlabsNoErrorObjectLoggingRule,
9443
+ "elevenlabs-fetch-timeout-required": elevenlabsFetchTimeoutRequiredRule,
9444
+ "elevenlabs-validate-agent-id-format": elevenlabsValidateAgentIdFormatRule,
9445
+ "elevenlabs-secure-session-id-generation": elevenlabsSecureSessionIdGenerationRule,
9446
+ "elevenlabs-conversation-error-recovery": elevenlabsConversationErrorRecoveryRule,
9447
+ "elevenlabs-api-version-pinning": elevenlabsApiVersionPinningRule,
9448
+ "elevenlabs-check-http-status-before-json": elevenlabsCheckHttpStatusBeforeJsonRule,
9449
+ "elevenlabs-conversation-cleanup-on-error": elevenlabsConversationCleanupOnErrorRule,
9450
+ "elevenlabs-env-var-validation": elevenlabsEnvVarValidationRule,
9451
+ "twilio-validate-webhook-signature": twilioValidateWebhookSignatureRule,
9452
+ "twilio-taskrouter-attributes-match-consumer": twilioTaskrouterAttributesMatchConsumerRule,
9453
+ "twilio-enqueue-task-json-stringify": twilioEnqueueTaskJsonStringifyRule,
9454
+ "twilio-media-streams-key-by-call-sid": twilioMediaStreamsKeyByCallSidRule,
9455
+ "twilio-await-or-catch-rest-calls-in-event-handlers": twilioAwaitOrCatchRestCallsInEventHandlersRule,
9456
+ "twilio-use-twiml-builder-not-string-templates": twilioUseTwimlBuilderNotStringTemplatesRule,
9457
+ "twilio-media-streams-mark-pacing": twilioMediaStreamsMarkPacingRule,
9458
+ "twilio-validate-all-request-inputs": twilioValidateAllRequestInputsRule,
9459
+ "twilio-media-streams-mark-name-string": twilioMediaStreamsMarkNameStringRule,
9460
+ "openai-realtime-migrate-beta-to-ga": openaiRealtimeMigrateBetaToGaRule,
9461
+ "openai-realtime-no-log-raw-message-payloads": openaiRealtimeNoLogRawMessagePayloadsRule,
9462
+ "openai-realtime-handle-error-server-event": openaiRealtimeHandleErrorServerEventRule,
9463
+ "openai-realtime-reconnect-on-drop": openaiRealtimeReconnectOnDropRule,
9464
+ "openai-realtime-avoid-dated-preview-snapshots": openaiRealtimeAvoidDatedPreviewSnapshotsRule,
9465
+ "openai-realtime-verify-deprecated-session-fields": openaiRealtimeVerifyDeprecatedSessionFieldsRule,
9466
+ "openai-realtime-buffer-audio-until-session-ready": openaiRealtimeBufferAudioUntilSessionReadyRule,
9467
+ "openai-realtime-send-safety-identifier": openaiRealtimeSendSafetyIdentifierRule,
9468
+ "openai-realtime-transcription-model-choice": openaiRealtimeTranscriptionModelChoiceRule
9469
+ }
9470
+ };
9471
+
9472
+ // src/plugin/rule-registry.ts
9473
+ function buildRegistry() {
9474
+ const registry2 = /* @__PURE__ */ new Map();
9475
+ for (const [key, rule109] of Object.entries(plugin.rules)) {
9476
+ const docs = rule109?.meta?.docs ?? {};
9477
+ registry2.set(key, {
9478
+ category: docs.category,
9479
+ description: docs.description ?? "",
9480
+ rationale: docs.rationale ?? "",
9481
+ docsUrl: docs.docsUrl,
9482
+ cwe: docs.cwe,
9483
+ owasp: docs.owasp
9484
+ });
9485
+ }
9486
+ return registry2;
9487
+ }
9488
+ var registry = buildRegistry();
9489
+ function getRuleDocsMeta(ruleKey) {
9490
+ return registry.get(ruleKey);
9491
+ }
9492
+
9493
+ // src/reporter/snippet.ts
9494
+ function extractCodeSnippet(content, line, contextLines = 2) {
9495
+ const allLines = content.split(/\r?\n/);
9496
+ const highlighted = Math.min(Math.max(line, 1), allLines.length || 1);
9497
+ const start = Math.max(1, highlighted - contextLines);
9498
+ const end = Math.min(allLines.length, highlighted + contextLines);
9499
+ const lines = [];
9500
+ for (let n = start; n <= end; n++) {
9501
+ lines.push({ number: n, text: allLines[n - 1] ?? "" });
9502
+ }
9503
+ return { lines, highlightedLine: highlighted };
9504
+ }
9505
+
9506
+ // src/types.ts
9507
+ var SEVERITY_ORDER = {
9508
+ error: 0,
9509
+ warning: 1,
9510
+ info: 2
9511
+ };
9512
+ function scoreToSeverityLabel(score) {
9513
+ if (score >= 80) return "excellent";
9514
+ if (score >= 60) return "good";
9515
+ if (score >= 40) return "needs-work";
9516
+ return "critical";
9517
+ }
9518
+
9519
+ // src/reporter/report-builder.ts
9520
+ function computeScore(errors, warnings) {
9521
+ return Math.max(0, 100 - errors * 15 - warnings * 5);
9522
+ }
9523
+ function buildSummary(results) {
9524
+ const errors = results.filter((r) => r.severity === "error").length;
9525
+ const warnings = results.filter((r) => r.severity === "warning").length;
9526
+ const info = results.filter((r) => r.severity === "info").length;
9527
+ const score = computeScore(errors, warnings);
9528
+ return {
9529
+ score,
9530
+ severity: scoreToSeverityLabel(score),
9531
+ errors,
9532
+ warnings,
9533
+ info,
9534
+ totalIssues: results.length
9535
+ };
9536
+ }
9537
+ function sortResults(results) {
9538
+ return [...results].sort((a, b) => {
9539
+ const bySeverity = SEVERITY_ORDER[a.severity] - SEVERITY_ORDER[b.severity];
9540
+ if (bySeverity !== 0) return bySeverity;
9541
+ const byFile = a.file.localeCompare(b.file);
9542
+ if (byFile !== 0) return byFile;
9543
+ return a.line - b.line;
9544
+ });
9545
+ }
9546
+ function toFinding(result, sequence, content) {
9547
+ const docs = getRuleDocsMeta(result.ruleKey);
9548
+ return {
9549
+ id: `${result.ruleKey}-${sequence}`,
9550
+ rule: result.rule,
9551
+ category: docs?.category ?? "correctness",
9552
+ severity: result.severity,
9553
+ message: result.message,
9554
+ fix: result.fix,
9555
+ docsUrl: result.docsUrl ?? docs?.docsUrl,
9556
+ cwe: docs?.cwe,
7159
9557
  owasp: docs?.owasp,
7160
9558
  location: {
7161
9559
  file: result.file,
@@ -7245,7 +9643,17 @@ async function detectProviders(directory, filesContent) {
7245
9643
  continue;
7246
9644
  }
7247
9645
  const urls = provider.detect.urlPatterns ?? [];
7248
- if (urls.some((u) => allSources.includes(u))) {
9646
+ const matchedUrl = urls.find((u) => {
9647
+ if (!allSources.includes(u)) return false;
9648
+ const isShadowedByMoreSpecificProvider = providers.some((other) => {
9649
+ if (other === provider) return false;
9650
+ return (other.detect.urlPatterns ?? []).some(
9651
+ (otherUrl) => otherUrl !== u && otherUrl.includes(u) && allSources.includes(otherUrl)
9652
+ );
9653
+ });
9654
+ return !isShadowedByMoreSpecificProvider;
9655
+ });
9656
+ if (matchedUrl) {
7249
9657
  detected.set(provider.name, {
7250
9658
  name: provider.name,
7251
9659
  source: "url-patterns",
@@ -7259,9 +9667,10 @@ async function detectProviders(directory, filesContent) {
7259
9667
  // src/scanner.ts
7260
9668
  var SKIP_DIRS = /* @__PURE__ */ new Set(["node_modules", "dist", "build", ".next"]);
7261
9669
  var SOURCE_EXT = /\.(tsx?|jsx?)$/;
9670
+ var NPX_CMD = process.platform === "win32" ? "npx.cmd" : "npx";
7262
9671
  function runOxlint(args, cwd) {
7263
9672
  return new Promise((resolveRun) => {
7264
- const child = spawn("npx", args, { cwd });
9673
+ const child = spawn(NPX_CMD, args, { cwd });
7265
9674
  let stdout = "";
7266
9675
  let stderr = "";
7267
9676
  child.stdout?.on("data", (chunk) => {
@@ -7300,9 +9709,9 @@ function buildOxlintConfig(detectedNames) {
7300
9709
  const ruleMetaByKey = /* @__PURE__ */ new Map();
7301
9710
  for (const provider of providers) {
7302
9711
  if (!detectedNames.has(provider.name)) continue;
7303
- for (const rule83 of provider.oxlintRules) {
7304
- oxlintRules[`${PLUGIN_NAME}/${rule83.key}`] = rule83.severity === "error" || rule83.severity === void 0 ? "error" : "warn";
7305
- ruleMetaByKey.set(rule83.key, rule83);
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);
7306
9715
  }
7307
9716
  }
7308
9717
  return { oxlintRules, ruleMetaByKey };
@@ -7420,9 +9829,9 @@ import { basename as basename2 } from "path";
7420
9829
  function rationaleByRule() {
7421
9830
  const map = /* @__PURE__ */ new Map();
7422
9831
  for (const provider of providers) {
7423
- for (const rule83 of provider.oxlintRules) {
7424
- const docs = getRuleDocsMeta(rule83.key);
7425
- if (docs?.rationale) map.set(rule83.resultRule, docs.rationale);
9832
+ for (const rule109 of provider.oxlintRules) {
9833
+ const docs = getRuleDocsMeta(rule109.key);
9834
+ if (docs?.rationale) map.set(rule109.resultRule, docs.rationale);
7426
9835
  }
7427
9836
  }
7428
9837
  return map;