@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/README.md +5 -2
- package/dist/cli.cjs +2889 -480
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.mjs +2889 -480
- package/dist/cli.mjs.map +1 -1
- package/dist/plugin.d.ts +538 -35
- package/dist/plugin.js +2413 -263
- package/dist/plugin.js.map +1 -1
- package/package.json +2 -1
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/
|
|
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/
|
|
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: "
|
|
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
|
|
311
|
-
fix: '
|
|
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: "
|
|
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:
|
|
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: "
|
|
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/
|
|
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/
|
|
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: "
|
|
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
|
|
585
|
-
fix: 'Import {
|
|
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/
|
|
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/
|
|
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/
|
|
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/
|
|
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/
|
|
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://
|
|
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: "
|
|
902
|
-
docsUrl: "https://github.com/
|
|
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/
|
|
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/
|
|
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
|
|
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:
|
|
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
|
-
|
|
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
|
|
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
|
|
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:
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
2409
|
-
|
|
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 (
|
|
2424
|
-
if (node.callee?.type !== "Identifier" || node.callee.name
|
|
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:
|
|
2493
|
-
docsUrl: "https://supabase.com/docs/guides/
|
|
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
|
|
2753
|
-
|
|
2754
|
-
|
|
2755
|
-
|
|
2756
|
-
|
|
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/
|
|
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
|
-
|
|
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: "
|
|
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
|
|
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
|
-
|
|
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
|
|
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 =
|
|
3829
|
-
|
|
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: "
|
|
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: "
|
|
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
|
-
|
|
3886
|
-
|
|
3887
|
-
|
|
3888
|
-
|
|
3889
|
-
|
|
3890
|
-
const
|
|
3891
|
-
|
|
3892
|
-
if (!
|
|
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" &&
|
|
3952
|
-
|
|
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
|
|
3955
|
-
|
|
4272
|
+
if (callee.property.name === "set" && isCookieReceiver(callee.object)) {
|
|
4273
|
+
checkNameValueOptsCall(node);
|
|
3956
4274
|
}
|
|
3957
4275
|
},
|
|
3958
|
-
|
|
3959
|
-
|
|
3960
|
-
|
|
3961
|
-
|
|
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
|
|
4290
|
+
var firebaseIdTokenCookieFlagsRule = rule39;
|
|
3969
4291
|
|
|
3970
4292
|
// src/providers/firebase/rules/hardcoded-user-id.ts
|
|
3971
|
-
var
|
|
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 =
|
|
4328
|
+
var firebaseHardcodedUserIdRule = rule40;
|
|
4004
4329
|
|
|
4005
4330
|
// src/providers/firebase/rules/auth-user-not-found-disclosure.ts
|
|
4006
|
-
var
|
|
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 =
|
|
4370
|
+
var firebaseAuthUserNotFoundDisclosureRule = rule41;
|
|
4046
4371
|
|
|
4047
4372
|
// src/providers/firebase/rules/signup-password-confirm.ts
|
|
4048
|
-
var
|
|
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 =
|
|
4430
|
+
var firebaseSignupPasswordConfirmRule = rule42;
|
|
4106
4431
|
|
|
4107
4432
|
// src/providers/firebase/rules/use-array-union-remove.ts
|
|
4108
|
-
var
|
|
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 =
|
|
4479
|
+
var firebaseUseArrayUnionRemoveRule = rule43;
|
|
4155
4480
|
|
|
4156
4481
|
// src/providers/firebase/rules/duplicate-initialize-app.ts
|
|
4157
|
-
var
|
|
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
|
-
|
|
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 =
|
|
4532
|
+
var firebaseDuplicateInitializeAppRule = rule44;
|
|
4206
4533
|
|
|
4207
4534
|
// src/providers/firebase/rules/onSnapshot-async-throw.ts
|
|
4208
|
-
var
|
|
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
|
|
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
|
|
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 =
|
|
4586
|
+
var firebaseOnSnapshotAsyncThrowRule = rule45;
|
|
4260
4587
|
|
|
4261
4588
|
// src/providers/firebase/rules/onSnapshot-missing-error-callback.ts
|
|
4262
|
-
var
|
|
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 =
|
|
4627
|
+
var firebaseOnSnapshotMissingErrorCallbackRule = rule46;
|
|
4301
4628
|
|
|
4302
4629
|
// src/providers/firebase/rules/firestore-document-size-guard.ts
|
|
4303
|
-
var
|
|
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
|
|
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
|
|
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 =
|
|
4681
|
+
var firebaseFirestoreDocumentSizeGuardRule = rule47;
|
|
4355
4682
|
|
|
4356
4683
|
// src/providers/firebase/rules/use-timestamp-now.ts
|
|
4357
|
-
var
|
|
4684
|
+
var rule48 = {
|
|
4358
4685
|
meta: {
|
|
4359
4686
|
type: "suggestion",
|
|
4360
4687
|
docs: {
|
|
4361
|
-
description: "new Date() used for Firestore timestamp instead of
|
|
4688
|
+
description: "new Date() used for Firestore timestamp instead of serverTimestamp()",
|
|
4362
4689
|
category: "correctness",
|
|
4363
|
-
rationale: "
|
|
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
|
|
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 =
|
|
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
|
|
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 =
|
|
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
|
|
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/
|
|
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 =
|
|
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
|
|
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/
|
|
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 =
|
|
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
|
|
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/
|
|
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 =
|
|
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
|
|
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 =
|
|
5313
|
+
var browserbaseNoConditionalAuthzOnAnonymousUserRule = rule53;
|
|
4987
5314
|
|
|
4988
5315
|
// src/providers/browserbase/rules/no-connect-url-in-api-response.ts
|
|
4989
|
-
var
|
|
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 =
|
|
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
|
|
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/
|
|
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 =
|
|
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
|
|
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 =
|
|
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
|
|
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/
|
|
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 =
|
|
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
|
|
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 =
|
|
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
|
|
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 =
|
|
5760
|
+
var browserbaseReleaseSessionOnConnectFailureRule = rule59;
|
|
5434
5761
|
|
|
5435
5762
|
// src/providers/browserbase/rules/dont-stack-custom-retry-on-sdk-retry.ts
|
|
5436
|
-
var
|
|
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 =
|
|
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
|
|
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 =
|
|
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
|
|
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 =
|
|
5934
|
+
var browserbaseUseSdkNotRawRequestsRule = rule62;
|
|
5608
5935
|
|
|
5609
5936
|
// src/providers/browserbase/rules/centralize-request-release.ts
|
|
5610
|
-
var
|
|
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 =
|
|
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
|
|
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 =
|
|
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
|
|
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 =
|
|
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
|
|
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 =
|
|
6284
|
+
var openaiCuaStructuredStepMetadataNotTextJsonRule = rule66;
|
|
5958
6285
|
|
|
5959
6286
|
// src/providers/openai-cua/rules/no-blind-safety-check-ack.ts
|
|
5960
|
-
var
|
|
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 =
|
|
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
|
|
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 =
|
|
6478
|
+
var openaiCuaRetryTransientTurnErrorsRule = rule68;
|
|
6152
6479
|
|
|
6153
6480
|
// src/providers/openai-cua/rules/check-response-status-incomplete.ts
|
|
6154
|
-
var
|
|
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 =
|
|
6590
|
+
var openaiCuaCheckResponseStatusIncompleteRule = rule69;
|
|
6264
6591
|
|
|
6265
6592
|
// src/providers/openai-cua/rules/set-safety-identifier.ts
|
|
6266
|
-
var
|
|
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://
|
|
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 =
|
|
6631
|
+
var openaiCuaSetSafetyIdentifierRule = rule70;
|
|
6305
6632
|
|
|
6306
6633
|
// src/providers/tiptap/rules/upload-validate-fn-void.ts
|
|
6307
|
-
var
|
|
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 =
|
|
6698
|
+
var tiptapUploadValidateFnVoidRule = rule71;
|
|
6372
6699
|
|
|
6373
6700
|
// src/providers/tiptap/rules/script-src-hardcoded-api-key.ts
|
|
6374
|
-
var
|
|
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 =
|
|
6759
|
+
var tiptapScriptSrcHardcodedApiKeyRule = rule72;
|
|
6433
6760
|
|
|
6434
6761
|
// src/providers/tiptap/rules/dynamic-script-no-sri.ts
|
|
6435
|
-
var
|
|
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 =
|
|
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
|
|
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 =
|
|
6936
|
+
var tiptapAddAttributesMissingRenderHTMLRule = rule74;
|
|
6610
6937
|
|
|
6611
6938
|
// src/providers/tiptap/rules/appendTransaction-add-to-history.ts
|
|
6612
|
-
var
|
|
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 =
|
|
7018
|
+
var tiptapAppendTransactionAddToHistoryRule = rule75;
|
|
6692
7019
|
|
|
6693
7020
|
// src/providers/tiptap/rules/appendTransaction-full-scan.ts
|
|
6694
|
-
var
|
|
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 =
|
|
7081
|
+
var tiptapAppendTransactionFullScanRule = rule76;
|
|
6755
7082
|
|
|
6756
7083
|
// src/providers/tiptap/rules/atom-node-wrap-in.ts
|
|
6757
|
-
var
|
|
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 =
|
|
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
|
|
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`.
|
|
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 =
|
|
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
|
|
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
|
|
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
|
|
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 =
|
|
7219
|
+
var tiptapPreferTableKitRule = rule79;
|
|
6934
7220
|
|
|
6935
7221
|
// src/providers/tiptap/rules/tiptap-markdown-missing-node-spec.ts
|
|
6936
|
-
var
|
|
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
|
|
6943
|
-
docsUrl: "https://github.com/
|
|
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.
|
|
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 =
|
|
7288
|
+
var tiptapTiptapMarkdownMissingNodeSpecRule = rule80;
|
|
7003
7289
|
|
|
7004
|
-
// src/
|
|
7005
|
-
|
|
7006
|
-
|
|
7007
|
-
|
|
7008
|
-
"
|
|
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
|
-
|
|
7094
|
-
|
|
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
|
|
7108
|
-
}
|
|
7109
|
-
var registry = buildRegistry();
|
|
7110
|
-
function getRuleDocsMeta(ruleKey) {
|
|
7111
|
-
return registry.get(ruleKey);
|
|
7301
|
+
return false;
|
|
7112
7302
|
}
|
|
7113
|
-
|
|
7114
|
-
|
|
7115
|
-
|
|
7116
|
-
|
|
7117
|
-
|
|
7118
|
-
|
|
7119
|
-
|
|
7120
|
-
|
|
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
|
-
|
|
7125
|
-
|
|
7126
|
-
|
|
7127
|
-
|
|
7128
|
-
|
|
7129
|
-
|
|
7130
|
-
|
|
7131
|
-
|
|
7132
|
-
|
|
7133
|
-
|
|
7134
|
-
|
|
7135
|
-
|
|
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
|
-
|
|
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
|
|
7145
|
-
|
|
7146
|
-
|
|
7147
|
-
|
|
7148
|
-
|
|
7149
|
-
|
|
7150
|
-
|
|
7151
|
-
|
|
7152
|
-
|
|
7153
|
-
|
|
7154
|
-
|
|
7155
|
-
|
|
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
|
|
7159
|
-
|
|
7160
|
-
|
|
7161
|
-
|
|
7162
|
-
|
|
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
|
-
|
|
7168
|
-
|
|
7169
|
-
|
|
7170
|
-
|
|
7171
|
-
|
|
7172
|
-
|
|
7173
|
-
|
|
7174
|
-
|
|
7175
|
-
|
|
7176
|
-
|
|
7177
|
-
|
|
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
|
-
|
|
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)(
|
|
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
|
|
7323
|
-
oxlintRules[`${PLUGIN_NAME}/${
|
|
7324
|
-
ruleMetaByKey.set(
|
|
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
|
|
7443
|
-
const docs = getRuleDocsMeta(
|
|
7444
|
-
if (docs?.rationale) map.set(
|
|
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;
|