@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.cjs CHANGED
@@ -160,7 +160,7 @@ var resendManifest = {
160
160
  resultRule: "resend/webhook-signature-missing",
161
161
  message: "This webhook handler appears to process Resend events without verifying the webhook signature.",
162
162
  fix: "Verify incoming webhooks with Svix (Resend uses Svix signatures). Validate headers and payload before handling events.",
163
- docsUrl: "https://resend.com/docs/dashboard/webhooks/introduction#verify-webhook-signatures",
163
+ docsUrl: "https://resend.com/docs/webhooks/verify-webhooks-requests",
164
164
  severity: "error"
165
165
  },
166
166
  {
@@ -240,7 +240,7 @@ var resendManifest = {
240
240
  resultRule: "resend/reliability/webhook-no-idempotency",
241
241
  message: "Resend webhook handler does not deduplicate retried events.",
242
242
  fix: "Track processed event ids (e.g. event.data.email_id) in a store or set, since Resend retries for 24h.",
243
- docsUrl: "https://resend.com/docs/dashboard/webhooks/introduction",
243
+ docsUrl: "https://resend.com/docs/webhooks/introduction",
244
244
  severity: "warning"
245
245
  },
246
246
  {
@@ -268,7 +268,7 @@ var supabaseManifest = {
268
268
  displayName: "Supabase",
269
269
  detect: {
270
270
  packages: ["@supabase/supabase-js"],
271
- imports: ["@supabase/supabase-js"],
271
+ imports: ["@supabase/supabase-js", "@supabase/ssr"],
272
272
  urlPatterns: ["supabase.co"]
273
273
  },
274
274
  oxlintRules: [
@@ -276,9 +276,9 @@ var supabaseManifest = {
276
276
  key: "supabase-scope-queries-by-tenant-column",
277
277
  resultRule: "supabase/correctness/scope-queries-by-tenant-column",
278
278
  message: "Query selects a tenant column but never filters by it.",
279
- fix: 'Add .eq("<column>", value) (or .match()/.filter()) to scope results to the caller.',
279
+ 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.',
280
280
  docsUrl: "https://supabase.com/docs/reference/javascript/eq",
281
- severity: "error"
281
+ severity: "warning"
282
282
  },
283
283
  {
284
284
  key: "supabase-validate-uuid-columns",
@@ -307,25 +307,25 @@ var supabaseManifest = {
307
307
  {
308
308
  key: "supabase-idempotent-mutations",
309
309
  resultRule: "supabase/reliability/idempotent-mutations",
310
- message: "Insert has no idempotency/dedupe key, so a retry can create a duplicate row.",
311
- fix: 'Add a client-generated idempotency key field backed by a unique constraint, or use .upsert(..., { onConflict: "<key column>" }).',
310
+ message: "Insert payload has no unique/idempotency key field, so a retried request can create a duplicate row.",
311
+ 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>" }).',
312
312
  docsUrl: "https://supabase.com/docs/reference/javascript/upsert",
313
- severity: "warning"
313
+ severity: "info"
314
314
  },
315
315
  {
316
316
  key: "supabase-fail-fast-env-validation",
317
317
  resultRule: "supabase/reliability/fail-fast-env-validation",
318
318
  message: "createClient is called with env vars that have no presence check.",
319
- fix: "Throw a clear error (e.g. if (!url || !key) throw new Error(...)) before calling createClient.",
319
+ 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.`,
320
320
  docsUrl: "https://supabase.com/docs/reference/javascript/initializing",
321
- severity: "warning"
321
+ severity: "info"
322
322
  },
323
323
  {
324
324
  key: "supabase-no-user-metadata-authz",
325
325
  resultRule: "supabase/security/no-user-metadata-authz",
326
326
  message: "Authorization data is read from or written to user_metadata, which clients can modify.",
327
327
  fix: "Store roles in app_metadata via a trusted server path, or in an RLS-protected profiles table \u2014 never user_metadata.",
328
- docsUrl: "https://supabase.com/docs/guides/database/postgres/row-level-security",
328
+ docsUrl: "https://supabase.com/docs/guides/auth/users",
329
329
  severity: "error"
330
330
  },
331
331
  {
@@ -430,8 +430,8 @@ var firebaseManifest = {
430
430
  key: "firebase-missing-app-check",
431
431
  resultRule: "firebase/security/missing-app-check",
432
432
  message: "Firebase app is initialized with no App Check configured.",
433
- fix: "Call initializeAppCheck(app, { provider: new ReCaptchaV3Provider(SITE_KEY), isTokenAutoRefreshEnabled: true }) alongside initializeApp.",
434
- docsUrl: "https://firebase.google.com/docs/database/web/start",
433
+ 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.",
434
+ docsUrl: "https://firebase.google.com/docs/app-check/web/recaptcha-provider",
435
435
  severity: "warning"
436
436
  },
437
437
  {
@@ -454,7 +454,7 @@ var firebaseManifest = {
454
454
  key: "firebase-unvalidated-external-data-to-rtdb",
455
455
  resultRule: "firebase/correctness/unvalidated-external-data-to-rtdb",
456
456
  message: "Parsed external data is written to the Realtime Database with no validation.",
457
- fix: "Validate shape/values (e.g. regex-check date fields) before calling set()/update()/push(), and surface a parse-quality warning for skipped items.",
457
+ 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.",
458
458
  docsUrl: "https://firebase.google.com/docs/database/web/read-and-write",
459
459
  severity: "error"
460
460
  },
@@ -488,7 +488,7 @@ var firebaseManifest = {
488
488
  message: "A Realtime Database write promise is neither awaited in a try/catch nor .catch-handled.",
489
489
  fix: "Wrap state-changing writes in try/catch and surface failures; do not navigate away on a write whose result was never checked.",
490
490
  docsUrl: "https://firebase.google.com/docs/database/web/read-and-write",
491
- severity: "error"
491
+ severity: "warning"
492
492
  },
493
493
  {
494
494
  key: "firebase-firestore-rules-expired",
@@ -506,14 +506,6 @@ var firebaseManifest = {
506
506
  docsUrl: "https://firebase.google.com/docs/auth/admin/manage-cookies",
507
507
  severity: "error"
508
508
  },
509
- {
510
- key: "firebase-middleware-token-not-verified",
511
- resultRule: "firebase/security/middleware-token-not-verified",
512
- message: "Middleware reads the auth cookie but never verifies it. Any non-empty cookie value bypasses the guard.",
513
- fix: "Call adminAuth.verifySessionCookie(cookie, true) in the middleware and redirect on failure.",
514
- docsUrl: "https://firebase.google.com/docs/auth/admin/manage-cookies#verify_session_cookies_and_check_permissions",
515
- severity: "error"
516
- },
517
509
  {
518
510
  key: "firebase-hardcoded-user-id",
519
511
  resultRule: "firebase/security/hardcoded-user-id",
@@ -542,7 +534,7 @@ var firebaseManifest = {
542
534
  key: "firebase-use-array-union-remove",
543
535
  resultRule: "firebase/correctness/use-array-union-remove",
544
536
  message: "Firestore array updated with read-modify-write spread/filter instead of atomic arrayUnion/arrayRemove.",
545
- fix: "Use arrayUnion(item) or arrayRemove(item) from firebase/firestore for atomic array operations.",
537
+ 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.",
546
538
  docsUrl: "https://firebase.google.com/docs/firestore/manage-data/add-data#update_elements_in_an_array",
547
539
  severity: "warning"
548
540
  },
@@ -574,15 +566,15 @@ var firebaseManifest = {
574
566
  key: "firebase-firestore-document-size-guard",
575
567
  resultRule: "firebase/reliability/firestore-document-size-guard",
576
568
  message: "Firestore write includes editor.getJSON() without a document size check.",
577
- fix: 'Check document size before writing: if (new Blob([payload]).size > 900_000) { setSaveStatus("Document too large"); return; }',
569
+ fix: 'Check document size before writing: if (new Blob([JSON.stringify(payload)]).size > 900_000) { setSaveStatus("Document too large"); return; }',
578
570
  docsUrl: "https://firebase.google.com/docs/firestore/quotas#limits",
579
571
  severity: "warning"
580
572
  },
581
573
  {
582
574
  key: "firebase-use-timestamp-now",
583
575
  resultRule: "firebase/correctness/use-timestamp-now",
584
- message: "new Date() used for a Firestore timestamp field instead of Timestamp.now() or serverTimestamp().",
585
- fix: 'Import { Timestamp } from "firebase/firestore" and use Timestamp.now() for consistency with Firestore security rules.',
576
+ message: "new Date() used for a Firestore timestamp field instead of serverTimestamp().",
577
+ 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.',
586
578
  docsUrl: "https://firebase.google.com/docs/reference/js/firestore_.timestamp",
587
579
  severity: "info"
588
580
  }
@@ -612,7 +604,7 @@ var lovableManifest = {
612
604
  resultRule: "lovable/security/paid-flag-without-edge-function",
613
605
  message: "A paid-looking flag is set via a direct database update with no payment-provider or Edge Function call.",
614
606
  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.",
615
- docsUrl: "https://docs.lovable.dev/features/cloud",
607
+ docsUrl: "https://docs.lovable.dev/integrations/stripe",
616
608
  severity: "error"
617
609
  },
618
610
  {
@@ -620,7 +612,7 @@ var lovableManifest = {
620
612
  resultRule: "lovable/correctness/expiry-column-never-checked",
621
613
  message: "An expiry column is written but never compared against the current time anywhere in this file.",
622
614
  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.",
623
- docsUrl: "https://docs.lovable.dev/features/cloud",
615
+ docsUrl: "https://docs.lovable.dev/integrations/stripe",
624
616
  severity: "warning"
625
617
  },
626
618
  {
@@ -628,7 +620,7 @@ var lovableManifest = {
628
620
  resultRule: "lovable/correctness/silent-catch-on-provider-call",
629
621
  message: 'A catch block around an LLM provider call has no logging, so failures look like "no key configured."',
630
622
  fix: "console.error (or log to your error tracker) the failure reason \u2014 status code and error body \u2014 before falling through.",
631
- docsUrl: "https://docs.lovable.dev/features/cloud",
623
+ docsUrl: "https://docs.lovable.dev/integrations/cloud",
632
624
  severity: "warning"
633
625
  }
634
626
  ]
@@ -665,7 +657,7 @@ var browserbaseManifest = {
665
657
  resultRule: "browserbase/security/session-id-requires-ownership-check",
666
658
  message: "sessionId flows into a live-view/recording call with no ownership check.",
667
659
  fix: "Look up the owning project/org by sessionId and verify the requesting user has access before calling sessions.debug()/recording.retrieve().",
668
- docsUrl: "https://docs.browserbase.com/features/session-live-view",
660
+ docsUrl: "https://docs.browserbase.com/platform/browser/observability/session-live-view",
669
661
  severity: "warning"
670
662
  },
671
663
  {
@@ -673,7 +665,7 @@ var browserbaseManifest = {
673
665
  resultRule: "browserbase/correctness/no-concurrent-shared-context",
674
666
  message: "The same Context id is passed into every session in a concurrent Promise.all batch.",
675
667
  fix: "Serialize sessions that share a context id, or create a fresh Context per concurrent run.",
676
- docsUrl: "https://docs.browserbase.com/features/contexts",
668
+ docsUrl: "https://docs.browserbase.com/platform/browser/core-features/contexts",
677
669
  severity: "error"
678
670
  },
679
671
  {
@@ -681,7 +673,7 @@ var browserbaseManifest = {
681
673
  resultRule: "browserbase/correctness/mobile-device-requires-os-setting",
682
674
  message: 'A "mobile" device branch resizes the viewport but never sets browserSettings.os.',
683
675
  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.",
684
- docsUrl: "https://docs.browserbase.com/features/stealth-mode",
676
+ docsUrl: "https://docs.browserbase.com/platform/identity/verified-customization",
685
677
  severity: "error"
686
678
  },
687
679
  {
@@ -798,7 +790,7 @@ var openaiCuaManifest = {
798
790
  resultRule: "openai-cua/integration/set-safety-identifier",
799
791
  message: "responses.create() call has no safety_identifier (or user) parameter.",
800
792
  fix: "Thread a stable, hashed per-customer identifier through to every responses.create() call as safety_identifier.",
801
- docsUrl: "https://help.openai.com/en/articles/5428082-how-to-incorporate-a-safety-identifier",
793
+ docsUrl: "https://developers.openai.com/api/docs/guides/safety-best-practices",
802
794
  severity: "warning"
803
795
  }
804
796
  ]
@@ -835,7 +827,7 @@ var tiptapManifest = {
835
827
  resultRule: "tiptap/security/dynamic-script-no-sri",
836
828
  message: "Dynamically injected script appended without SRI integrity attribute.",
837
829
  fix: 'Add script.setAttribute("integrity", "sha384-...") before appending the script.',
838
- docsUrl: "https://developer.mozilla.org/en-US/docs/Web/Security/Subresource_Integrity",
830
+ docsUrl: "https://developer.mozilla.org/en-US/docs/Web/Security/Defenses/Subresource_Integrity",
839
831
  severity: "warning"
840
832
  },
841
833
  {
@@ -870,14 +862,6 @@ var tiptapManifest = {
870
862
  docsUrl: "https://tiptap.dev/docs/editor/extensions/custom-extensions/create-new/node",
871
863
  severity: "warning"
872
864
  },
873
- {
874
- key: "tiptap-twitter-url-regex",
875
- resultRule: "tiptap/integration/twitter-url-regex",
876
- message: "Twitter/X regex matches x.com but not twitter.com \u2014 legacy URLs silently rejected.",
877
- fix: "Update pattern to (x\\.com|twitter\\.com) to support both domains.",
878
- docsUrl: "https://tiptap.dev/docs/editor/extensions/nodes",
879
- severity: "warning"
880
- },
881
865
  {
882
866
  key: "tiptap-drop-handler-pos-precedence",
883
867
  resultRule: "tiptap/correctness/drop-handler-pos-precedence",
@@ -898,9 +882,270 @@ var tiptapManifest = {
898
882
  key: "tiptap-tiptap-markdown-missing-node-spec",
899
883
  resultRule: "tiptap/integration/tiptap-markdown-missing-node-spec",
900
884
  message: "TipTap node used with tiptap-markdown has no markdown serialization spec \u2014 content lost on export.",
901
- fix: "Add a markdown serializer to addStorage or addOptions using MarkdownNodeSpec.",
902
- docsUrl: "https://github.com/ueberdosis/tiptap-markdown",
885
+ fix: "Return a markdown serialize/parse spec (MarkdownNodeSpec) from addStorage \u2014 tiptap-markdown reads only extension.storage.markdown.",
886
+ docsUrl: "https://github.com/aguingand/tiptap-markdown",
887
+ severity: "warning"
888
+ }
889
+ ]
890
+ };
891
+
892
+ // src/providers/elevenlabs/manifest.ts
893
+ var elevenlabsManifest = {
894
+ name: "elevenlabs",
895
+ displayName: "ElevenLabs",
896
+ detect: {
897
+ packages: ["@11labs/client", "elevenlabs", "@elevenlabs/elevenlabs-js"],
898
+ imports: ["@11labs/client", "elevenlabs", "@elevenlabs/elevenlabs-js"],
899
+ urlPatterns: ["api.elevenlabs.io"]
900
+ },
901
+ oxlintRules: [
902
+ {
903
+ key: "elevenlabs-validate-signed-url-response",
904
+ resultRule: "elevenlabs/correctness/validate-signed-url-response",
905
+ message: "The signed URL response is used without validating that signed_url is present.",
906
+ fix: "Check that data.signed_url exists before using it, and throw/return an error response if it is missing.",
907
+ docsUrl: "https://elevenlabs.io/docs/eleven-agents/api-reference/conversations/get-signed-url",
908
+ severity: "error"
909
+ },
910
+ {
911
+ key: "elevenlabs-no-error-object-logging",
912
+ resultRule: "elevenlabs/security/no-error-object-logging",
913
+ message: "A catch block around an ElevenLabs API call logs the raw error object.",
914
+ fix: 'Log a sanitized message (e.g. error instanceof Error ? error.message : "Unknown error") instead of the raw error object.',
915
+ docsUrl: "https://elevenlabs.io/docs/api-reference/authentication",
916
+ severity: "error"
917
+ },
918
+ {
919
+ key: "elevenlabs-fetch-timeout-required",
920
+ resultRule: "elevenlabs/reliability/fetch-timeout-required",
921
+ message: "A fetch call to the ElevenLabs API has no abort signal/timeout.",
922
+ fix: "Pass an AbortController signal (e.g. via setTimeout(() => controller.abort(), 5000)) so a slow or unresponsive API does not hang the request indefinitely.",
923
+ docsUrl: "https://elevenlabs.io/docs/eleven-agents/api-reference/conversations/get-signed-url",
924
+ severity: "warning"
925
+ },
926
+ {
927
+ key: "elevenlabs-validate-agent-id-format",
928
+ resultRule: "elevenlabs/correctness/validate-agent-id-format",
929
+ message: "agentId is checked for existence but not validated against an expected format.",
930
+ fix: "Validate agentId against an allowed pattern (e.g. /^[a-zA-Z0-9\\-]{1,64}$/) before using it in an ElevenLabs API request.",
931
+ docsUrl: "https://elevenlabs.io/docs/eleven-agents/customization/authentication",
932
+ severity: "error"
933
+ },
934
+ {
935
+ key: "elevenlabs-secure-session-id-generation",
936
+ resultRule: "elevenlabs/security/secure-session-id-generation",
937
+ message: "A session id is generated with Math.random(), which is not cryptographically secure.",
938
+ fix: "Use crypto.getRandomValues() to generate session ids instead of Math.random().",
939
+ docsUrl: "https://elevenlabs.io/docs/eleven-api/guides/how-to/best-practices/security",
940
+ severity: "error"
941
+ },
942
+ {
943
+ key: "elevenlabs-conversation-error-recovery",
944
+ resultRule: "elevenlabs/reliability/conversation-error-recovery",
945
+ message: "A loading flag set before starting a conversation is never reset on failure.",
946
+ fix: "Reset the loading flag in the catch block (or a finally block) so a failed attempt does not leave the UI stuck loading.",
947
+ docsUrl: "https://elevenlabs.io/docs/eleven-agents/libraries/java-script",
948
+ severity: "warning"
949
+ },
950
+ {
951
+ key: "elevenlabs-api-version-pinning",
952
+ resultRule: "elevenlabs/reliability/api-version-pinning",
953
+ message: "A fetch call to the ElevenLabs API has no explicit API version header.",
954
+ fix: "Add an explicit version header (e.g. elevenlabs-version) to the request instead of relying on the URL path alone.",
955
+ docsUrl: "https://elevenlabs.io/docs/eleven-api/resources/breaking-changes-policy",
956
+ severity: "warning"
957
+ },
958
+ {
959
+ key: "elevenlabs-check-http-status-before-json",
960
+ resultRule: "elevenlabs/correctness/check-http-status-before-json",
961
+ message: "response.json() is called on an ElevenLabs API response without checking response.ok/status first.",
962
+ fix: "Check response.ok (or response.status) before calling response.json(), so an error body is not parsed as valid data.",
963
+ docsUrl: "https://elevenlabs.io/docs/eleven-api/resources/errors",
964
+ severity: "error"
965
+ },
966
+ {
967
+ key: "elevenlabs-conversation-cleanup-on-error",
968
+ resultRule: "elevenlabs/reliability/conversation-cleanup-on-error",
969
+ message: "A call to end the conversation has no surrounding try/catch.",
970
+ fix: "Wrap conversation.endSession()/endConversation() in try/catch so a rejection does not leave the conversation state in memory.",
971
+ docsUrl: "https://elevenlabs.io/docs/eleven-agents/libraries/java-script",
972
+ severity: "warning"
973
+ },
974
+ {
975
+ key: "elevenlabs-env-var-validation",
976
+ resultRule: "elevenlabs/correctness/env-var-validation",
977
+ message: "An ElevenLabs API key is only validated inside a request handler, not at module load.",
978
+ fix: "Read and validate the env var at module scope (and throw if missing) so a misconfigured deployment fails at startup.",
979
+ docsUrl: "https://elevenlabs.io/docs/api-reference/authentication",
980
+ severity: "warning"
981
+ }
982
+ ]
983
+ };
984
+
985
+ // src/providers/twilio/manifest.ts
986
+ var twilioManifest = {
987
+ name: "twilio",
988
+ displayName: "Twilio",
989
+ detect: {
990
+ packages: ["twilio"],
991
+ imports: ["twilio"],
992
+ urlPatterns: ["api.twilio.com"]
993
+ },
994
+ oxlintRules: [
995
+ {
996
+ key: "twilio-validate-webhook-signature",
997
+ resultRule: "twilio/security/validate-webhook-signature",
998
+ message: "A POST webhook route reads req.body but never validates the X-Twilio-Signature header.",
999
+ fix: "Validate the X-Twilio-Signature header with twilio.validateRequest()/RequestValidator before trusting the body.",
1000
+ docsUrl: "https://www.twilio.com/docs/usage/webhooks/webhooks-security",
1001
+ severity: "error"
1002
+ },
1003
+ {
1004
+ key: "twilio-taskrouter-attributes-match-consumer",
1005
+ resultRule: "twilio/correctness/taskrouter-attributes-match-consumer",
1006
+ message: "A .task() attributes object is missing a field that a reservation handler reads back out of TaskAttributes.",
1007
+ fix: "Add the missing field to the Task attributes object so the consumer's JSON.parse(TaskAttributes) destructure actually finds it.",
1008
+ docsUrl: "https://www.twilio.com/docs/taskrouter/twiml-queue-calls",
1009
+ severity: "error"
1010
+ },
1011
+ {
1012
+ key: "twilio-enqueue-task-json-stringify",
1013
+ resultRule: "twilio/security/enqueue-task-json-stringify",
1014
+ message: "A .task() call builds JSON attributes via raw string interpolation instead of JSON.stringify().",
1015
+ fix: "Build the Task attributes as an object and serialize with JSON.stringify() instead of interpolating values into a JSON string/template literal.",
1016
+ docsUrl: "https://www.twilio.com/docs/api/errors/14221",
1017
+ severity: "error"
1018
+ },
1019
+ {
1020
+ key: "twilio-media-streams-key-by-call-sid",
1021
+ resultRule: "twilio/reliability/media-streams-key-by-call-sid",
1022
+ message: "A Media Streams session map is keyed by a phone-number-like field instead of callSid.",
1023
+ fix: "Key the map by start.callSid instead of the caller phone number, and propagate callSid (not the number) downstream as the lookup key.",
1024
+ docsUrl: "https://www.twilio.com/docs/voice/media-streams/websocket-messages",
1025
+ severity: "error"
1026
+ },
1027
+ {
1028
+ key: "twilio-await-or-catch-rest-calls-in-event-handlers",
1029
+ resultRule: "twilio/reliability/await-or-catch-rest-calls-in-event-handlers",
1030
+ message: "A Twilio REST call inside an event-handler callback has no surrounding try/catch.",
1031
+ fix: "Wrap the REST call in try/catch inside the handler so a rejected promise does not become an unhandled rejection.",
1032
+ docsUrl: "https://www.twilio.com/docs/usage/webhooks/webhooks-faq",
1033
+ severity: "error"
1034
+ },
1035
+ {
1036
+ key: "twilio-use-twiml-builder-not-string-templates",
1037
+ resultRule: "twilio/security/use-twiml-builder-not-string-templates",
1038
+ message: "TwiML is built as a raw template literal instead of the VoiceResponse builder.",
1039
+ fix: "Build TwiML with new VoiceResponse() and its builder methods (say, connect, stream, parameter) instead of interpolating values into an XML template literal.",
1040
+ docsUrl: "https://www.twilio.com/docs/voice/twiml/stream",
1041
+ severity: "warning"
1042
+ },
1043
+ {
1044
+ key: "twilio-media-streams-mark-pacing",
1045
+ resultRule: "twilio/reliability/media-streams-mark-pacing",
1046
+ message: "Media is forwarded to a Stream with no mark-based pacing (isLast) anywhere in the file.",
1047
+ fix: "Pace sends with mark events (pass isLast: true periodically) and wait for the inbound mark acknowledgment before queuing more audio.",
1048
+ docsUrl: "https://www.twilio.com/docs/api/errors/31931",
1049
+ severity: "warning"
1050
+ },
1051
+ {
1052
+ key: "twilio-validate-all-request-inputs",
1053
+ resultRule: "twilio/correctness/validate-all-request-inputs",
1054
+ message: "A webhook route reads a request field that is not covered by its schema (or has no schema at all).",
1055
+ 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.",
1056
+ docsUrl: "https://www.twilio.com/docs/voice/twiml/stream",
1057
+ severity: "warning"
1058
+ },
1059
+ {
1060
+ key: "twilio-media-streams-mark-name-string",
1061
+ resultRule: "twilio/correctness/media-streams-mark-name-string",
1062
+ message: "mark.name is set to a non-string value (Twilio requires a string).",
1063
+ fix: "Wrap the value in String(...) (e.g. String(Date.now())) so mark.name matches Twilio's documented string schema.",
1064
+ docsUrl: "https://www.twilio.com/docs/voice/media-streams/websocket-messages",
1065
+ severity: "warning"
1066
+ }
1067
+ ]
1068
+ };
1069
+
1070
+ // src/providers/openai-realtime/manifest.ts
1071
+ var openaiRealtimeManifest = {
1072
+ name: "openai-realtime",
1073
+ displayName: "OpenAI Realtime API",
1074
+ detect: {
1075
+ urlPatterns: ["api.openai.com/v1/realtime"]
1076
+ },
1077
+ oxlintRules: [
1078
+ {
1079
+ key: "openai-realtime-migrate-beta-to-ga",
1080
+ resultRule: "openai-realtime/correctness/migrate-beta-to-ga",
1081
+ message: "This Realtime connection sends the deprecated OpenAI-Beta: realtime=v1 header.",
1082
+ 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).",
1083
+ docsUrl: "https://developers.openai.com/api/docs/guides/realtime",
1084
+ severity: "error"
1085
+ },
1086
+ {
1087
+ key: "openai-realtime-no-log-raw-message-payloads",
1088
+ resultRule: "openai-realtime/security/no-log-raw-message-payloads",
1089
+ message: "A raw OpenAI Realtime message is logged verbatim, which can include live call audio or transcript content.",
1090
+ 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.",
1091
+ docsUrl: "https://developers.openai.com/api/docs/guides/realtime",
1092
+ severity: "error"
1093
+ },
1094
+ {
1095
+ key: "openai-realtime-handle-error-server-event",
1096
+ resultRule: "openai-realtime/reliability/handle-error-server-event",
1097
+ message: 'This Realtime message handler branches on event types but never checks for the API-level "error" event.',
1098
+ fix: "Add an explicit branch for message.type === 'error' that logs the error and surfaces/fails over, instead of letting it fall through silently.",
1099
+ docsUrl: "https://developers.openai.com/api/reference/resources/realtime/server-events",
1100
+ severity: "error"
1101
+ },
1102
+ {
1103
+ key: "openai-realtime-reconnect-on-drop",
1104
+ resultRule: "openai-realtime/reliability/reconnect-on-drop",
1105
+ message: "This Realtime socket's close handler only logs and never attempts to reconnect.",
1106
+ fix: "On close, attempt a bounded number of reconnects with a fresh session.update, and proactively end/flag the call if reconnection ultimately fails.",
1107
+ docsUrl: "https://developers.openai.com/api/docs/guides/realtime",
1108
+ severity: "error"
1109
+ },
1110
+ {
1111
+ key: "openai-realtime-avoid-dated-preview-snapshots",
1112
+ resultRule: "openai-realtime/correctness/avoid-dated-preview-snapshots",
1113
+ message: "The Realtime connection is pinned to a dated preview model snapshot instead of the GA alias.",
1114
+ fix: "Use the GA model id (e.g. 'gpt-realtime') or a current dated snapshot tracked against OpenAI's deprecation notices.",
1115
+ docsUrl: "https://developers.openai.com/api/reference/resources/realtime/subresources/client_secrets",
903
1116
  severity: "warning"
1117
+ },
1118
+ {
1119
+ key: "openai-realtime-verify-deprecated-session-fields",
1120
+ resultRule: "openai-realtime/correctness/verify-deprecated-session-fields",
1121
+ message: "The session config sets 'temperature', a field not documented in the current GA Realtime sessions schema.",
1122
+ 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.",
1123
+ docsUrl: "https://developers.openai.com/api/reference/resources/realtime/subresources/client_secrets",
1124
+ severity: "warning"
1125
+ },
1126
+ {
1127
+ key: "openai-realtime-buffer-audio-until-session-ready",
1128
+ resultRule: "openai-realtime/reliability/buffer-audio-until-session-ready",
1129
+ message: "Audio sent before the Realtime socket reaches the open state is dropped instead of buffered.",
1130
+ fix: "Queue outbound input_audio_buffer.append messages until the open event fires, then flush them in order.",
1131
+ docsUrl: "https://developers.openai.com/api/reference/resources/realtime/client-events",
1132
+ severity: "warning"
1133
+ },
1134
+ {
1135
+ key: "openai-realtime-send-safety-identifier",
1136
+ resultRule: "openai-realtime/security/send-safety-identifier",
1137
+ message: "This Realtime WebSocket connection does not send an OpenAI-Safety-Identifier header.",
1138
+ 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.",
1139
+ docsUrl: "https://developers.openai.com/api/docs/guides/realtime",
1140
+ severity: "info"
1141
+ },
1142
+ {
1143
+ key: "openai-realtime-transcription-model-choice",
1144
+ resultRule: "openai-realtime/correctness/transcription-model-choice",
1145
+ message: "The session's input transcription is configured with 'whisper-1', which is not natively streaming and not optimized for realtime sessions.",
1146
+ fix: "Switch to 'gpt-realtime-whisper' if transcription output is consumed, or drop input_audio_transcription entirely if it isn't.",
1147
+ docsUrl: "https://developers.openai.com/api/docs/guides/realtime-transcription",
1148
+ severity: "info"
904
1149
  }
905
1150
  ]
906
1151
  };
@@ -914,7 +1159,10 @@ var providers = [
914
1159
  lovableManifest,
915
1160
  browserbaseManifest,
916
1161
  openaiCuaManifest,
917
- tiptapManifest
1162
+ tiptapManifest,
1163
+ elevenlabsManifest,
1164
+ twilioManifest,
1165
+ openaiRealtimeManifest
918
1166
  ];
919
1167
 
920
1168
  // src/reporter/animate.ts
@@ -987,7 +1235,7 @@ var rule = {
987
1235
  cwe: "CWE-345",
988
1236
  owasp: "API2:2023 Broken Authentication",
989
1237
  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.",
990
- docsUrl: "https://resend.com/docs/dashboard/webhooks/introduction#verify-webhook-signatures",
1238
+ docsUrl: "https://resend.com/docs/webhooks/verify-webhooks-requests",
991
1239
  recommended: true
992
1240
  },
993
1241
  messages: {
@@ -1064,6 +1312,15 @@ var rule = {
1064
1312
  const obj = callee.object;
1065
1313
  return obj?.type === "Identifier" && (obj.name === "req" || obj.name === "request");
1066
1314
  }
1315
+ function isReqTextCall(n) {
1316
+ if (n?.type !== "CallExpression") return false;
1317
+ const callee = n.callee;
1318
+ if (callee?.type !== "MemberExpression") return false;
1319
+ const prop = callee.property;
1320
+ if (prop?.type !== "Identifier" || prop.name !== "text") return false;
1321
+ const obj = callee.object;
1322
+ return obj?.type === "Identifier" && (obj.name === "req" || obj.name === "request");
1323
+ }
1067
1324
  function isBodyMember(n) {
1068
1325
  if (n?.type !== "MemberExpression") return false;
1069
1326
  const prop = n.property;
@@ -1088,6 +1345,17 @@ var rule = {
1088
1345
  const obj = callee.object;
1089
1346
  return obj?.type === "Identifier" && svixImports.has(obj.name);
1090
1347
  }
1348
+ function isResendWebhooksVerifyCall(n) {
1349
+ if (n?.type !== "CallExpression") return false;
1350
+ const callee = n.callee;
1351
+ if (callee?.type !== "MemberExpression") return false;
1352
+ const prop = callee.property;
1353
+ if (prop?.type !== "Identifier" || prop.name !== "verify") return false;
1354
+ const obj = callee.object;
1355
+ if (obj?.type !== "MemberExpression") return false;
1356
+ const webhooksProp = obj.property;
1357
+ return webhooksProp?.type === "Identifier" && webhooksProp.name === "webhooks";
1358
+ }
1091
1359
  function recordFirst(posKey, handler, pos) {
1092
1360
  const existing = handler[posKey];
1093
1361
  if (!existing) {
@@ -1102,6 +1370,11 @@ var rule = {
1102
1370
  ImportDeclaration(node) {
1103
1371
  const importSource = node?.source?.value;
1104
1372
  if (importSource === "resend") importsResend = true;
1373
+ for (const s of node.specifiers ?? []) {
1374
+ if ((s?.type === "ImportSpecifier" || s?.type === "ImportDefaultSpecifier") && s.local?.type === "Identifier" && /^resend$/i.test(s.local.name)) {
1375
+ importsResend = true;
1376
+ }
1377
+ }
1105
1378
  if (importSource === "svix") {
1106
1379
  for (const s of node.specifiers ?? []) {
1107
1380
  if (s?.type === "ImportSpecifier" && s.local?.type === "Identifier") {
@@ -1123,8 +1396,10 @@ var rule = {
1123
1396
  for (const handler of postHandlers) {
1124
1397
  if (!within(handler, node)) continue;
1125
1398
  if (isReqJsonCall(node)) recordFirst("firstBodyPos", handler, pos);
1399
+ if (isReqTextCall(node)) recordFirst("firstRawBodyPos", handler, pos);
1126
1400
  if (isSvixVerifyCall(node)) recordFirst("firstVerifyPos", handler, pos);
1127
1401
  if (isCryptoCreateHmacCall(node)) recordFirst("firstVerifyPos", handler, pos);
1402
+ if (isResendWebhooksVerifyCall(node)) recordFirst("firstVerifyPos", handler, pos);
1128
1403
  }
1129
1404
  },
1130
1405
  MemberExpression(node) {
@@ -1139,6 +1414,8 @@ var rule = {
1139
1414
  "Program:exit"() {
1140
1415
  if (!importsResend) return;
1141
1416
  for (const handler of postHandlers) {
1417
+ const consumesRequest = handler.firstBodyPos || handler.firstRawBodyPos;
1418
+ if (!consumesRequest) continue;
1142
1419
  if (!handler.firstVerifyPos) {
1143
1420
  context.report({ node: handler.node, messageId: "missingVerification" });
1144
1421
  continue;
@@ -1710,7 +1987,7 @@ var rule11 = {
1710
1987
  description: "Resend webhook handlers should deduplicate retried events",
1711
1988
  category: "reliability",
1712
1989
  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.",
1713
- docsUrl: "https://resend.com/docs/dashboard/webhooks/introduction",
1990
+ docsUrl: "https://resend.com/docs/webhooks/introduction",
1714
1991
  recommended: true
1715
1992
  },
1716
1993
  messages: {
@@ -2012,12 +2289,12 @@ var rule14 = {
2012
2289
  docs: {
2013
2290
  description: "Supabase queries that select a tenant column must filter by it",
2014
2291
  category: "correctness",
2015
- 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.",
2292
+ 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.",
2016
2293
  docsUrl: "https://supabase.com/docs/reference/javascript/eq",
2017
2294
  recommended: true
2018
2295
  },
2019
2296
  messages: {
2020
- missingTenantFilter: 'This query selects "{{column}}" but never filters by it. Add .eq("{{column}}", ...) (or .match()/.filter()) to scope results to the caller.'
2297
+ 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.`
2021
2298
  },
2022
2299
  schema: []
2023
2300
  },
@@ -2289,7 +2566,8 @@ function objectHasIdempotencyKey(objectExpression) {
2289
2566
  return (objectExpression.properties ?? []).some((p) => {
2290
2567
  if (p?.type !== "Property") return false;
2291
2568
  const name = p.key?.type === "Identifier" ? p.key.name : p.key?.type === "Literal" ? p.key.value : void 0;
2292
- return typeof name === "string" && /idempot|dedupe/i.test(name);
2569
+ if (typeof name !== "string") return false;
2570
+ return /idempot|dedupe/i.test(name) || /^(id|uuid)$/i.test(name) || /_(key|uuid)$/i.test(name);
2293
2571
  });
2294
2572
  }
2295
2573
  function insertPayloadHasIdempotencyKey(arg) {
@@ -2305,12 +2583,12 @@ var rule18 = {
2305
2583
  docs: {
2306
2584
  description: "Supabase insert calls should be retry-safe via an idempotency key",
2307
2585
  category: "reliability",
2308
- 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>" }).',
2586
+ 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.',
2309
2587
  docsUrl: "https://supabase.com/docs/reference/javascript/upsert",
2310
2588
  recommended: true
2311
2589
  },
2312
2590
  messages: {
2313
- 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>" }).'
2591
+ 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>" }).'
2314
2592
  },
2315
2593
  schema: []
2316
2594
  },
@@ -2359,7 +2637,7 @@ var rule19 = {
2359
2637
  docs: {
2360
2638
  description: "createClient must fail fast when required env vars are missing",
2361
2639
  category: "reliability",
2362
- 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.",
2640
+ 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.`,
2363
2641
  docsUrl: "https://supabase.com/docs/reference/javascript/initializing",
2364
2642
  recommended: true
2365
2643
  },
@@ -2369,7 +2647,11 @@ var rule19 = {
2369
2647
  schema: []
2370
2648
  },
2371
2649
  create(context) {
2372
- let createClientLocalName;
2650
+ const FACTORY_IMPORTS = {
2651
+ "@supabase/supabase-js": ["createClient"],
2652
+ "@supabase/ssr": ["createBrowserClient", "createServerClient"]
2653
+ };
2654
+ const factoryLocalNames = /* @__PURE__ */ new Set();
2373
2655
  const envVarOfVariable = /* @__PURE__ */ new Map();
2374
2656
  const validatedVarNames = /* @__PURE__ */ new Set();
2375
2657
  const validatedEnvNames = /* @__PURE__ */ new Set();
@@ -2403,10 +2685,11 @@ var rule19 = {
2403
2685
  }
2404
2686
  return {
2405
2687
  ImportDeclaration(node) {
2406
- if (node.source?.value !== "@supabase/supabase-js") return;
2688
+ const factories = FACTORY_IMPORTS[node.source?.value];
2689
+ if (!factories) return;
2407
2690
  for (const s of node.specifiers ?? []) {
2408
- if (s?.type === "ImportSpecifier" && s.imported?.type === "Identifier" && s.imported.name === "createClient" && s.local?.type === "Identifier") {
2409
- createClientLocalName = s.local.name;
2691
+ if (s?.type === "ImportSpecifier" && s.imported?.type === "Identifier" && factories.includes(s.imported.name) && s.local?.type === "Identifier") {
2692
+ factoryLocalNames.add(s.local.name);
2410
2693
  }
2411
2694
  }
2412
2695
  },
@@ -2420,8 +2703,8 @@ var rule19 = {
2420
2703
  collectGuardTargets(node.test);
2421
2704
  },
2422
2705
  CallExpression(node) {
2423
- if (!createClientLocalName) return;
2424
- if (node.callee?.type !== "Identifier" || node.callee.name !== createClientLocalName) return;
2706
+ if (factoryLocalNames.size === 0) return;
2707
+ if (node.callee?.type !== "Identifier" || !factoryLocalNames.has(node.callee.name)) return;
2425
2708
  const missing = [];
2426
2709
  for (const rawArg of node.arguments ?? []) {
2427
2710
  const arg = unwrapNonNull(rawArg);
@@ -2489,8 +2772,8 @@ var rule20 = {
2489
2772
  category: "security",
2490
2773
  cwe: "CWE-285",
2491
2774
  owasp: "A01:2021 Broken Access Control",
2492
- 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.",
2493
- docsUrl: "https://supabase.com/docs/guides/database/postgres/row-level-security",
2775
+ 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.',
2776
+ docsUrl: "https://supabase.com/docs/guides/auth/users",
2494
2777
  recommended: true
2495
2778
  },
2496
2779
  messages: {
@@ -2526,7 +2809,7 @@ var rule21 = {
2526
2809
  docs: {
2527
2810
  description: "Supabase .single() calls must inspect the returned error field",
2528
2811
  category: "correctness",
2529
- 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.",
2812
+ 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.",
2530
2813
  docsUrl: "https://supabase.com/docs/reference/javascript/single",
2531
2814
  recommended: true
2532
2815
  },
@@ -2536,14 +2819,29 @@ var rule21 = {
2536
2819
  schema: []
2537
2820
  },
2538
2821
  create(context) {
2822
+ const deferredBindings = [];
2823
+ const errorReadNames = /* @__PURE__ */ new Set();
2539
2824
  function checkAwaitBinding(node, pattern, awaitExpr) {
2540
2825
  if (!isSingleSupabaseQuery(awaitExpr.argument)) return;
2826
+ if (chainHasMethod(awaitExpr.argument, "throwOnError")) return;
2827
+ if (pattern?.type === "Identifier") {
2828
+ deferredBindings.push({ node, name: pattern.name });
2829
+ return;
2830
+ }
2541
2831
  const names = destructuredNames(pattern);
2542
2832
  if (names.has("error")) return;
2543
2833
  context.report({ node, messageId: "missingErrorCheck" });
2544
2834
  }
2545
2835
  return {
2836
+ MemberExpression(node) {
2837
+ if (!node.computed && node.object?.type === "Identifier" && node.property?.type === "Identifier" && node.property.name === "error") {
2838
+ errorReadNames.add(node.object.name);
2839
+ }
2840
+ },
2546
2841
  VariableDeclarator(node) {
2842
+ if (node.init?.type === "Identifier" && node.id?.type === "ObjectPattern") {
2843
+ if (destructuredNames(node.id).has("error")) errorReadNames.add(node.init.name);
2844
+ }
2547
2845
  if (node.init?.type !== "AwaitExpression") return;
2548
2846
  checkAwaitBinding(node, node.id, node.init);
2549
2847
  },
@@ -2555,9 +2853,17 @@ var rule21 = {
2555
2853
  const expr = node.expression;
2556
2854
  if (expr?.type !== "AwaitExpression") return;
2557
2855
  if (!isSingleSupabaseQuery(expr.argument)) return;
2856
+ if (chainHasMethod(expr.argument, "throwOnError")) return;
2558
2857
  if (memberPropName(expr.argument) === "single") {
2559
2858
  context.report({ node, messageId: "missingErrorCheck" });
2560
2859
  }
2860
+ },
2861
+ "Program:exit"() {
2862
+ for (const { node, name } of deferredBindings) {
2863
+ if (!errorReadNames.has(name)) {
2864
+ context.report({ node, messageId: "missingErrorCheck" });
2865
+ }
2866
+ }
2561
2867
  }
2562
2868
  };
2563
2869
  }
@@ -2666,7 +2972,7 @@ var rule23 = {
2666
2972
  docs: {
2667
2973
  description: "Supabase mutations must check the returned error field",
2668
2974
  category: "correctness",
2669
- 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.",
2975
+ 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.",
2670
2976
  docsUrl: "https://supabase.com/docs/reference/javascript/insert",
2671
2977
  recommended: true
2672
2978
  },
@@ -2676,30 +2982,52 @@ var rule23 = {
2676
2982
  schema: []
2677
2983
  },
2678
2984
  create(context) {
2985
+ const deferredBindings = [];
2986
+ const errorReadNames = /* @__PURE__ */ new Set();
2679
2987
  function checkMutationAwait(node, pattern, awaitExpr) {
2680
2988
  if (!isSupabaseMutationCall(awaitExpr.argument)) return;
2989
+ if (chainHasMethod(awaitExpr.argument, "throwOnError")) return;
2681
2990
  if (!pattern) {
2682
2991
  context.report({ node, messageId: "uncheckedMutation" });
2683
2992
  return;
2684
2993
  }
2994
+ if (pattern.type === "Identifier") {
2995
+ deferredBindings.push({ node, name: pattern.name });
2996
+ return;
2997
+ }
2685
2998
  const names = destructuredNames(pattern);
2686
2999
  if (!names.has("error")) {
2687
3000
  context.report({ node, messageId: "uncheckedMutation" });
2688
3001
  }
2689
3002
  }
2690
3003
  return {
3004
+ MemberExpression(node) {
3005
+ if (!node.computed && node.object?.type === "Identifier" && node.property?.type === "Identifier" && node.property.name === "error") {
3006
+ errorReadNames.add(node.object.name);
3007
+ }
3008
+ },
2691
3009
  ExpressionStatement(node) {
2692
3010
  const expr = node.expression;
2693
3011
  if (expr?.type !== "AwaitExpression") return;
2694
3012
  checkMutationAwait(node, void 0, expr);
2695
3013
  },
2696
3014
  VariableDeclarator(node) {
3015
+ if (node.init?.type === "Identifier" && node.id?.type === "ObjectPattern") {
3016
+ if (destructuredNames(node.id).has("error")) errorReadNames.add(node.init.name);
3017
+ }
2697
3018
  if (node.init?.type !== "AwaitExpression") return;
2698
3019
  checkMutationAwait(node, node.id, node.init);
2699
3020
  },
2700
3021
  AssignmentExpression(node) {
2701
3022
  if (node.right?.type !== "AwaitExpression") return;
2702
3023
  checkMutationAwait(node, node.left, node.right);
3024
+ },
3025
+ "Program:exit"() {
3026
+ for (const { node, name } of deferredBindings) {
3027
+ if (!errorReadNames.has(name)) {
3028
+ context.report({ node, messageId: "uncheckedMutation" });
3029
+ }
3030
+ }
2703
3031
  }
2704
3032
  };
2705
3033
  }
@@ -2749,11 +3077,20 @@ function isStorageUploadCall(node) {
2749
3077
  let current = node;
2750
3078
  let sawStorage = false;
2751
3079
  let sawUpload = false;
2752
- while (current?.type === "CallExpression") {
2753
- const prop = memberPropName(current);
2754
- if (prop === "storage") sawStorage = true;
2755
- if (prop === "upload") sawUpload = true;
2756
- current = chainObjectCall(current);
3080
+ while (current) {
3081
+ if (current.type === "CallExpression") {
3082
+ const prop = memberPropName(current);
3083
+ if (prop === "storage") sawStorage = true;
3084
+ if (prop === "upload") sawUpload = true;
3085
+ current = current.callee?.object;
3086
+ } else if (current.type === "MemberExpression") {
3087
+ const p = current.property;
3088
+ const name = !current.computed && p?.type === "Identifier" ? p.name : p?.type === "Literal" && typeof p.value === "string" ? p.value : void 0;
3089
+ if (name === "storage") sawStorage = true;
3090
+ current = current.object;
3091
+ } else {
3092
+ break;
3093
+ }
2757
3094
  }
2758
3095
  return sawStorage && sawUpload;
2759
3096
  }
@@ -3334,8 +3671,8 @@ var rule30 = {
3334
3671
  category: "security",
3335
3672
  cwe: "CWE-285",
3336
3673
  owasp: "A04:2021 Insecure Design",
3337
- 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.",
3338
- docsUrl: "https://firebase.google.com/docs/database/web/start",
3674
+ 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.",
3675
+ docsUrl: "https://firebase.google.com/docs/app-check/web/recaptcha-provider",
3339
3676
  recommended: true
3340
3677
  },
3341
3678
  messages: {
@@ -3509,7 +3846,13 @@ function isValidationLikeCall(node) {
3509
3846
  const callee = node.callee;
3510
3847
  const name = callee?.type === "Identifier" ? callee.name : callee?.type === "MemberExpression" && !callee.computed && callee.property?.type === "Identifier" ? callee.property.name : void 0;
3511
3848
  if (!name) return false;
3512
- return /valid|^test$|check|assert|schema/i.test(name);
3849
+ if (/valid|^test$|check|assert|schema/i.test(name)) return true;
3850
+ if (callee?.type === "MemberExpression") {
3851
+ if (/^(safeParse|safeParseAsync|parseAsync)$/.test(name)) return true;
3852
+ const objName = callee.object?.type === "Identifier" ? callee.object.name : void 0;
3853
+ if (name === "parse" && objName !== "JSON") return true;
3854
+ }
3855
+ return false;
3513
3856
  }
3514
3857
  var rule33 = {
3515
3858
  meta: {
@@ -3698,18 +4041,24 @@ var firebaseRtdbListenerErrorNotHandledRule = rule35;
3698
4041
  function isUseEffectCall(node) {
3699
4042
  return node?.type === "CallExpression" && node.callee?.type === "Identifier" && node.callee.name === "useEffect";
3700
4043
  }
4044
+ function isNonUidMemberAccess(node, objName) {
4045
+ if (node?.type !== "MemberExpression") return false;
4046
+ if (node.object?.type !== "Identifier" || node.object.name !== objName) return false;
4047
+ if (node.computed) return true;
4048
+ return node.property?.type === "Identifier" && node.property.name !== "uid";
4049
+ }
3701
4050
  var rule36 = {
3702
4051
  meta: {
3703
4052
  type: "suggestion",
3704
4053
  docs: {
3705
4054
  description: "useEffect should depend on user?.uid, not the whole user object",
3706
4055
  category: "reliability",
3707
- 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.",
4056
+ 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.",
3708
4057
  docsUrl: "https://firebase.google.com/docs/reference/js/auth.md#onauthstatechanged",
3709
4058
  recommended: true
3710
4059
  },
3711
4060
  messages: {
3712
- 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."
4061
+ 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."
3713
4062
  },
3714
4063
  schema: []
3715
4064
  },
@@ -3722,9 +4071,9 @@ var rule36 = {
3722
4071
  for (const el of depsArg.elements ?? []) {
3723
4072
  if (el?.type !== "Identifier") continue;
3724
4073
  const name = el.name;
3725
- if (someDescendant(callback, (n) => isUidMemberAccess(n, name))) {
3726
- context.report({ node, messageId: "wholeUserObjectDep", data: { name } });
3727
- }
4074
+ if (!someDescendant(callback, (n) => isUidMemberAccess(n, name))) continue;
4075
+ if (someDescendant(callback, (n) => isNonUidMemberAccess(n, name))) continue;
4076
+ context.report({ node, messageId: "wholeUserObjectDep", data: { name } });
3728
4077
  }
3729
4078
  }
3730
4079
  };
@@ -3820,13 +4169,13 @@ var rule38 = {
3820
4169
  schema: []
3821
4170
  },
3822
4171
  create(context) {
3823
- const EXPIRED_YEAR = 2025;
3824
4172
  function checkStringForExpiredDate(value, reportNode) {
3825
- const re = /timestamp\.date\(\s*(\d{4})\s*,/g;
4173
+ const re = /timestamp\.date\(\s*(\d{4})\s*,\s*(\d{1,2})\s*,\s*(\d{1,2})\s*\)/g;
3826
4174
  let match;
3827
4175
  while ((match = re.exec(value)) !== null) {
3828
- const year = parseInt(match[1], 10);
3829
- if (year <= EXPIRED_YEAR) {
4176
+ const [, year, month, day] = match;
4177
+ const expiry = new Date(parseInt(year, 10), parseInt(month, 10) - 1, parseInt(day, 10));
4178
+ if (expiry.getTime() <= Date.now()) {
3830
4179
  context.report({ node: reportNode, messageId: "firestoreRulesExpired" });
3831
4180
  return;
3832
4181
  }
@@ -3858,6 +4207,18 @@ function findProp(obj, name) {
3858
4207
  (p) => p?.type === "Property" && (p.key?.type === "Identifier" && p.key.name === name || p.key?.type === "Literal" && p.key.value === name)
3859
4208
  );
3860
4209
  }
4210
+ var TOKEN_NAME = /token|session/i;
4211
+ function isTokenLiteral(node) {
4212
+ return node?.type === "Literal" && typeof node.value === "string" && TOKEN_NAME.test(node.value);
4213
+ }
4214
+ function isCookieReceiver(obj) {
4215
+ if (obj?.type === "Identifier") return /cookie/i.test(obj.name);
4216
+ if (obj?.type === "CallExpression" && obj.callee?.type === "Identifier") return /^cookies$/i.test(obj.callee.name);
4217
+ if (obj?.type === "MemberExpression" && !obj.computed && obj.property?.type === "Identifier") {
4218
+ return /cookie/i.test(obj.property.name);
4219
+ }
4220
+ return false;
4221
+ }
3861
4222
  var rule39 = {
3862
4223
  meta: {
3863
4224
  type: "problem",
@@ -3865,110 +4226,71 @@ var rule39 = {
3865
4226
  description: "Firebase ID token stored in a cookie without httpOnly flag",
3866
4227
  category: "security",
3867
4228
  cwe: "CWE-1004",
3868
- owasp: "A02:2021 Cryptographic Failures",
3869
- 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.",
4229
+ owasp: "A05:2021 Security Misconfiguration",
4230
+ 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.",
3870
4231
  docsUrl: "https://firebase.google.com/docs/auth/admin/manage-cookies",
3871
4232
  recommended: true
3872
4233
  },
3873
4234
  messages: {
3874
- 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."
4235
+ idTokenCookieMissingHttpOnly: "Auth token stored in cookie without httpOnly: true. Any XSS vulnerability can steal the token. Use the Firebase Admin SDK createSessionCookie flow instead."
3875
4236
  },
3876
4237
  schema: []
3877
4238
  },
3878
4239
  create(context) {
3879
- function isTokenName(val) {
3880
- return /token/i.test(val);
3881
- }
3882
4240
  function hasHttpOnlyTrue(optsNode) {
3883
- if (optsNode?.type !== "ObjectExpression") return false;
3884
4241
  const prop = findProp(optsNode, "httpOnly");
3885
- if (!prop) return false;
3886
- return prop.value?.type === "Literal" && prop.value.value === true;
3887
- }
3888
- return {
3889
- CallExpression(node) {
3890
- const callee = node.callee;
3891
- const isCookieSet = callee?.type === "Identifier" && callee.name === "setCookie";
3892
- if (!isCookieSet) return;
3893
- const args = node.arguments ?? [];
3894
- const nameArg = args[0];
3895
- if (nameArg?.type !== "Literal" || typeof nameArg.value !== "string") return;
3896
- if (!isTokenName(nameArg.value)) return;
3897
- const optsArg = args.length >= 3 ? args[2] : null;
3898
- if (!optsArg || optsArg.type !== "ObjectExpression") {
3899
- context.report({ node, messageId: "idTokenCookieMissingHttpOnly" });
3900
- return;
3901
- }
3902
- if (!hasHttpOnlyTrue(optsArg)) {
4242
+ return prop?.value?.type === "Literal" && prop.value.value === true;
4243
+ }
4244
+ function checkNameValueOptsCall(node) {
4245
+ const args = node.arguments ?? [];
4246
+ if (args.length === 1 && args[0]?.type === "ObjectExpression") {
4247
+ const nameProp = findProp(args[0], "name");
4248
+ if (!isTokenLiteral(nameProp?.value)) return;
4249
+ if (!hasHttpOnlyTrue(args[0])) {
3903
4250
  context.report({ node, messageId: "idTokenCookieMissingHttpOnly" });
3904
4251
  }
4252
+ return;
4253
+ }
4254
+ if (!isTokenLiteral(args[0])) return;
4255
+ const optsArg = args.length >= 3 ? args[2] : null;
4256
+ if (!optsArg || optsArg.type !== "ObjectExpression" || !hasHttpOnlyTrue(optsArg)) {
4257
+ context.report({ node, messageId: "idTokenCookieMissingHttpOnly" });
3905
4258
  }
3906
- };
3907
- }
3908
- };
3909
- var firebaseIdTokenCookieFlagsRule = rule39;
3910
-
3911
- // src/providers/firebase/rules/middleware-token-not-verified.ts
3912
- var rule40 = {
3913
- meta: {
3914
- type: "problem",
3915
- docs: {
3916
- description: "Next.js middleware reads auth cookie but never verifies it",
3917
- category: "security",
3918
- cwe: "CWE-345",
3919
- owasp: "A07:2021 Identification and Authentication Failures",
3920
- 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.",
3921
- docsUrl: "https://firebase.google.com/docs/auth/admin/manage-cookies#verify_session_cookies_and_check_permissions",
3922
- recommended: true
3923
- },
3924
- messages: {
3925
- 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."
3926
- },
3927
- schema: []
3928
- },
3929
- create(context) {
3930
- const AUTH_COOKIE_NAMES = /token|session/i;
3931
- const VERIFY_METHOD_NAMES = /* @__PURE__ */ new Set(["verifySessionCookie", "verifyIdToken", "verify"]);
3932
- let readsCookieWithAuthName = false;
3933
- let hasVerifyCall = false;
3934
- const cookieReadNodes = [];
3935
- function isAuthCookieGet(node) {
3936
- if (node?.type !== "CallExpression") return false;
3937
- const callee = node.callee;
3938
- if (callee?.type !== "MemberExpression") return false;
3939
- if (callee.property?.type !== "Identifier" || callee.property.name !== "get") return false;
3940
- const arg = node.arguments?.[0];
3941
- if (arg?.type !== "Literal" || typeof arg.value !== "string") return false;
3942
- return AUTH_COOKIE_NAMES.test(arg.value);
3943
4259
  }
3944
4260
  return {
3945
4261
  CallExpression(node) {
3946
- if (isAuthCookieGet(node)) {
3947
- readsCookieWithAuthName = true;
3948
- cookieReadNodes.push(node);
3949
- }
3950
4262
  const callee = node.callee;
3951
- if (callee?.type === "Identifier" && VERIFY_METHOD_NAMES.has(callee.name)) {
3952
- hasVerifyCall = true;
4263
+ if (callee?.type === "Identifier" && callee.name === "setCookie") {
4264
+ checkNameValueOptsCall(node);
4265
+ return;
4266
+ }
4267
+ if (callee?.type !== "MemberExpression" || callee.computed || callee.property?.type !== "Identifier") return;
4268
+ if (callee.property.name === "cookie") {
4269
+ checkNameValueOptsCall(node);
4270
+ return;
3953
4271
  }
3954
- if (callee?.type === "MemberExpression" && callee.property?.type === "Identifier" && VERIFY_METHOD_NAMES.has(callee.property.name)) {
3955
- hasVerifyCall = true;
4272
+ if (callee.property.name === "set" && isCookieReceiver(callee.object)) {
4273
+ checkNameValueOptsCall(node);
3956
4274
  }
3957
4275
  },
3958
- "Program:exit"() {
3959
- if (readsCookieWithAuthName && !hasVerifyCall) {
3960
- for (const node of cookieReadNodes) {
3961
- context.report({ node, messageId: "tokenNotVerified" });
3962
- }
4276
+ // document.cookie = `token=${idToken}` — can never be httpOnly
4277
+ AssignmentExpression(node) {
4278
+ const left = node.left;
4279
+ const isDocumentCookie = left?.type === "MemberExpression" && !left.computed && left.object?.type === "Identifier" && left.object.name === "document" && left.property?.type === "Identifier" && left.property.name === "cookie";
4280
+ if (!isDocumentCookie) return;
4281
+ const right = node.right;
4282
+ const text = right?.type === "Literal" && typeof right.value === "string" ? right.value : right?.type === "TemplateLiteral" ? (right.quasis ?? []).map((q) => q?.value?.cooked ?? "").join("") : "";
4283
+ if (/(?:^|;\s*)[^=;]*(?:token|session)[^=;]*=/i.test(text)) {
4284
+ context.report({ node, messageId: "idTokenCookieMissingHttpOnly" });
3963
4285
  }
3964
4286
  }
3965
4287
  };
3966
4288
  }
3967
4289
  };
3968
- var firebaseMiddlewareTokenNotVerifiedRule = rule40;
4290
+ var firebaseIdTokenCookieFlagsRule = rule39;
3969
4291
 
3970
4292
  // src/providers/firebase/rules/hardcoded-user-id.ts
3971
- var rule41 = {
4293
+ var rule40 = {
3972
4294
  meta: {
3973
4295
  type: "problem",
3974
4296
  docs: {
@@ -3986,6 +4308,9 @@ var rule41 = {
3986
4308
  schema: []
3987
4309
  },
3988
4310
  create(context) {
4311
+ if (isInsideTestFile2(String(context.filename ?? ""))) {
4312
+ return {};
4313
+ }
3989
4314
  const USER_ID_NAMES = /* @__PURE__ */ new Set(["userId", "uid", "userID", "user_id"]);
3990
4315
  return {
3991
4316
  VariableDeclarator(node) {
@@ -4000,10 +4325,10 @@ var rule41 = {
4000
4325
  };
4001
4326
  }
4002
4327
  };
4003
- var firebaseHardcodedUserIdRule = rule41;
4328
+ var firebaseHardcodedUserIdRule = rule40;
4004
4329
 
4005
4330
  // src/providers/firebase/rules/auth-user-not-found-disclosure.ts
4006
- var rule42 = {
4331
+ var rule41 = {
4007
4332
  meta: {
4008
4333
  type: "suggestion",
4009
4334
  docs: {
@@ -4042,10 +4367,10 @@ var rule42 = {
4042
4367
  };
4043
4368
  }
4044
4369
  };
4045
- var firebaseAuthUserNotFoundDisclosureRule = rule42;
4370
+ var firebaseAuthUserNotFoundDisclosureRule = rule41;
4046
4371
 
4047
4372
  // src/providers/firebase/rules/signup-password-confirm.ts
4048
- var rule43 = {
4373
+ var rule42 = {
4049
4374
  meta: {
4050
4375
  type: "suggestion",
4051
4376
  docs: {
@@ -4079,7 +4404,7 @@ var rule43 = {
4079
4404
  },
4080
4405
  BinaryExpression(node) {
4081
4406
  if (node.operator !== "===" && node.operator !== "==" && node.operator !== "!==" && node.operator !== "!=") return;
4082
- const mentionsConfirm = (n) => n?.type === "Identifier" && n.name === "confirmPassword";
4407
+ const mentionsConfirm = (n) => n?.type === "Identifier" && n.name === "confirmPassword" || n?.type === "MemberExpression" && !n.computed && n.property?.type === "Identifier" && n.property.name === "confirmPassword";
4083
4408
  if (mentionsConfirm(node.left) || mentionsConfirm(node.right)) {
4084
4409
  hasPasswordComparison = true;
4085
4410
  }
@@ -4102,16 +4427,16 @@ var rule43 = {
4102
4427
  };
4103
4428
  }
4104
4429
  };
4105
- var firebaseSignupPasswordConfirmRule = rule43;
4430
+ var firebaseSignupPasswordConfirmRule = rule42;
4106
4431
 
4107
4432
  // src/providers/firebase/rules/use-array-union-remove.ts
4108
- var rule44 = {
4433
+ var rule43 = {
4109
4434
  meta: {
4110
4435
  type: "suggestion",
4111
4436
  docs: {
4112
4437
  description: "Firestore array field updated with read-modify-write spread/filter instead of arrayUnion/arrayRemove",
4113
4438
  category: "correctness",
4114
- 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.",
4439
+ 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.",
4115
4440
  docsUrl: "https://firebase.google.com/docs/firestore/manage-data/add-data#update_elements_in_an_array",
4116
4441
  recommended: true
4117
4442
  },
@@ -4151,10 +4476,10 @@ var rule44 = {
4151
4476
  };
4152
4477
  }
4153
4478
  };
4154
- var firebaseUseArrayUnionRemoveRule = rule44;
4479
+ var firebaseUseArrayUnionRemoveRule = rule43;
4155
4480
 
4156
4481
  // src/providers/firebase/rules/duplicate-initialize-app.ts
4157
- var rule45 = {
4482
+ var rule44 = {
4158
4483
  meta: {
4159
4484
  type: "suggestion",
4160
4485
  docs: {
@@ -4187,9 +4512,11 @@ var rule45 = {
4187
4512
  const callee = node.callee;
4188
4513
  if (callee?.type !== "Identifier") return;
4189
4514
  if (initializeAppLocalName && callee.name === initializeAppLocalName) {
4190
- initializeAppCalls.push(node);
4515
+ if ((node.arguments ?? []).length < 2) {
4516
+ initializeAppCalls.push(node);
4517
+ }
4191
4518
  }
4192
- if (callee.name === "getApps") {
4519
+ if (callee.name === "getApps" || callee.name === "getApp") {
4193
4520
  hasGetAppsCall = true;
4194
4521
  }
4195
4522
  },
@@ -4202,21 +4529,21 @@ var rule45 = {
4202
4529
  };
4203
4530
  }
4204
4531
  };
4205
- var firebaseDuplicateInitializeAppRule = rule45;
4532
+ var firebaseDuplicateInitializeAppRule = rule44;
4206
4533
 
4207
4534
  // src/providers/firebase/rules/onSnapshot-async-throw.ts
4208
- var rule46 = {
4535
+ var rule45 = {
4209
4536
  meta: {
4210
4537
  type: "suggestion",
4211
4538
  docs: {
4212
4539
  description: "throw inside async onSnapshot callback creates an unhandled promise rejection",
4213
4540
  category: "reliability",
4214
- 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.",
4541
+ 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.",
4215
4542
  docsUrl: "https://firebase.google.com/docs/firestore/query-data/listen#handle_listen_errors",
4216
4543
  recommended: true
4217
4544
  },
4218
4545
  messages: {
4219
- 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."
4546
+ 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."
4220
4547
  },
4221
4548
  schema: []
4222
4549
  },
@@ -4256,10 +4583,10 @@ var rule46 = {
4256
4583
  };
4257
4584
  }
4258
4585
  };
4259
- var firebaseOnSnapshotAsyncThrowRule = rule46;
4586
+ var firebaseOnSnapshotAsyncThrowRule = rule45;
4260
4587
 
4261
4588
  // src/providers/firebase/rules/onSnapshot-missing-error-callback.ts
4262
- var rule47 = {
4589
+ var rule46 = {
4263
4590
  meta: {
4264
4591
  type: "suggestion",
4265
4592
  docs: {
@@ -4297,21 +4624,21 @@ var rule47 = {
4297
4624
  };
4298
4625
  }
4299
4626
  };
4300
- var firebaseOnSnapshotMissingErrorCallbackRule = rule47;
4627
+ var firebaseOnSnapshotMissingErrorCallbackRule = rule46;
4301
4628
 
4302
4629
  // src/providers/firebase/rules/firestore-document-size-guard.ts
4303
- var rule48 = {
4630
+ var rule47 = {
4304
4631
  meta: {
4305
4632
  type: "suggestion",
4306
4633
  docs: {
4307
4634
  description: "Firestore write includes editor.getJSON() without a document size guard",
4308
4635
  category: "reliability",
4309
- 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.",
4636
+ 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.",
4310
4637
  docsUrl: "https://firebase.google.com/docs/firestore/quotas#limits",
4311
4638
  recommended: true
4312
4639
  },
4313
4640
  messages: {
4314
- 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."
4641
+ 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."
4315
4642
  },
4316
4643
  schema: []
4317
4644
  },
@@ -4351,21 +4678,21 @@ var rule48 = {
4351
4678
  };
4352
4679
  }
4353
4680
  };
4354
- var firebaseFirestoreDocumentSizeGuardRule = rule48;
4681
+ var firebaseFirestoreDocumentSizeGuardRule = rule47;
4355
4682
 
4356
4683
  // src/providers/firebase/rules/use-timestamp-now.ts
4357
- var rule49 = {
4684
+ var rule48 = {
4358
4685
  meta: {
4359
4686
  type: "suggestion",
4360
4687
  docs: {
4361
- description: "new Date() used for Firestore timestamp instead of Timestamp.now() or serverTimestamp()",
4688
+ description: "new Date() used for Firestore timestamp instead of serverTimestamp()",
4362
4689
  category: "correctness",
4363
- 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.",
4690
+ 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().",
4364
4691
  docsUrl: "https://firebase.google.com/docs/reference/js/firestore_.timestamp",
4365
4692
  recommended: true
4366
4693
  },
4367
4694
  messages: {
4368
- 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."
4695
+ 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."
4369
4696
  },
4370
4697
  schema: []
4371
4698
  },
@@ -4374,7 +4701,7 @@ var rule49 = {
4374
4701
  return {
4375
4702
  ImportDeclaration(node) {
4376
4703
  const src = node.source?.value;
4377
- if (typeof src === "string" && (src.startsWith("firebase/") || src === "firebase-admin/firestore")) {
4704
+ if (typeof src === "string" && (src.startsWith("firebase/firestore") || src === "firebase-admin/firestore")) {
4378
4705
  importsFromFirestore = true;
4379
4706
  }
4380
4707
  },
@@ -4387,7 +4714,7 @@ var rule49 = {
4387
4714
  };
4388
4715
  }
4389
4716
  };
4390
- var firebaseUseTimestampNowRule = rule49;
4717
+ var firebaseUseTimestampNowRule = rule48;
4391
4718
 
4392
4719
  // src/providers/lovable/utils.ts
4393
4720
  var LLM_HOST_PATTERN = /api\.anthropic\.com|api\.openai\.com/;
@@ -4402,7 +4729,7 @@ function containsKnownLlmHost(node) {
4402
4729
  }
4403
4730
 
4404
4731
  // src/providers/lovable/rules/no-client-side-secret-fetch.ts
4405
- var rule50 = {
4732
+ var rule49 = {
4406
4733
  meta: {
4407
4734
  type: "problem",
4408
4735
  docs: {
@@ -4475,13 +4802,13 @@ var rule50 = {
4475
4802
  };
4476
4803
  }
4477
4804
  };
4478
- var lovableNoClientSideSecretFetchRule = rule50;
4805
+ var lovableNoClientSideSecretFetchRule = rule49;
4479
4806
 
4480
4807
  // src/providers/lovable/rules/paid-flag-without-edge-function.ts
4481
4808
  var PRICE_PATTERN = /\$\s?\d/;
4482
4809
  var FLAG_PROPERTY_PATTERN = /^is_[a-z0-9_]+$/i;
4483
4810
  var FLAG_SUFFIX_PATTERN = /_(unlocked|active|enabled)$/i;
4484
- var rule51 = {
4811
+ var rule50 = {
4485
4812
  meta: {
4486
4813
  type: "problem",
4487
4814
  docs: {
@@ -4490,7 +4817,7 @@ var rule51 = {
4490
4817
  cwe: "CWE-840",
4491
4818
  owasp: "A04:2021 \u2013 Insecure Design",
4492
4819
  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.",
4493
- docsUrl: "https://docs.lovable.dev/features/cloud",
4820
+ docsUrl: "https://docs.lovable.dev/integrations/stripe",
4494
4821
  recommended: true
4495
4822
  },
4496
4823
  messages: {
@@ -4623,20 +4950,20 @@ var rule51 = {
4623
4950
  };
4624
4951
  }
4625
4952
  };
4626
- var lovablePaidFlagWithoutEdgeFunctionRule = rule51;
4953
+ var lovablePaidFlagWithoutEdgeFunctionRule = rule50;
4627
4954
 
4628
4955
  // src/providers/lovable/rules/expiry-column-never-checked.ts
4629
4956
  var EXPIRY_COLUMN_PATTERN = /_(until|expires?_at|expiry)$/i;
4630
4957
  var DATE_COMPARISON_OPERATORS = /* @__PURE__ */ new Set([">", "<", ">=", "<="]);
4631
4958
  var FILTER_METHODS = /* @__PURE__ */ new Set(["gt", "gte", "lt", "lte"]);
4632
- var rule52 = {
4959
+ var rule51 = {
4633
4960
  meta: {
4634
4961
  type: "suggestion",
4635
4962
  docs: {
4636
4963
  description: "An expiry column must be checked against the current time somewhere",
4637
4964
  category: "correctness",
4638
4965
  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.",
4639
- docsUrl: "https://docs.lovable.dev/features/cloud",
4966
+ docsUrl: "https://docs.lovable.dev/integrations/stripe",
4640
4967
  recommended: true
4641
4968
  },
4642
4969
  messages: {
@@ -4721,18 +5048,18 @@ var rule52 = {
4721
5048
  };
4722
5049
  }
4723
5050
  };
4724
- var lovableExpiryColumnNeverCheckedRule = rule52;
5051
+ var lovableExpiryColumnNeverCheckedRule = rule51;
4725
5052
 
4726
5053
  // src/providers/lovable/rules/silent-catch-on-provider-call.ts
4727
5054
  var LOGGING_CONSOLE_METHODS = /* @__PURE__ */ new Set(["error", "warn", "log", "info"]);
4728
- var rule53 = {
5055
+ var rule52 = {
4729
5056
  meta: {
4730
5057
  type: "suggestion",
4731
5058
  docs: {
4732
5059
  description: "A catch block around an LLM provider call must log the failure",
4733
5060
  category: "correctness",
4734
5061
  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.`,
4735
- docsUrl: "https://docs.lovable.dev/features/cloud",
5062
+ docsUrl: "https://docs.lovable.dev/integrations/cloud",
4736
5063
  recommended: true
4737
5064
  },
4738
5065
  messages: {
@@ -4798,7 +5125,7 @@ var rule53 = {
4798
5125
  };
4799
5126
  }
4800
5127
  };
4801
- var lovableSilentCatchOnProviderCallRule = rule53;
5128
+ var lovableSilentCatchOnProviderCallRule = rule52;
4802
5129
 
4803
5130
  // src/providers/browserbase/utils.ts
4804
5131
  function memberPropName3(node) {
@@ -4911,7 +5238,7 @@ function isTruthyGateTest(test) {
4911
5238
  }
4912
5239
  return false;
4913
5240
  }
4914
- var rule54 = {
5241
+ var rule53 = {
4915
5242
  meta: {
4916
5243
  type: "problem",
4917
5244
  docs: {
@@ -4983,10 +5310,10 @@ var rule54 = {
4983
5310
  };
4984
5311
  }
4985
5312
  };
4986
- var browserbaseNoConditionalAuthzOnAnonymousUserRule = rule54;
5313
+ var browserbaseNoConditionalAuthzOnAnonymousUserRule = rule53;
4987
5314
 
4988
5315
  // src/providers/browserbase/rules/no-connect-url-in-api-response.ts
4989
- var rule55 = {
5316
+ var rule54 = {
4990
5317
  meta: {
4991
5318
  type: "problem",
4992
5319
  docs: {
@@ -5016,7 +5343,7 @@ var rule55 = {
5016
5343
  };
5017
5344
  }
5018
5345
  };
5019
- var browserbaseNoConnectUrlInApiResponseRule = rule55;
5346
+ var browserbaseNoConnectUrlInApiResponseRule = rule54;
5020
5347
 
5021
5348
  // src/providers/browserbase/rules/session-id-requires-ownership-check.ts
5022
5349
  function isOwnershipCheckCall(node) {
@@ -5033,7 +5360,7 @@ function isSessionIdLikeArg(node) {
5033
5360
  }
5034
5361
  return false;
5035
5362
  }
5036
- var rule56 = {
5363
+ var rule55 = {
5037
5364
  meta: {
5038
5365
  type: "suggestion",
5039
5366
  docs: {
@@ -5041,7 +5368,7 @@ var rule56 = {
5041
5368
  category: "security",
5042
5369
  cwe: "CWE-862",
5043
5370
  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.",
5044
- docsUrl: "https://docs.browserbase.com/features/session-live-view",
5371
+ docsUrl: "https://docs.browserbase.com/platform/browser/observability/session-live-view",
5045
5372
  recommended: true
5046
5373
  },
5047
5374
  messages: {
@@ -5097,7 +5424,7 @@ var rule56 = {
5097
5424
  };
5098
5425
  }
5099
5426
  };
5100
- var browserbaseSessionIdRequiresOwnershipCheckRule = rule56;
5427
+ var browserbaseSessionIdRequiresOwnershipCheckRule = rule55;
5101
5428
 
5102
5429
  // src/providers/browserbase/rules/no-concurrent-shared-context.ts
5103
5430
  function isPromiseAllCall2(node) {
@@ -5154,14 +5481,14 @@ function findSharedContextCreateCall(callback) {
5154
5481
  visit(body);
5155
5482
  return found;
5156
5483
  }
5157
- var rule57 = {
5484
+ var rule56 = {
5158
5485
  meta: {
5159
5486
  type: "problem",
5160
5487
  docs: {
5161
5488
  description: "A Browserbase Context must not be attached to concurrent sessions",
5162
5489
  category: "correctness",
5163
5490
  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.`,
5164
- docsUrl: "https://docs.browserbase.com/features/contexts",
5491
+ docsUrl: "https://docs.browserbase.com/platform/browser/core-features/contexts",
5165
5492
  recommended: true
5166
5493
  },
5167
5494
  messages: {
@@ -5201,7 +5528,7 @@ var rule57 = {
5201
5528
  };
5202
5529
  }
5203
5530
  };
5204
- var browserbaseNoConcurrentSharedContextRule = rule57;
5531
+ var browserbaseNoConcurrentSharedContextRule = rule56;
5205
5532
 
5206
5533
  // src/providers/browserbase/rules/mobile-device-requires-os-setting.ts
5207
5534
  function isMobileLiteral(node) {
@@ -5234,14 +5561,14 @@ function isOsMobileSetting(node) {
5234
5561
  }
5235
5562
  return false;
5236
5563
  }
5237
- var rule58 = {
5564
+ var rule57 = {
5238
5565
  meta: {
5239
5566
  type: "problem",
5240
5567
  docs: {
5241
5568
  description: "Mobile device combos must set browserSettings.os, not just resize the viewport",
5242
5569
  category: "correctness",
5243
5570
  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.`,
5244
- docsUrl: "https://docs.browserbase.com/features/stealth-mode",
5571
+ docsUrl: "https://docs.browserbase.com/platform/identity/verified-customization",
5245
5572
  recommended: true
5246
5573
  },
5247
5574
  messages: {
@@ -5273,7 +5600,7 @@ var rule58 = {
5273
5600
  };
5274
5601
  }
5275
5602
  };
5276
- var browserbaseMobileDeviceRequiresOsSettingRule = rule58;
5603
+ var browserbaseMobileDeviceRequiresOsSettingRule = rule57;
5277
5604
 
5278
5605
  // src/providers/browserbase/rules/use-typed-exception-status-not-substring.ts
5279
5606
  function isStringifiedErrorSource(node, errName) {
@@ -5335,7 +5662,7 @@ function collectStringifiedVars(body, errName) {
5335
5662
  });
5336
5663
  return names;
5337
5664
  }
5338
- var rule59 = {
5665
+ var rule58 = {
5339
5666
  meta: {
5340
5667
  type: "suggestion",
5341
5668
  docs: {
@@ -5365,7 +5692,7 @@ var rule59 = {
5365
5692
  };
5366
5693
  }
5367
5694
  };
5368
- var browserbaseUseTypedExceptionStatusNotSubstringRule = rule59;
5695
+ var browserbaseUseTypedExceptionStatusNotSubstringRule = rule58;
5369
5696
 
5370
5697
  // src/providers/browserbase/rules/release-session-on-connect-failure.ts
5371
5698
  function isSessionsCreateAwait(node) {
@@ -5388,7 +5715,7 @@ function isReleaseCall(node) {
5388
5715
  const name = callee?.type === "Identifier" ? callee.name : memberPropName3(node);
5389
5716
  return !!name && /requeststop|releasesession|release_session/i.test(name);
5390
5717
  }
5391
- var rule60 = {
5718
+ var rule59 = {
5392
5719
  meta: {
5393
5720
  type: "problem",
5394
5721
  docs: {
@@ -5430,10 +5757,10 @@ var rule60 = {
5430
5757
  };
5431
5758
  }
5432
5759
  };
5433
- var browserbaseReleaseSessionOnConnectFailureRule = rule60;
5760
+ var browserbaseReleaseSessionOnConnectFailureRule = rule59;
5434
5761
 
5435
5762
  // src/providers/browserbase/rules/dont-stack-custom-retry-on-sdk-retry.ts
5436
- var rule61 = {
5763
+ var rule60 = {
5437
5764
  meta: {
5438
5765
  type: "suggestion",
5439
5766
  docs: {
@@ -5478,7 +5805,7 @@ var rule61 = {
5478
5805
  };
5479
5806
  }
5480
5807
  };
5481
- var browserbaseDontStackCustomRetryOnSdkRetryRule = rule61;
5808
+ var browserbaseDontStackCustomRetryOnSdkRetryRule = rule60;
5482
5809
 
5483
5810
  // src/providers/browserbase/rules/no-overbroad-error-substring-match.ts
5484
5811
  var OVERBROAD_TERMS = /* @__PURE__ */ new Set(["session", "context", "browser", "browserbase"]);
@@ -5500,7 +5827,7 @@ function isCleanupCall(node) {
5500
5827
  const name = callee?.type === "Identifier" ? callee.name : memberPropName3(node);
5501
5828
  return !!name && /release|remove|cleanup|stop|teardown/i.test(name);
5502
5829
  }
5503
- var rule62 = {
5830
+ var rule61 = {
5504
5831
  meta: {
5505
5832
  type: "problem",
5506
5833
  docs: {
@@ -5530,7 +5857,7 @@ var rule62 = {
5530
5857
  };
5531
5858
  }
5532
5859
  };
5533
- var browserbaseNoOverbroadErrorSubstringMatchRule = rule62;
5860
+ var browserbaseNoOverbroadErrorSubstringMatchRule = rule61;
5534
5861
 
5535
5862
  // src/providers/browserbase/rules/use-sdk-not-raw-requests.ts
5536
5863
  var BROWSERBASE_HOST = "api.browserbase.com";
@@ -5570,7 +5897,7 @@ function isRawHttpCall(node) {
5570
5897
  }
5571
5898
  return { isCall: false, urlArg: void 0 };
5572
5899
  }
5573
- var rule63 = {
5900
+ var rule62 = {
5574
5901
  meta: {
5575
5902
  type: "suggestion",
5576
5903
  docs: {
@@ -5604,10 +5931,10 @@ var rule63 = {
5604
5931
  };
5605
5932
  }
5606
5933
  };
5607
- var browserbaseUseSdkNotRawRequestsRule = rule63;
5934
+ var browserbaseUseSdkNotRawRequestsRule = rule62;
5608
5935
 
5609
5936
  // src/providers/browserbase/rules/centralize-request-release.ts
5610
- var rule64 = {
5937
+ var rule63 = {
5611
5938
  meta: {
5612
5939
  type: "suggestion",
5613
5940
  docs: {
@@ -5640,11 +5967,11 @@ var rule64 = {
5640
5967
  };
5641
5968
  }
5642
5969
  };
5643
- var browserbaseCentralizeRequestReleaseRule = rule64;
5970
+ var browserbaseCentralizeRequestReleaseRule = rule63;
5644
5971
 
5645
5972
  // src/providers/openai-cua/rules/no-domain-allowlist.ts
5646
5973
  var ACTION_METHOD_NAMES = /* @__PURE__ */ new Set(["click", "type", "press", "move", "goto", "fill", "dblclick", "dragAndDrop"]);
5647
- var rule65 = {
5974
+ var rule64 = {
5648
5975
  meta: {
5649
5976
  type: "problem",
5650
5977
  docs: {
@@ -5770,11 +6097,11 @@ var rule65 = {
5770
6097
  };
5771
6098
  }
5772
6099
  };
5773
- var openaiCuaNoDomainAllowlistRule = rule65;
6100
+ var openaiCuaNoDomainAllowlistRule = rule64;
5774
6101
 
5775
6102
  // src/providers/openai-cua/rules/scroll-delta-default-zero.ts
5776
6103
  var VERTICAL_DELTA_NAME_PATTERN = /^(dy|delta_?y|scroll_?y)$/i;
5777
- var rule66 = {
6104
+ var rule65 = {
5778
6105
  meta: {
5779
6106
  type: "problem",
5780
6107
  docs: {
@@ -5850,12 +6177,12 @@ var rule66 = {
5850
6177
  };
5851
6178
  }
5852
6179
  };
5853
- var openaiCuaScrollDeltaDefaultZeroRule = rule66;
6180
+ var openaiCuaScrollDeltaDefaultZeroRule = rule65;
5854
6181
 
5855
6182
  // src/providers/openai-cua/rules/structured-step-metadata-not-text-json.ts
5856
6183
  var BRACE_SEARCH_METHODS = /* @__PURE__ */ new Set(["indexOf", "lastIndexOf"]);
5857
6184
  var SLICE_METHODS = /* @__PURE__ */ new Set(["slice", "substring", "substr"]);
5858
- var rule67 = {
6185
+ var rule66 = {
5859
6186
  meta: {
5860
6187
  type: "suggestion",
5861
6188
  docs: {
@@ -5954,10 +6281,10 @@ var rule67 = {
5954
6281
  };
5955
6282
  }
5956
6283
  };
5957
- var openaiCuaStructuredStepMetadataNotTextJsonRule = rule67;
6284
+ var openaiCuaStructuredStepMetadataNotTextJsonRule = rule66;
5958
6285
 
5959
6286
  // src/providers/openai-cua/rules/no-blind-safety-check-ack.ts
5960
- var rule68 = {
6287
+ var rule67 = {
5961
6288
  meta: {
5962
6289
  type: "suggestion",
5963
6290
  docs: {
@@ -6020,7 +6347,7 @@ var rule68 = {
6020
6347
  };
6021
6348
  }
6022
6349
  };
6023
- var openaiCuaNoBlindSafetyCheckAckRule = rule68;
6350
+ var openaiCuaNoBlindSafetyCheckAckRule = rule67;
6024
6351
 
6025
6352
  // src/providers/openai-cua/utils.ts
6026
6353
  function propName(node) {
@@ -6066,7 +6393,7 @@ function findResponsesCreateCall(node, depth = 0) {
6066
6393
  }
6067
6394
 
6068
6395
  // src/providers/openai-cua/rules/retry-transient-turn-errors.ts
6069
- var rule69 = {
6396
+ var rule68 = {
6070
6397
  meta: {
6071
6398
  type: "problem",
6072
6399
  docs: {
@@ -6148,10 +6475,10 @@ var rule69 = {
6148
6475
  };
6149
6476
  }
6150
6477
  };
6151
- var openaiCuaRetryTransientTurnErrorsRule = rule69;
6478
+ var openaiCuaRetryTransientTurnErrorsRule = rule68;
6152
6479
 
6153
6480
  // src/providers/openai-cua/rules/check-response-status-incomplete.ts
6154
- var rule70 = {
6481
+ var rule69 = {
6155
6482
  meta: {
6156
6483
  type: "problem",
6157
6484
  docs: {
@@ -6260,17 +6587,17 @@ var rule70 = {
6260
6587
  };
6261
6588
  }
6262
6589
  };
6263
- var openaiCuaCheckResponseStatusIncompleteRule = rule70;
6590
+ var openaiCuaCheckResponseStatusIncompleteRule = rule69;
6264
6591
 
6265
6592
  // src/providers/openai-cua/rules/set-safety-identifier.ts
6266
- var rule71 = {
6593
+ var rule70 = {
6267
6594
  meta: {
6268
6595
  type: "problem",
6269
6596
  docs: {
6270
6597
  description: "responses.create() must set safety_identifier for per-end-user policy attribution",
6271
6598
  category: "integration",
6272
6599
  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.",
6273
- docsUrl: "https://help.openai.com/en/articles/5428082-how-to-incorporate-a-safety-identifier",
6600
+ docsUrl: "https://developers.openai.com/api/docs/guides/safety-best-practices",
6274
6601
  recommended: true
6275
6602
  },
6276
6603
  messages: {
@@ -6301,10 +6628,10 @@ var rule71 = {
6301
6628
  };
6302
6629
  }
6303
6630
  };
6304
- var openaiCuaSetSafetyIdentifierRule = rule71;
6631
+ var openaiCuaSetSafetyIdentifierRule = rule70;
6305
6632
 
6306
6633
  // src/providers/tiptap/rules/upload-validate-fn-void.ts
6307
- var rule72 = {
6634
+ var rule71 = {
6308
6635
  meta: {
6309
6636
  type: "problem",
6310
6637
  docs: {
@@ -6368,10 +6695,10 @@ var rule72 = {
6368
6695
  };
6369
6696
  }
6370
6697
  };
6371
- var tiptapUploadValidateFnVoidRule = rule72;
6698
+ var tiptapUploadValidateFnVoidRule = rule71;
6372
6699
 
6373
6700
  // src/providers/tiptap/rules/script-src-hardcoded-api-key.ts
6374
- var rule73 = {
6701
+ var rule72 = {
6375
6702
  meta: {
6376
6703
  type: "problem",
6377
6704
  docs: {
@@ -6429,10 +6756,10 @@ var rule73 = {
6429
6756
  };
6430
6757
  }
6431
6758
  };
6432
- var tiptapScriptSrcHardcodedApiKeyRule = rule73;
6759
+ var tiptapScriptSrcHardcodedApiKeyRule = rule72;
6433
6760
 
6434
6761
  // src/providers/tiptap/rules/dynamic-script-no-sri.ts
6435
- var rule74 = {
6762
+ var rule73 = {
6436
6763
  meta: {
6437
6764
  type: "problem",
6438
6765
  docs: {
@@ -6441,7 +6768,7 @@ var rule74 = {
6441
6768
  cwe: "CWE-829",
6442
6769
  owasp: "API8:2023 Security Misconfiguration",
6443
6770
  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.',
6444
- docsUrl: "https://developer.mozilla.org/en-US/docs/Web/Security/Subresource_Integrity",
6771
+ docsUrl: "https://developer.mozilla.org/en-US/docs/Web/Security/Defenses/Subresource_Integrity",
6445
6772
  recommended: true
6446
6773
  },
6447
6774
  messages: {
@@ -6528,7 +6855,7 @@ var rule74 = {
6528
6855
  };
6529
6856
  }
6530
6857
  };
6531
- var tiptapDynamicScriptNoSriRule = rule74;
6858
+ var tiptapDynamicScriptNoSriRule = rule73;
6532
6859
 
6533
6860
  // src/providers/tiptap/utils.ts
6534
6861
  function findProperty3(objectExpression, name) {
@@ -6553,7 +6880,7 @@ function walk(node, visit) {
6553
6880
  }
6554
6881
 
6555
6882
  // src/providers/tiptap/rules/addAttributes-missing-renderHTML.ts
6556
- var rule75 = {
6883
+ var rule74 = {
6557
6884
  meta: {
6558
6885
  type: "problem",
6559
6886
  docs: {
@@ -6606,10 +6933,10 @@ var rule75 = {
6606
6933
  };
6607
6934
  }
6608
6935
  };
6609
- var tiptapAddAttributesMissingRenderHTMLRule = rule75;
6936
+ var tiptapAddAttributesMissingRenderHTMLRule = rule74;
6610
6937
 
6611
6938
  // src/providers/tiptap/rules/appendTransaction-add-to-history.ts
6612
- var rule76 = {
6939
+ var rule75 = {
6613
6940
  meta: {
6614
6941
  type: "suggestion",
6615
6942
  docs: {
@@ -6688,10 +7015,10 @@ var rule76 = {
6688
7015
  };
6689
7016
  }
6690
7017
  };
6691
- var tiptapAppendTransactionAddToHistoryRule = rule76;
7018
+ var tiptapAppendTransactionAddToHistoryRule = rule75;
6692
7019
 
6693
7020
  // src/providers/tiptap/rules/appendTransaction-full-scan.ts
6694
- var rule77 = {
7021
+ var rule76 = {
6695
7022
  meta: {
6696
7023
  type: "suggestion",
6697
7024
  docs: {
@@ -6751,10 +7078,10 @@ var rule77 = {
6751
7078
  };
6752
7079
  }
6753
7080
  };
6754
- var tiptapAppendTransactionFullScanRule = rule77;
7081
+ var tiptapAppendTransactionFullScanRule = rule76;
6755
7082
 
6756
7083
  // src/providers/tiptap/rules/atom-node-wrap-in.ts
6757
- var rule78 = {
7084
+ var rule77 = {
6758
7085
  meta: {
6759
7086
  type: "problem",
6760
7087
  docs: {
@@ -6817,57 +7144,16 @@ var rule78 = {
6817
7144
  };
6818
7145
  }
6819
7146
  };
6820
- var tiptapAtomNodeWrapInRule = rule78;
6821
-
6822
- // src/providers/tiptap/rules/twitter-url-regex.ts
6823
- var rule79 = {
6824
- meta: {
6825
- type: "suggestion",
6826
- docs: {
6827
- description: "Twitter/X URL regex must match both x.com and twitter.com",
6828
- category: "integration",
6829
- 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.",
6830
- docsUrl: "https://tiptap.dev/docs/editor/extensions/nodes",
6831
- recommended: true
6832
- },
6833
- messages: {
6834
- 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."
6835
- },
6836
- schema: []
6837
- },
6838
- create(context) {
6839
- function checkPattern(pattern, node) {
6840
- if (!pattern.includes("x\\.com") && !pattern.includes("x.com")) return;
6841
- if (pattern.includes("twitter\\.com") || pattern.includes("twitter.com")) return;
6842
- context.report({ node, messageId: "twitterRegexMissingLegacyDomain" });
6843
- }
6844
- return {
6845
- Literal(node) {
6846
- if (node.regex?.pattern) {
6847
- checkPattern(node.regex.pattern, node);
6848
- }
6849
- },
6850
- NewExpression(node) {
6851
- if (node.callee?.type === "Identifier" && node.callee.name === "RegExp") {
6852
- const firstArg = node.arguments?.[0];
6853
- if (firstArg?.type === "Literal" && typeof firstArg.value === "string") {
6854
- checkPattern(firstArg.value, node);
6855
- }
6856
- }
6857
- }
6858
- };
6859
- }
6860
- };
6861
- var tiptapTwitterUrlRegexRule = rule79;
7147
+ var tiptapAtomNodeWrapInRule = rule77;
6862
7148
 
6863
7149
  // src/providers/tiptap/rules/drop-handler-pos-precedence.ts
6864
- var rule80 = {
7150
+ var rule78 = {
6865
7151
  meta: {
6866
7152
  type: "problem",
6867
7153
  docs: {
6868
7154
  description: "x ?? 0 - 1 is parsed as x ?? (0 - 1) = x ?? -1 due to operator precedence",
6869
7155
  category: "correctness",
6870
- 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.",
7156
+ 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.",
6871
7157
  docsUrl: "https://prosemirror.net/docs/ref/#view.EditorView.posAtCoords",
6872
7158
  recommended: true
6873
7159
  },
@@ -6888,7 +7174,7 @@ var rule80 = {
6888
7174
  };
6889
7175
  }
6890
7176
  };
6891
- var tiptapDropHandlerPosPrecedenceRule = rule80;
7177
+ var tiptapDropHandlerPosPrecedenceRule = rule78;
6892
7178
 
6893
7179
  // src/providers/tiptap/rules/prefer-table-kit.ts
6894
7180
  var INDIVIDUAL_TABLE_PACKAGES = /* @__PURE__ */ new Set([
@@ -6896,18 +7182,18 @@ var INDIVIDUAL_TABLE_PACKAGES = /* @__PURE__ */ new Set([
6896
7182
  "@tiptap/extension-table-cell",
6897
7183
  "@tiptap/extension-table-header"
6898
7184
  ]);
6899
- var rule81 = {
7185
+ var rule79 = {
6900
7186
  meta: {
6901
7187
  type: "suggestion",
6902
7188
  docs: {
6903
7189
  description: "Use TableKit from @tiptap/extension-table instead of individual table packages",
6904
7190
  category: "integration",
6905
- 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.",
7191
+ 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.",
6906
7192
  docsUrl: "https://tiptap.dev/docs/editor/extensions/functionality/table-kit",
6907
7193
  recommended: true
6908
7194
  },
6909
7195
  messages: {
6910
- preferTableKit: "Individual TipTap table extension imported. Use TableKit from @tiptap/extension-table for coordinated configuration and the mergeOrSplit command."
7196
+ preferTableKit: "Individual TipTap table extension imported. Use TableKit from @tiptap/extension-table to configure all table elements together."
6911
7197
  },
6912
7198
  schema: []
6913
7199
  },
@@ -6930,21 +7216,21 @@ var rule81 = {
6930
7216
  };
6931
7217
  }
6932
7218
  };
6933
- var tiptapPreferTableKitRule = rule81;
7219
+ var tiptapPreferTableKitRule = rule79;
6934
7220
 
6935
7221
  // src/providers/tiptap/rules/tiptap-markdown-missing-node-spec.ts
6936
- var rule82 = {
7222
+ var rule80 = {
6937
7223
  meta: {
6938
7224
  type: "suggestion",
6939
7225
  docs: {
6940
7226
  description: "TipTap nodes used with tiptap-markdown must define a markdown serialization spec",
6941
7227
  category: "integration",
6942
- 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.",
6943
- docsUrl: "https://github.com/ueberdosis/tiptap-markdown",
7228
+ 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.",
7229
+ docsUrl: "https://github.com/aguingand/tiptap-markdown",
6944
7230
  recommended: true
6945
7231
  },
6946
7232
  messages: {
6947
- 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."
7233
+ 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."
6948
7234
  },
6949
7235
  schema: []
6950
7236
  },
@@ -6999,182 +7285,2294 @@ var rule82 = {
6999
7285
  };
7000
7286
  }
7001
7287
  };
7002
- var tiptapTiptapMarkdownMissingNodeSpecRule = rule82;
7288
+ var tiptapTiptapMarkdownMissingNodeSpecRule = rule80;
7003
7289
 
7004
- // src/plugin/index.ts
7005
- var plugin = {
7006
- meta: { name: PLUGIN_NAME, version: "0.0.1" },
7007
- rules: {
7008
- "resend-webhook-signature": resendWebhookSignatureRule,
7009
- "resend-api-key-hardcoded": resendApiKeyHardcodedRule,
7010
- "resend-api-key-in-client-bundle": resendApiKeyInClientBundleRule,
7011
- "resend-marketing-via-batch-send": resendMarketingViaBatchSendRule,
7012
- "resend-marketing-missing-unsubscribe": resendMarketingMissingUnsubscribeRule,
7013
- "resend-test-domain-in-production-path": resendTestDomainInProductionPathRule,
7014
- "resend-from-address-not-friendly-format": resendFromAddressNotFriendlyFormatRule,
7015
- "resend-batch-size-not-enforced": resendBatchSizeNotEnforcedRule,
7016
- "resend-missing-idempotency-key": resendMissingIdempotencyKeyRule,
7017
- "resend-no-error-code-mapping": resendNoErrorCodeMappingRule,
7018
- "resend-webhook-no-idempotency": resendWebhookNoIdempotencyRule,
7019
- "resend-missing-tags": resendMissingTagsRule,
7020
- "resend-request-id-not-logged": resendRequestIdNotLoggedRule,
7021
- "supabase-scope-queries-by-tenant-column": supabaseScopeQueriesByTenantColumnRule,
7022
- "supabase-validate-uuid-columns": supabaseValidateUuidColumnsRule,
7023
- "supabase-order-by-timestamp-not-identity": supabaseOrderByTimestampNotIdentityRule,
7024
- "supabase-consistent-input-length-limits": supabaseConsistentInputLengthLimitsRule,
7025
- "supabase-idempotent-mutations": supabaseIdempotentMutationsRule,
7026
- "supabase-fail-fast-env-validation": supabaseFailFastEnvValidationRule,
7027
- "supabase-no-user-metadata-authz": supabaseNoUserMetadataAuthzRule,
7028
- "supabase-single-without-error-check": supabaseSingleWithoutErrorCheckRule,
7029
- "supabase-non-atomic-replace-pattern": supabaseNonAtomicReplacePatternRule,
7030
- "supabase-unchecked-mutation-error": supabaseUncheckedMutationErrorRule,
7031
- "supabase-realtime-missing-filter": supabaseRealtimeMissingFilterRule,
7032
- "supabase-storage-error-not-surfaced": supabaseStorageErrorNotSurfacedRule,
7033
- "auth0-required-audience-validation": auth0RequiredAudienceValidationRule,
7034
- "auth0-no-account-link-without-verified-email": auth0NoAccountLinkWithoutVerifiedEmailRule,
7035
- "auth0-dead-claim-verification-check": auth0DeadClaimVerificationCheckRule,
7036
- "auth0-jwks-refresh-on-unknown-kid": auth0JwksRefreshOnUnknownKidRule,
7037
- "firebase-missing-app-check": firebaseMissingAppCheckRule,
7038
- "firebase-unhandled-auth-popup-rejection": firebaseUnhandledAuthPopupRejectionRule,
7039
- "firebase-rtdb-list-read-for-single-item": firebaseRtdbListReadForSingleItemRule,
7040
- "firebase-unvalidated-external-data-to-rtdb": firebaseUnvalidatedExternalDataToRtdbRule,
7041
- "firebase-rtdb-batch-write-not-atomic": firebaseRtdbBatchWriteNotAtomicRule,
7042
- "firebase-rtdb-listener-error-not-handled": firebaseRtdbListenerErrorNotHandledRule,
7043
- "firebase-effect-deps-whole-user-object": firebaseEffectDepsWholeUserObjectRule,
7044
- "firebase-rtdb-write-promise-not-handled": firebaseRtdbWritePromiseNotHandledRule,
7045
- "firebase-firestore-rules-expired": firebaseFirestoreRulesExpiredRule,
7046
- "firebase-id-token-cookie-flags": firebaseIdTokenCookieFlagsRule,
7047
- "firebase-middleware-token-not-verified": firebaseMiddlewareTokenNotVerifiedRule,
7048
- "firebase-hardcoded-user-id": firebaseHardcodedUserIdRule,
7049
- "firebase-auth-user-not-found-disclosure": firebaseAuthUserNotFoundDisclosureRule,
7050
- "firebase-signup-password-confirm": firebaseSignupPasswordConfirmRule,
7051
- "firebase-use-array-union-remove": firebaseUseArrayUnionRemoveRule,
7052
- "firebase-duplicate-initialize-app": firebaseDuplicateInitializeAppRule,
7053
- "firebase-onSnapshot-async-throw": firebaseOnSnapshotAsyncThrowRule,
7054
- "firebase-onSnapshot-missing-error-callback": firebaseOnSnapshotMissingErrorCallbackRule,
7055
- "firebase-firestore-document-size-guard": firebaseFirestoreDocumentSizeGuardRule,
7056
- "firebase-use-timestamp-now": firebaseUseTimestampNowRule,
7057
- "lovable-no-client-side-secret-fetch": lovableNoClientSideSecretFetchRule,
7058
- "lovable-paid-flag-without-edge-function": lovablePaidFlagWithoutEdgeFunctionRule,
7059
- "lovable-expiry-column-never-checked": lovableExpiryColumnNeverCheckedRule,
7060
- "lovable-silent-catch-on-provider-call": lovableSilentCatchOnProviderCallRule,
7061
- "browserbase-no-conditional-authz-on-anonymous-user": browserbaseNoConditionalAuthzOnAnonymousUserRule,
7062
- "browserbase-no-connect-url-in-api-response": browserbaseNoConnectUrlInApiResponseRule,
7063
- "browserbase-session-id-requires-ownership-check": browserbaseSessionIdRequiresOwnershipCheckRule,
7064
- "browserbase-no-concurrent-shared-context": browserbaseNoConcurrentSharedContextRule,
7065
- "browserbase-mobile-device-requires-os-setting": browserbaseMobileDeviceRequiresOsSettingRule,
7066
- "browserbase-use-typed-exception-status-not-substring": browserbaseUseTypedExceptionStatusNotSubstringRule,
7067
- "browserbase-release-session-on-connect-failure": browserbaseReleaseSessionOnConnectFailureRule,
7068
- "browserbase-dont-stack-custom-retry-on-sdk-retry": browserbaseDontStackCustomRetryOnSdkRetryRule,
7069
- "browserbase-no-overbroad-error-substring-match": browserbaseNoOverbroadErrorSubstringMatchRule,
7070
- "browserbase-use-sdk-not-raw-requests": browserbaseUseSdkNotRawRequestsRule,
7071
- "browserbase-centralize-request-release": browserbaseCentralizeRequestReleaseRule,
7072
- "openai-cua-no-domain-allowlist": openaiCuaNoDomainAllowlistRule,
7073
- "openai-cua-scroll-delta-default-zero": openaiCuaScrollDeltaDefaultZeroRule,
7074
- "openai-cua-structured-step-metadata-not-text-json": openaiCuaStructuredStepMetadataNotTextJsonRule,
7075
- "openai-cua-no-blind-safety-check-ack": openaiCuaNoBlindSafetyCheckAckRule,
7076
- "openai-cua-retry-transient-turn-errors": openaiCuaRetryTransientTurnErrorsRule,
7077
- "openai-cua-check-response-status-incomplete": openaiCuaCheckResponseStatusIncompleteRule,
7078
- "openai-cua-set-safety-identifier": openaiCuaSetSafetyIdentifierRule,
7079
- "tiptap-upload-validate-fn-void": tiptapUploadValidateFnVoidRule,
7080
- "tiptap-script-src-hardcoded-api-key": tiptapScriptSrcHardcodedApiKeyRule,
7081
- "tiptap-dynamic-script-no-sri": tiptapDynamicScriptNoSriRule,
7082
- "tiptap-addAttributes-missing-renderHTML": tiptapAddAttributesMissingRenderHTMLRule,
7083
- "tiptap-appendTransaction-add-to-history": tiptapAppendTransactionAddToHistoryRule,
7084
- "tiptap-appendTransaction-full-scan": tiptapAppendTransactionFullScanRule,
7085
- "tiptap-atom-node-wrap-in": tiptapAtomNodeWrapInRule,
7086
- "tiptap-twitter-url-regex": tiptapTwitterUrlRegexRule,
7087
- "tiptap-drop-handler-pos-precedence": tiptapDropHandlerPosPrecedenceRule,
7088
- "tiptap-prefer-table-kit": tiptapPreferTableKitRule,
7089
- "tiptap-tiptap-markdown-missing-node-spec": tiptapTiptapMarkdownMissingNodeSpecRule
7290
+ // src/providers/elevenlabs/utils.ts
7291
+ function isElevenLabsUrlArg(node) {
7292
+ if (!node) return false;
7293
+ if (node.type === "Literal" && typeof node.value === "string") {
7294
+ return node.value.includes("elevenlabs.io");
7090
7295
  }
7091
- };
7092
-
7093
- // src/plugin/rule-registry.ts
7094
- function buildRegistry() {
7095
- const registry2 = /* @__PURE__ */ new Map();
7096
- for (const [key, rule83] of Object.entries(plugin.rules)) {
7097
- const docs = rule83?.meta?.docs ?? {};
7098
- registry2.set(key, {
7099
- category: docs.category,
7100
- description: docs.description ?? "",
7101
- rationale: docs.rationale ?? "",
7102
- docsUrl: docs.docsUrl,
7103
- cwe: docs.cwe,
7104
- owasp: docs.owasp
7105
- });
7296
+ if (node.type === "TemplateLiteral") {
7297
+ return (node.quasis ?? []).some(
7298
+ (q) => typeof q?.value?.raw === "string" && q.value.raw.includes("elevenlabs.io")
7299
+ );
7106
7300
  }
7107
- return registry2;
7108
- }
7109
- var registry = buildRegistry();
7110
- function getRuleDocsMeta(ruleKey) {
7111
- return registry.get(ruleKey);
7301
+ return false;
7112
7302
  }
7113
-
7114
- // src/reporter/snippet.ts
7115
- function extractCodeSnippet(content, line, contextLines = 2) {
7116
- const allLines = content.split(/\r?\n/);
7117
- const highlighted = Math.min(Math.max(line, 1), allLines.length || 1);
7118
- const start = Math.max(1, highlighted - contextLines);
7119
- const end = Math.min(allLines.length, highlighted + contextLines);
7120
- const lines = [];
7121
- for (let n = start; n <= end; n++) {
7122
- lines.push({ number: n, text: allLines[n - 1] ?? "" });
7303
+ function findElevenLabsFetchCall(node, depth = 0) {
7304
+ if (!node || typeof node !== "object" || depth > 20) return null;
7305
+ if (Array.isArray(node)) {
7306
+ for (const n of node) {
7307
+ const found = findElevenLabsFetchCall(n, depth + 1);
7308
+ if (found) return found;
7309
+ }
7310
+ return null;
7123
7311
  }
7124
- return { lines, highlightedLine: highlighted };
7125
- }
7126
-
7127
- // src/types.ts
7128
- var SEVERITY_ORDER = {
7129
- error: 0,
7130
- warning: 1,
7131
- info: 2
7132
- };
7133
- function scoreToSeverityLabel(score) {
7134
- if (score >= 80) return "excellent";
7135
- if (score >= 60) return "good";
7136
- if (score >= 40) return "needs-work";
7137
- return "critical";
7312
+ if (node.type === "CallExpression" && node.callee?.type === "Identifier" && node.callee.name === "fetch") {
7313
+ if (isElevenLabsUrlArg(node.arguments?.[0])) return node;
7314
+ }
7315
+ for (const key of Object.keys(node)) {
7316
+ if (key === "parent" || key === "loc" || key === "range") continue;
7317
+ const val = node[key];
7318
+ if (val && typeof val === "object") {
7319
+ const found = findElevenLabsFetchCall(val, depth + 1);
7320
+ if (found) return found;
7321
+ }
7322
+ }
7323
+ return null;
7138
7324
  }
7139
-
7140
- // src/reporter/report-builder.ts
7141
- function computeScore(errors, warnings) {
7142
- return Math.max(0, 100 - errors * 15 - warnings * 5);
7325
+ function unwrapAwait(node) {
7326
+ return node?.type === "AwaitExpression" ? node.argument : node;
7143
7327
  }
7144
- function buildSummary(results) {
7145
- const errors = results.filter((r) => r.severity === "error").length;
7146
- const warnings = results.filter((r) => r.severity === "warning").length;
7147
- const info = results.filter((r) => r.severity === "info").length;
7148
- const score = computeScore(errors, warnings);
7149
- return {
7150
- score,
7151
- severity: scoreToSeverityLabel(score),
7152
- errors,
7153
- warnings,
7154
- info,
7155
- totalIssues: results.length
7156
- };
7328
+ function collectVarDeclarators(node, out, depth = 0) {
7329
+ if (!node || typeof node !== "object" || depth > 24) return;
7330
+ if (Array.isArray(node)) {
7331
+ for (const n of node) collectVarDeclarators(n, out, depth + 1);
7332
+ return;
7333
+ }
7334
+ if (node.type === "VariableDeclarator") out.push(node);
7335
+ for (const key of Object.keys(node)) {
7336
+ if (key === "parent" || key === "loc" || key === "range") continue;
7337
+ const val = node[key];
7338
+ if (val && typeof val === "object") collectVarDeclarators(val, out, depth + 1);
7339
+ }
7157
7340
  }
7158
- function sortResults(results) {
7159
- return [...results].sort((a, b) => {
7160
- const bySeverity = SEVERITY_ORDER[a.severity] - SEVERITY_ORDER[b.severity];
7161
- if (bySeverity !== 0) return bySeverity;
7162
- const byFile = a.file.localeCompare(b.file);
7163
- if (byFile !== 0) return byFile;
7164
- return a.line - b.line;
7165
- });
7341
+ function posOf(n) {
7342
+ if (typeof n?.range?.[0] === "number") return n.range[0];
7343
+ const line = n?.loc?.start?.line ?? 0;
7344
+ const column = n?.loc?.start?.column ?? 0;
7345
+ return line * 1e6 + column;
7166
7346
  }
7167
- function toFinding(result, sequence, content) {
7168
- const docs = getRuleDocsMeta(result.ruleKey);
7169
- return {
7170
- id: `${result.ruleKey}-${sequence}`,
7171
- rule: result.rule,
7172
- category: docs?.category ?? "correctness",
7173
- severity: result.severity,
7174
- message: result.message,
7175
- fix: result.fix,
7176
- docsUrl: result.docsUrl ?? docs?.docsUrl,
7177
- cwe: docs?.cwe,
7347
+
7348
+ // src/providers/elevenlabs/rules/validate-signed-url-response.ts
7349
+ var rule81 = {
7350
+ meta: {
7351
+ type: "problem",
7352
+ docs: {
7353
+ description: "ElevenLabs signed URL responses must be validated before the signed_url field is used",
7354
+ category: "correctness",
7355
+ cwe: "CWE-252",
7356
+ 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.",
7357
+ docsUrl: "https://elevenlabs.io/docs/eleven-agents/api-reference/conversations/get-signed-url",
7358
+ recommended: true
7359
+ },
7360
+ messages: {
7361
+ 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."
7362
+ }
7363
+ },
7364
+ create(context) {
7365
+ function analyzeFunction(fnNode) {
7366
+ const declarators = [];
7367
+ collectVarDeclarators(fnNode.body, declarators);
7368
+ const responseVarNames = [];
7369
+ for (const d of declarators) {
7370
+ const init = unwrapAwait(d.init);
7371
+ if (init?.type === "CallExpression" && init.callee?.type === "Identifier" && init.callee.name === "fetch" && isElevenLabsUrlArg(init.arguments?.[0]) && d.id?.type === "Identifier") {
7372
+ responseVarNames.push(d.id.name);
7373
+ }
7374
+ }
7375
+ if (responseVarNames.length === 0) return;
7376
+ for (const responseVarName of responseVarNames) {
7377
+ for (const d of declarators) {
7378
+ const init = unwrapAwait(d.init);
7379
+ 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") {
7380
+ analyzeDataVar(fnNode, d.id.name, d);
7381
+ }
7382
+ }
7383
+ }
7384
+ }
7385
+ function analyzeDataVar(fnNode, dataVarName, dataDeclaratorNode) {
7386
+ function isSignedUrlMember(n) {
7387
+ if (n?.type !== "MemberExpression") return false;
7388
+ if (n.object?.type !== "Identifier" || n.object.name !== dataVarName) return false;
7389
+ if (!n.computed) return n.property?.type === "Identifier" && n.property.name === "signed_url";
7390
+ return n.property?.type === "Literal" && n.property.value === "signed_url";
7391
+ }
7392
+ function isNullishLiteral(n) {
7393
+ return n?.type === "Identifier" && n.name === "undefined" || n?.type === "Literal" && n.value === null;
7394
+ }
7395
+ function isFalsyGuardOnSignedUrl(test) {
7396
+ if (!test) return false;
7397
+ if (test.type === "UnaryExpression" && test.operator === "!") {
7398
+ return isSignedUrlMember(test.argument);
7399
+ }
7400
+ if (test.type === "BinaryExpression" && (test.operator === "==" || test.operator === "===")) {
7401
+ return isSignedUrlMember(test.left) && isNullishLiteral(test.right) || isSignedUrlMember(test.right) && isNullishLiteral(test.left);
7402
+ }
7403
+ if (test.type === "LogicalExpression") {
7404
+ return isFalsyGuardOnSignedUrl(test.left) || isFalsyGuardOnSignedUrl(test.right);
7405
+ }
7406
+ return false;
7407
+ }
7408
+ let guardPos = null;
7409
+ let usagePos = null;
7410
+ function walk3(n, depth = 0) {
7411
+ if (!n || typeof n !== "object" || depth > 40) return;
7412
+ if (Array.isArray(n)) {
7413
+ for (const item of n) walk3(item, depth + 1);
7414
+ return;
7415
+ }
7416
+ if (n.type === "IfStatement" && isFalsyGuardOnSignedUrl(n.test)) {
7417
+ const p = posOf(n);
7418
+ if (guardPos === null || p < guardPos) guardPos = p;
7419
+ }
7420
+ if (isSignedUrlMember(n) && n !== dataDeclaratorNode) {
7421
+ const p = posOf(n);
7422
+ if (usagePos === null || p < usagePos) usagePos = p;
7423
+ }
7424
+ for (const key of Object.keys(n)) {
7425
+ if (key === "parent" || key === "loc" || key === "range") continue;
7426
+ const val = n[key];
7427
+ if (val && typeof val === "object") walk3(val, depth + 1);
7428
+ }
7429
+ }
7430
+ walk3(fnNode.body);
7431
+ if (usagePos === null) return;
7432
+ if (guardPos !== null && guardPos < usagePos) return;
7433
+ context.report({ node: dataDeclaratorNode, messageId: "missingValidation" });
7434
+ }
7435
+ return {
7436
+ FunctionDeclaration(node) {
7437
+ analyzeFunction(node);
7438
+ },
7439
+ FunctionExpression(node) {
7440
+ analyzeFunction(node);
7441
+ },
7442
+ ArrowFunctionExpression(node) {
7443
+ analyzeFunction(node);
7444
+ }
7445
+ };
7446
+ }
7447
+ };
7448
+ var elevenlabsValidateSignedUrlResponseRule = rule81;
7449
+
7450
+ // src/providers/elevenlabs/rules/no-error-object-logging.ts
7451
+ var LOGGING_METHODS = /* @__PURE__ */ new Set(["error", "warn", "log"]);
7452
+ var rule82 = {
7453
+ meta: {
7454
+ type: "problem",
7455
+ docs: {
7456
+ description: "Catch blocks must not log the raw caught error object",
7457
+ category: "security",
7458
+ cwe: "CWE-532",
7459
+ 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.",
7460
+ docsUrl: "https://elevenlabs.io/docs/api-reference/authentication",
7461
+ recommended: true
7462
+ },
7463
+ messages: {
7464
+ 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."
7465
+ }
7466
+ },
7467
+ create(context) {
7468
+ function isConsoleLogCall(node) {
7469
+ if (node?.type !== "CallExpression") return false;
7470
+ const callee = node.callee;
7471
+ if (callee?.type !== "MemberExpression") return false;
7472
+ if (callee.object?.type !== "Identifier" || callee.object.name !== "console") return false;
7473
+ return callee.property?.type === "Identifier" && LOGGING_METHODS.has(callee.property.name);
7474
+ }
7475
+ function referencesRawIdentifier(node, paramName, depth = 0) {
7476
+ if (!node || typeof node !== "object" || depth > 10) return false;
7477
+ if (Array.isArray(node)) return node.some((n) => referencesRawIdentifier(n, paramName, depth + 1));
7478
+ if (node.type === "Identifier" && node.name === paramName) return true;
7479
+ if (node.type === "ObjectExpression") {
7480
+ return (node.properties ?? []).some((p) => {
7481
+ if (p.type !== "Property") return false;
7482
+ return referencesRawIdentifier(p.value, paramName, depth + 1);
7483
+ });
7484
+ }
7485
+ if (node.type === "ArrayExpression") {
7486
+ return (node.elements ?? []).some((el) => referencesRawIdentifier(el, paramName, depth + 1));
7487
+ }
7488
+ return false;
7489
+ }
7490
+ return {
7491
+ TryStatement(node) {
7492
+ if (!findElevenLabsFetchCall(node.block)) return;
7493
+ const handler = node.handler;
7494
+ const paramName = handler?.param?.type === "Identifier" ? handler.param.name : null;
7495
+ if (!paramName) return;
7496
+ function walk3(n, depth = 0) {
7497
+ if (!n || typeof n !== "object" || depth > 30) return;
7498
+ if (Array.isArray(n)) {
7499
+ for (const item of n) walk3(item, depth + 1);
7500
+ return;
7501
+ }
7502
+ if (isConsoleLogCall(n)) {
7503
+ const args = n.arguments ?? [];
7504
+ const loggedRaw = args.some((a) => referencesRawIdentifier(a, paramName));
7505
+ if (loggedRaw) {
7506
+ context.report({ node: n, messageId: "rawErrorLogged" });
7507
+ }
7508
+ }
7509
+ for (const key of Object.keys(n)) {
7510
+ if (key === "parent" || key === "loc" || key === "range") continue;
7511
+ const val = n[key];
7512
+ if (val && typeof val === "object") walk3(val, depth + 1);
7513
+ }
7514
+ }
7515
+ walk3(handler.body);
7516
+ }
7517
+ };
7518
+ }
7519
+ };
7520
+ var elevenlabsNoErrorObjectLoggingRule = rule82;
7521
+
7522
+ // src/providers/elevenlabs/rules/fetch-timeout-required.ts
7523
+ var rule83 = {
7524
+ meta: {
7525
+ type: "suggestion",
7526
+ docs: {
7527
+ description: "Fetch calls to the ElevenLabs API must set an abort timeout",
7528
+ category: "reliability",
7529
+ 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.",
7530
+ docsUrl: "https://elevenlabs.io/docs/eleven-agents/api-reference/conversations/get-signed-url",
7531
+ recommended: true
7532
+ },
7533
+ messages: {
7534
+ missingTimeout: "This fetch call to the ElevenLabs API has no abort signal/timeout \u2014 a slow or unresponsive API will hang the request indefinitely."
7535
+ }
7536
+ },
7537
+ create(context) {
7538
+ function hasSignalOption(optionsArg) {
7539
+ if (optionsArg?.type !== "ObjectExpression") return false;
7540
+ return (optionsArg.properties ?? []).some((p) => {
7541
+ if (p.type === "SpreadElement") return true;
7542
+ return p.type === "Property" && p.key?.type === "Identifier" && p.key.name === "signal";
7543
+ });
7544
+ }
7545
+ return {
7546
+ CallExpression(node) {
7547
+ if (node.callee?.type !== "Identifier" || node.callee.name !== "fetch") return;
7548
+ if (!isElevenLabsUrlArg(node.arguments?.[0])) return;
7549
+ const optionsArg = node.arguments?.[1];
7550
+ if (hasSignalOption(optionsArg)) return;
7551
+ context.report({ node, messageId: "missingTimeout" });
7552
+ }
7553
+ };
7554
+ }
7555
+ };
7556
+ var elevenlabsFetchTimeoutRequiredRule = rule83;
7557
+
7558
+ // src/providers/elevenlabs/rules/validate-agent-id-format.ts
7559
+ var rule84 = {
7560
+ meta: {
7561
+ type: "problem",
7562
+ docs: {
7563
+ description: "agentId must be format-validated, not just checked for existence, before use",
7564
+ category: "correctness",
7565
+ cwe: "CWE-20",
7566
+ 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.",
7567
+ docsUrl: "https://elevenlabs.io/docs/eleven-agents/customization/authentication",
7568
+ recommended: true
7569
+ },
7570
+ messages: {
7571
+ missingFormatValidation: "agentId is checked for existence but never validated against an expected format before being used in an ElevenLabs API request."
7572
+ }
7573
+ },
7574
+ create(context) {
7575
+ function isSearchParamsGetAgentId(node) {
7576
+ if (node?.type !== "CallExpression") return false;
7577
+ const callee = node.callee;
7578
+ if (callee?.type !== "MemberExpression") return false;
7579
+ if (callee.property?.type !== "Identifier" || callee.property.name !== "get") return false;
7580
+ const arg = node.arguments?.[0];
7581
+ return arg?.type === "Literal" && arg.value === "agentId";
7582
+ }
7583
+ function analyzeFunction(fnNode) {
7584
+ const declarators = [];
7585
+ collectVarDeclarators(fnNode.body, declarators);
7586
+ let agentIdVarName = null;
7587
+ let agentIdDeclaratorNode = null;
7588
+ for (const d of declarators) {
7589
+ const init = unwrapAwait(d.init);
7590
+ if (isSearchParamsGetAgentId(init) && d.id?.type === "Identifier") {
7591
+ agentIdVarName = d.id.name;
7592
+ agentIdDeclaratorNode = d;
7593
+ }
7594
+ }
7595
+ if (!agentIdVarName) {
7596
+ for (const param of fnNode.params ?? []) {
7597
+ if (param?.type === "Identifier" && param.name === "agentId") {
7598
+ agentIdVarName = param.name;
7599
+ agentIdDeclaratorNode = param;
7600
+ }
7601
+ }
7602
+ }
7603
+ if (!agentIdVarName) return;
7604
+ function isAgentIdMember(n) {
7605
+ return n?.type === "Identifier" && n.name === agentIdVarName;
7606
+ }
7607
+ function isFormatCheck(n, depth = 0) {
7608
+ if (!n || typeof n !== "object" || depth > 10) return false;
7609
+ if (Array.isArray(n)) return n.some((x) => isFormatCheck(x, depth + 1));
7610
+ if (n.type === "CallExpression" && n.callee?.type === "MemberExpression") {
7611
+ const propName2 = n.callee.property?.type === "Identifier" ? n.callee.property.name : null;
7612
+ if (propName2 === "test" && isAgentIdMember(n.arguments?.[0])) return true;
7613
+ if ((propName2 === "match" || propName2 === "matchAll") && isAgentIdMember(n.callee.object)) return true;
7614
+ }
7615
+ if (n.type === "LogicalExpression") {
7616
+ return isFormatCheck(n.left, depth + 1) || isFormatCheck(n.right, depth + 1);
7617
+ }
7618
+ if (n.type === "UnaryExpression") return isFormatCheck(n.argument, depth + 1);
7619
+ return false;
7620
+ }
7621
+ let formatGuardPos = null;
7622
+ let usagePos = null;
7623
+ function walk3(n, depth = 0) {
7624
+ if (!n || typeof n !== "object" || depth > 40) return;
7625
+ if (Array.isArray(n)) {
7626
+ for (const item of n) walk3(item, depth + 1);
7627
+ return;
7628
+ }
7629
+ if (n.type === "IfStatement" && isFormatCheck(n.test)) {
7630
+ const p = posOf(n);
7631
+ if (formatGuardPos === null || p < formatGuardPos) formatGuardPos = p;
7632
+ }
7633
+ if (n.type === "CallExpression" && n.callee?.type === "Identifier" && n.callee.name === "fetch" && isElevenLabsUrlArg(n.arguments?.[0])) {
7634
+ const urlArg = n.arguments[0];
7635
+ const containsAgentId = urlArg.type === "TemplateLiteral" && (urlArg.expressions ?? []).some((e) => isAgentIdMember(e));
7636
+ if (containsAgentId) {
7637
+ const p = posOf(n);
7638
+ if (usagePos === null || p < usagePos) usagePos = p;
7639
+ }
7640
+ }
7641
+ for (const key of Object.keys(n)) {
7642
+ if (key === "parent" || key === "loc" || key === "range") continue;
7643
+ const val = n[key];
7644
+ if (val && typeof val === "object") walk3(val, depth + 1);
7645
+ }
7646
+ }
7647
+ walk3(fnNode.body);
7648
+ if (usagePos === null) return;
7649
+ if (formatGuardPos !== null && formatGuardPos < usagePos) return;
7650
+ context.report({ node: agentIdDeclaratorNode, messageId: "missingFormatValidation" });
7651
+ }
7652
+ return {
7653
+ FunctionDeclaration(node) {
7654
+ analyzeFunction(node);
7655
+ },
7656
+ FunctionExpression(node) {
7657
+ analyzeFunction(node);
7658
+ },
7659
+ ArrowFunctionExpression(node) {
7660
+ analyzeFunction(node);
7661
+ }
7662
+ };
7663
+ }
7664
+ };
7665
+ var elevenlabsValidateAgentIdFormatRule = rule84;
7666
+
7667
+ // src/providers/elevenlabs/rules/secure-session-id-generation.ts
7668
+ var rule85 = {
7669
+ meta: {
7670
+ type: "problem",
7671
+ docs: {
7672
+ description: "Session ids must be generated with a cryptographically secure RNG",
7673
+ category: "security",
7674
+ cwe: "CWE-338",
7675
+ 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.',
7676
+ docsUrl: "https://elevenlabs.io/docs/eleven-api/guides/how-to/best-practices/security",
7677
+ recommended: true
7678
+ },
7679
+ messages: {
7680
+ insecureSessionId: "This session id is generated with Math.random(), which is not cryptographically secure and can be predicted. Use crypto.getRandomValues() instead."
7681
+ }
7682
+ },
7683
+ create(context) {
7684
+ function containsMathRandomCall(node, depth = 0) {
7685
+ if (!node || typeof node !== "object" || depth > 20) return false;
7686
+ if (Array.isArray(node)) return node.some((n) => containsMathRandomCall(n, depth + 1));
7687
+ 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") {
7688
+ return true;
7689
+ }
7690
+ for (const key of Object.keys(node)) {
7691
+ if (key === "parent" || key === "loc" || key === "range") continue;
7692
+ const val = node[key];
7693
+ if (val && typeof val === "object" && containsMathRandomCall(val, depth + 1)) return true;
7694
+ }
7695
+ return false;
7696
+ }
7697
+ function looksLikeSessionId(name) {
7698
+ return typeof name === "string" && /session/i.test(name);
7699
+ }
7700
+ function isSessionIdValue(init) {
7701
+ if (init?.type === "TemplateLiteral") {
7702
+ const firstQuasi = init.quasis?.[0]?.value?.raw ?? "";
7703
+ if (/session[-_]?/i.test(firstQuasi) && containsMathRandomCall(init)) return true;
7704
+ }
7705
+ if (init?.type === "BinaryExpression" && init.operator === "+") {
7706
+ const leftLiteral = init.left?.type === "Literal" && typeof init.left.value === "string";
7707
+ if (leftLiteral && /session[-_]?/i.test(init.left.value) && containsMathRandomCall(init)) return true;
7708
+ }
7709
+ return false;
7710
+ }
7711
+ return {
7712
+ VariableDeclarator(node) {
7713
+ const init = node.init;
7714
+ if (!init) return;
7715
+ 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;
7716
+ let valueNode = init;
7717
+ if (init.type === "CallExpression" && init.callee?.type === "Identifier" && init.callee.name === "useState" && init.arguments?.[0]?.type === "ArrowFunctionExpression") {
7718
+ const body = init.arguments[0].body;
7719
+ valueNode = body?.type === "BlockStatement" ? null : body;
7720
+ if (body?.type === "BlockStatement") {
7721
+ const returnStmt = (body.body ?? []).find((s) => s.type === "ReturnStatement");
7722
+ valueNode = returnStmt?.argument ?? null;
7723
+ }
7724
+ }
7725
+ if (!valueNode) return;
7726
+ if (!looksLikeSessionId(declaredName) && !isSessionIdValue(valueNode)) return;
7727
+ if (!containsMathRandomCall(valueNode)) return;
7728
+ context.report({ node, messageId: "insecureSessionId" });
7729
+ }
7730
+ };
7731
+ }
7732
+ };
7733
+ var elevenlabsSecureSessionIdGenerationRule = rule85;
7734
+
7735
+ // src/providers/elevenlabs/rules/conversation-error-recovery.ts
7736
+ var rule86 = {
7737
+ meta: {
7738
+ type: "suggestion",
7739
+ docs: {
7740
+ description: "A loading flag set before starting a conversation must be reset on failure",
7741
+ category: "reliability",
7742
+ 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.',
7743
+ docsUrl: "https://elevenlabs.io/docs/eleven-agents/libraries/java-script",
7744
+ recommended: true
7745
+ },
7746
+ messages: {
7747
+ 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."
7748
+ }
7749
+ },
7750
+ create(context) {
7751
+ function isSetterCallWithBoolean(n, setterName, expected) {
7752
+ if (n?.type !== "CallExpression") return false;
7753
+ if (n.callee?.type !== "Identifier" || n.callee.name !== setterName) return false;
7754
+ const arg = n.arguments?.[0];
7755
+ return arg?.type === "Literal" && arg.value === expected;
7756
+ }
7757
+ function containsCall(node, predicate, depth = 0) {
7758
+ if (!node || typeof node !== "object" || depth > 20) return false;
7759
+ if (Array.isArray(node)) return node.some((n) => containsCall(n, predicate, depth + 1));
7760
+ if (predicate(node)) return true;
7761
+ for (const key of Object.keys(node)) {
7762
+ if (key === "parent" || key === "loc" || key === "range") continue;
7763
+ const val = node[key];
7764
+ if (val && typeof val === "object" && containsCall(val, predicate, depth + 1)) return true;
7765
+ }
7766
+ return false;
7767
+ }
7768
+ function findLoadingSetterFromUseState(fnNode) {
7769
+ let setterName = null;
7770
+ function collect(n, depth = 0) {
7771
+ if (setterName || !n || typeof n !== "object" || depth > 20) return;
7772
+ if (Array.isArray(n)) {
7773
+ for (const item of n) collect(item, depth + 1);
7774
+ return;
7775
+ }
7776
+ 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") {
7777
+ setterName = n.id.elements[1].name;
7778
+ return;
7779
+ }
7780
+ for (const key of Object.keys(n)) {
7781
+ if (key === "parent" || key === "loc" || key === "range") continue;
7782
+ const val = n[key];
7783
+ if (val && typeof val === "object") collect(val, depth + 1);
7784
+ }
7785
+ }
7786
+ collect(fnNode);
7787
+ return setterName;
7788
+ }
7789
+ function isLoadingTrueStatement(stmt, setterName) {
7790
+ return stmt?.type === "ExpressionStatement" && isSetterCallWithBoolean(stmt.expression, setterName, true);
7791
+ }
7792
+ function analyzeFunction(fnNode) {
7793
+ const setterName = findLoadingSetterFromUseState(fnNode);
7794
+ if (!setterName) return;
7795
+ function walk3(n, depth = 0) {
7796
+ if (!n || typeof n !== "object" || depth > 30) return;
7797
+ if (Array.isArray(n)) {
7798
+ for (const item of n) walk3(item, depth + 1);
7799
+ return;
7800
+ }
7801
+ if (n.type === "BlockStatement" && Array.isArray(n.body)) {
7802
+ for (let i = 0; i < n.body.length - 1; i++) {
7803
+ if (!isLoadingTrueStatement(n.body[i], setterName)) continue;
7804
+ const tryNode = n.body[i + 1];
7805
+ if (tryNode?.type !== "TryStatement") continue;
7806
+ const handler = tryNode.handler;
7807
+ const finalizer = tryNode.finalizer;
7808
+ const finallyResets = finalizer && containsCall(finalizer, (x) => isSetterCallWithBoolean(x, setterName, false));
7809
+ const catchResets = handler && containsCall(handler.body, (x) => isSetterCallWithBoolean(x, setterName, false));
7810
+ if (!finallyResets && !catchResets) {
7811
+ context.report({ node: tryNode, messageId: "missingLoadingReset" });
7812
+ }
7813
+ }
7814
+ }
7815
+ for (const key of Object.keys(n)) {
7816
+ if (key === "parent" || key === "loc" || key === "range") continue;
7817
+ const val = n[key];
7818
+ if (val && typeof val === "object") walk3(val, depth + 1);
7819
+ }
7820
+ }
7821
+ walk3(fnNode.body);
7822
+ }
7823
+ return {
7824
+ FunctionDeclaration(node) {
7825
+ analyzeFunction(node);
7826
+ },
7827
+ FunctionExpression(node) {
7828
+ analyzeFunction(node);
7829
+ },
7830
+ ArrowFunctionExpression(node) {
7831
+ analyzeFunction(node);
7832
+ }
7833
+ };
7834
+ }
7835
+ };
7836
+ var elevenlabsConversationErrorRecoveryRule = rule86;
7837
+
7838
+ // src/providers/elevenlabs/rules/api-version-pinning.ts
7839
+ var rule87 = {
7840
+ meta: {
7841
+ type: "suggestion",
7842
+ docs: {
7843
+ description: "ElevenLabs API requests should pin an explicit API version header",
7844
+ category: "reliability",
7845
+ 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.",
7846
+ docsUrl: "https://elevenlabs.io/docs/eleven-api/resources/breaking-changes-policy",
7847
+ recommended: true
7848
+ },
7849
+ messages: {
7850
+ missingVersionHeader: "This fetch call to the ElevenLabs API has no explicit API version header \u2014 a future API change could silently alter behavior."
7851
+ }
7852
+ },
7853
+ create(context) {
7854
+ function hasVersionHeader(optionsArg) {
7855
+ if (optionsArg?.type !== "ObjectExpression") return false;
7856
+ const headersProp = (optionsArg.properties ?? []).find(
7857
+ (p) => p.type === "Property" && p.key?.type === "Identifier" && p.key.name === "headers"
7858
+ );
7859
+ if (!headersProp) return false;
7860
+ if (headersProp.value?.type !== "ObjectExpression") return true;
7861
+ return (headersProp.value.properties ?? []).some((p) => {
7862
+ if (p.type === "SpreadElement") return true;
7863
+ if (p.type !== "Property") return false;
7864
+ const keyName = p.key?.type === "Identifier" ? p.key.name : p.key?.type === "Literal" ? p.key.value : null;
7865
+ return typeof keyName === "string" && /version/i.test(keyName);
7866
+ });
7867
+ }
7868
+ return {
7869
+ CallExpression(node) {
7870
+ if (node.callee?.type !== "Identifier" || node.callee.name !== "fetch") return;
7871
+ if (!isElevenLabsUrlArg(node.arguments?.[0])) return;
7872
+ const optionsArg = node.arguments?.[1];
7873
+ if (hasVersionHeader(optionsArg)) return;
7874
+ context.report({ node, messageId: "missingVersionHeader" });
7875
+ }
7876
+ };
7877
+ }
7878
+ };
7879
+ var elevenlabsApiVersionPinningRule = rule87;
7880
+
7881
+ // src/providers/elevenlabs/rules/check-http-status-before-json.ts
7882
+ var rule88 = {
7883
+ meta: {
7884
+ type: "problem",
7885
+ docs: {
7886
+ description: "response.json() must not be called before checking the HTTP status of an ElevenLabs response",
7887
+ category: "correctness",
7888
+ 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.",
7889
+ docsUrl: "https://elevenlabs.io/docs/eleven-api/resources/errors",
7890
+ recommended: true
7891
+ },
7892
+ messages: {
7893
+ 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."
7894
+ }
7895
+ },
7896
+ create(context) {
7897
+ function analyzeFunction(fnNode) {
7898
+ const declarators = [];
7899
+ collectVarDeclarators(fnNode.body, declarators);
7900
+ const responseVarNames = [];
7901
+ for (const d of declarators) {
7902
+ const init = unwrapAwait(d.init);
7903
+ if (init?.type === "CallExpression" && init.callee?.type === "Identifier" && init.callee.name === "fetch" && isElevenLabsUrlArg(init.arguments?.[0]) && d.id?.type === "Identifier") {
7904
+ responseVarNames.push(d.id.name);
7905
+ }
7906
+ }
7907
+ for (const responseVarName of responseVarNames) {
7908
+ analyzeResponseVar(fnNode, responseVarName);
7909
+ }
7910
+ }
7911
+ function analyzeResponseVar(fnNode, responseVarName) {
7912
+ function isResponseStatusCheck(test) {
7913
+ if (!test) return false;
7914
+ if (test.type === "UnaryExpression" && test.operator === "!") return isResponseStatusCheck(test.argument);
7915
+ if (test.type === "MemberExpression" && test.object?.type === "Identifier" && test.object.name === responseVarName && test.property?.type === "Identifier" && test.property.name === "ok") {
7916
+ return true;
7917
+ }
7918
+ if (test.type === "BinaryExpression") {
7919
+ const sideIsStatus = (n) => n?.type === "MemberExpression" && n.object?.type === "Identifier" && n.object.name === responseVarName && n.property?.type === "Identifier" && n.property.name === "status";
7920
+ if (sideIsStatus(test.left) || sideIsStatus(test.right)) return true;
7921
+ }
7922
+ if (test.type === "LogicalExpression") {
7923
+ return isResponseStatusCheck(test.left) || isResponseStatusCheck(test.right);
7924
+ }
7925
+ return false;
7926
+ }
7927
+ function isResponseJsonCall2(n) {
7928
+ if (n?.type !== "CallExpression") return false;
7929
+ const callee = n.callee;
7930
+ if (callee?.type !== "MemberExpression") return false;
7931
+ if (callee.property?.type !== "Identifier" || callee.property.name !== "json") return false;
7932
+ return callee.object?.type === "Identifier" && callee.object.name === responseVarName;
7933
+ }
7934
+ let guardPos = null;
7935
+ let jsonCallNode = null;
7936
+ let jsonCallPos = null;
7937
+ function walk3(n, depth = 0) {
7938
+ if (!n || typeof n !== "object" || depth > 40) return;
7939
+ if (Array.isArray(n)) {
7940
+ for (const item of n) walk3(item, depth + 1);
7941
+ return;
7942
+ }
7943
+ if (n.type === "IfStatement" && isResponseStatusCheck(n.test)) {
7944
+ const p = posOf(n);
7945
+ if (guardPos === null || p < guardPos) guardPos = p;
7946
+ }
7947
+ if (isResponseJsonCall2(n)) {
7948
+ const p = posOf(n);
7949
+ if (jsonCallPos === null || p < jsonCallPos) {
7950
+ jsonCallPos = p;
7951
+ jsonCallNode = n;
7952
+ }
7953
+ }
7954
+ for (const key of Object.keys(n)) {
7955
+ if (key === "parent" || key === "loc" || key === "range") continue;
7956
+ const val = n[key];
7957
+ if (val && typeof val === "object") walk3(val, depth + 1);
7958
+ }
7959
+ }
7960
+ walk3(fnNode.body);
7961
+ if (jsonCallPos === null) return;
7962
+ if (guardPos !== null && guardPos < jsonCallPos) return;
7963
+ context.report({ node: jsonCallNode, messageId: "missingStatusCheck" });
7964
+ }
7965
+ return {
7966
+ FunctionDeclaration(node) {
7967
+ analyzeFunction(node);
7968
+ },
7969
+ FunctionExpression(node) {
7970
+ analyzeFunction(node);
7971
+ },
7972
+ ArrowFunctionExpression(node) {
7973
+ analyzeFunction(node);
7974
+ }
7975
+ };
7976
+ }
7977
+ };
7978
+ var elevenlabsCheckHttpStatusBeforeJsonRule = rule88;
7979
+
7980
+ // src/providers/elevenlabs/rules/conversation-cleanup-on-error.ts
7981
+ var END_METHODS = /* @__PURE__ */ new Set(["endSession", "endConversation"]);
7982
+ var rule89 = {
7983
+ meta: {
7984
+ type: "suggestion",
7985
+ docs: {
7986
+ description: "Ending a conversation must be wrapped in try/catch so a rejected call is handled",
7987
+ category: "reliability",
7988
+ 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.",
7989
+ docsUrl: "https://elevenlabs.io/docs/eleven-agents/libraries/java-script",
7990
+ recommended: true
7991
+ },
7992
+ messages: {
7993
+ 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."
7994
+ }
7995
+ },
7996
+ create(context) {
7997
+ function posEnd(n) {
7998
+ if (typeof n?.range?.[1] === "number") return n.range[1];
7999
+ const line = n?.loc?.end?.line ?? n?.loc?.start?.line ?? 0;
8000
+ const column = n?.loc?.end?.column ?? n?.loc?.start?.column ?? 0;
8001
+ return line * 1e6 + column;
8002
+ }
8003
+ function isConversationEndCall(node) {
8004
+ if (node?.type !== "CallExpression") return false;
8005
+ const callee = node.callee;
8006
+ if (callee?.type === "MemberExpression") {
8007
+ return callee.property?.type === "Identifier" && END_METHODS.has(callee.property.name);
8008
+ }
8009
+ return callee?.type === "Identifier" && END_METHODS.has(callee.name);
8010
+ }
8011
+ return {
8012
+ "Program:exit"(program2) {
8013
+ const tryRanges = [];
8014
+ const endCalls = [];
8015
+ function walk3(n, depth = 0) {
8016
+ if (!n || typeof n !== "object" || depth > 60) return;
8017
+ if (Array.isArray(n)) {
8018
+ for (const item of n) walk3(item, depth + 1);
8019
+ return;
8020
+ }
8021
+ if (n.type === "TryStatement" && n.block) {
8022
+ tryRanges.push([posOf(n.block), posEnd(n.block)]);
8023
+ }
8024
+ if (isConversationEndCall(n)) endCalls.push(n);
8025
+ for (const key of Object.keys(n)) {
8026
+ if (key === "parent" || key === "loc" || key === "range") continue;
8027
+ const val = n[key];
8028
+ if (val && typeof val === "object") walk3(val, depth + 1);
8029
+ }
8030
+ }
8031
+ walk3(program2);
8032
+ for (const call of endCalls) {
8033
+ const p = posOf(call);
8034
+ const covered = tryRanges.some(([start, end]) => p >= start && p <= end);
8035
+ if (!covered) {
8036
+ context.report({ node: call, messageId: "missingTryCatch" });
8037
+ }
8038
+ }
8039
+ }
8040
+ };
8041
+ }
8042
+ };
8043
+ var elevenlabsConversationCleanupOnErrorRule = rule89;
8044
+
8045
+ // src/providers/elevenlabs/rules/env-var-validation.ts
8046
+ var ELEVENLABS_ENV_VAR_PATTERN = /^(XI_API_KEY|ELEVENLABS_API_KEY)$/;
8047
+ var rule90 = {
8048
+ meta: {
8049
+ type: "suggestion",
8050
+ docs: {
8051
+ description: "ElevenLabs API key environment variables should be validated at module load, not only at request time",
8052
+ category: "correctness",
8053
+ 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.",
8054
+ docsUrl: "https://elevenlabs.io/docs/api-reference/authentication",
8055
+ recommended: true
8056
+ },
8057
+ messages: {
8058
+ 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."
8059
+ }
8060
+ },
8061
+ create(context) {
8062
+ function isElevenLabsEnvAccess(node) {
8063
+ if (node?.type !== "MemberExpression") return false;
8064
+ if (node.object?.type !== "MemberExpression") return false;
8065
+ const inner = node.object;
8066
+ if (inner.object?.type !== "Identifier" || inner.object.name !== "process") return false;
8067
+ if (inner.property?.type !== "Identifier" || inner.property.name !== "env") return false;
8068
+ const propName2 = node.property?.type === "Identifier" ? node.property.name : node.property?.value;
8069
+ return typeof propName2 === "string" && ELEVENLABS_ENV_VAR_PATTERN.test(propName2);
8070
+ }
8071
+ function isInsideFunction(node, ancestors) {
8072
+ return ancestors.some(
8073
+ (a) => a?.type === "FunctionDeclaration" || a?.type === "FunctionExpression" || a?.type === "ArrowFunctionExpression"
8074
+ );
8075
+ }
8076
+ return {
8077
+ "Program:exit"(program2) {
8078
+ let moduleScopeAccess = false;
8079
+ let handlerScopeAccess = false;
8080
+ function walk3(n, ancestors, depth = 0) {
8081
+ if (!n || typeof n !== "object" || depth > 60) return;
8082
+ if (Array.isArray(n)) {
8083
+ for (const item of n) walk3(item, ancestors, depth + 1);
8084
+ return;
8085
+ }
8086
+ if (isElevenLabsEnvAccess(n)) {
8087
+ if (isInsideFunction(n, ancestors)) {
8088
+ handlerScopeAccess = true;
8089
+ } else {
8090
+ moduleScopeAccess = true;
8091
+ }
8092
+ }
8093
+ const nextAncestors = n.type === "FunctionDeclaration" || n.type === "FunctionExpression" || n.type === "ArrowFunctionExpression" ? [...ancestors, n] : ancestors;
8094
+ for (const key of Object.keys(n)) {
8095
+ if (key === "parent" || key === "loc" || key === "range") continue;
8096
+ const val = n[key];
8097
+ if (val && typeof val === "object") walk3(val, nextAncestors, depth + 1);
8098
+ }
8099
+ }
8100
+ walk3(program2, []);
8101
+ if (handlerScopeAccess && !moduleScopeAccess) {
8102
+ context.report({ node: program2, messageId: "missingStartupValidation" });
8103
+ }
8104
+ }
8105
+ };
8106
+ }
8107
+ };
8108
+ var elevenlabsEnvVarValidationRule = rule90;
8109
+
8110
+ // src/providers/twilio/utils.ts
8111
+ var POST_METHOD_NAMES = /* @__PURE__ */ new Set(["post"]);
8112
+ var ROUTER_OBJECT_NAMES = /* @__PURE__ */ new Set(["server", "app", "router", "fastify"]);
8113
+ function isPostRouteRegistration(node) {
8114
+ if (node?.type !== "CallExpression") return false;
8115
+ const callee = node.callee;
8116
+ if (callee?.type !== "MemberExpression") return false;
8117
+ if (callee.property?.type !== "Identifier" || !POST_METHOD_NAMES.has(callee.property.name)) return false;
8118
+ return callee.object?.type === "Identifier" && ROUTER_OBJECT_NAMES.has(callee.object.name);
8119
+ }
8120
+ function findInSubtree(node, predicate, depth = 0) {
8121
+ if (!node || typeof node !== "object" || depth > 40) return null;
8122
+ if (Array.isArray(node)) {
8123
+ for (const n of node) {
8124
+ const found = findInSubtree(n, predicate, depth + 1);
8125
+ if (found) return found;
8126
+ }
8127
+ return null;
8128
+ }
8129
+ if (predicate(node)) return node;
8130
+ for (const key of Object.keys(node)) {
8131
+ if (key === "parent" || key === "loc" || key === "range") continue;
8132
+ const val = node[key];
8133
+ if (val && typeof val === "object") {
8134
+ const found = findInSubtree(val, predicate, depth + 1);
8135
+ if (found) return found;
8136
+ }
8137
+ }
8138
+ return null;
8139
+ }
8140
+ function referencesRequestBody(node) {
8141
+ return !!findInSubtree(node, (n) => {
8142
+ if (n?.type !== "MemberExpression") return false;
8143
+ if (n.property?.type !== "Identifier" || n.property.name !== "body") return false;
8144
+ return n.object?.type === "Identifier" && (n.object.name === "req" || n.object.name === "request");
8145
+ });
8146
+ }
8147
+ function collectVarDeclarators2(node, out, depth = 0) {
8148
+ if (!node || typeof node !== "object" || depth > 24) return;
8149
+ if (Array.isArray(node)) {
8150
+ for (const n of node) collectVarDeclarators2(n, out, depth + 1);
8151
+ return;
8152
+ }
8153
+ if (node.type === "VariableDeclarator") out.push(node);
8154
+ for (const key of Object.keys(node)) {
8155
+ if (key === "parent" || key === "loc" || key === "range") continue;
8156
+ const val = node[key];
8157
+ if (val && typeof val === "object") collectVarDeclarators2(val, out, depth + 1);
8158
+ }
8159
+ }
8160
+
8161
+ // src/providers/twilio/rules/validate-webhook-signature.ts
8162
+ var rule91 = {
8163
+ meta: {
8164
+ type: "problem",
8165
+ docs: {
8166
+ description: "Twilio webhook routes must validate the X-Twilio-Signature header before trusting the body",
8167
+ category: "security",
8168
+ cwe: "CWE-345",
8169
+ owasp: "A07:2021 Identification and Authentication Failures",
8170
+ 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.",
8171
+ docsUrl: "https://www.twilio.com/docs/usage/webhooks/webhooks-security",
8172
+ recommended: true
8173
+ },
8174
+ messages: {
8175
+ 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."
8176
+ }
8177
+ },
8178
+ create(context) {
8179
+ function isValidateRequestCall(n) {
8180
+ if (n?.type !== "CallExpression") return false;
8181
+ const callee = n.callee;
8182
+ if (callee?.type !== "MemberExpression") return false;
8183
+ return callee.property?.type === "Identifier" && callee.property.name === "validateRequest";
8184
+ }
8185
+ function isRequestValidatorConstruction(n) {
8186
+ return n?.type === "NewExpression" && n.callee?.type === "Identifier" && n.callee.name === "RequestValidator";
8187
+ }
8188
+ return {
8189
+ "Program:exit"(program2) {
8190
+ const hasValidation = !!findInSubtree(program2, isValidateRequestCall) || !!findInSubtree(program2, isRequestValidatorConstruction);
8191
+ if (hasValidation) return;
8192
+ const postRoutes = [];
8193
+ findInSubtree(program2, (n) => {
8194
+ if (isPostRouteRegistration(n) && referencesRequestBody(n)) postRoutes.push(n);
8195
+ return false;
8196
+ });
8197
+ for (const route of postRoutes) {
8198
+ context.report({ node: route, messageId: "missingSignatureValidation" });
8199
+ }
8200
+ }
8201
+ };
8202
+ }
8203
+ };
8204
+ var twilioValidateWebhookSignatureRule = rule91;
8205
+
8206
+ // src/providers/twilio/rules/taskrouter-attributes-match-consumer.ts
8207
+ var rule92 = {
8208
+ meta: {
8209
+ type: "problem",
8210
+ docs: {
8211
+ description: "TaskRouter Task attributes must include every field the reservation handler reads back out",
8212
+ category: "correctness",
8213
+ 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.",
8214
+ docsUrl: "https://www.twilio.com/docs/taskrouter/twiml-queue-calls",
8215
+ recommended: true
8216
+ },
8217
+ messages: {
8218
+ 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.'
8219
+ }
8220
+ },
8221
+ create(context) {
8222
+ function isTaskCall(n) {
8223
+ if (n?.type !== "CallExpression") return false;
8224
+ const callee = n.callee;
8225
+ if (callee?.type !== "MemberExpression") return false;
8226
+ return callee.property?.type === "Identifier" && callee.property.name === "task";
8227
+ }
8228
+ function extractAttributeKeys(taskCallNode) {
8229
+ const arg = taskCallNode.arguments?.[0];
8230
+ if (!arg) return null;
8231
+ 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") {
8232
+ return objectKeys(arg.arguments[0]);
8233
+ }
8234
+ if (arg.type === "TemplateLiteral") {
8235
+ const raw = arg.quasis.map((q) => q.value?.raw ?? "").join("");
8236
+ const keys = /* @__PURE__ */ new Set();
8237
+ for (const m of raw.matchAll(/"([a-zA-Z0-9_]+)"\s*:/g)) keys.add(m[1]);
8238
+ return keys;
8239
+ }
8240
+ if (arg.type === "Literal" && typeof arg.value === "string") {
8241
+ const keys = /* @__PURE__ */ new Set();
8242
+ for (const m of arg.value.matchAll(/"([a-zA-Z0-9_]+)"\s*:/g)) keys.add(m[1]);
8243
+ return keys;
8244
+ }
8245
+ return null;
8246
+ }
8247
+ function objectKeys(objExpr) {
8248
+ const keys = /* @__PURE__ */ new Set();
8249
+ for (const p of objExpr.properties ?? []) {
8250
+ if (p.type !== "Property") continue;
8251
+ const name = p.key?.type === "Identifier" ? p.key.name : p.key?.type === "Literal" ? p.key.value : null;
8252
+ if (typeof name === "string") keys.add(name);
8253
+ }
8254
+ return keys;
8255
+ }
8256
+ function isTaskAttributesJsonParse(n) {
8257
+ if (n?.type !== "CallExpression") return false;
8258
+ const callee = n.callee;
8259
+ if (callee?.type !== "MemberExpression") return false;
8260
+ if (callee.object?.type !== "Identifier" || callee.object.name !== "JSON") return false;
8261
+ if (callee.property?.type !== "Identifier" || callee.property.name !== "parse") return false;
8262
+ const arg = n.arguments?.[0];
8263
+ if (arg?.type !== "MemberExpression") return false;
8264
+ return arg.property?.type === "Identifier" && arg.property.name === "TaskAttributes";
8265
+ }
8266
+ function findConsumedFields(program2) {
8267
+ const fields = /* @__PURE__ */ new Set();
8268
+ const declarators = [];
8269
+ collectVarDeclarators2(program2, declarators);
8270
+ for (const d of declarators) {
8271
+ if (!isTaskAttributesJsonParse(d.init)) continue;
8272
+ if (d.id?.type === "ObjectPattern") {
8273
+ for (const p of d.id.properties ?? []) {
8274
+ if (p.type !== "Property") continue;
8275
+ const name = p.key?.type === "Identifier" ? p.key.name : null;
8276
+ if (name) fields.add(name);
8277
+ }
8278
+ }
8279
+ }
8280
+ return fields;
8281
+ }
8282
+ return {
8283
+ "Program:exit"(program2) {
8284
+ const consumedFields = findConsumedFields(program2);
8285
+ if (consumedFields.size === 0) return;
8286
+ const taskCalls = [];
8287
+ findInSubtree(program2, (n) => {
8288
+ if (isTaskCall(n)) taskCalls.push(n);
8289
+ return false;
8290
+ });
8291
+ for (const taskCall of taskCalls) {
8292
+ const producedKeys = extractAttributeKeys(taskCall);
8293
+ if (!producedKeys) continue;
8294
+ for (const field of consumedFields) {
8295
+ if (!producedKeys.has(field)) {
8296
+ context.report({ node: taskCall, messageId: "attributeMismatch", data: { field } });
8297
+ }
8298
+ }
8299
+ }
8300
+ }
8301
+ };
8302
+ }
8303
+ };
8304
+ var twilioTaskrouterAttributesMatchConsumerRule = rule92;
8305
+
8306
+ // src/providers/twilio/rules/enqueue-task-json-stringify.ts
8307
+ var rule93 = {
8308
+ meta: {
8309
+ type: "problem",
8310
+ docs: {
8311
+ description: "TaskRouter .task() attributes must be built with JSON.stringify(), not raw string interpolation",
8312
+ category: "security",
8313
+ cwe: "CWE-116",
8314
+ owasp: "CWE-91 XML/JSON Injection",
8315
+ 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.',
8316
+ docsUrl: "https://www.twilio.com/docs/api/errors/14221",
8317
+ recommended: true
8318
+ },
8319
+ messages: {
8320
+ 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.'
8321
+ }
8322
+ },
8323
+ create(context) {
8324
+ function isTaskCall(n) {
8325
+ if (n?.type !== "CallExpression") return false;
8326
+ const callee = n.callee;
8327
+ if (callee?.type !== "MemberExpression") return false;
8328
+ return callee.property?.type === "Identifier" && callee.property.name === "task";
8329
+ }
8330
+ function hasInterpolatedExpression(templateLiteral) {
8331
+ return (templateLiteral.expressions ?? []).length > 0;
8332
+ }
8333
+ return {
8334
+ CallExpression(node) {
8335
+ if (!isTaskCall(node)) return;
8336
+ const arg = node.arguments?.[0];
8337
+ if (!arg) return;
8338
+ if (arg.type === "TemplateLiteral" && hasInterpolatedExpression(arg)) {
8339
+ context.report({ node, messageId: "rawJsonInterpolation" });
8340
+ return;
8341
+ }
8342
+ if (arg.type === "BinaryExpression" && arg.operator === "+") {
8343
+ context.report({ node, messageId: "rawJsonInterpolation" });
8344
+ }
8345
+ }
8346
+ };
8347
+ }
8348
+ };
8349
+ var twilioEnqueueTaskJsonStringifyRule = rule93;
8350
+
8351
+ // src/providers/twilio/rules/media-streams-key-by-call-sid.ts
8352
+ var rule94 = {
8353
+ meta: {
8354
+ type: "problem",
8355
+ docs: {
8356
+ description: "Media Streams session maps must be keyed by callSid, not phone number",
8357
+ category: "reliability",
8358
+ 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.",
8359
+ docsUrl: "https://www.twilio.com/docs/voice/media-streams/websocket-messages",
8360
+ recommended: true
8361
+ },
8362
+ messages: {
8363
+ 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.`
8364
+ }
8365
+ },
8366
+ create(context) {
8367
+ function isFromLikeIdentifier(n) {
8368
+ if (n?.type === "Identifier" && n.name === "from") return "from";
8369
+ if (n?.type === "MemberExpression" && n.property?.type === "Identifier" && n.property.name === "from" && !n.computed) {
8370
+ return "from";
8371
+ }
8372
+ return null;
8373
+ }
8374
+ function isMapMutationCall(n, methodNames) {
8375
+ if (n?.type !== "CallExpression") return false;
8376
+ const callee = n.callee;
8377
+ if (callee?.type !== "MemberExpression") return false;
8378
+ return callee.property?.type === "Identifier" && methodNames.has(callee.property.name);
8379
+ }
8380
+ const SET_OR_GET = /* @__PURE__ */ new Set(["set", "get"]);
8381
+ return {
8382
+ "Program:exit"(program2) {
8383
+ let callSidAvailable = false;
8384
+ function scanForCallSid(n, depth = 0) {
8385
+ if (callSidAvailable || !n || typeof n !== "object" || depth > 60) return;
8386
+ if (Array.isArray(n)) {
8387
+ for (const item of n) scanForCallSid(item, depth + 1);
8388
+ return;
8389
+ }
8390
+ if (n.type === "Identifier" && n.name === "callSid") {
8391
+ callSidAvailable = true;
8392
+ return;
8393
+ }
8394
+ if (n.type === "MemberExpression" && !n.computed && n.property?.type === "Identifier" && n.property.name === "callSid") {
8395
+ callSidAvailable = true;
8396
+ return;
8397
+ }
8398
+ for (const key of Object.keys(n)) {
8399
+ if (key === "parent" || key === "loc" || key === "range") continue;
8400
+ const val = n[key];
8401
+ if (val && typeof val === "object") scanForCallSid(val, depth + 1);
8402
+ }
8403
+ }
8404
+ scanForCallSid(program2);
8405
+ if (!callSidAvailable) return;
8406
+ function walk3(n, depth = 0) {
8407
+ if (!n || typeof n !== "object" || depth > 60) return;
8408
+ if (Array.isArray(n)) {
8409
+ for (const item of n) walk3(item, depth + 1);
8410
+ return;
8411
+ }
8412
+ if (isMapMutationCall(n, SET_OR_GET)) {
8413
+ const firstArg = n.arguments?.[0];
8414
+ const key = isFromLikeIdentifier(firstArg);
8415
+ if (key) context.report({ node: n, messageId: "keyedByPhoneNumber", data: { key } });
8416
+ }
8417
+ for (const k of Object.keys(n)) {
8418
+ if (k === "parent" || k === "loc" || k === "range") continue;
8419
+ const val = n[k];
8420
+ if (val && typeof val === "object") walk3(val, depth + 1);
8421
+ }
8422
+ }
8423
+ walk3(program2);
8424
+ }
8425
+ };
8426
+ }
8427
+ };
8428
+ var twilioMediaStreamsKeyByCallSidRule = rule94;
8429
+
8430
+ // src/providers/twilio/rules/await-or-catch-rest-calls-in-event-handlers.ts
8431
+ var REST_RESOURCE_METHODS = /* @__PURE__ */ new Set(["create", "update", "remove", "fetch"]);
8432
+ var EVENT_REGISTRATION_METHODS = /* @__PURE__ */ new Set(["onStart", "onStop", "onMedia", "onConnected", "on"]);
8433
+ var rule95 = {
8434
+ meta: {
8435
+ type: "problem",
8436
+ docs: {
8437
+ description: "Twilio REST calls inside event-handler callbacks must be wrapped in try/catch",
8438
+ category: "reliability",
8439
+ 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.',
8440
+ docsUrl: "https://www.twilio.com/docs/usage/webhooks/webhooks-faq",
8441
+ recommended: true
8442
+ },
8443
+ messages: {
8444
+ 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."
8445
+ }
8446
+ },
8447
+ create(context) {
8448
+ function isAwaitedTwilioRestCall(n) {
8449
+ if (n?.type !== "AwaitExpression") return null;
8450
+ const call = n.argument;
8451
+ if (call?.type !== "CallExpression") return null;
8452
+ const callee = call.callee;
8453
+ if (callee?.type !== "MemberExpression") return null;
8454
+ const method = callee.property?.type === "Identifier" ? callee.property.name : null;
8455
+ if (!method || !REST_RESOURCE_METHODS.has(method)) return null;
8456
+ const obj = callee.object;
8457
+ if (obj?.type !== "MemberExpression") return null;
8458
+ const resource = obj.property?.type === "Identifier" ? obj.property.name : null;
8459
+ if (!resource) return null;
8460
+ const base = obj.object;
8461
+ const baseName = base?.type === "Identifier" ? base.name : null;
8462
+ if (!baseName || !/^(twilio\w*|client)$/i.test(baseName)) return null;
8463
+ return { resource, method };
8464
+ }
8465
+ function isEventHandlerRegistration(n) {
8466
+ if (n?.type !== "CallExpression") return false;
8467
+ const callee = n.callee;
8468
+ if (callee?.type !== "MemberExpression") return false;
8469
+ return callee.property?.type === "Identifier" && EVENT_REGISTRATION_METHODS.has(callee.property.name);
8470
+ }
8471
+ function isAsyncCallback(n) {
8472
+ return (n?.type === "ArrowFunctionExpression" || n?.type === "FunctionExpression") && n.async === true;
8473
+ }
8474
+ function posOf2(n) {
8475
+ if (typeof n?.range?.[0] === "number") return n.range[0];
8476
+ const line = n?.loc?.start?.line ?? 0;
8477
+ const column = n?.loc?.start?.column ?? 0;
8478
+ return line * 1e6 + column;
8479
+ }
8480
+ function posEnd(n) {
8481
+ if (typeof n?.range?.[1] === "number") return n.range[1];
8482
+ const line = n?.loc?.end?.line ?? n?.loc?.start?.line ?? 0;
8483
+ const column = n?.loc?.end?.column ?? n?.loc?.start?.column ?? 0;
8484
+ return line * 1e6 + column;
8485
+ }
8486
+ function isWithinTryBlock(node, root) {
8487
+ const tryRanges = [];
8488
+ function collectTryRanges(n, depth = 0) {
8489
+ if (!n || typeof n !== "object" || depth > 60) return;
8490
+ if (Array.isArray(n)) {
8491
+ for (const item of n) collectTryRanges(item, depth + 1);
8492
+ return;
8493
+ }
8494
+ if (n.type === "TryStatement" && n.block) {
8495
+ tryRanges.push([posOf2(n.block), posEnd(n.block)]);
8496
+ }
8497
+ for (const key of Object.keys(n)) {
8498
+ if (key === "parent" || key === "loc" || key === "range") continue;
8499
+ const val = n[key];
8500
+ if (val && typeof val === "object") collectTryRanges(val, depth + 1);
8501
+ }
8502
+ }
8503
+ collectTryRanges(root);
8504
+ const p = posOf2(node);
8505
+ return tryRanges.some(([start, end]) => p >= start && p <= end);
8506
+ }
8507
+ return {
8508
+ CallExpression(node) {
8509
+ if (!isEventHandlerRegistration(node)) return;
8510
+ const callback = node.arguments?.find((a) => isAsyncCallback(a));
8511
+ if (!callback) return;
8512
+ const restCalls = [];
8513
+ function collect(n, depth = 0) {
8514
+ if (!n || typeof n !== "object" || depth > 40) return;
8515
+ if (Array.isArray(n)) {
8516
+ for (const item of n) collect(item, depth + 1);
8517
+ return;
8518
+ }
8519
+ const info = isAwaitedTwilioRestCall(n);
8520
+ if (info) restCalls.push({ node: n, ...info });
8521
+ for (const key of Object.keys(n)) {
8522
+ if (key === "parent" || key === "loc" || key === "range") continue;
8523
+ const val = n[key];
8524
+ if (val && typeof val === "object") collect(val, depth + 1);
8525
+ }
8526
+ }
8527
+ collect(callback.body);
8528
+ for (const call of restCalls) {
8529
+ if (!isWithinTryBlock(call.node, callback.body)) {
8530
+ context.report({
8531
+ node: call.node,
8532
+ messageId: "missingTryCatch",
8533
+ data: { resource: call.resource, method: call.method }
8534
+ });
8535
+ }
8536
+ }
8537
+ }
8538
+ };
8539
+ }
8540
+ };
8541
+ var twilioAwaitOrCatchRestCallsInEventHandlersRule = rule95;
8542
+
8543
+ // src/providers/twilio/rules/use-twiml-builder-not-string-templates.ts
8544
+ var rule96 = {
8545
+ meta: {
8546
+ type: "suggestion",
8547
+ docs: {
8548
+ description: "TwiML responses must be built with the VoiceResponse builder, not raw XML template strings",
8549
+ category: "security",
8550
+ cwe: "CWE-91",
8551
+ 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.",
8552
+ docsUrl: "https://www.twilio.com/docs/voice/twiml/stream",
8553
+ recommended: true
8554
+ },
8555
+ messages: {
8556
+ 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()."
8557
+ }
8558
+ },
8559
+ create(context) {
8560
+ function looksLikeTwimlXml(raw) {
8561
+ return /<Response>|<Connect>|<Say>|<Stream\b|<Enqueue\b|<Dial\b/.test(raw);
8562
+ }
8563
+ return {
8564
+ TemplateLiteral(node) {
8565
+ if ((node.expressions ?? []).length === 0) return;
8566
+ const raw = (node.quasis ?? []).map((q) => q.value?.raw ?? "").join("");
8567
+ if (!looksLikeTwimlXml(raw)) return;
8568
+ context.report({ node, messageId: "rawTwimlTemplate" });
8569
+ }
8570
+ };
8571
+ }
8572
+ };
8573
+ var twilioUseTwimlBuilderNotStringTemplatesRule = rule96;
8574
+
8575
+ // src/providers/twilio/rules/media-streams-mark-pacing.ts
8576
+ var rule97 = {
8577
+ meta: {
8578
+ type: "suggestion",
8579
+ docs: {
8580
+ description: "Media Streams audio forwarding should use mark-based pacing to avoid buffer overflow",
8581
+ category: "reliability",
8582
+ 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.',
8583
+ docsUrl: "https://www.twilio.com/docs/api/errors/31931",
8584
+ recommended: true
8585
+ },
8586
+ messages: {
8587
+ 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)."
8588
+ }
8589
+ },
8590
+ create(context) {
8591
+ function isMediaSendCall(n) {
8592
+ if (n?.type !== "CallExpression") return false;
8593
+ const callee = n.callee;
8594
+ if (callee?.type !== "MemberExpression") return false;
8595
+ if (callee.property?.type !== "Identifier" || callee.property.name !== "send") return false;
8596
+ const arg = n.arguments?.[0];
8597
+ const argText = arg?.type === "ArrayExpression" && (arg.elements ?? []).some(
8598
+ (el) => el?.type === "MemberExpression" && el.property?.type === "Identifier" && el.property.name === "delta"
8599
+ ) || arg?.type === "Identifier" && /delta|payload|media/i.test(arg.name);
8600
+ return !!argText;
8601
+ }
8602
+ function hasIsLastOrMarkSignal(program2) {
8603
+ let found = false;
8604
+ function walk3(n, depth = 0) {
8605
+ if (found || !n || typeof n !== "object" || depth > 60) return;
8606
+ if (Array.isArray(n)) {
8607
+ for (const item of n) walk3(item, depth + 1);
8608
+ return;
8609
+ }
8610
+ if (typeof n.type === "string" && n.type.startsWith("TS")) return;
8611
+ if (n.type === "CallExpression") {
8612
+ const callee = n.callee;
8613
+ if (callee?.type === "MemberExpression" && callee.property?.type === "Identifier" && callee.property.name === "send") {
8614
+ const second = n.arguments?.[1];
8615
+ if (second?.type === "Literal" && second.value === true) {
8616
+ found = true;
8617
+ return;
8618
+ }
8619
+ if (second?.type === "Identifier" && /isLast/i.test(second.name)) {
8620
+ found = true;
8621
+ return;
8622
+ }
8623
+ }
8624
+ }
8625
+ if (n.type === "Identifier" && n.name === "isLast") {
8626
+ found = true;
8627
+ return;
8628
+ }
8629
+ for (const key of Object.keys(n)) {
8630
+ if (key === "parent" || key === "loc" || key === "range" || key === "typeAnnotation" || key === "returnType") continue;
8631
+ const val = n[key];
8632
+ if (val && typeof val === "object") walk3(val, depth + 1);
8633
+ }
8634
+ }
8635
+ walk3(program2);
8636
+ return found;
8637
+ }
8638
+ return {
8639
+ "Program:exit"(program2) {
8640
+ const sendCalls = [];
8641
+ findInSubtree(program2, (n) => {
8642
+ if (isMediaSendCall(n)) sendCalls.push(n);
8643
+ return false;
8644
+ });
8645
+ if (sendCalls.length === 0) return;
8646
+ if (hasIsLastOrMarkSignal(program2)) return;
8647
+ context.report({ node: sendCalls[0], messageId: "noMarkPacing" });
8648
+ }
8649
+ };
8650
+ }
8651
+ };
8652
+ var twilioMediaStreamsMarkPacingRule = rule97;
8653
+
8654
+ // src/providers/twilio/rules/validate-all-request-inputs.ts
8655
+ var rule98 = {
8656
+ meta: {
8657
+ type: "suggestion",
8658
+ docs: {
8659
+ description: "Fastify Twilio webhook routes must declare a schema for every request input they read",
8660
+ category: "correctness",
8661
+ 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.",
8662
+ docsUrl: "https://www.twilio.com/docs/voice/twiml/stream",
8663
+ recommended: true
8664
+ },
8665
+ messages: {
8666
+ 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.",
8667
+ missingSchemaEntirely: "This route reads req.body.{{field}} but declares no request schema at all \u2014 req.body is untyped and unvalidated before use."
8668
+ }
8669
+ },
8670
+ create(context) {
8671
+ function getOptionsArg(routeCall) {
8672
+ return routeCall.arguments?.length === 3 ? routeCall.arguments[1] : null;
8673
+ }
8674
+ function getHandlerArg(routeCall) {
8675
+ const args = routeCall.arguments ?? [];
8676
+ return args[args.length - 1];
8677
+ }
8678
+ function hasSchemaProperty(optionsArg, propName2) {
8679
+ if (optionsArg?.type !== "ObjectExpression") return false;
8680
+ const schemaProp = (optionsArg.properties ?? []).find(
8681
+ (p) => p.type === "Property" && p.key?.type === "Identifier" && p.key.name === "schema"
8682
+ );
8683
+ if (!schemaProp || schemaProp.value?.type !== "ObjectExpression") return false;
8684
+ return (schemaProp.value.properties ?? []).some(
8685
+ (p) => p.type === "Property" && p.key?.type === "Identifier" && p.key.name === propName2
8686
+ );
8687
+ }
8688
+ function hasAnySchema(optionsArg) {
8689
+ if (optionsArg?.type !== "ObjectExpression") return false;
8690
+ return (optionsArg.properties ?? []).some(
8691
+ (p) => p.type === "Property" && p.key?.type === "Identifier" && p.key.name === "schema"
8692
+ );
8693
+ }
8694
+ function findReadFields(handlerBody, objectNames) {
8695
+ const fields = /* @__PURE__ */ new Set();
8696
+ findInSubtree(handlerBody, (n) => {
8697
+ if (n?.type !== "MemberExpression") return false;
8698
+ const obj = n.object;
8699
+ if (obj?.type !== "MemberExpression") return false;
8700
+ if (obj.object?.type !== "Identifier" || obj.object.name !== "req" && obj.object.name !== "request") return false;
8701
+ if (obj.property?.type !== "Identifier" || !objectNames.has(obj.property.name)) return false;
8702
+ const field = n.property?.type === "Identifier" ? n.property.name : null;
8703
+ if (field) fields.add(field);
8704
+ return false;
8705
+ });
8706
+ return fields;
8707
+ }
8708
+ return {
8709
+ CallExpression(node) {
8710
+ if (!isPostRouteRegistration(node)) return;
8711
+ const optionsArg = getOptionsArg(node);
8712
+ const handler = getHandlerArg(node);
8713
+ if (!handler || handler.type !== "ArrowFunctionExpression" && handler.type !== "FunctionExpression") return;
8714
+ const queryFields = findReadFields(handler.body, /* @__PURE__ */ new Set(["query"]));
8715
+ const bodyFields = findReadFields(handler.body, /* @__PURE__ */ new Set(["body"]));
8716
+ const hasBodySchema = hasSchemaProperty(optionsArg, "body");
8717
+ const hasQuerystringSchema = hasSchemaProperty(optionsArg, "querystring");
8718
+ if (queryFields.size > 0 && hasBodySchema && !hasQuerystringSchema) {
8719
+ const field = [...queryFields][0];
8720
+ context.report({ node, messageId: "missingQuerystringSchema", data: { field } });
8721
+ }
8722
+ if (bodyFields.size > 0 && !hasAnySchema(optionsArg)) {
8723
+ const field = [...bodyFields][0];
8724
+ context.report({ node, messageId: "missingSchemaEntirely", data: { field } });
8725
+ }
8726
+ }
8727
+ };
8728
+ }
8729
+ };
8730
+ var twilioValidateAllRequestInputsRule = rule98;
8731
+
8732
+ // src/providers/twilio/rules/media-streams-mark-name-string.ts
8733
+ var rule99 = {
8734
+ meta: {
8735
+ type: "suggestion",
8736
+ docs: {
8737
+ description: "Media Streams mark.name must be a string, not a number",
8738
+ category: "correctness",
8739
+ 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.',
8740
+ docsUrl: "https://www.twilio.com/docs/voice/media-streams/websocket-messages",
8741
+ recommended: true
8742
+ },
8743
+ messages: {
8744
+ 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()))."
8745
+ }
8746
+ },
8747
+ create(context) {
8748
+ const NUMERIC_CALLS = /* @__PURE__ */ new Set(["Date.now", "performance.now", "Math.random", "Math.floor", "Math.round"]);
8749
+ function isNumericLooking(n) {
8750
+ if (!n) return false;
8751
+ if (n.type === "Literal" && typeof n.value === "number") return true;
8752
+ if (n.type === "CallExpression") {
8753
+ const callee = n.callee;
8754
+ if (callee?.type === "Identifier") return false;
8755
+ if (callee?.type === "MemberExpression" && callee.object?.type === "Identifier" && callee.property?.type === "Identifier") {
8756
+ return NUMERIC_CALLS.has(`${callee.object.name}.${callee.property.name}`);
8757
+ }
8758
+ return false;
8759
+ }
8760
+ return false;
8761
+ }
8762
+ function findMarkNameProperty(markObjExpr) {
8763
+ for (const p of markObjExpr.properties ?? []) {
8764
+ if (p.type !== "Property") continue;
8765
+ const keyName = p.key?.type === "Identifier" ? p.key.name : p.key?.type === "Literal" ? p.key.value : null;
8766
+ if (keyName === "name") return p;
8767
+ }
8768
+ return null;
8769
+ }
8770
+ return {
8771
+ Property(node) {
8772
+ const keyName = node.key?.type === "Identifier" ? node.key.name : node.key?.type === "Literal" ? node.key.value : null;
8773
+ if (keyName !== "mark") return;
8774
+ if (node.value?.type !== "ObjectExpression") return;
8775
+ const nameProp = findMarkNameProperty(node.value);
8776
+ if (!nameProp) return;
8777
+ if (!isNumericLooking(nameProp.value)) return;
8778
+ context.report({ node: nameProp, messageId: "markNameNotString" });
8779
+ }
8780
+ };
8781
+ }
8782
+ };
8783
+ var twilioMediaStreamsMarkNameStringRule = rule99;
8784
+
8785
+ // src/providers/openai-realtime/utils.ts
8786
+ function isOpenAIRealtimeUrlArg(node) {
8787
+ if (!node) return false;
8788
+ if (node.type === "Literal" && typeof node.value === "string") {
8789
+ return node.value.includes("api.openai.com/v1/realtime");
8790
+ }
8791
+ if (node.type === "TemplateLiteral") {
8792
+ return (node.quasis ?? []).some(
8793
+ (q) => typeof q?.value?.raw === "string" && q.value.raw.includes("api.openai.com/v1/realtime")
8794
+ );
8795
+ }
8796
+ return false;
8797
+ }
8798
+ function collectOpenAIRealtimeUrlVarNames(program2) {
8799
+ const names = /* @__PURE__ */ new Set();
8800
+ visit(program2);
8801
+ return names;
8802
+ function visit(node, depth = 0) {
8803
+ if (!node || typeof node !== "object" || depth > 40) return;
8804
+ if (Array.isArray(node)) {
8805
+ for (const n of node) visit(n, depth + 1);
8806
+ return;
8807
+ }
8808
+ if (node.type === "VariableDeclarator" && node.id?.type === "Identifier" && isOpenAIRealtimeUrlArg(node.init)) {
8809
+ names.add(node.id.name);
8810
+ }
8811
+ for (const key of Object.keys(node)) {
8812
+ if (key === "parent" || key === "loc" || key === "range") continue;
8813
+ const val = node[key];
8814
+ if (val && typeof val === "object") visit(val, depth + 1);
8815
+ }
8816
+ }
8817
+ }
8818
+ function isOpenAIRealtimeUrlNode(urlArgNode, urlVarNames) {
8819
+ if (isOpenAIRealtimeUrlArg(urlArgNode)) return true;
8820
+ return urlArgNode?.type === "Identifier" && urlVarNames.has(urlArgNode.name);
8821
+ }
8822
+ function isOpenAIRealtimeNewWebSocket(node, urlVarNames = /* @__PURE__ */ new Set()) {
8823
+ if (node?.type !== "NewExpression") return false;
8824
+ if (node.callee?.type !== "Identifier" || node.callee.name !== "WebSocket") return false;
8825
+ return isOpenAIRealtimeUrlNode(node.arguments?.[0], urlVarNames);
8826
+ }
8827
+ function findProperty4(objectExpression, propertyName) {
8828
+ if (objectExpression?.type !== "ObjectExpression") return null;
8829
+ for (const prop of objectExpression.properties ?? []) {
8830
+ if (prop?.type !== "Property") continue;
8831
+ const key = prop.key;
8832
+ const name = key?.type === "Identifier" ? key.name : key?.type === "Literal" ? key.value : null;
8833
+ if (name === propertyName) return prop;
8834
+ }
8835
+ return null;
8836
+ }
8837
+ function collectOpenAIRealtimeSocketVarNames(program2) {
8838
+ const urlVarNames = collectOpenAIRealtimeUrlVarNames(program2);
8839
+ const names = /* @__PURE__ */ new Set();
8840
+ visit(program2);
8841
+ return names;
8842
+ function visit(node, depth = 0) {
8843
+ if (!node || typeof node !== "object" || depth > 40) return;
8844
+ if (Array.isArray(node)) {
8845
+ for (const n of node) visit(n, depth + 1);
8846
+ return;
8847
+ }
8848
+ if (node.type === "VariableDeclarator" && node.id?.type === "Identifier" && isOpenAIRealtimeNewWebSocket(node.init, urlVarNames)) {
8849
+ names.add(node.id.name);
8850
+ }
8851
+ if (node.type === "AssignmentExpression" && node.operator === "=" && isOpenAIRealtimeNewWebSocket(node.right, urlVarNames)) {
8852
+ const left = node.left;
8853
+ if (left?.type === "Identifier") {
8854
+ names.add(left.name);
8855
+ } else if (left?.type === "MemberExpression" && left.property) {
8856
+ const propName2 = left.property.name;
8857
+ if (typeof propName2 === "string") names.add(propName2);
8858
+ }
8859
+ }
8860
+ for (const key of Object.keys(node)) {
8861
+ if (key === "parent" || key === "loc" || key === "range") continue;
8862
+ const val = node[key];
8863
+ if (val && typeof val === "object") visit(val, depth + 1);
8864
+ }
8865
+ }
8866
+ }
8867
+ function isTrackedSocketRef(objNode, socketVarNames) {
8868
+ if (objNode?.type === "Identifier") return socketVarNames.has(objNode.name);
8869
+ if (objNode?.type === "MemberExpression" && objNode.property) {
8870
+ const propName2 = objNode.property.name;
8871
+ return typeof propName2 === "string" && socketVarNames.has(propName2);
8872
+ }
8873
+ return false;
8874
+ }
8875
+ function isSocketOnCall(node, socketVarNames, eventName) {
8876
+ if (node?.type !== "CallExpression") return false;
8877
+ const callee = node.callee;
8878
+ if (callee?.type !== "MemberExpression") return false;
8879
+ const prop = callee.property;
8880
+ if (prop?.type !== "Identifier" || prop.name !== "on") return false;
8881
+ if (!isTrackedSocketRef(callee.object, socketVarNames)) return false;
8882
+ const firstArg = node.arguments?.[0];
8883
+ return firstArg?.type === "Literal" && firstArg.value === eventName;
8884
+ }
8885
+ function collectStringVarValues(program2) {
8886
+ const values = /* @__PURE__ */ new Map();
8887
+ visit(program2);
8888
+ return values;
8889
+ function visit(node, depth = 0) {
8890
+ if (!node || typeof node !== "object" || depth > 40) return;
8891
+ if (Array.isArray(node)) {
8892
+ for (const n of node) visit(n, depth + 1);
8893
+ return;
8894
+ }
8895
+ if (node.type === "VariableDeclarator" && node.id?.type === "Identifier") {
8896
+ const init = node.init;
8897
+ if (init?.type === "Literal" && typeof init.value === "string") {
8898
+ values.set(node.id.name, init.value);
8899
+ } else if (init?.type === "TemplateLiteral" && (init.expressions ?? []).length === 0) {
8900
+ values.set(node.id.name, (init.quasis ?? []).map((q) => q.value?.raw ?? "").join(""));
8901
+ }
8902
+ }
8903
+ for (const key of Object.keys(node)) {
8904
+ if (key === "parent" || key === "loc" || key === "range") continue;
8905
+ const val = node[key];
8906
+ if (val && typeof val === "object") visit(val, depth + 1);
8907
+ }
8908
+ }
8909
+ }
8910
+ function resolveStringValue(node, stringVarValues) {
8911
+ if (!node) return null;
8912
+ if (node.type === "Literal" && typeof node.value === "string") return node.value;
8913
+ if (node.type === "TemplateLiteral" && (node.expressions ?? []).length === 0) {
8914
+ return (node.quasis ?? []).map((q) => q.value?.raw ?? "").join("");
8915
+ }
8916
+ if (node.type === "Identifier" && stringVarValues.has(node.name)) {
8917
+ return stringVarValues.get(node.name) ?? null;
8918
+ }
8919
+ return null;
8920
+ }
8921
+
8922
+ // src/providers/openai-realtime/rules/migrate-beta-to-ga.ts
8923
+ var rule100 = {
8924
+ meta: {
8925
+ type: "problem",
8926
+ docs: {
8927
+ description: "OpenAI Realtime connections must not pin to the deprecated beta interface",
8928
+ category: "correctness",
8929
+ docsUrl: "https://developers.openai.com/api/docs/guides/realtime",
8930
+ 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.",
8931
+ recommended: true
8932
+ },
8933
+ messages: {
8934
+ betaHeaderPresent: "This OpenAI Realtime connection sends the deprecated 'OpenAI-Beta: realtime=v1' header instead of using the GA interface."
8935
+ },
8936
+ schema: []
8937
+ },
8938
+ create(context) {
8939
+ let urlVarNames = /* @__PURE__ */ new Set();
8940
+ return {
8941
+ Program(node) {
8942
+ urlVarNames = collectOpenAIRealtimeUrlVarNames(node);
8943
+ },
8944
+ NewExpression(node) {
8945
+ if (!isOpenAIRealtimeNewWebSocket(node, urlVarNames)) return;
8946
+ const optionsArg = node.arguments?.[1];
8947
+ if (optionsArg?.type !== "ObjectExpression") return;
8948
+ const headersProp = findProperty4(optionsArg, "headers");
8949
+ if (headersProp?.value?.type !== "ObjectExpression") return;
8950
+ const betaProp = findProperty4(headersProp.value, "OpenAI-Beta");
8951
+ if (!betaProp) return;
8952
+ const value = betaProp.value;
8953
+ const isRealtimeV1 = value?.type === "Literal" && typeof value.value === "string" && value.value.includes("realtime=v1");
8954
+ if (!isRealtimeV1) return;
8955
+ context.report({ node: betaProp, messageId: "betaHeaderPresent" });
8956
+ }
8957
+ };
8958
+ }
8959
+ };
8960
+ var openaiRealtimeMigrateBetaToGaRule = rule100;
8961
+
8962
+ // src/providers/openai-realtime/rules/no-log-raw-message-payloads.ts
8963
+ var LOG_METHOD_NAMES = /* @__PURE__ */ new Set(["info", "warn", "error", "log"]);
8964
+ function isLogCall(node) {
8965
+ if (node?.type !== "CallExpression") return { matches: false };
8966
+ const callee = node.callee;
8967
+ if (callee?.type !== "MemberExpression") return { matches: false };
8968
+ const prop = callee.property;
8969
+ if (prop?.type !== "Identifier" || !LOG_METHOD_NAMES.has(prop.name)) return { matches: false };
8970
+ return { matches: true, calleeProp: prop };
8971
+ }
8972
+ function referencesRawParam(argNode, paramName) {
8973
+ if (!argNode) return false;
8974
+ if (argNode.type === "Identifier" && argNode.name === paramName) return true;
8975
+ 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") {
8976
+ return true;
8977
+ }
8978
+ if (argNode.type === "TemplateLiteral") {
8979
+ return (argNode.expressions ?? []).some((expr) => referencesRawParam(expr, paramName));
8980
+ }
8981
+ return false;
8982
+ }
8983
+ function findCallExpressions(node, out, depth = 0) {
8984
+ if (!node || typeof node !== "object" || depth > 40) return;
8985
+ if (Array.isArray(node)) {
8986
+ for (const n of node) findCallExpressions(n, out, depth + 1);
8987
+ return;
8988
+ }
8989
+ if (node.type === "CallExpression") out.push(node);
8990
+ for (const key of Object.keys(node)) {
8991
+ if (key === "parent" || key === "loc" || key === "range") continue;
8992
+ const val = node[key];
8993
+ if (val && typeof val === "object") findCallExpressions(val, out, depth + 1);
8994
+ }
8995
+ }
8996
+ var rule101 = {
8997
+ meta: {
8998
+ type: "problem",
8999
+ docs: {
9000
+ description: "Raw OpenAI Realtime message payloads must not be logged verbatim",
9001
+ category: "security",
9002
+ cwe: "CWE-532",
9003
+ docsUrl: "https://developers.openai.com/api/docs/guides/realtime",
9004
+ 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.",
9005
+ recommended: true
9006
+ },
9007
+ messages: {
9008
+ rawPayloadLogged: "A raw OpenAI Realtime message is logged verbatim here, which can include live call audio or transcript content."
9009
+ },
9010
+ schema: []
9011
+ },
9012
+ create(context) {
9013
+ let socketVarNames = /* @__PURE__ */ new Set();
9014
+ return {
9015
+ Program(node) {
9016
+ socketVarNames = collectOpenAIRealtimeSocketVarNames(node);
9017
+ },
9018
+ CallExpression(node) {
9019
+ if (socketVarNames.size === 0) return;
9020
+ if (!isSocketOnCall(node, socketVarNames, "message")) return;
9021
+ const handler = node.arguments?.[1];
9022
+ if (handler?.type !== "ArrowFunctionExpression" && handler?.type !== "FunctionExpression") return;
9023
+ const param = handler.params?.[0];
9024
+ if (param?.type !== "Identifier") return;
9025
+ const paramName = param.name;
9026
+ const calls = [];
9027
+ findCallExpressions(handler.body, calls);
9028
+ for (const call of calls) {
9029
+ const { matches } = isLogCall(call);
9030
+ if (!matches) continue;
9031
+ const hasRawArg = (call.arguments ?? []).some((arg) => referencesRawParam(arg, paramName));
9032
+ if (hasRawArg) {
9033
+ context.report({ node: call, messageId: "rawPayloadLogged" });
9034
+ }
9035
+ }
9036
+ }
9037
+ };
9038
+ }
9039
+ };
9040
+ var openaiRealtimeNoLogRawMessagePayloadsRule = rule101;
9041
+
9042
+ // src/providers/openai-realtime/rules/handle-error-server-event.ts
9043
+ function isTypeMemberExpression(node) {
9044
+ return node?.type === "MemberExpression" && node.property?.type === "Identifier" && node.property.name === "type";
9045
+ }
9046
+ function collectTypeComparisonLiterals(node, out, depth = 0) {
9047
+ if (!node || typeof node !== "object" || depth > 60) return;
9048
+ if (Array.isArray(node)) {
9049
+ for (const n of node) collectTypeComparisonLiterals(n, out, depth + 1);
9050
+ return;
9051
+ }
9052
+ if (node.type === "BinaryExpression" && (node.operator === "===" || node.operator === "==")) {
9053
+ if (isTypeMemberExpression(node.left) && node.right?.type === "Literal" && typeof node.right.value === "string") {
9054
+ out.add(node.right.value);
9055
+ }
9056
+ if (isTypeMemberExpression(node.right) && node.left?.type === "Literal" && typeof node.left.value === "string") {
9057
+ out.add(node.left.value);
9058
+ }
9059
+ }
9060
+ if (node.type === "SwitchStatement" && isTypeMemberExpression(node.discriminant)) {
9061
+ for (const c of node.cases ?? []) {
9062
+ if (c?.test?.type === "Literal" && typeof c.test.value === "string") out.add(c.test.value);
9063
+ }
9064
+ }
9065
+ for (const key of Object.keys(node)) {
9066
+ if (key === "parent" || key === "loc" || key === "range") continue;
9067
+ const val = node[key];
9068
+ if (val && typeof val === "object") collectTypeComparisonLiterals(val, out, depth + 1);
9069
+ }
9070
+ }
9071
+ var rule102 = {
9072
+ meta: {
9073
+ type: "problem",
9074
+ docs: {
9075
+ description: 'OpenAI Realtime message handlers must check for the API-level "error" server event',
9076
+ category: "reliability",
9077
+ docsUrl: "https://developers.openai.com/api/reference/resources/realtime/server-events",
9078
+ 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.",
9079
+ recommended: true
9080
+ },
9081
+ messages: {
9082
+ missingErrorBranch: "This Realtime message handler branches on event types but never checks for message.type === 'error'."
9083
+ },
9084
+ schema: []
9085
+ },
9086
+ create(context) {
9087
+ let socketVarNames = /* @__PURE__ */ new Set();
9088
+ return {
9089
+ Program(node) {
9090
+ socketVarNames = collectOpenAIRealtimeSocketVarNames(node);
9091
+ },
9092
+ CallExpression(node) {
9093
+ if (socketVarNames.size === 0) return;
9094
+ if (!isSocketOnCall(node, socketVarNames, "message")) return;
9095
+ const handler = node.arguments?.[1];
9096
+ if (handler?.type !== "ArrowFunctionExpression" && handler?.type !== "FunctionExpression") return;
9097
+ const literals = /* @__PURE__ */ new Set();
9098
+ collectTypeComparisonLiterals(handler.body, literals);
9099
+ if (literals.size === 0) return;
9100
+ if (!literals.has("error")) {
9101
+ context.report({ node, messageId: "missingErrorBranch" });
9102
+ }
9103
+ }
9104
+ };
9105
+ }
9106
+ };
9107
+ var openaiRealtimeHandleErrorServerEventRule = rule102;
9108
+
9109
+ // src/providers/openai-realtime/rules/reconnect-on-drop.ts
9110
+ var RECONNECT_NAME_PATTERN = /reconnect/i;
9111
+ function hasReconnectAttempt(node, depth = 0) {
9112
+ if (!node || typeof node !== "object" || depth > 60) return false;
9113
+ if (Array.isArray(node)) {
9114
+ return node.some((n) => hasReconnectAttempt(n, depth + 1));
9115
+ }
9116
+ if (node.type === "NewExpression" && node.callee?.type === "Identifier" && node.callee.name === "WebSocket") {
9117
+ return true;
9118
+ }
9119
+ if (node.type === "CallExpression") {
9120
+ const callee = node.callee;
9121
+ const calleeName = callee?.type === "Identifier" ? callee.name : callee?.type === "MemberExpression" && callee.property?.type === "Identifier" ? callee.property.name : null;
9122
+ if (typeof calleeName === "string" && RECONNECT_NAME_PATTERN.test(calleeName)) return true;
9123
+ }
9124
+ for (const key of Object.keys(node)) {
9125
+ if (key === "parent" || key === "loc" || key === "range") continue;
9126
+ const val = node[key];
9127
+ if (val && typeof val === "object" && hasReconnectAttempt(val, depth + 1)) return true;
9128
+ }
9129
+ return false;
9130
+ }
9131
+ var rule103 = {
9132
+ meta: {
9133
+ type: "problem",
9134
+ docs: {
9135
+ description: "A dropped OpenAI Realtime connection must be retried, not just logged",
9136
+ category: "reliability",
9137
+ docsUrl: "https://developers.openai.com/api/docs/guides/realtime",
9138
+ 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.",
9139
+ recommended: true
9140
+ },
9141
+ messages: {
9142
+ noReconnectAttempt: "This Realtime socket's close handler only logs and never attempts to reconnect."
9143
+ },
9144
+ schema: []
9145
+ },
9146
+ create(context) {
9147
+ let socketVarNames = /* @__PURE__ */ new Set();
9148
+ return {
9149
+ Program(node) {
9150
+ socketVarNames = collectOpenAIRealtimeSocketVarNames(node);
9151
+ },
9152
+ CallExpression(node) {
9153
+ if (socketVarNames.size === 0) return;
9154
+ if (!isSocketOnCall(node, socketVarNames, "close")) return;
9155
+ const handler = node.arguments?.[1];
9156
+ if (handler?.type !== "ArrowFunctionExpression" && handler?.type !== "FunctionExpression") return;
9157
+ if (!hasReconnectAttempt(handler.body)) {
9158
+ context.report({ node, messageId: "noReconnectAttempt" });
9159
+ }
9160
+ }
9161
+ };
9162
+ }
9163
+ };
9164
+ var openaiRealtimeReconnectOnDropRule = rule103;
9165
+
9166
+ // src/providers/openai-realtime/rules/avoid-dated-preview-snapshots.ts
9167
+ var DATED_PREVIEW_MODEL_PATTERN = /model=[^&'"`]*-preview-\d{4}-\d{2}-\d{2}/;
9168
+ var rule104 = {
9169
+ meta: {
9170
+ type: "suggestion",
9171
+ docs: {
9172
+ description: "OpenAI Realtime connections should not pin to a dated preview model snapshot",
9173
+ category: "correctness",
9174
+ docsUrl: "https://developers.openai.com/api/reference/resources/realtime/subresources/client_secrets",
9175
+ 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.",
9176
+ recommended: true
9177
+ },
9178
+ messages: {
9179
+ datedPreviewSnapshot: "This Realtime connection is pinned to a dated preview model snapshot instead of the GA alias."
9180
+ },
9181
+ schema: []
9182
+ },
9183
+ create(context) {
9184
+ let stringVarValues = /* @__PURE__ */ new Map();
9185
+ let urlVarNames = /* @__PURE__ */ new Set();
9186
+ return {
9187
+ Program(node) {
9188
+ stringVarValues = collectStringVarValues(node);
9189
+ urlVarNames = collectOpenAIRealtimeUrlVarNames(node);
9190
+ },
9191
+ NewExpression(node) {
9192
+ if (!isOpenAIRealtimeNewWebSocket(node, urlVarNames)) return;
9193
+ const urlArg = node.arguments?.[0];
9194
+ const urlString = resolveStringValue(urlArg, stringVarValues);
9195
+ if (!urlString) return;
9196
+ if (DATED_PREVIEW_MODEL_PATTERN.test(urlString)) {
9197
+ context.report({ node: urlArg, messageId: "datedPreviewSnapshot" });
9198
+ }
9199
+ }
9200
+ };
9201
+ }
9202
+ };
9203
+ var openaiRealtimeAvoidDatedPreviewSnapshotsRule = rule104;
9204
+
9205
+ // src/providers/openai-realtime/rules/verify-deprecated-session-fields.ts
9206
+ var rule105 = {
9207
+ meta: {
9208
+ type: "suggestion",
9209
+ docs: {
9210
+ description: "OpenAI Realtime session.update payloads should not rely on an unverified 'temperature' field",
9211
+ category: "correctness",
9212
+ docsUrl: "https://developers.openai.com/api/reference/resources/realtime/subresources/client_secrets",
9213
+ 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.",
9214
+ recommended: true
9215
+ },
9216
+ messages: {
9217
+ unverifiedTemperatureField: "This session.update payload sets 'temperature', a field not documented in the current GA Realtime sessions schema."
9218
+ },
9219
+ schema: []
9220
+ },
9221
+ create(context) {
9222
+ return {
9223
+ ObjectExpression(node) {
9224
+ const typeProp = findProperty4(node, "type");
9225
+ const typeValue = typeProp?.value;
9226
+ const isSessionUpdate = typeValue?.type === "Literal" && typeof typeValue.value === "string" && typeValue.value === "session.update";
9227
+ if (!isSessionUpdate) return;
9228
+ const sessionProp = findProperty4(node, "session");
9229
+ if (sessionProp?.value?.type !== "ObjectExpression") return;
9230
+ const temperatureProp = findProperty4(sessionProp.value, "temperature");
9231
+ if (!temperatureProp) return;
9232
+ context.report({ node: temperatureProp, messageId: "unverifiedTemperatureField" });
9233
+ }
9234
+ };
9235
+ }
9236
+ };
9237
+ var openaiRealtimeVerifyDeprecatedSessionFieldsRule = rule105;
9238
+
9239
+ // src/providers/openai-realtime/rules/buffer-audio-until-session-ready.ts
9240
+ var QUEUE_CALL_NAME_PATTERN = /^(push|unshift|enqueue|queue)$/i;
9241
+ function readyStateOpenCheckKind(test) {
9242
+ if (test?.type !== "BinaryExpression") return null;
9243
+ const isEq = test.operator === "===" || test.operator === "==";
9244
+ const isNeq = test.operator === "!==" || test.operator === "!=";
9245
+ if (!isEq && !isNeq) return null;
9246
+ const isReadyStateMember = (n) => n?.type === "MemberExpression" && n.property?.type === "Identifier" && n.property.name === "readyState";
9247
+ const isOpenValue = (n) => n?.type === "MemberExpression" && n.property?.type === "Identifier" && n.property.name === "OPEN" || n?.type === "Literal" && n.value === 1;
9248
+ const matches = isReadyStateMember(test.left) && isOpenValue(test.right) || isReadyStateMember(test.right) && isOpenValue(test.left);
9249
+ if (!matches) return null;
9250
+ return isEq ? "is-open" : "is-not-open";
9251
+ }
9252
+ function hasQueueCall(node, depth = 0) {
9253
+ if (!node || typeof node !== "object" || depth > 40) return false;
9254
+ if (Array.isArray(node)) return node.some((n) => hasQueueCall(n, depth + 1));
9255
+ if (node.type === "CallExpression") {
9256
+ const callee = node.callee;
9257
+ const calleeName = callee?.type === "MemberExpression" && callee.property?.type === "Identifier" ? callee.property.name : callee?.type === "Identifier" ? callee.name : null;
9258
+ if (typeof calleeName === "string" && QUEUE_CALL_NAME_PATTERN.test(calleeName)) return true;
9259
+ }
9260
+ for (const key of Object.keys(node)) {
9261
+ if (key === "parent" || key === "loc" || key === "range") continue;
9262
+ const val = node[key];
9263
+ if (val && typeof val === "object" && hasQueueCall(val, depth + 1)) return true;
9264
+ }
9265
+ return false;
9266
+ }
9267
+ var rule106 = {
9268
+ meta: {
9269
+ type: "suggestion",
9270
+ docs: {
9271
+ description: "Audio sent before an OpenAI Realtime socket is open must be buffered, not dropped",
9272
+ category: "reliability",
9273
+ docsUrl: "https://developers.openai.com/api/reference/resources/realtime/client-events",
9274
+ 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.",
9275
+ recommended: true
9276
+ },
9277
+ messages: {
9278
+ audioDroppedNotBuffered: "Audio sent while this Realtime socket is not yet open is dropped here instead of being buffered and flushed once open."
9279
+ },
9280
+ schema: []
9281
+ },
9282
+ create(context) {
9283
+ return {
9284
+ IfStatement(node) {
9285
+ const kind = readyStateOpenCheckKind(node.test);
9286
+ if (!kind) return;
9287
+ const notOpenBranch = kind === "is-open" ? node.alternate : node.consequent;
9288
+ if (!notOpenBranch) return;
9289
+ if (hasQueueCall(notOpenBranch)) return;
9290
+ context.report({ node: notOpenBranch, messageId: "audioDroppedNotBuffered" });
9291
+ }
9292
+ };
9293
+ }
9294
+ };
9295
+ var openaiRealtimeBufferAudioUntilSessionReadyRule = rule106;
9296
+
9297
+ // src/providers/openai-realtime/rules/send-safety-identifier.ts
9298
+ var rule107 = {
9299
+ meta: {
9300
+ type: "suggestion",
9301
+ docs: {
9302
+ description: "OpenAI Realtime connections should send an OpenAI-Safety-Identifier header",
9303
+ category: "security",
9304
+ docsUrl: "https://developers.openai.com/api/docs/guides/realtime",
9305
+ 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.",
9306
+ recommended: true
9307
+ },
9308
+ messages: {
9309
+ missingSafetyIdentifier: "This OpenAI Realtime WebSocket connection does not send an OpenAI-Safety-Identifier header."
9310
+ },
9311
+ schema: []
9312
+ },
9313
+ create(context) {
9314
+ let urlVarNames = /* @__PURE__ */ new Set();
9315
+ return {
9316
+ Program(node) {
9317
+ urlVarNames = collectOpenAIRealtimeUrlVarNames(node);
9318
+ },
9319
+ NewExpression(node) {
9320
+ if (!isOpenAIRealtimeNewWebSocket(node, urlVarNames)) return;
9321
+ const optionsArg = node.arguments?.[1];
9322
+ const headersProp = optionsArg?.type === "ObjectExpression" ? findProperty4(optionsArg, "headers") : null;
9323
+ const headersObj = headersProp?.value?.type === "ObjectExpression" ? headersProp.value : null;
9324
+ const safetyIdProp = headersObj ? findProperty4(headersObj, "OpenAI-Safety-Identifier") : null;
9325
+ if (safetyIdProp) return;
9326
+ context.report({ node: headersObj ?? optionsArg ?? node, messageId: "missingSafetyIdentifier" });
9327
+ }
9328
+ };
9329
+ }
9330
+ };
9331
+ var openaiRealtimeSendSafetyIdentifierRule = rule107;
9332
+
9333
+ // src/providers/openai-realtime/rules/transcription-model-choice.ts
9334
+ var rule108 = {
9335
+ meta: {
9336
+ type: "suggestion",
9337
+ docs: {
9338
+ description: "OpenAI Realtime sessions should not configure 'whisper-1' for input transcription",
9339
+ category: "correctness",
9340
+ docsUrl: "https://developers.openai.com/api/docs/guides/realtime-transcription",
9341
+ 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.",
9342
+ recommended: true
9343
+ },
9344
+ messages: {
9345
+ nonStreamingTranscriptionModel: "This session's input transcription is configured with 'whisper-1', which is not natively streaming and not optimized for realtime sessions."
9346
+ },
9347
+ schema: []
9348
+ },
9349
+ create(context) {
9350
+ return {
9351
+ ObjectExpression(node) {
9352
+ const typeProp = findProperty4(node, "type");
9353
+ const typeValue = typeProp?.value;
9354
+ const isSessionUpdate = typeValue?.type === "Literal" && typeof typeValue.value === "string" && typeValue.value === "session.update";
9355
+ if (!isSessionUpdate) return;
9356
+ const sessionProp = findProperty4(node, "session");
9357
+ if (sessionProp?.value?.type !== "ObjectExpression") return;
9358
+ let transcriptionProp = findProperty4(sessionProp.value, "input_audio_transcription");
9359
+ if (!transcriptionProp) {
9360
+ const audioProp = findProperty4(sessionProp.value, "audio");
9361
+ const inputProp = audioProp?.value?.type === "ObjectExpression" ? findProperty4(audioProp.value, "input") : null;
9362
+ transcriptionProp = inputProp?.value?.type === "ObjectExpression" ? findProperty4(inputProp.value, "transcription") : null;
9363
+ }
9364
+ if (transcriptionProp?.value?.type !== "ObjectExpression") return;
9365
+ const modelProp = findProperty4(transcriptionProp.value, "model");
9366
+ const modelValue = modelProp?.value;
9367
+ const isWhisper1 = modelValue?.type === "Literal" && typeof modelValue.value === "string" && modelValue.value === "whisper-1";
9368
+ if (!isWhisper1) return;
9369
+ context.report({ node: modelProp, messageId: "nonStreamingTranscriptionModel" });
9370
+ }
9371
+ };
9372
+ }
9373
+ };
9374
+ var openaiRealtimeTranscriptionModelChoiceRule = rule108;
9375
+
9376
+ // src/plugin/index.ts
9377
+ var plugin = {
9378
+ meta: { name: PLUGIN_NAME, version: "0.0.1" },
9379
+ rules: {
9380
+ "resend-webhook-signature": resendWebhookSignatureRule,
9381
+ "resend-api-key-hardcoded": resendApiKeyHardcodedRule,
9382
+ "resend-api-key-in-client-bundle": resendApiKeyInClientBundleRule,
9383
+ "resend-marketing-via-batch-send": resendMarketingViaBatchSendRule,
9384
+ "resend-marketing-missing-unsubscribe": resendMarketingMissingUnsubscribeRule,
9385
+ "resend-test-domain-in-production-path": resendTestDomainInProductionPathRule,
9386
+ "resend-from-address-not-friendly-format": resendFromAddressNotFriendlyFormatRule,
9387
+ "resend-batch-size-not-enforced": resendBatchSizeNotEnforcedRule,
9388
+ "resend-missing-idempotency-key": resendMissingIdempotencyKeyRule,
9389
+ "resend-no-error-code-mapping": resendNoErrorCodeMappingRule,
9390
+ "resend-webhook-no-idempotency": resendWebhookNoIdempotencyRule,
9391
+ "resend-missing-tags": resendMissingTagsRule,
9392
+ "resend-request-id-not-logged": resendRequestIdNotLoggedRule,
9393
+ "supabase-scope-queries-by-tenant-column": supabaseScopeQueriesByTenantColumnRule,
9394
+ "supabase-validate-uuid-columns": supabaseValidateUuidColumnsRule,
9395
+ "supabase-order-by-timestamp-not-identity": supabaseOrderByTimestampNotIdentityRule,
9396
+ "supabase-consistent-input-length-limits": supabaseConsistentInputLengthLimitsRule,
9397
+ "supabase-idempotent-mutations": supabaseIdempotentMutationsRule,
9398
+ "supabase-fail-fast-env-validation": supabaseFailFastEnvValidationRule,
9399
+ "supabase-no-user-metadata-authz": supabaseNoUserMetadataAuthzRule,
9400
+ "supabase-single-without-error-check": supabaseSingleWithoutErrorCheckRule,
9401
+ "supabase-non-atomic-replace-pattern": supabaseNonAtomicReplacePatternRule,
9402
+ "supabase-unchecked-mutation-error": supabaseUncheckedMutationErrorRule,
9403
+ "supabase-realtime-missing-filter": supabaseRealtimeMissingFilterRule,
9404
+ "supabase-storage-error-not-surfaced": supabaseStorageErrorNotSurfacedRule,
9405
+ "auth0-required-audience-validation": auth0RequiredAudienceValidationRule,
9406
+ "auth0-no-account-link-without-verified-email": auth0NoAccountLinkWithoutVerifiedEmailRule,
9407
+ "auth0-dead-claim-verification-check": auth0DeadClaimVerificationCheckRule,
9408
+ "auth0-jwks-refresh-on-unknown-kid": auth0JwksRefreshOnUnknownKidRule,
9409
+ "firebase-missing-app-check": firebaseMissingAppCheckRule,
9410
+ "firebase-unhandled-auth-popup-rejection": firebaseUnhandledAuthPopupRejectionRule,
9411
+ "firebase-rtdb-list-read-for-single-item": firebaseRtdbListReadForSingleItemRule,
9412
+ "firebase-unvalidated-external-data-to-rtdb": firebaseUnvalidatedExternalDataToRtdbRule,
9413
+ "firebase-rtdb-batch-write-not-atomic": firebaseRtdbBatchWriteNotAtomicRule,
9414
+ "firebase-rtdb-listener-error-not-handled": firebaseRtdbListenerErrorNotHandledRule,
9415
+ "firebase-effect-deps-whole-user-object": firebaseEffectDepsWholeUserObjectRule,
9416
+ "firebase-rtdb-write-promise-not-handled": firebaseRtdbWritePromiseNotHandledRule,
9417
+ "firebase-firestore-rules-expired": firebaseFirestoreRulesExpiredRule,
9418
+ "firebase-id-token-cookie-flags": firebaseIdTokenCookieFlagsRule,
9419
+ "firebase-hardcoded-user-id": firebaseHardcodedUserIdRule,
9420
+ "firebase-auth-user-not-found-disclosure": firebaseAuthUserNotFoundDisclosureRule,
9421
+ "firebase-signup-password-confirm": firebaseSignupPasswordConfirmRule,
9422
+ "firebase-use-array-union-remove": firebaseUseArrayUnionRemoveRule,
9423
+ "firebase-duplicate-initialize-app": firebaseDuplicateInitializeAppRule,
9424
+ "firebase-onSnapshot-async-throw": firebaseOnSnapshotAsyncThrowRule,
9425
+ "firebase-onSnapshot-missing-error-callback": firebaseOnSnapshotMissingErrorCallbackRule,
9426
+ "firebase-firestore-document-size-guard": firebaseFirestoreDocumentSizeGuardRule,
9427
+ "firebase-use-timestamp-now": firebaseUseTimestampNowRule,
9428
+ "lovable-no-client-side-secret-fetch": lovableNoClientSideSecretFetchRule,
9429
+ "lovable-paid-flag-without-edge-function": lovablePaidFlagWithoutEdgeFunctionRule,
9430
+ "lovable-expiry-column-never-checked": lovableExpiryColumnNeverCheckedRule,
9431
+ "lovable-silent-catch-on-provider-call": lovableSilentCatchOnProviderCallRule,
9432
+ "browserbase-no-conditional-authz-on-anonymous-user": browserbaseNoConditionalAuthzOnAnonymousUserRule,
9433
+ "browserbase-no-connect-url-in-api-response": browserbaseNoConnectUrlInApiResponseRule,
9434
+ "browserbase-session-id-requires-ownership-check": browserbaseSessionIdRequiresOwnershipCheckRule,
9435
+ "browserbase-no-concurrent-shared-context": browserbaseNoConcurrentSharedContextRule,
9436
+ "browserbase-mobile-device-requires-os-setting": browserbaseMobileDeviceRequiresOsSettingRule,
9437
+ "browserbase-use-typed-exception-status-not-substring": browserbaseUseTypedExceptionStatusNotSubstringRule,
9438
+ "browserbase-release-session-on-connect-failure": browserbaseReleaseSessionOnConnectFailureRule,
9439
+ "browserbase-dont-stack-custom-retry-on-sdk-retry": browserbaseDontStackCustomRetryOnSdkRetryRule,
9440
+ "browserbase-no-overbroad-error-substring-match": browserbaseNoOverbroadErrorSubstringMatchRule,
9441
+ "browserbase-use-sdk-not-raw-requests": browserbaseUseSdkNotRawRequestsRule,
9442
+ "browserbase-centralize-request-release": browserbaseCentralizeRequestReleaseRule,
9443
+ "openai-cua-no-domain-allowlist": openaiCuaNoDomainAllowlistRule,
9444
+ "openai-cua-scroll-delta-default-zero": openaiCuaScrollDeltaDefaultZeroRule,
9445
+ "openai-cua-structured-step-metadata-not-text-json": openaiCuaStructuredStepMetadataNotTextJsonRule,
9446
+ "openai-cua-no-blind-safety-check-ack": openaiCuaNoBlindSafetyCheckAckRule,
9447
+ "openai-cua-retry-transient-turn-errors": openaiCuaRetryTransientTurnErrorsRule,
9448
+ "openai-cua-check-response-status-incomplete": openaiCuaCheckResponseStatusIncompleteRule,
9449
+ "openai-cua-set-safety-identifier": openaiCuaSetSafetyIdentifierRule,
9450
+ "tiptap-upload-validate-fn-void": tiptapUploadValidateFnVoidRule,
9451
+ "tiptap-script-src-hardcoded-api-key": tiptapScriptSrcHardcodedApiKeyRule,
9452
+ "tiptap-dynamic-script-no-sri": tiptapDynamicScriptNoSriRule,
9453
+ "tiptap-addAttributes-missing-renderHTML": tiptapAddAttributesMissingRenderHTMLRule,
9454
+ "tiptap-appendTransaction-add-to-history": tiptapAppendTransactionAddToHistoryRule,
9455
+ "tiptap-appendTransaction-full-scan": tiptapAppendTransactionFullScanRule,
9456
+ "tiptap-atom-node-wrap-in": tiptapAtomNodeWrapInRule,
9457
+ "tiptap-drop-handler-pos-precedence": tiptapDropHandlerPosPrecedenceRule,
9458
+ "tiptap-prefer-table-kit": tiptapPreferTableKitRule,
9459
+ "tiptap-tiptap-markdown-missing-node-spec": tiptapTiptapMarkdownMissingNodeSpecRule,
9460
+ "elevenlabs-validate-signed-url-response": elevenlabsValidateSignedUrlResponseRule,
9461
+ "elevenlabs-no-error-object-logging": elevenlabsNoErrorObjectLoggingRule,
9462
+ "elevenlabs-fetch-timeout-required": elevenlabsFetchTimeoutRequiredRule,
9463
+ "elevenlabs-validate-agent-id-format": elevenlabsValidateAgentIdFormatRule,
9464
+ "elevenlabs-secure-session-id-generation": elevenlabsSecureSessionIdGenerationRule,
9465
+ "elevenlabs-conversation-error-recovery": elevenlabsConversationErrorRecoveryRule,
9466
+ "elevenlabs-api-version-pinning": elevenlabsApiVersionPinningRule,
9467
+ "elevenlabs-check-http-status-before-json": elevenlabsCheckHttpStatusBeforeJsonRule,
9468
+ "elevenlabs-conversation-cleanup-on-error": elevenlabsConversationCleanupOnErrorRule,
9469
+ "elevenlabs-env-var-validation": elevenlabsEnvVarValidationRule,
9470
+ "twilio-validate-webhook-signature": twilioValidateWebhookSignatureRule,
9471
+ "twilio-taskrouter-attributes-match-consumer": twilioTaskrouterAttributesMatchConsumerRule,
9472
+ "twilio-enqueue-task-json-stringify": twilioEnqueueTaskJsonStringifyRule,
9473
+ "twilio-media-streams-key-by-call-sid": twilioMediaStreamsKeyByCallSidRule,
9474
+ "twilio-await-or-catch-rest-calls-in-event-handlers": twilioAwaitOrCatchRestCallsInEventHandlersRule,
9475
+ "twilio-use-twiml-builder-not-string-templates": twilioUseTwimlBuilderNotStringTemplatesRule,
9476
+ "twilio-media-streams-mark-pacing": twilioMediaStreamsMarkPacingRule,
9477
+ "twilio-validate-all-request-inputs": twilioValidateAllRequestInputsRule,
9478
+ "twilio-media-streams-mark-name-string": twilioMediaStreamsMarkNameStringRule,
9479
+ "openai-realtime-migrate-beta-to-ga": openaiRealtimeMigrateBetaToGaRule,
9480
+ "openai-realtime-no-log-raw-message-payloads": openaiRealtimeNoLogRawMessagePayloadsRule,
9481
+ "openai-realtime-handle-error-server-event": openaiRealtimeHandleErrorServerEventRule,
9482
+ "openai-realtime-reconnect-on-drop": openaiRealtimeReconnectOnDropRule,
9483
+ "openai-realtime-avoid-dated-preview-snapshots": openaiRealtimeAvoidDatedPreviewSnapshotsRule,
9484
+ "openai-realtime-verify-deprecated-session-fields": openaiRealtimeVerifyDeprecatedSessionFieldsRule,
9485
+ "openai-realtime-buffer-audio-until-session-ready": openaiRealtimeBufferAudioUntilSessionReadyRule,
9486
+ "openai-realtime-send-safety-identifier": openaiRealtimeSendSafetyIdentifierRule,
9487
+ "openai-realtime-transcription-model-choice": openaiRealtimeTranscriptionModelChoiceRule
9488
+ }
9489
+ };
9490
+
9491
+ // src/plugin/rule-registry.ts
9492
+ function buildRegistry() {
9493
+ const registry2 = /* @__PURE__ */ new Map();
9494
+ for (const [key, rule109] of Object.entries(plugin.rules)) {
9495
+ const docs = rule109?.meta?.docs ?? {};
9496
+ registry2.set(key, {
9497
+ category: docs.category,
9498
+ description: docs.description ?? "",
9499
+ rationale: docs.rationale ?? "",
9500
+ docsUrl: docs.docsUrl,
9501
+ cwe: docs.cwe,
9502
+ owasp: docs.owasp
9503
+ });
9504
+ }
9505
+ return registry2;
9506
+ }
9507
+ var registry = buildRegistry();
9508
+ function getRuleDocsMeta(ruleKey) {
9509
+ return registry.get(ruleKey);
9510
+ }
9511
+
9512
+ // src/reporter/snippet.ts
9513
+ function extractCodeSnippet(content, line, contextLines = 2) {
9514
+ const allLines = content.split(/\r?\n/);
9515
+ const highlighted = Math.min(Math.max(line, 1), allLines.length || 1);
9516
+ const start = Math.max(1, highlighted - contextLines);
9517
+ const end = Math.min(allLines.length, highlighted + contextLines);
9518
+ const lines = [];
9519
+ for (let n = start; n <= end; n++) {
9520
+ lines.push({ number: n, text: allLines[n - 1] ?? "" });
9521
+ }
9522
+ return { lines, highlightedLine: highlighted };
9523
+ }
9524
+
9525
+ // src/types.ts
9526
+ var SEVERITY_ORDER = {
9527
+ error: 0,
9528
+ warning: 1,
9529
+ info: 2
9530
+ };
9531
+ function scoreToSeverityLabel(score) {
9532
+ if (score >= 80) return "excellent";
9533
+ if (score >= 60) return "good";
9534
+ if (score >= 40) return "needs-work";
9535
+ return "critical";
9536
+ }
9537
+
9538
+ // src/reporter/report-builder.ts
9539
+ function computeScore(errors, warnings) {
9540
+ return Math.max(0, 100 - errors * 15 - warnings * 5);
9541
+ }
9542
+ function buildSummary(results) {
9543
+ const errors = results.filter((r) => r.severity === "error").length;
9544
+ const warnings = results.filter((r) => r.severity === "warning").length;
9545
+ const info = results.filter((r) => r.severity === "info").length;
9546
+ const score = computeScore(errors, warnings);
9547
+ return {
9548
+ score,
9549
+ severity: scoreToSeverityLabel(score),
9550
+ errors,
9551
+ warnings,
9552
+ info,
9553
+ totalIssues: results.length
9554
+ };
9555
+ }
9556
+ function sortResults(results) {
9557
+ return [...results].sort((a, b) => {
9558
+ const bySeverity = SEVERITY_ORDER[a.severity] - SEVERITY_ORDER[b.severity];
9559
+ if (bySeverity !== 0) return bySeverity;
9560
+ const byFile = a.file.localeCompare(b.file);
9561
+ if (byFile !== 0) return byFile;
9562
+ return a.line - b.line;
9563
+ });
9564
+ }
9565
+ function toFinding(result, sequence, content) {
9566
+ const docs = getRuleDocsMeta(result.ruleKey);
9567
+ return {
9568
+ id: `${result.ruleKey}-${sequence}`,
9569
+ rule: result.rule,
9570
+ category: docs?.category ?? "correctness",
9571
+ severity: result.severity,
9572
+ message: result.message,
9573
+ fix: result.fix,
9574
+ docsUrl: result.docsUrl ?? docs?.docsUrl,
9575
+ cwe: docs?.cwe,
7178
9576
  owasp: docs?.owasp,
7179
9577
  location: {
7180
9578
  file: result.file,
@@ -7264,7 +9662,17 @@ async function detectProviders(directory, filesContent) {
7264
9662
  continue;
7265
9663
  }
7266
9664
  const urls = provider.detect.urlPatterns ?? [];
7267
- if (urls.some((u) => allSources.includes(u))) {
9665
+ const matchedUrl = urls.find((u) => {
9666
+ if (!allSources.includes(u)) return false;
9667
+ const isShadowedByMoreSpecificProvider = providers.some((other) => {
9668
+ if (other === provider) return false;
9669
+ return (other.detect.urlPatterns ?? []).some(
9670
+ (otherUrl) => otherUrl !== u && otherUrl.includes(u) && allSources.includes(otherUrl)
9671
+ );
9672
+ });
9673
+ return !isShadowedByMoreSpecificProvider;
9674
+ });
9675
+ if (matchedUrl) {
7268
9676
  detected.set(provider.name, {
7269
9677
  name: provider.name,
7270
9678
  source: "url-patterns",
@@ -7278,9 +9686,10 @@ async function detectProviders(directory, filesContent) {
7278
9686
  // src/scanner.ts
7279
9687
  var SKIP_DIRS = /* @__PURE__ */ new Set(["node_modules", "dist", "build", ".next"]);
7280
9688
  var SOURCE_EXT = /\.(tsx?|jsx?)$/;
9689
+ var NPX_CMD = process.platform === "win32" ? "npx.cmd" : "npx";
7281
9690
  function runOxlint(args, cwd) {
7282
9691
  return new Promise((resolveRun) => {
7283
- const child = (0, import_node_child_process.spawn)("npx", args, { cwd });
9692
+ const child = (0, import_node_child_process.spawn)(NPX_CMD, args, { cwd });
7284
9693
  let stdout = "";
7285
9694
  let stderr = "";
7286
9695
  child.stdout?.on("data", (chunk) => {
@@ -7319,9 +9728,9 @@ function buildOxlintConfig(detectedNames) {
7319
9728
  const ruleMetaByKey = /* @__PURE__ */ new Map();
7320
9729
  for (const provider of providers) {
7321
9730
  if (!detectedNames.has(provider.name)) continue;
7322
- for (const rule83 of provider.oxlintRules) {
7323
- oxlintRules[`${PLUGIN_NAME}/${rule83.key}`] = rule83.severity === "error" || rule83.severity === void 0 ? "error" : "warn";
7324
- ruleMetaByKey.set(rule83.key, rule83);
9731
+ for (const rule109 of provider.oxlintRules) {
9732
+ oxlintRules[`${PLUGIN_NAME}/${rule109.key}`] = rule109.severity === "error" || rule109.severity === void 0 ? "error" : "warn";
9733
+ ruleMetaByKey.set(rule109.key, rule109);
7325
9734
  }
7326
9735
  }
7327
9736
  return { oxlintRules, ruleMetaByKey };
@@ -7439,9 +9848,9 @@ var import_node_path5 = require("path");
7439
9848
  function rationaleByRule() {
7440
9849
  const map = /* @__PURE__ */ new Map();
7441
9850
  for (const provider of providers) {
7442
- for (const rule83 of provider.oxlintRules) {
7443
- const docs = getRuleDocsMeta(rule83.key);
7444
- if (docs?.rationale) map.set(rule83.resultRule, docs.rationale);
9851
+ for (const rule109 of provider.oxlintRules) {
9852
+ const docs = getRuleDocsMeta(rule109.key);
9853
+ if (docs?.rationale) map.set(rule109.resultRule, docs.rationale);
7445
9854
  }
7446
9855
  }
7447
9856
  return map;