@api-doctor/cli 0.0.5 → 0.0.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -2
- package/dist/cli.cjs +423 -390
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.mjs +423 -390
- package/dist/cli.mjs.map +1 -1
- package/dist/plugin.d.ts +7 -42
- package/dist/plugin.js +379 -331
- package/dist/plugin.js.map +1 -1
- package/package.json +2 -1
package/dist/cli.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,8 +882,8 @@ 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",
|
|
903
887
|
severity: "warning"
|
|
904
888
|
}
|
|
905
889
|
]
|
|
@@ -1112,7 +1096,7 @@ var openaiRealtimeManifest = {
|
|
|
1112
1096
|
resultRule: "openai-realtime/reliability/handle-error-server-event",
|
|
1113
1097
|
message: 'This Realtime message handler branches on event types but never checks for the API-level "error" event.',
|
|
1114
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.",
|
|
1115
|
-
docsUrl: "https://developers.openai.com/api/
|
|
1099
|
+
docsUrl: "https://developers.openai.com/api/reference/resources/realtime/server-events",
|
|
1116
1100
|
severity: "error"
|
|
1117
1101
|
},
|
|
1118
1102
|
{
|
|
@@ -1128,7 +1112,7 @@ var openaiRealtimeManifest = {
|
|
|
1128
1112
|
resultRule: "openai-realtime/correctness/avoid-dated-preview-snapshots",
|
|
1129
1113
|
message: "The Realtime connection is pinned to a dated preview model snapshot instead of the GA alias.",
|
|
1130
1114
|
fix: "Use the GA model id (e.g. 'gpt-realtime') or a current dated snapshot tracked against OpenAI's deprecation notices.",
|
|
1131
|
-
docsUrl: "https://developers.openai.com/api/
|
|
1115
|
+
docsUrl: "https://developers.openai.com/api/reference/resources/realtime/subresources/client_secrets",
|
|
1132
1116
|
severity: "warning"
|
|
1133
1117
|
},
|
|
1134
1118
|
{
|
|
@@ -1136,7 +1120,7 @@ var openaiRealtimeManifest = {
|
|
|
1136
1120
|
resultRule: "openai-realtime/correctness/verify-deprecated-session-fields",
|
|
1137
1121
|
message: "The session config sets 'temperature', a field not documented in the current GA Realtime sessions schema.",
|
|
1138
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.",
|
|
1139
|
-
docsUrl: "https://developers.openai.com/api/
|
|
1123
|
+
docsUrl: "https://developers.openai.com/api/reference/resources/realtime/subresources/client_secrets",
|
|
1140
1124
|
severity: "warning"
|
|
1141
1125
|
},
|
|
1142
1126
|
{
|
|
@@ -1144,7 +1128,7 @@ var openaiRealtimeManifest = {
|
|
|
1144
1128
|
resultRule: "openai-realtime/reliability/buffer-audio-until-session-ready",
|
|
1145
1129
|
message: "Audio sent before the Realtime socket reaches the open state is dropped instead of buffered.",
|
|
1146
1130
|
fix: "Queue outbound input_audio_buffer.append messages until the open event fires, then flush them in order.",
|
|
1147
|
-
docsUrl: "https://developers.openai.com/api/
|
|
1131
|
+
docsUrl: "https://developers.openai.com/api/reference/resources/realtime/client-events",
|
|
1148
1132
|
severity: "warning"
|
|
1149
1133
|
},
|
|
1150
1134
|
{
|
|
@@ -1158,7 +1142,7 @@ var openaiRealtimeManifest = {
|
|
|
1158
1142
|
{
|
|
1159
1143
|
key: "openai-realtime-transcription-model-choice",
|
|
1160
1144
|
resultRule: "openai-realtime/correctness/transcription-model-choice",
|
|
1161
|
-
message: "
|
|
1145
|
+
message: "The session's input transcription is configured with 'whisper-1', which is not natively streaming and not optimized for realtime sessions.",
|
|
1162
1146
|
fix: "Switch to 'gpt-realtime-whisper' if transcription output is consumed, or drop input_audio_transcription entirely if it isn't.",
|
|
1163
1147
|
docsUrl: "https://developers.openai.com/api/docs/guides/realtime-transcription",
|
|
1164
1148
|
severity: "info"
|
|
@@ -1251,7 +1235,7 @@ var rule = {
|
|
|
1251
1235
|
cwe: "CWE-345",
|
|
1252
1236
|
owasp: "API2:2023 Broken Authentication",
|
|
1253
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.",
|
|
1254
|
-
docsUrl: "https://resend.com/docs/
|
|
1238
|
+
docsUrl: "https://resend.com/docs/webhooks/verify-webhooks-requests",
|
|
1255
1239
|
recommended: true
|
|
1256
1240
|
},
|
|
1257
1241
|
messages: {
|
|
@@ -1328,6 +1312,15 @@ var rule = {
|
|
|
1328
1312
|
const obj = callee.object;
|
|
1329
1313
|
return obj?.type === "Identifier" && (obj.name === "req" || obj.name === "request");
|
|
1330
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
|
+
}
|
|
1331
1324
|
function isBodyMember(n) {
|
|
1332
1325
|
if (n?.type !== "MemberExpression") return false;
|
|
1333
1326
|
const prop = n.property;
|
|
@@ -1352,6 +1345,17 @@ var rule = {
|
|
|
1352
1345
|
const obj = callee.object;
|
|
1353
1346
|
return obj?.type === "Identifier" && svixImports.has(obj.name);
|
|
1354
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
|
+
}
|
|
1355
1359
|
function recordFirst(posKey, handler, pos) {
|
|
1356
1360
|
const existing = handler[posKey];
|
|
1357
1361
|
if (!existing) {
|
|
@@ -1366,6 +1370,11 @@ var rule = {
|
|
|
1366
1370
|
ImportDeclaration(node) {
|
|
1367
1371
|
const importSource = node?.source?.value;
|
|
1368
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
|
+
}
|
|
1369
1378
|
if (importSource === "svix") {
|
|
1370
1379
|
for (const s of node.specifiers ?? []) {
|
|
1371
1380
|
if (s?.type === "ImportSpecifier" && s.local?.type === "Identifier") {
|
|
@@ -1387,8 +1396,10 @@ var rule = {
|
|
|
1387
1396
|
for (const handler of postHandlers) {
|
|
1388
1397
|
if (!within(handler, node)) continue;
|
|
1389
1398
|
if (isReqJsonCall(node)) recordFirst("firstBodyPos", handler, pos);
|
|
1399
|
+
if (isReqTextCall(node)) recordFirst("firstRawBodyPos", handler, pos);
|
|
1390
1400
|
if (isSvixVerifyCall(node)) recordFirst("firstVerifyPos", handler, pos);
|
|
1391
1401
|
if (isCryptoCreateHmacCall(node)) recordFirst("firstVerifyPos", handler, pos);
|
|
1402
|
+
if (isResendWebhooksVerifyCall(node)) recordFirst("firstVerifyPos", handler, pos);
|
|
1392
1403
|
}
|
|
1393
1404
|
},
|
|
1394
1405
|
MemberExpression(node) {
|
|
@@ -1403,6 +1414,8 @@ var rule = {
|
|
|
1403
1414
|
"Program:exit"() {
|
|
1404
1415
|
if (!importsResend) return;
|
|
1405
1416
|
for (const handler of postHandlers) {
|
|
1417
|
+
const consumesRequest = handler.firstBodyPos || handler.firstRawBodyPos;
|
|
1418
|
+
if (!consumesRequest) continue;
|
|
1406
1419
|
if (!handler.firstVerifyPos) {
|
|
1407
1420
|
context.report({ node: handler.node, messageId: "missingVerification" });
|
|
1408
1421
|
continue;
|
|
@@ -1974,7 +1987,7 @@ var rule11 = {
|
|
|
1974
1987
|
description: "Resend webhook handlers should deduplicate retried events",
|
|
1975
1988
|
category: "reliability",
|
|
1976
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.",
|
|
1977
|
-
docsUrl: "https://resend.com/docs/
|
|
1990
|
+
docsUrl: "https://resend.com/docs/webhooks/introduction",
|
|
1978
1991
|
recommended: true
|
|
1979
1992
|
},
|
|
1980
1993
|
messages: {
|
|
@@ -2276,12 +2289,12 @@ var rule14 = {
|
|
|
2276
2289
|
docs: {
|
|
2277
2290
|
description: "Supabase queries that select a tenant column must filter by it",
|
|
2278
2291
|
category: "correctness",
|
|
2279
|
-
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.",
|
|
2280
2293
|
docsUrl: "https://supabase.com/docs/reference/javascript/eq",
|
|
2281
2294
|
recommended: true
|
|
2282
2295
|
},
|
|
2283
2296
|
messages: {
|
|
2284
|
-
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.`
|
|
2285
2298
|
},
|
|
2286
2299
|
schema: []
|
|
2287
2300
|
},
|
|
@@ -2553,7 +2566,8 @@ function objectHasIdempotencyKey(objectExpression) {
|
|
|
2553
2566
|
return (objectExpression.properties ?? []).some((p) => {
|
|
2554
2567
|
if (p?.type !== "Property") return false;
|
|
2555
2568
|
const name = p.key?.type === "Identifier" ? p.key.name : p.key?.type === "Literal" ? p.key.value : void 0;
|
|
2556
|
-
|
|
2569
|
+
if (typeof name !== "string") return false;
|
|
2570
|
+
return /idempot|dedupe/i.test(name) || /^(id|uuid)$/i.test(name) || /_(key|uuid)$/i.test(name);
|
|
2557
2571
|
});
|
|
2558
2572
|
}
|
|
2559
2573
|
function insertPayloadHasIdempotencyKey(arg) {
|
|
@@ -2569,12 +2583,12 @@ var rule18 = {
|
|
|
2569
2583
|
docs: {
|
|
2570
2584
|
description: "Supabase insert calls should be retry-safe via an idempotency key",
|
|
2571
2585
|
category: "reliability",
|
|
2572
|
-
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.',
|
|
2573
2587
|
docsUrl: "https://supabase.com/docs/reference/javascript/upsert",
|
|
2574
2588
|
recommended: true
|
|
2575
2589
|
},
|
|
2576
2590
|
messages: {
|
|
2577
|
-
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>" }).'
|
|
2578
2592
|
},
|
|
2579
2593
|
schema: []
|
|
2580
2594
|
},
|
|
@@ -2623,7 +2637,7 @@ var rule19 = {
|
|
|
2623
2637
|
docs: {
|
|
2624
2638
|
description: "createClient must fail fast when required env vars are missing",
|
|
2625
2639
|
category: "reliability",
|
|
2626
|
-
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.`,
|
|
2627
2641
|
docsUrl: "https://supabase.com/docs/reference/javascript/initializing",
|
|
2628
2642
|
recommended: true
|
|
2629
2643
|
},
|
|
@@ -2633,7 +2647,11 @@ var rule19 = {
|
|
|
2633
2647
|
schema: []
|
|
2634
2648
|
},
|
|
2635
2649
|
create(context) {
|
|
2636
|
-
|
|
2650
|
+
const FACTORY_IMPORTS = {
|
|
2651
|
+
"@supabase/supabase-js": ["createClient"],
|
|
2652
|
+
"@supabase/ssr": ["createBrowserClient", "createServerClient"]
|
|
2653
|
+
};
|
|
2654
|
+
const factoryLocalNames = /* @__PURE__ */ new Set();
|
|
2637
2655
|
const envVarOfVariable = /* @__PURE__ */ new Map();
|
|
2638
2656
|
const validatedVarNames = /* @__PURE__ */ new Set();
|
|
2639
2657
|
const validatedEnvNames = /* @__PURE__ */ new Set();
|
|
@@ -2667,10 +2685,11 @@ var rule19 = {
|
|
|
2667
2685
|
}
|
|
2668
2686
|
return {
|
|
2669
2687
|
ImportDeclaration(node) {
|
|
2670
|
-
|
|
2688
|
+
const factories = FACTORY_IMPORTS[node.source?.value];
|
|
2689
|
+
if (!factories) return;
|
|
2671
2690
|
for (const s of node.specifiers ?? []) {
|
|
2672
|
-
if (s?.type === "ImportSpecifier" && s.imported?.type === "Identifier" && s.imported.name
|
|
2673
|
-
|
|
2691
|
+
if (s?.type === "ImportSpecifier" && s.imported?.type === "Identifier" && factories.includes(s.imported.name) && s.local?.type === "Identifier") {
|
|
2692
|
+
factoryLocalNames.add(s.local.name);
|
|
2674
2693
|
}
|
|
2675
2694
|
}
|
|
2676
2695
|
},
|
|
@@ -2684,8 +2703,8 @@ var rule19 = {
|
|
|
2684
2703
|
collectGuardTargets(node.test);
|
|
2685
2704
|
},
|
|
2686
2705
|
CallExpression(node) {
|
|
2687
|
-
if (
|
|
2688
|
-
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;
|
|
2689
2708
|
const missing = [];
|
|
2690
2709
|
for (const rawArg of node.arguments ?? []) {
|
|
2691
2710
|
const arg = unwrapNonNull(rawArg);
|
|
@@ -2753,8 +2772,8 @@ var rule20 = {
|
|
|
2753
2772
|
category: "security",
|
|
2754
2773
|
cwe: "CWE-285",
|
|
2755
2774
|
owasp: "A01:2021 Broken Access Control",
|
|
2756
|
-
rationale:
|
|
2757
|
-
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",
|
|
2758
2777
|
recommended: true
|
|
2759
2778
|
},
|
|
2760
2779
|
messages: {
|
|
@@ -2790,7 +2809,7 @@ var rule21 = {
|
|
|
2790
2809
|
docs: {
|
|
2791
2810
|
description: "Supabase .single() calls must inspect the returned error field",
|
|
2792
2811
|
category: "correctness",
|
|
2793
|
-
rationale: ".single() signals zero-or-one-row intent via the error field (PGRST116), not by leaving data undefined silently. Destructuring only data and never reading error produces infinite spinners on deleted rows, bad IDs, or RLS-denied reads. Prefer .maybeSingle() or branch on error before rendering.",
|
|
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.",
|
|
2794
2813
|
docsUrl: "https://supabase.com/docs/reference/javascript/single",
|
|
2795
2814
|
recommended: true
|
|
2796
2815
|
},
|
|
@@ -2800,14 +2819,29 @@ var rule21 = {
|
|
|
2800
2819
|
schema: []
|
|
2801
2820
|
},
|
|
2802
2821
|
create(context) {
|
|
2822
|
+
const deferredBindings = [];
|
|
2823
|
+
const errorReadNames = /* @__PURE__ */ new Set();
|
|
2803
2824
|
function checkAwaitBinding(node, pattern, awaitExpr) {
|
|
2804
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
|
+
}
|
|
2805
2831
|
const names = destructuredNames(pattern);
|
|
2806
2832
|
if (names.has("error")) return;
|
|
2807
2833
|
context.report({ node, messageId: "missingErrorCheck" });
|
|
2808
2834
|
}
|
|
2809
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
|
+
},
|
|
2810
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
|
+
}
|
|
2811
2845
|
if (node.init?.type !== "AwaitExpression") return;
|
|
2812
2846
|
checkAwaitBinding(node, node.id, node.init);
|
|
2813
2847
|
},
|
|
@@ -2819,9 +2853,17 @@ var rule21 = {
|
|
|
2819
2853
|
const expr = node.expression;
|
|
2820
2854
|
if (expr?.type !== "AwaitExpression") return;
|
|
2821
2855
|
if (!isSingleSupabaseQuery(expr.argument)) return;
|
|
2856
|
+
if (chainHasMethod(expr.argument, "throwOnError")) return;
|
|
2822
2857
|
if (memberPropName(expr.argument) === "single") {
|
|
2823
2858
|
context.report({ node, messageId: "missingErrorCheck" });
|
|
2824
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
|
+
}
|
|
2825
2867
|
}
|
|
2826
2868
|
};
|
|
2827
2869
|
}
|
|
@@ -2930,7 +2972,7 @@ var rule23 = {
|
|
|
2930
2972
|
docs: {
|
|
2931
2973
|
description: "Supabase mutations must check the returned error field",
|
|
2932
2974
|
category: "correctness",
|
|
2933
|
-
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.",
|
|
2934
2976
|
docsUrl: "https://supabase.com/docs/reference/javascript/insert",
|
|
2935
2977
|
recommended: true
|
|
2936
2978
|
},
|
|
@@ -2940,30 +2982,52 @@ var rule23 = {
|
|
|
2940
2982
|
schema: []
|
|
2941
2983
|
},
|
|
2942
2984
|
create(context) {
|
|
2985
|
+
const deferredBindings = [];
|
|
2986
|
+
const errorReadNames = /* @__PURE__ */ new Set();
|
|
2943
2987
|
function checkMutationAwait(node, pattern, awaitExpr) {
|
|
2944
2988
|
if (!isSupabaseMutationCall(awaitExpr.argument)) return;
|
|
2989
|
+
if (chainHasMethod(awaitExpr.argument, "throwOnError")) return;
|
|
2945
2990
|
if (!pattern) {
|
|
2946
2991
|
context.report({ node, messageId: "uncheckedMutation" });
|
|
2947
2992
|
return;
|
|
2948
2993
|
}
|
|
2994
|
+
if (pattern.type === "Identifier") {
|
|
2995
|
+
deferredBindings.push({ node, name: pattern.name });
|
|
2996
|
+
return;
|
|
2997
|
+
}
|
|
2949
2998
|
const names = destructuredNames(pattern);
|
|
2950
2999
|
if (!names.has("error")) {
|
|
2951
3000
|
context.report({ node, messageId: "uncheckedMutation" });
|
|
2952
3001
|
}
|
|
2953
3002
|
}
|
|
2954
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
|
+
},
|
|
2955
3009
|
ExpressionStatement(node) {
|
|
2956
3010
|
const expr = node.expression;
|
|
2957
3011
|
if (expr?.type !== "AwaitExpression") return;
|
|
2958
3012
|
checkMutationAwait(node, void 0, expr);
|
|
2959
3013
|
},
|
|
2960
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
|
+
}
|
|
2961
3018
|
if (node.init?.type !== "AwaitExpression") return;
|
|
2962
3019
|
checkMutationAwait(node, node.id, node.init);
|
|
2963
3020
|
},
|
|
2964
3021
|
AssignmentExpression(node) {
|
|
2965
3022
|
if (node.right?.type !== "AwaitExpression") return;
|
|
2966
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
|
+
}
|
|
2967
3031
|
}
|
|
2968
3032
|
};
|
|
2969
3033
|
}
|
|
@@ -3013,11 +3077,20 @@ function isStorageUploadCall(node) {
|
|
|
3013
3077
|
let current = node;
|
|
3014
3078
|
let sawStorage = false;
|
|
3015
3079
|
let sawUpload = false;
|
|
3016
|
-
while (current
|
|
3017
|
-
|
|
3018
|
-
|
|
3019
|
-
|
|
3020
|
-
|
|
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
|
+
}
|
|
3021
3094
|
}
|
|
3022
3095
|
return sawStorage && sawUpload;
|
|
3023
3096
|
}
|
|
@@ -3598,8 +3671,8 @@ var rule30 = {
|
|
|
3598
3671
|
category: "security",
|
|
3599
3672
|
cwe: "CWE-285",
|
|
3600
3673
|
owasp: "A04:2021 Insecure Design",
|
|
3601
|
-
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.",
|
|
3602
|
-
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",
|
|
3603
3676
|
recommended: true
|
|
3604
3677
|
},
|
|
3605
3678
|
messages: {
|
|
@@ -3773,7 +3846,13 @@ function isValidationLikeCall(node) {
|
|
|
3773
3846
|
const callee = node.callee;
|
|
3774
3847
|
const name = callee?.type === "Identifier" ? callee.name : callee?.type === "MemberExpression" && !callee.computed && callee.property?.type === "Identifier" ? callee.property.name : void 0;
|
|
3775
3848
|
if (!name) return false;
|
|
3776
|
-
|
|
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;
|
|
3777
3856
|
}
|
|
3778
3857
|
var rule33 = {
|
|
3779
3858
|
meta: {
|
|
@@ -3962,18 +4041,24 @@ var firebaseRtdbListenerErrorNotHandledRule = rule35;
|
|
|
3962
4041
|
function isUseEffectCall(node) {
|
|
3963
4042
|
return node?.type === "CallExpression" && node.callee?.type === "Identifier" && node.callee.name === "useEffect";
|
|
3964
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
|
+
}
|
|
3965
4050
|
var rule36 = {
|
|
3966
4051
|
meta: {
|
|
3967
4052
|
type: "suggestion",
|
|
3968
4053
|
docs: {
|
|
3969
4054
|
description: "useEffect should depend on user?.uid, not the whole user object",
|
|
3970
4055
|
category: "reliability",
|
|
3971
|
-
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.",
|
|
3972
4057
|
docsUrl: "https://firebase.google.com/docs/reference/js/auth.md#onauthstatechanged",
|
|
3973
4058
|
recommended: true
|
|
3974
4059
|
},
|
|
3975
4060
|
messages: {
|
|
3976
|
-
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."
|
|
3977
4062
|
},
|
|
3978
4063
|
schema: []
|
|
3979
4064
|
},
|
|
@@ -3986,9 +4071,9 @@ var rule36 = {
|
|
|
3986
4071
|
for (const el of depsArg.elements ?? []) {
|
|
3987
4072
|
if (el?.type !== "Identifier") continue;
|
|
3988
4073
|
const name = el.name;
|
|
3989
|
-
if (someDescendant(callback, (n) => isUidMemberAccess(n, name)))
|
|
3990
|
-
|
|
3991
|
-
}
|
|
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 } });
|
|
3992
4077
|
}
|
|
3993
4078
|
}
|
|
3994
4079
|
};
|
|
@@ -4084,13 +4169,13 @@ var rule38 = {
|
|
|
4084
4169
|
schema: []
|
|
4085
4170
|
},
|
|
4086
4171
|
create(context) {
|
|
4087
|
-
const EXPIRED_YEAR = 2025;
|
|
4088
4172
|
function checkStringForExpiredDate(value, reportNode) {
|
|
4089
|
-
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;
|
|
4090
4174
|
let match;
|
|
4091
4175
|
while ((match = re.exec(value)) !== null) {
|
|
4092
|
-
const year =
|
|
4093
|
-
|
|
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()) {
|
|
4094
4179
|
context.report({ node: reportNode, messageId: "firestoreRulesExpired" });
|
|
4095
4180
|
return;
|
|
4096
4181
|
}
|
|
@@ -4122,6 +4207,18 @@ function findProp(obj, name) {
|
|
|
4122
4207
|
(p) => p?.type === "Property" && (p.key?.type === "Identifier" && p.key.name === name || p.key?.type === "Literal" && p.key.value === name)
|
|
4123
4208
|
);
|
|
4124
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
|
+
}
|
|
4125
4222
|
var rule39 = {
|
|
4126
4223
|
meta: {
|
|
4127
4224
|
type: "problem",
|
|
@@ -4129,110 +4226,71 @@ var rule39 = {
|
|
|
4129
4226
|
description: "Firebase ID token stored in a cookie without httpOnly flag",
|
|
4130
4227
|
category: "security",
|
|
4131
4228
|
cwe: "CWE-1004",
|
|
4132
|
-
owasp: "
|
|
4133
|
-
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.",
|
|
4134
4231
|
docsUrl: "https://firebase.google.com/docs/auth/admin/manage-cookies",
|
|
4135
4232
|
recommended: true
|
|
4136
4233
|
},
|
|
4137
4234
|
messages: {
|
|
4138
|
-
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."
|
|
4139
4236
|
},
|
|
4140
4237
|
schema: []
|
|
4141
4238
|
},
|
|
4142
4239
|
create(context) {
|
|
4143
|
-
function isTokenName(val) {
|
|
4144
|
-
return /token/i.test(val);
|
|
4145
|
-
}
|
|
4146
4240
|
function hasHttpOnlyTrue(optsNode) {
|
|
4147
|
-
if (optsNode?.type !== "ObjectExpression") return false;
|
|
4148
4241
|
const prop = findProp(optsNode, "httpOnly");
|
|
4149
|
-
|
|
4150
|
-
|
|
4151
|
-
|
|
4152
|
-
|
|
4153
|
-
|
|
4154
|
-
const
|
|
4155
|
-
|
|
4156
|
-
if (!
|
|
4157
|
-
const args = node.arguments ?? [];
|
|
4158
|
-
const nameArg = args[0];
|
|
4159
|
-
if (nameArg?.type !== "Literal" || typeof nameArg.value !== "string") return;
|
|
4160
|
-
if (!isTokenName(nameArg.value)) return;
|
|
4161
|
-
const optsArg = args.length >= 3 ? args[2] : null;
|
|
4162
|
-
if (!optsArg || optsArg.type !== "ObjectExpression") {
|
|
4163
|
-
context.report({ node, messageId: "idTokenCookieMissingHttpOnly" });
|
|
4164
|
-
return;
|
|
4165
|
-
}
|
|
4166
|
-
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])) {
|
|
4167
4250
|
context.report({ node, messageId: "idTokenCookieMissingHttpOnly" });
|
|
4168
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" });
|
|
4169
4258
|
}
|
|
4170
|
-
};
|
|
4171
|
-
}
|
|
4172
|
-
};
|
|
4173
|
-
var firebaseIdTokenCookieFlagsRule = rule39;
|
|
4174
|
-
|
|
4175
|
-
// src/providers/firebase/rules/middleware-token-not-verified.ts
|
|
4176
|
-
var rule40 = {
|
|
4177
|
-
meta: {
|
|
4178
|
-
type: "problem",
|
|
4179
|
-
docs: {
|
|
4180
|
-
description: "Next.js middleware reads auth cookie but never verifies it",
|
|
4181
|
-
category: "security",
|
|
4182
|
-
cwe: "CWE-345",
|
|
4183
|
-
owasp: "A07:2021 Identification and Authentication Failures",
|
|
4184
|
-
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.",
|
|
4185
|
-
docsUrl: "https://firebase.google.com/docs/auth/admin/manage-cookies#verify_session_cookies_and_check_permissions",
|
|
4186
|
-
recommended: true
|
|
4187
|
-
},
|
|
4188
|
-
messages: {
|
|
4189
|
-
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."
|
|
4190
|
-
},
|
|
4191
|
-
schema: []
|
|
4192
|
-
},
|
|
4193
|
-
create(context) {
|
|
4194
|
-
const AUTH_COOKIE_NAMES = /token|session/i;
|
|
4195
|
-
const VERIFY_METHOD_NAMES = /* @__PURE__ */ new Set(["verifySessionCookie", "verifyIdToken", "verify"]);
|
|
4196
|
-
let readsCookieWithAuthName = false;
|
|
4197
|
-
let hasVerifyCall = false;
|
|
4198
|
-
const cookieReadNodes = [];
|
|
4199
|
-
function isAuthCookieGet(node) {
|
|
4200
|
-
if (node?.type !== "CallExpression") return false;
|
|
4201
|
-
const callee = node.callee;
|
|
4202
|
-
if (callee?.type !== "MemberExpression") return false;
|
|
4203
|
-
if (callee.property?.type !== "Identifier" || callee.property.name !== "get") return false;
|
|
4204
|
-
const arg = node.arguments?.[0];
|
|
4205
|
-
if (arg?.type !== "Literal" || typeof arg.value !== "string") return false;
|
|
4206
|
-
return AUTH_COOKIE_NAMES.test(arg.value);
|
|
4207
4259
|
}
|
|
4208
4260
|
return {
|
|
4209
4261
|
CallExpression(node) {
|
|
4210
|
-
if (isAuthCookieGet(node)) {
|
|
4211
|
-
readsCookieWithAuthName = true;
|
|
4212
|
-
cookieReadNodes.push(node);
|
|
4213
|
-
}
|
|
4214
4262
|
const callee = node.callee;
|
|
4215
|
-
if (callee?.type === "Identifier" &&
|
|
4216
|
-
|
|
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;
|
|
4217
4271
|
}
|
|
4218
|
-
if (callee
|
|
4219
|
-
|
|
4272
|
+
if (callee.property.name === "set" && isCookieReceiver(callee.object)) {
|
|
4273
|
+
checkNameValueOptsCall(node);
|
|
4220
4274
|
}
|
|
4221
4275
|
},
|
|
4222
|
-
|
|
4223
|
-
|
|
4224
|
-
|
|
4225
|
-
|
|
4226
|
-
|
|
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" });
|
|
4227
4285
|
}
|
|
4228
4286
|
}
|
|
4229
4287
|
};
|
|
4230
4288
|
}
|
|
4231
4289
|
};
|
|
4232
|
-
var
|
|
4290
|
+
var firebaseIdTokenCookieFlagsRule = rule39;
|
|
4233
4291
|
|
|
4234
4292
|
// src/providers/firebase/rules/hardcoded-user-id.ts
|
|
4235
|
-
var
|
|
4293
|
+
var rule40 = {
|
|
4236
4294
|
meta: {
|
|
4237
4295
|
type: "problem",
|
|
4238
4296
|
docs: {
|
|
@@ -4250,6 +4308,9 @@ var rule41 = {
|
|
|
4250
4308
|
schema: []
|
|
4251
4309
|
},
|
|
4252
4310
|
create(context) {
|
|
4311
|
+
if (isInsideTestFile2(String(context.filename ?? ""))) {
|
|
4312
|
+
return {};
|
|
4313
|
+
}
|
|
4253
4314
|
const USER_ID_NAMES = /* @__PURE__ */ new Set(["userId", "uid", "userID", "user_id"]);
|
|
4254
4315
|
return {
|
|
4255
4316
|
VariableDeclarator(node) {
|
|
@@ -4264,10 +4325,10 @@ var rule41 = {
|
|
|
4264
4325
|
};
|
|
4265
4326
|
}
|
|
4266
4327
|
};
|
|
4267
|
-
var firebaseHardcodedUserIdRule =
|
|
4328
|
+
var firebaseHardcodedUserIdRule = rule40;
|
|
4268
4329
|
|
|
4269
4330
|
// src/providers/firebase/rules/auth-user-not-found-disclosure.ts
|
|
4270
|
-
var
|
|
4331
|
+
var rule41 = {
|
|
4271
4332
|
meta: {
|
|
4272
4333
|
type: "suggestion",
|
|
4273
4334
|
docs: {
|
|
@@ -4306,10 +4367,10 @@ var rule42 = {
|
|
|
4306
4367
|
};
|
|
4307
4368
|
}
|
|
4308
4369
|
};
|
|
4309
|
-
var firebaseAuthUserNotFoundDisclosureRule =
|
|
4370
|
+
var firebaseAuthUserNotFoundDisclosureRule = rule41;
|
|
4310
4371
|
|
|
4311
4372
|
// src/providers/firebase/rules/signup-password-confirm.ts
|
|
4312
|
-
var
|
|
4373
|
+
var rule42 = {
|
|
4313
4374
|
meta: {
|
|
4314
4375
|
type: "suggestion",
|
|
4315
4376
|
docs: {
|
|
@@ -4343,7 +4404,7 @@ var rule43 = {
|
|
|
4343
4404
|
},
|
|
4344
4405
|
BinaryExpression(node) {
|
|
4345
4406
|
if (node.operator !== "===" && node.operator !== "==" && node.operator !== "!==" && node.operator !== "!=") return;
|
|
4346
|
-
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";
|
|
4347
4408
|
if (mentionsConfirm(node.left) || mentionsConfirm(node.right)) {
|
|
4348
4409
|
hasPasswordComparison = true;
|
|
4349
4410
|
}
|
|
@@ -4366,16 +4427,16 @@ var rule43 = {
|
|
|
4366
4427
|
};
|
|
4367
4428
|
}
|
|
4368
4429
|
};
|
|
4369
|
-
var firebaseSignupPasswordConfirmRule =
|
|
4430
|
+
var firebaseSignupPasswordConfirmRule = rule42;
|
|
4370
4431
|
|
|
4371
4432
|
// src/providers/firebase/rules/use-array-union-remove.ts
|
|
4372
|
-
var
|
|
4433
|
+
var rule43 = {
|
|
4373
4434
|
meta: {
|
|
4374
4435
|
type: "suggestion",
|
|
4375
4436
|
docs: {
|
|
4376
4437
|
description: "Firestore array field updated with read-modify-write spread/filter instead of arrayUnion/arrayRemove",
|
|
4377
4438
|
category: "correctness",
|
|
4378
|
-
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.",
|
|
4379
4440
|
docsUrl: "https://firebase.google.com/docs/firestore/manage-data/add-data#update_elements_in_an_array",
|
|
4380
4441
|
recommended: true
|
|
4381
4442
|
},
|
|
@@ -4415,10 +4476,10 @@ var rule44 = {
|
|
|
4415
4476
|
};
|
|
4416
4477
|
}
|
|
4417
4478
|
};
|
|
4418
|
-
var firebaseUseArrayUnionRemoveRule =
|
|
4479
|
+
var firebaseUseArrayUnionRemoveRule = rule43;
|
|
4419
4480
|
|
|
4420
4481
|
// src/providers/firebase/rules/duplicate-initialize-app.ts
|
|
4421
|
-
var
|
|
4482
|
+
var rule44 = {
|
|
4422
4483
|
meta: {
|
|
4423
4484
|
type: "suggestion",
|
|
4424
4485
|
docs: {
|
|
@@ -4451,9 +4512,11 @@ var rule45 = {
|
|
|
4451
4512
|
const callee = node.callee;
|
|
4452
4513
|
if (callee?.type !== "Identifier") return;
|
|
4453
4514
|
if (initializeAppLocalName && callee.name === initializeAppLocalName) {
|
|
4454
|
-
|
|
4515
|
+
if ((node.arguments ?? []).length < 2) {
|
|
4516
|
+
initializeAppCalls.push(node);
|
|
4517
|
+
}
|
|
4455
4518
|
}
|
|
4456
|
-
if (callee.name === "getApps") {
|
|
4519
|
+
if (callee.name === "getApps" || callee.name === "getApp") {
|
|
4457
4520
|
hasGetAppsCall = true;
|
|
4458
4521
|
}
|
|
4459
4522
|
},
|
|
@@ -4466,21 +4529,21 @@ var rule45 = {
|
|
|
4466
4529
|
};
|
|
4467
4530
|
}
|
|
4468
4531
|
};
|
|
4469
|
-
var firebaseDuplicateInitializeAppRule =
|
|
4532
|
+
var firebaseDuplicateInitializeAppRule = rule44;
|
|
4470
4533
|
|
|
4471
4534
|
// src/providers/firebase/rules/onSnapshot-async-throw.ts
|
|
4472
|
-
var
|
|
4535
|
+
var rule45 = {
|
|
4473
4536
|
meta: {
|
|
4474
4537
|
type: "suggestion",
|
|
4475
4538
|
docs: {
|
|
4476
4539
|
description: "throw inside async onSnapshot callback creates an unhandled promise rejection",
|
|
4477
4540
|
category: "reliability",
|
|
4478
|
-
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.",
|
|
4479
4542
|
docsUrl: "https://firebase.google.com/docs/firestore/query-data/listen#handle_listen_errors",
|
|
4480
4543
|
recommended: true
|
|
4481
4544
|
},
|
|
4482
4545
|
messages: {
|
|
4483
|
-
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."
|
|
4484
4547
|
},
|
|
4485
4548
|
schema: []
|
|
4486
4549
|
},
|
|
@@ -4520,10 +4583,10 @@ var rule46 = {
|
|
|
4520
4583
|
};
|
|
4521
4584
|
}
|
|
4522
4585
|
};
|
|
4523
|
-
var firebaseOnSnapshotAsyncThrowRule =
|
|
4586
|
+
var firebaseOnSnapshotAsyncThrowRule = rule45;
|
|
4524
4587
|
|
|
4525
4588
|
// src/providers/firebase/rules/onSnapshot-missing-error-callback.ts
|
|
4526
|
-
var
|
|
4589
|
+
var rule46 = {
|
|
4527
4590
|
meta: {
|
|
4528
4591
|
type: "suggestion",
|
|
4529
4592
|
docs: {
|
|
@@ -4561,21 +4624,21 @@ var rule47 = {
|
|
|
4561
4624
|
};
|
|
4562
4625
|
}
|
|
4563
4626
|
};
|
|
4564
|
-
var firebaseOnSnapshotMissingErrorCallbackRule =
|
|
4627
|
+
var firebaseOnSnapshotMissingErrorCallbackRule = rule46;
|
|
4565
4628
|
|
|
4566
4629
|
// src/providers/firebase/rules/firestore-document-size-guard.ts
|
|
4567
|
-
var
|
|
4630
|
+
var rule47 = {
|
|
4568
4631
|
meta: {
|
|
4569
4632
|
type: "suggestion",
|
|
4570
4633
|
docs: {
|
|
4571
4634
|
description: "Firestore write includes editor.getJSON() without a document size guard",
|
|
4572
4635
|
category: "reliability",
|
|
4573
|
-
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.",
|
|
4574
4637
|
docsUrl: "https://firebase.google.com/docs/firestore/quotas#limits",
|
|
4575
4638
|
recommended: true
|
|
4576
4639
|
},
|
|
4577
4640
|
messages: {
|
|
4578
|
-
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."
|
|
4579
4642
|
},
|
|
4580
4643
|
schema: []
|
|
4581
4644
|
},
|
|
@@ -4615,21 +4678,21 @@ var rule48 = {
|
|
|
4615
4678
|
};
|
|
4616
4679
|
}
|
|
4617
4680
|
};
|
|
4618
|
-
var firebaseFirestoreDocumentSizeGuardRule =
|
|
4681
|
+
var firebaseFirestoreDocumentSizeGuardRule = rule47;
|
|
4619
4682
|
|
|
4620
4683
|
// src/providers/firebase/rules/use-timestamp-now.ts
|
|
4621
|
-
var
|
|
4684
|
+
var rule48 = {
|
|
4622
4685
|
meta: {
|
|
4623
4686
|
type: "suggestion",
|
|
4624
4687
|
docs: {
|
|
4625
|
-
description: "new Date() used for Firestore timestamp instead of
|
|
4688
|
+
description: "new Date() used for Firestore timestamp instead of serverTimestamp()",
|
|
4626
4689
|
category: "correctness",
|
|
4627
|
-
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().",
|
|
4628
4691
|
docsUrl: "https://firebase.google.com/docs/reference/js/firestore_.timestamp",
|
|
4629
4692
|
recommended: true
|
|
4630
4693
|
},
|
|
4631
4694
|
messages: {
|
|
4632
|
-
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."
|
|
4633
4696
|
},
|
|
4634
4697
|
schema: []
|
|
4635
4698
|
},
|
|
@@ -4638,7 +4701,7 @@ var rule49 = {
|
|
|
4638
4701
|
return {
|
|
4639
4702
|
ImportDeclaration(node) {
|
|
4640
4703
|
const src = node.source?.value;
|
|
4641
|
-
if (typeof src === "string" && (src.startsWith("firebase/") || src === "firebase-admin/firestore")) {
|
|
4704
|
+
if (typeof src === "string" && (src.startsWith("firebase/firestore") || src === "firebase-admin/firestore")) {
|
|
4642
4705
|
importsFromFirestore = true;
|
|
4643
4706
|
}
|
|
4644
4707
|
},
|
|
@@ -4651,7 +4714,7 @@ var rule49 = {
|
|
|
4651
4714
|
};
|
|
4652
4715
|
}
|
|
4653
4716
|
};
|
|
4654
|
-
var firebaseUseTimestampNowRule =
|
|
4717
|
+
var firebaseUseTimestampNowRule = rule48;
|
|
4655
4718
|
|
|
4656
4719
|
// src/providers/lovable/utils.ts
|
|
4657
4720
|
var LLM_HOST_PATTERN = /api\.anthropic\.com|api\.openai\.com/;
|
|
@@ -4666,7 +4729,7 @@ function containsKnownLlmHost(node) {
|
|
|
4666
4729
|
}
|
|
4667
4730
|
|
|
4668
4731
|
// src/providers/lovable/rules/no-client-side-secret-fetch.ts
|
|
4669
|
-
var
|
|
4732
|
+
var rule49 = {
|
|
4670
4733
|
meta: {
|
|
4671
4734
|
type: "problem",
|
|
4672
4735
|
docs: {
|
|
@@ -4739,13 +4802,13 @@ var rule50 = {
|
|
|
4739
4802
|
};
|
|
4740
4803
|
}
|
|
4741
4804
|
};
|
|
4742
|
-
var lovableNoClientSideSecretFetchRule =
|
|
4805
|
+
var lovableNoClientSideSecretFetchRule = rule49;
|
|
4743
4806
|
|
|
4744
4807
|
// src/providers/lovable/rules/paid-flag-without-edge-function.ts
|
|
4745
4808
|
var PRICE_PATTERN = /\$\s?\d/;
|
|
4746
4809
|
var FLAG_PROPERTY_PATTERN = /^is_[a-z0-9_]+$/i;
|
|
4747
4810
|
var FLAG_SUFFIX_PATTERN = /_(unlocked|active|enabled)$/i;
|
|
4748
|
-
var
|
|
4811
|
+
var rule50 = {
|
|
4749
4812
|
meta: {
|
|
4750
4813
|
type: "problem",
|
|
4751
4814
|
docs: {
|
|
@@ -4754,7 +4817,7 @@ var rule51 = {
|
|
|
4754
4817
|
cwe: "CWE-840",
|
|
4755
4818
|
owasp: "A04:2021 \u2013 Insecure Design",
|
|
4756
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.",
|
|
4757
|
-
docsUrl: "https://docs.lovable.dev/
|
|
4820
|
+
docsUrl: "https://docs.lovable.dev/integrations/stripe",
|
|
4758
4821
|
recommended: true
|
|
4759
4822
|
},
|
|
4760
4823
|
messages: {
|
|
@@ -4887,20 +4950,20 @@ var rule51 = {
|
|
|
4887
4950
|
};
|
|
4888
4951
|
}
|
|
4889
4952
|
};
|
|
4890
|
-
var lovablePaidFlagWithoutEdgeFunctionRule =
|
|
4953
|
+
var lovablePaidFlagWithoutEdgeFunctionRule = rule50;
|
|
4891
4954
|
|
|
4892
4955
|
// src/providers/lovable/rules/expiry-column-never-checked.ts
|
|
4893
4956
|
var EXPIRY_COLUMN_PATTERN = /_(until|expires?_at|expiry)$/i;
|
|
4894
4957
|
var DATE_COMPARISON_OPERATORS = /* @__PURE__ */ new Set([">", "<", ">=", "<="]);
|
|
4895
4958
|
var FILTER_METHODS = /* @__PURE__ */ new Set(["gt", "gte", "lt", "lte"]);
|
|
4896
|
-
var
|
|
4959
|
+
var rule51 = {
|
|
4897
4960
|
meta: {
|
|
4898
4961
|
type: "suggestion",
|
|
4899
4962
|
docs: {
|
|
4900
4963
|
description: "An expiry column must be checked against the current time somewhere",
|
|
4901
4964
|
category: "correctness",
|
|
4902
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.",
|
|
4903
|
-
docsUrl: "https://docs.lovable.dev/
|
|
4966
|
+
docsUrl: "https://docs.lovable.dev/integrations/stripe",
|
|
4904
4967
|
recommended: true
|
|
4905
4968
|
},
|
|
4906
4969
|
messages: {
|
|
@@ -4985,18 +5048,18 @@ var rule52 = {
|
|
|
4985
5048
|
};
|
|
4986
5049
|
}
|
|
4987
5050
|
};
|
|
4988
|
-
var lovableExpiryColumnNeverCheckedRule =
|
|
5051
|
+
var lovableExpiryColumnNeverCheckedRule = rule51;
|
|
4989
5052
|
|
|
4990
5053
|
// src/providers/lovable/rules/silent-catch-on-provider-call.ts
|
|
4991
5054
|
var LOGGING_CONSOLE_METHODS = /* @__PURE__ */ new Set(["error", "warn", "log", "info"]);
|
|
4992
|
-
var
|
|
5055
|
+
var rule52 = {
|
|
4993
5056
|
meta: {
|
|
4994
5057
|
type: "suggestion",
|
|
4995
5058
|
docs: {
|
|
4996
5059
|
description: "A catch block around an LLM provider call must log the failure",
|
|
4997
5060
|
category: "correctness",
|
|
4998
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.`,
|
|
4999
|
-
docsUrl: "https://docs.lovable.dev/
|
|
5062
|
+
docsUrl: "https://docs.lovable.dev/integrations/cloud",
|
|
5000
5063
|
recommended: true
|
|
5001
5064
|
},
|
|
5002
5065
|
messages: {
|
|
@@ -5062,7 +5125,7 @@ var rule53 = {
|
|
|
5062
5125
|
};
|
|
5063
5126
|
}
|
|
5064
5127
|
};
|
|
5065
|
-
var lovableSilentCatchOnProviderCallRule =
|
|
5128
|
+
var lovableSilentCatchOnProviderCallRule = rule52;
|
|
5066
5129
|
|
|
5067
5130
|
// src/providers/browserbase/utils.ts
|
|
5068
5131
|
function memberPropName3(node) {
|
|
@@ -5175,7 +5238,7 @@ function isTruthyGateTest(test) {
|
|
|
5175
5238
|
}
|
|
5176
5239
|
return false;
|
|
5177
5240
|
}
|
|
5178
|
-
var
|
|
5241
|
+
var rule53 = {
|
|
5179
5242
|
meta: {
|
|
5180
5243
|
type: "problem",
|
|
5181
5244
|
docs: {
|
|
@@ -5247,10 +5310,10 @@ var rule54 = {
|
|
|
5247
5310
|
};
|
|
5248
5311
|
}
|
|
5249
5312
|
};
|
|
5250
|
-
var browserbaseNoConditionalAuthzOnAnonymousUserRule =
|
|
5313
|
+
var browserbaseNoConditionalAuthzOnAnonymousUserRule = rule53;
|
|
5251
5314
|
|
|
5252
5315
|
// src/providers/browserbase/rules/no-connect-url-in-api-response.ts
|
|
5253
|
-
var
|
|
5316
|
+
var rule54 = {
|
|
5254
5317
|
meta: {
|
|
5255
5318
|
type: "problem",
|
|
5256
5319
|
docs: {
|
|
@@ -5280,7 +5343,7 @@ var rule55 = {
|
|
|
5280
5343
|
};
|
|
5281
5344
|
}
|
|
5282
5345
|
};
|
|
5283
|
-
var browserbaseNoConnectUrlInApiResponseRule =
|
|
5346
|
+
var browserbaseNoConnectUrlInApiResponseRule = rule54;
|
|
5284
5347
|
|
|
5285
5348
|
// src/providers/browserbase/rules/session-id-requires-ownership-check.ts
|
|
5286
5349
|
function isOwnershipCheckCall(node) {
|
|
@@ -5297,7 +5360,7 @@ function isSessionIdLikeArg(node) {
|
|
|
5297
5360
|
}
|
|
5298
5361
|
return false;
|
|
5299
5362
|
}
|
|
5300
|
-
var
|
|
5363
|
+
var rule55 = {
|
|
5301
5364
|
meta: {
|
|
5302
5365
|
type: "suggestion",
|
|
5303
5366
|
docs: {
|
|
@@ -5305,7 +5368,7 @@ var rule56 = {
|
|
|
5305
5368
|
category: "security",
|
|
5306
5369
|
cwe: "CWE-862",
|
|
5307
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.",
|
|
5308
|
-
docsUrl: "https://docs.browserbase.com/
|
|
5371
|
+
docsUrl: "https://docs.browserbase.com/platform/browser/observability/session-live-view",
|
|
5309
5372
|
recommended: true
|
|
5310
5373
|
},
|
|
5311
5374
|
messages: {
|
|
@@ -5361,7 +5424,7 @@ var rule56 = {
|
|
|
5361
5424
|
};
|
|
5362
5425
|
}
|
|
5363
5426
|
};
|
|
5364
|
-
var browserbaseSessionIdRequiresOwnershipCheckRule =
|
|
5427
|
+
var browserbaseSessionIdRequiresOwnershipCheckRule = rule55;
|
|
5365
5428
|
|
|
5366
5429
|
// src/providers/browserbase/rules/no-concurrent-shared-context.ts
|
|
5367
5430
|
function isPromiseAllCall2(node) {
|
|
@@ -5418,14 +5481,14 @@ function findSharedContextCreateCall(callback) {
|
|
|
5418
5481
|
visit(body);
|
|
5419
5482
|
return found;
|
|
5420
5483
|
}
|
|
5421
|
-
var
|
|
5484
|
+
var rule56 = {
|
|
5422
5485
|
meta: {
|
|
5423
5486
|
type: "problem",
|
|
5424
5487
|
docs: {
|
|
5425
5488
|
description: "A Browserbase Context must not be attached to concurrent sessions",
|
|
5426
5489
|
category: "correctness",
|
|
5427
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.`,
|
|
5428
|
-
docsUrl: "https://docs.browserbase.com/features/contexts",
|
|
5491
|
+
docsUrl: "https://docs.browserbase.com/platform/browser/core-features/contexts",
|
|
5429
5492
|
recommended: true
|
|
5430
5493
|
},
|
|
5431
5494
|
messages: {
|
|
@@ -5465,7 +5528,7 @@ var rule57 = {
|
|
|
5465
5528
|
};
|
|
5466
5529
|
}
|
|
5467
5530
|
};
|
|
5468
|
-
var browserbaseNoConcurrentSharedContextRule =
|
|
5531
|
+
var browserbaseNoConcurrentSharedContextRule = rule56;
|
|
5469
5532
|
|
|
5470
5533
|
// src/providers/browserbase/rules/mobile-device-requires-os-setting.ts
|
|
5471
5534
|
function isMobileLiteral(node) {
|
|
@@ -5498,14 +5561,14 @@ function isOsMobileSetting(node) {
|
|
|
5498
5561
|
}
|
|
5499
5562
|
return false;
|
|
5500
5563
|
}
|
|
5501
|
-
var
|
|
5564
|
+
var rule57 = {
|
|
5502
5565
|
meta: {
|
|
5503
5566
|
type: "problem",
|
|
5504
5567
|
docs: {
|
|
5505
5568
|
description: "Mobile device combos must set browserSettings.os, not just resize the viewport",
|
|
5506
5569
|
category: "correctness",
|
|
5507
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.`,
|
|
5508
|
-
docsUrl: "https://docs.browserbase.com/
|
|
5571
|
+
docsUrl: "https://docs.browserbase.com/platform/identity/verified-customization",
|
|
5509
5572
|
recommended: true
|
|
5510
5573
|
},
|
|
5511
5574
|
messages: {
|
|
@@ -5537,7 +5600,7 @@ var rule58 = {
|
|
|
5537
5600
|
};
|
|
5538
5601
|
}
|
|
5539
5602
|
};
|
|
5540
|
-
var browserbaseMobileDeviceRequiresOsSettingRule =
|
|
5603
|
+
var browserbaseMobileDeviceRequiresOsSettingRule = rule57;
|
|
5541
5604
|
|
|
5542
5605
|
// src/providers/browserbase/rules/use-typed-exception-status-not-substring.ts
|
|
5543
5606
|
function isStringifiedErrorSource(node, errName) {
|
|
@@ -5599,7 +5662,7 @@ function collectStringifiedVars(body, errName) {
|
|
|
5599
5662
|
});
|
|
5600
5663
|
return names;
|
|
5601
5664
|
}
|
|
5602
|
-
var
|
|
5665
|
+
var rule58 = {
|
|
5603
5666
|
meta: {
|
|
5604
5667
|
type: "suggestion",
|
|
5605
5668
|
docs: {
|
|
@@ -5629,7 +5692,7 @@ var rule59 = {
|
|
|
5629
5692
|
};
|
|
5630
5693
|
}
|
|
5631
5694
|
};
|
|
5632
|
-
var browserbaseUseTypedExceptionStatusNotSubstringRule =
|
|
5695
|
+
var browserbaseUseTypedExceptionStatusNotSubstringRule = rule58;
|
|
5633
5696
|
|
|
5634
5697
|
// src/providers/browserbase/rules/release-session-on-connect-failure.ts
|
|
5635
5698
|
function isSessionsCreateAwait(node) {
|
|
@@ -5652,7 +5715,7 @@ function isReleaseCall(node) {
|
|
|
5652
5715
|
const name = callee?.type === "Identifier" ? callee.name : memberPropName3(node);
|
|
5653
5716
|
return !!name && /requeststop|releasesession|release_session/i.test(name);
|
|
5654
5717
|
}
|
|
5655
|
-
var
|
|
5718
|
+
var rule59 = {
|
|
5656
5719
|
meta: {
|
|
5657
5720
|
type: "problem",
|
|
5658
5721
|
docs: {
|
|
@@ -5694,10 +5757,10 @@ var rule60 = {
|
|
|
5694
5757
|
};
|
|
5695
5758
|
}
|
|
5696
5759
|
};
|
|
5697
|
-
var browserbaseReleaseSessionOnConnectFailureRule =
|
|
5760
|
+
var browserbaseReleaseSessionOnConnectFailureRule = rule59;
|
|
5698
5761
|
|
|
5699
5762
|
// src/providers/browserbase/rules/dont-stack-custom-retry-on-sdk-retry.ts
|
|
5700
|
-
var
|
|
5763
|
+
var rule60 = {
|
|
5701
5764
|
meta: {
|
|
5702
5765
|
type: "suggestion",
|
|
5703
5766
|
docs: {
|
|
@@ -5742,7 +5805,7 @@ var rule61 = {
|
|
|
5742
5805
|
};
|
|
5743
5806
|
}
|
|
5744
5807
|
};
|
|
5745
|
-
var browserbaseDontStackCustomRetryOnSdkRetryRule =
|
|
5808
|
+
var browserbaseDontStackCustomRetryOnSdkRetryRule = rule60;
|
|
5746
5809
|
|
|
5747
5810
|
// src/providers/browserbase/rules/no-overbroad-error-substring-match.ts
|
|
5748
5811
|
var OVERBROAD_TERMS = /* @__PURE__ */ new Set(["session", "context", "browser", "browserbase"]);
|
|
@@ -5764,7 +5827,7 @@ function isCleanupCall(node) {
|
|
|
5764
5827
|
const name = callee?.type === "Identifier" ? callee.name : memberPropName3(node);
|
|
5765
5828
|
return !!name && /release|remove|cleanup|stop|teardown/i.test(name);
|
|
5766
5829
|
}
|
|
5767
|
-
var
|
|
5830
|
+
var rule61 = {
|
|
5768
5831
|
meta: {
|
|
5769
5832
|
type: "problem",
|
|
5770
5833
|
docs: {
|
|
@@ -5794,7 +5857,7 @@ var rule62 = {
|
|
|
5794
5857
|
};
|
|
5795
5858
|
}
|
|
5796
5859
|
};
|
|
5797
|
-
var browserbaseNoOverbroadErrorSubstringMatchRule =
|
|
5860
|
+
var browserbaseNoOverbroadErrorSubstringMatchRule = rule61;
|
|
5798
5861
|
|
|
5799
5862
|
// src/providers/browserbase/rules/use-sdk-not-raw-requests.ts
|
|
5800
5863
|
var BROWSERBASE_HOST = "api.browserbase.com";
|
|
@@ -5834,7 +5897,7 @@ function isRawHttpCall(node) {
|
|
|
5834
5897
|
}
|
|
5835
5898
|
return { isCall: false, urlArg: void 0 };
|
|
5836
5899
|
}
|
|
5837
|
-
var
|
|
5900
|
+
var rule62 = {
|
|
5838
5901
|
meta: {
|
|
5839
5902
|
type: "suggestion",
|
|
5840
5903
|
docs: {
|
|
@@ -5868,10 +5931,10 @@ var rule63 = {
|
|
|
5868
5931
|
};
|
|
5869
5932
|
}
|
|
5870
5933
|
};
|
|
5871
|
-
var browserbaseUseSdkNotRawRequestsRule =
|
|
5934
|
+
var browserbaseUseSdkNotRawRequestsRule = rule62;
|
|
5872
5935
|
|
|
5873
5936
|
// src/providers/browserbase/rules/centralize-request-release.ts
|
|
5874
|
-
var
|
|
5937
|
+
var rule63 = {
|
|
5875
5938
|
meta: {
|
|
5876
5939
|
type: "suggestion",
|
|
5877
5940
|
docs: {
|
|
@@ -5904,11 +5967,11 @@ var rule64 = {
|
|
|
5904
5967
|
};
|
|
5905
5968
|
}
|
|
5906
5969
|
};
|
|
5907
|
-
var browserbaseCentralizeRequestReleaseRule =
|
|
5970
|
+
var browserbaseCentralizeRequestReleaseRule = rule63;
|
|
5908
5971
|
|
|
5909
5972
|
// src/providers/openai-cua/rules/no-domain-allowlist.ts
|
|
5910
5973
|
var ACTION_METHOD_NAMES = /* @__PURE__ */ new Set(["click", "type", "press", "move", "goto", "fill", "dblclick", "dragAndDrop"]);
|
|
5911
|
-
var
|
|
5974
|
+
var rule64 = {
|
|
5912
5975
|
meta: {
|
|
5913
5976
|
type: "problem",
|
|
5914
5977
|
docs: {
|
|
@@ -6034,11 +6097,11 @@ var rule65 = {
|
|
|
6034
6097
|
};
|
|
6035
6098
|
}
|
|
6036
6099
|
};
|
|
6037
|
-
var openaiCuaNoDomainAllowlistRule =
|
|
6100
|
+
var openaiCuaNoDomainAllowlistRule = rule64;
|
|
6038
6101
|
|
|
6039
6102
|
// src/providers/openai-cua/rules/scroll-delta-default-zero.ts
|
|
6040
6103
|
var VERTICAL_DELTA_NAME_PATTERN = /^(dy|delta_?y|scroll_?y)$/i;
|
|
6041
|
-
var
|
|
6104
|
+
var rule65 = {
|
|
6042
6105
|
meta: {
|
|
6043
6106
|
type: "problem",
|
|
6044
6107
|
docs: {
|
|
@@ -6114,12 +6177,12 @@ var rule66 = {
|
|
|
6114
6177
|
};
|
|
6115
6178
|
}
|
|
6116
6179
|
};
|
|
6117
|
-
var openaiCuaScrollDeltaDefaultZeroRule =
|
|
6180
|
+
var openaiCuaScrollDeltaDefaultZeroRule = rule65;
|
|
6118
6181
|
|
|
6119
6182
|
// src/providers/openai-cua/rules/structured-step-metadata-not-text-json.ts
|
|
6120
6183
|
var BRACE_SEARCH_METHODS = /* @__PURE__ */ new Set(["indexOf", "lastIndexOf"]);
|
|
6121
6184
|
var SLICE_METHODS = /* @__PURE__ */ new Set(["slice", "substring", "substr"]);
|
|
6122
|
-
var
|
|
6185
|
+
var rule66 = {
|
|
6123
6186
|
meta: {
|
|
6124
6187
|
type: "suggestion",
|
|
6125
6188
|
docs: {
|
|
@@ -6218,10 +6281,10 @@ var rule67 = {
|
|
|
6218
6281
|
};
|
|
6219
6282
|
}
|
|
6220
6283
|
};
|
|
6221
|
-
var openaiCuaStructuredStepMetadataNotTextJsonRule =
|
|
6284
|
+
var openaiCuaStructuredStepMetadataNotTextJsonRule = rule66;
|
|
6222
6285
|
|
|
6223
6286
|
// src/providers/openai-cua/rules/no-blind-safety-check-ack.ts
|
|
6224
|
-
var
|
|
6287
|
+
var rule67 = {
|
|
6225
6288
|
meta: {
|
|
6226
6289
|
type: "suggestion",
|
|
6227
6290
|
docs: {
|
|
@@ -6284,7 +6347,7 @@ var rule68 = {
|
|
|
6284
6347
|
};
|
|
6285
6348
|
}
|
|
6286
6349
|
};
|
|
6287
|
-
var openaiCuaNoBlindSafetyCheckAckRule =
|
|
6350
|
+
var openaiCuaNoBlindSafetyCheckAckRule = rule67;
|
|
6288
6351
|
|
|
6289
6352
|
// src/providers/openai-cua/utils.ts
|
|
6290
6353
|
function propName(node) {
|
|
@@ -6330,7 +6393,7 @@ function findResponsesCreateCall(node, depth = 0) {
|
|
|
6330
6393
|
}
|
|
6331
6394
|
|
|
6332
6395
|
// src/providers/openai-cua/rules/retry-transient-turn-errors.ts
|
|
6333
|
-
var
|
|
6396
|
+
var rule68 = {
|
|
6334
6397
|
meta: {
|
|
6335
6398
|
type: "problem",
|
|
6336
6399
|
docs: {
|
|
@@ -6412,10 +6475,10 @@ var rule69 = {
|
|
|
6412
6475
|
};
|
|
6413
6476
|
}
|
|
6414
6477
|
};
|
|
6415
|
-
var openaiCuaRetryTransientTurnErrorsRule =
|
|
6478
|
+
var openaiCuaRetryTransientTurnErrorsRule = rule68;
|
|
6416
6479
|
|
|
6417
6480
|
// src/providers/openai-cua/rules/check-response-status-incomplete.ts
|
|
6418
|
-
var
|
|
6481
|
+
var rule69 = {
|
|
6419
6482
|
meta: {
|
|
6420
6483
|
type: "problem",
|
|
6421
6484
|
docs: {
|
|
@@ -6524,17 +6587,17 @@ var rule70 = {
|
|
|
6524
6587
|
};
|
|
6525
6588
|
}
|
|
6526
6589
|
};
|
|
6527
|
-
var openaiCuaCheckResponseStatusIncompleteRule =
|
|
6590
|
+
var openaiCuaCheckResponseStatusIncompleteRule = rule69;
|
|
6528
6591
|
|
|
6529
6592
|
// src/providers/openai-cua/rules/set-safety-identifier.ts
|
|
6530
|
-
var
|
|
6593
|
+
var rule70 = {
|
|
6531
6594
|
meta: {
|
|
6532
6595
|
type: "problem",
|
|
6533
6596
|
docs: {
|
|
6534
6597
|
description: "responses.create() must set safety_identifier for per-end-user policy attribution",
|
|
6535
6598
|
category: "integration",
|
|
6536
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.",
|
|
6537
|
-
docsUrl: "https://
|
|
6600
|
+
docsUrl: "https://developers.openai.com/api/docs/guides/safety-best-practices",
|
|
6538
6601
|
recommended: true
|
|
6539
6602
|
},
|
|
6540
6603
|
messages: {
|
|
@@ -6565,10 +6628,10 @@ var rule71 = {
|
|
|
6565
6628
|
};
|
|
6566
6629
|
}
|
|
6567
6630
|
};
|
|
6568
|
-
var openaiCuaSetSafetyIdentifierRule =
|
|
6631
|
+
var openaiCuaSetSafetyIdentifierRule = rule70;
|
|
6569
6632
|
|
|
6570
6633
|
// src/providers/tiptap/rules/upload-validate-fn-void.ts
|
|
6571
|
-
var
|
|
6634
|
+
var rule71 = {
|
|
6572
6635
|
meta: {
|
|
6573
6636
|
type: "problem",
|
|
6574
6637
|
docs: {
|
|
@@ -6632,10 +6695,10 @@ var rule72 = {
|
|
|
6632
6695
|
};
|
|
6633
6696
|
}
|
|
6634
6697
|
};
|
|
6635
|
-
var tiptapUploadValidateFnVoidRule =
|
|
6698
|
+
var tiptapUploadValidateFnVoidRule = rule71;
|
|
6636
6699
|
|
|
6637
6700
|
// src/providers/tiptap/rules/script-src-hardcoded-api-key.ts
|
|
6638
|
-
var
|
|
6701
|
+
var rule72 = {
|
|
6639
6702
|
meta: {
|
|
6640
6703
|
type: "problem",
|
|
6641
6704
|
docs: {
|
|
@@ -6693,10 +6756,10 @@ var rule73 = {
|
|
|
6693
6756
|
};
|
|
6694
6757
|
}
|
|
6695
6758
|
};
|
|
6696
|
-
var tiptapScriptSrcHardcodedApiKeyRule =
|
|
6759
|
+
var tiptapScriptSrcHardcodedApiKeyRule = rule72;
|
|
6697
6760
|
|
|
6698
6761
|
// src/providers/tiptap/rules/dynamic-script-no-sri.ts
|
|
6699
|
-
var
|
|
6762
|
+
var rule73 = {
|
|
6700
6763
|
meta: {
|
|
6701
6764
|
type: "problem",
|
|
6702
6765
|
docs: {
|
|
@@ -6705,7 +6768,7 @@ var rule74 = {
|
|
|
6705
6768
|
cwe: "CWE-829",
|
|
6706
6769
|
owasp: "API8:2023 Security Misconfiguration",
|
|
6707
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.',
|
|
6708
|
-
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",
|
|
6709
6772
|
recommended: true
|
|
6710
6773
|
},
|
|
6711
6774
|
messages: {
|
|
@@ -6792,7 +6855,7 @@ var rule74 = {
|
|
|
6792
6855
|
};
|
|
6793
6856
|
}
|
|
6794
6857
|
};
|
|
6795
|
-
var tiptapDynamicScriptNoSriRule =
|
|
6858
|
+
var tiptapDynamicScriptNoSriRule = rule73;
|
|
6796
6859
|
|
|
6797
6860
|
// src/providers/tiptap/utils.ts
|
|
6798
6861
|
function findProperty3(objectExpression, name) {
|
|
@@ -6817,7 +6880,7 @@ function walk(node, visit) {
|
|
|
6817
6880
|
}
|
|
6818
6881
|
|
|
6819
6882
|
// src/providers/tiptap/rules/addAttributes-missing-renderHTML.ts
|
|
6820
|
-
var
|
|
6883
|
+
var rule74 = {
|
|
6821
6884
|
meta: {
|
|
6822
6885
|
type: "problem",
|
|
6823
6886
|
docs: {
|
|
@@ -6870,10 +6933,10 @@ var rule75 = {
|
|
|
6870
6933
|
};
|
|
6871
6934
|
}
|
|
6872
6935
|
};
|
|
6873
|
-
var tiptapAddAttributesMissingRenderHTMLRule =
|
|
6936
|
+
var tiptapAddAttributesMissingRenderHTMLRule = rule74;
|
|
6874
6937
|
|
|
6875
6938
|
// src/providers/tiptap/rules/appendTransaction-add-to-history.ts
|
|
6876
|
-
var
|
|
6939
|
+
var rule75 = {
|
|
6877
6940
|
meta: {
|
|
6878
6941
|
type: "suggestion",
|
|
6879
6942
|
docs: {
|
|
@@ -6952,10 +7015,10 @@ var rule76 = {
|
|
|
6952
7015
|
};
|
|
6953
7016
|
}
|
|
6954
7017
|
};
|
|
6955
|
-
var tiptapAppendTransactionAddToHistoryRule =
|
|
7018
|
+
var tiptapAppendTransactionAddToHistoryRule = rule75;
|
|
6956
7019
|
|
|
6957
7020
|
// src/providers/tiptap/rules/appendTransaction-full-scan.ts
|
|
6958
|
-
var
|
|
7021
|
+
var rule76 = {
|
|
6959
7022
|
meta: {
|
|
6960
7023
|
type: "suggestion",
|
|
6961
7024
|
docs: {
|
|
@@ -7015,10 +7078,10 @@ var rule77 = {
|
|
|
7015
7078
|
};
|
|
7016
7079
|
}
|
|
7017
7080
|
};
|
|
7018
|
-
var tiptapAppendTransactionFullScanRule =
|
|
7081
|
+
var tiptapAppendTransactionFullScanRule = rule76;
|
|
7019
7082
|
|
|
7020
7083
|
// src/providers/tiptap/rules/atom-node-wrap-in.ts
|
|
7021
|
-
var
|
|
7084
|
+
var rule77 = {
|
|
7022
7085
|
meta: {
|
|
7023
7086
|
type: "problem",
|
|
7024
7087
|
docs: {
|
|
@@ -7081,57 +7144,16 @@ var rule78 = {
|
|
|
7081
7144
|
};
|
|
7082
7145
|
}
|
|
7083
7146
|
};
|
|
7084
|
-
var tiptapAtomNodeWrapInRule =
|
|
7085
|
-
|
|
7086
|
-
// src/providers/tiptap/rules/twitter-url-regex.ts
|
|
7087
|
-
var rule79 = {
|
|
7088
|
-
meta: {
|
|
7089
|
-
type: "suggestion",
|
|
7090
|
-
docs: {
|
|
7091
|
-
description: "Twitter/X URL regex must match both x.com and twitter.com",
|
|
7092
|
-
category: "integration",
|
|
7093
|
-
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.",
|
|
7094
|
-
docsUrl: "https://tiptap.dev/docs/editor/extensions/nodes",
|
|
7095
|
-
recommended: true
|
|
7096
|
-
},
|
|
7097
|
-
messages: {
|
|
7098
|
-
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."
|
|
7099
|
-
},
|
|
7100
|
-
schema: []
|
|
7101
|
-
},
|
|
7102
|
-
create(context) {
|
|
7103
|
-
function checkPattern(pattern, node) {
|
|
7104
|
-
if (!pattern.includes("x\\.com") && !pattern.includes("x.com")) return;
|
|
7105
|
-
if (pattern.includes("twitter\\.com") || pattern.includes("twitter.com")) return;
|
|
7106
|
-
context.report({ node, messageId: "twitterRegexMissingLegacyDomain" });
|
|
7107
|
-
}
|
|
7108
|
-
return {
|
|
7109
|
-
Literal(node) {
|
|
7110
|
-
if (node.regex?.pattern) {
|
|
7111
|
-
checkPattern(node.regex.pattern, node);
|
|
7112
|
-
}
|
|
7113
|
-
},
|
|
7114
|
-
NewExpression(node) {
|
|
7115
|
-
if (node.callee?.type === "Identifier" && node.callee.name === "RegExp") {
|
|
7116
|
-
const firstArg = node.arguments?.[0];
|
|
7117
|
-
if (firstArg?.type === "Literal" && typeof firstArg.value === "string") {
|
|
7118
|
-
checkPattern(firstArg.value, node);
|
|
7119
|
-
}
|
|
7120
|
-
}
|
|
7121
|
-
}
|
|
7122
|
-
};
|
|
7123
|
-
}
|
|
7124
|
-
};
|
|
7125
|
-
var tiptapTwitterUrlRegexRule = rule79;
|
|
7147
|
+
var tiptapAtomNodeWrapInRule = rule77;
|
|
7126
7148
|
|
|
7127
7149
|
// src/providers/tiptap/rules/drop-handler-pos-precedence.ts
|
|
7128
|
-
var
|
|
7150
|
+
var rule78 = {
|
|
7129
7151
|
meta: {
|
|
7130
7152
|
type: "problem",
|
|
7131
7153
|
docs: {
|
|
7132
7154
|
description: "x ?? 0 - 1 is parsed as x ?? (0 - 1) = x ?? -1 due to operator precedence",
|
|
7133
7155
|
category: "correctness",
|
|
7134
|
-
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.",
|
|
7135
7157
|
docsUrl: "https://prosemirror.net/docs/ref/#view.EditorView.posAtCoords",
|
|
7136
7158
|
recommended: true
|
|
7137
7159
|
},
|
|
@@ -7152,7 +7174,7 @@ var rule80 = {
|
|
|
7152
7174
|
};
|
|
7153
7175
|
}
|
|
7154
7176
|
};
|
|
7155
|
-
var tiptapDropHandlerPosPrecedenceRule =
|
|
7177
|
+
var tiptapDropHandlerPosPrecedenceRule = rule78;
|
|
7156
7178
|
|
|
7157
7179
|
// src/providers/tiptap/rules/prefer-table-kit.ts
|
|
7158
7180
|
var INDIVIDUAL_TABLE_PACKAGES = /* @__PURE__ */ new Set([
|
|
@@ -7160,18 +7182,18 @@ var INDIVIDUAL_TABLE_PACKAGES = /* @__PURE__ */ new Set([
|
|
|
7160
7182
|
"@tiptap/extension-table-cell",
|
|
7161
7183
|
"@tiptap/extension-table-header"
|
|
7162
7184
|
]);
|
|
7163
|
-
var
|
|
7185
|
+
var rule79 = {
|
|
7164
7186
|
meta: {
|
|
7165
7187
|
type: "suggestion",
|
|
7166
7188
|
docs: {
|
|
7167
7189
|
description: "Use TableKit from @tiptap/extension-table instead of individual table packages",
|
|
7168
7190
|
category: "integration",
|
|
7169
|
-
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.",
|
|
7170
7192
|
docsUrl: "https://tiptap.dev/docs/editor/extensions/functionality/table-kit",
|
|
7171
7193
|
recommended: true
|
|
7172
7194
|
},
|
|
7173
7195
|
messages: {
|
|
7174
|
-
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."
|
|
7175
7197
|
},
|
|
7176
7198
|
schema: []
|
|
7177
7199
|
},
|
|
@@ -7194,21 +7216,21 @@ var rule81 = {
|
|
|
7194
7216
|
};
|
|
7195
7217
|
}
|
|
7196
7218
|
};
|
|
7197
|
-
var tiptapPreferTableKitRule =
|
|
7219
|
+
var tiptapPreferTableKitRule = rule79;
|
|
7198
7220
|
|
|
7199
7221
|
// src/providers/tiptap/rules/tiptap-markdown-missing-node-spec.ts
|
|
7200
|
-
var
|
|
7222
|
+
var rule80 = {
|
|
7201
7223
|
meta: {
|
|
7202
7224
|
type: "suggestion",
|
|
7203
7225
|
docs: {
|
|
7204
7226
|
description: "TipTap nodes used with tiptap-markdown must define a markdown serialization spec",
|
|
7205
7227
|
category: "integration",
|
|
7206
|
-
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
|
|
7207
|
-
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",
|
|
7208
7230
|
recommended: true
|
|
7209
7231
|
},
|
|
7210
7232
|
messages: {
|
|
7211
|
-
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."
|
|
7212
7234
|
},
|
|
7213
7235
|
schema: []
|
|
7214
7236
|
},
|
|
@@ -7263,7 +7285,7 @@ var rule82 = {
|
|
|
7263
7285
|
};
|
|
7264
7286
|
}
|
|
7265
7287
|
};
|
|
7266
|
-
var tiptapTiptapMarkdownMissingNodeSpecRule =
|
|
7288
|
+
var tiptapTiptapMarkdownMissingNodeSpecRule = rule80;
|
|
7267
7289
|
|
|
7268
7290
|
// src/providers/elevenlabs/utils.ts
|
|
7269
7291
|
function isElevenLabsUrlArg(node) {
|
|
@@ -7324,7 +7346,7 @@ function posOf(n) {
|
|
|
7324
7346
|
}
|
|
7325
7347
|
|
|
7326
7348
|
// src/providers/elevenlabs/rules/validate-signed-url-response.ts
|
|
7327
|
-
var
|
|
7349
|
+
var rule81 = {
|
|
7328
7350
|
meta: {
|
|
7329
7351
|
type: "problem",
|
|
7330
7352
|
docs: {
|
|
@@ -7423,11 +7445,11 @@ var rule83 = {
|
|
|
7423
7445
|
};
|
|
7424
7446
|
}
|
|
7425
7447
|
};
|
|
7426
|
-
var elevenlabsValidateSignedUrlResponseRule =
|
|
7448
|
+
var elevenlabsValidateSignedUrlResponseRule = rule81;
|
|
7427
7449
|
|
|
7428
7450
|
// src/providers/elevenlabs/rules/no-error-object-logging.ts
|
|
7429
7451
|
var LOGGING_METHODS = /* @__PURE__ */ new Set(["error", "warn", "log"]);
|
|
7430
|
-
var
|
|
7452
|
+
var rule82 = {
|
|
7431
7453
|
meta: {
|
|
7432
7454
|
type: "problem",
|
|
7433
7455
|
docs: {
|
|
@@ -7495,10 +7517,10 @@ var rule84 = {
|
|
|
7495
7517
|
};
|
|
7496
7518
|
}
|
|
7497
7519
|
};
|
|
7498
|
-
var elevenlabsNoErrorObjectLoggingRule =
|
|
7520
|
+
var elevenlabsNoErrorObjectLoggingRule = rule82;
|
|
7499
7521
|
|
|
7500
7522
|
// src/providers/elevenlabs/rules/fetch-timeout-required.ts
|
|
7501
|
-
var
|
|
7523
|
+
var rule83 = {
|
|
7502
7524
|
meta: {
|
|
7503
7525
|
type: "suggestion",
|
|
7504
7526
|
docs: {
|
|
@@ -7531,10 +7553,10 @@ var rule85 = {
|
|
|
7531
7553
|
};
|
|
7532
7554
|
}
|
|
7533
7555
|
};
|
|
7534
|
-
var elevenlabsFetchTimeoutRequiredRule =
|
|
7556
|
+
var elevenlabsFetchTimeoutRequiredRule = rule83;
|
|
7535
7557
|
|
|
7536
7558
|
// src/providers/elevenlabs/rules/validate-agent-id-format.ts
|
|
7537
|
-
var
|
|
7559
|
+
var rule84 = {
|
|
7538
7560
|
meta: {
|
|
7539
7561
|
type: "problem",
|
|
7540
7562
|
docs: {
|
|
@@ -7640,10 +7662,10 @@ var rule86 = {
|
|
|
7640
7662
|
};
|
|
7641
7663
|
}
|
|
7642
7664
|
};
|
|
7643
|
-
var elevenlabsValidateAgentIdFormatRule =
|
|
7665
|
+
var elevenlabsValidateAgentIdFormatRule = rule84;
|
|
7644
7666
|
|
|
7645
7667
|
// src/providers/elevenlabs/rules/secure-session-id-generation.ts
|
|
7646
|
-
var
|
|
7668
|
+
var rule85 = {
|
|
7647
7669
|
meta: {
|
|
7648
7670
|
type: "problem",
|
|
7649
7671
|
docs: {
|
|
@@ -7708,10 +7730,10 @@ var rule87 = {
|
|
|
7708
7730
|
};
|
|
7709
7731
|
}
|
|
7710
7732
|
};
|
|
7711
|
-
var elevenlabsSecureSessionIdGenerationRule =
|
|
7733
|
+
var elevenlabsSecureSessionIdGenerationRule = rule85;
|
|
7712
7734
|
|
|
7713
7735
|
// src/providers/elevenlabs/rules/conversation-error-recovery.ts
|
|
7714
|
-
var
|
|
7736
|
+
var rule86 = {
|
|
7715
7737
|
meta: {
|
|
7716
7738
|
type: "suggestion",
|
|
7717
7739
|
docs: {
|
|
@@ -7811,10 +7833,10 @@ var rule88 = {
|
|
|
7811
7833
|
};
|
|
7812
7834
|
}
|
|
7813
7835
|
};
|
|
7814
|
-
var elevenlabsConversationErrorRecoveryRule =
|
|
7836
|
+
var elevenlabsConversationErrorRecoveryRule = rule86;
|
|
7815
7837
|
|
|
7816
7838
|
// src/providers/elevenlabs/rules/api-version-pinning.ts
|
|
7817
|
-
var
|
|
7839
|
+
var rule87 = {
|
|
7818
7840
|
meta: {
|
|
7819
7841
|
type: "suggestion",
|
|
7820
7842
|
docs: {
|
|
@@ -7854,10 +7876,10 @@ var rule89 = {
|
|
|
7854
7876
|
};
|
|
7855
7877
|
}
|
|
7856
7878
|
};
|
|
7857
|
-
var elevenlabsApiVersionPinningRule =
|
|
7879
|
+
var elevenlabsApiVersionPinningRule = rule87;
|
|
7858
7880
|
|
|
7859
7881
|
// src/providers/elevenlabs/rules/check-http-status-before-json.ts
|
|
7860
|
-
var
|
|
7882
|
+
var rule88 = {
|
|
7861
7883
|
meta: {
|
|
7862
7884
|
type: "problem",
|
|
7863
7885
|
docs: {
|
|
@@ -7953,11 +7975,11 @@ var rule90 = {
|
|
|
7953
7975
|
};
|
|
7954
7976
|
}
|
|
7955
7977
|
};
|
|
7956
|
-
var elevenlabsCheckHttpStatusBeforeJsonRule =
|
|
7978
|
+
var elevenlabsCheckHttpStatusBeforeJsonRule = rule88;
|
|
7957
7979
|
|
|
7958
7980
|
// src/providers/elevenlabs/rules/conversation-cleanup-on-error.ts
|
|
7959
7981
|
var END_METHODS = /* @__PURE__ */ new Set(["endSession", "endConversation"]);
|
|
7960
|
-
var
|
|
7982
|
+
var rule89 = {
|
|
7961
7983
|
meta: {
|
|
7962
7984
|
type: "suggestion",
|
|
7963
7985
|
docs: {
|
|
@@ -8018,11 +8040,11 @@ var rule91 = {
|
|
|
8018
8040
|
};
|
|
8019
8041
|
}
|
|
8020
8042
|
};
|
|
8021
|
-
var elevenlabsConversationCleanupOnErrorRule =
|
|
8043
|
+
var elevenlabsConversationCleanupOnErrorRule = rule89;
|
|
8022
8044
|
|
|
8023
8045
|
// src/providers/elevenlabs/rules/env-var-validation.ts
|
|
8024
8046
|
var ELEVENLABS_ENV_VAR_PATTERN = /^(XI_API_KEY|ELEVENLABS_API_KEY)$/;
|
|
8025
|
-
var
|
|
8047
|
+
var rule90 = {
|
|
8026
8048
|
meta: {
|
|
8027
8049
|
type: "suggestion",
|
|
8028
8050
|
docs: {
|
|
@@ -8083,7 +8105,7 @@ var rule92 = {
|
|
|
8083
8105
|
};
|
|
8084
8106
|
}
|
|
8085
8107
|
};
|
|
8086
|
-
var elevenlabsEnvVarValidationRule =
|
|
8108
|
+
var elevenlabsEnvVarValidationRule = rule90;
|
|
8087
8109
|
|
|
8088
8110
|
// src/providers/twilio/utils.ts
|
|
8089
8111
|
var POST_METHOD_NAMES = /* @__PURE__ */ new Set(["post"]);
|
|
@@ -8137,7 +8159,7 @@ function collectVarDeclarators2(node, out, depth = 0) {
|
|
|
8137
8159
|
}
|
|
8138
8160
|
|
|
8139
8161
|
// src/providers/twilio/rules/validate-webhook-signature.ts
|
|
8140
|
-
var
|
|
8162
|
+
var rule91 = {
|
|
8141
8163
|
meta: {
|
|
8142
8164
|
type: "problem",
|
|
8143
8165
|
docs: {
|
|
@@ -8179,10 +8201,10 @@ var rule93 = {
|
|
|
8179
8201
|
};
|
|
8180
8202
|
}
|
|
8181
8203
|
};
|
|
8182
|
-
var twilioValidateWebhookSignatureRule =
|
|
8204
|
+
var twilioValidateWebhookSignatureRule = rule91;
|
|
8183
8205
|
|
|
8184
8206
|
// src/providers/twilio/rules/taskrouter-attributes-match-consumer.ts
|
|
8185
|
-
var
|
|
8207
|
+
var rule92 = {
|
|
8186
8208
|
meta: {
|
|
8187
8209
|
type: "problem",
|
|
8188
8210
|
docs: {
|
|
@@ -8279,10 +8301,10 @@ var rule94 = {
|
|
|
8279
8301
|
};
|
|
8280
8302
|
}
|
|
8281
8303
|
};
|
|
8282
|
-
var twilioTaskrouterAttributesMatchConsumerRule =
|
|
8304
|
+
var twilioTaskrouterAttributesMatchConsumerRule = rule92;
|
|
8283
8305
|
|
|
8284
8306
|
// src/providers/twilio/rules/enqueue-task-json-stringify.ts
|
|
8285
|
-
var
|
|
8307
|
+
var rule93 = {
|
|
8286
8308
|
meta: {
|
|
8287
8309
|
type: "problem",
|
|
8288
8310
|
docs: {
|
|
@@ -8324,10 +8346,10 @@ var rule95 = {
|
|
|
8324
8346
|
};
|
|
8325
8347
|
}
|
|
8326
8348
|
};
|
|
8327
|
-
var twilioEnqueueTaskJsonStringifyRule =
|
|
8349
|
+
var twilioEnqueueTaskJsonStringifyRule = rule93;
|
|
8328
8350
|
|
|
8329
8351
|
// src/providers/twilio/rules/media-streams-key-by-call-sid.ts
|
|
8330
|
-
var
|
|
8352
|
+
var rule94 = {
|
|
8331
8353
|
meta: {
|
|
8332
8354
|
type: "problem",
|
|
8333
8355
|
docs: {
|
|
@@ -8403,12 +8425,12 @@ var rule96 = {
|
|
|
8403
8425
|
};
|
|
8404
8426
|
}
|
|
8405
8427
|
};
|
|
8406
|
-
var twilioMediaStreamsKeyByCallSidRule =
|
|
8428
|
+
var twilioMediaStreamsKeyByCallSidRule = rule94;
|
|
8407
8429
|
|
|
8408
8430
|
// src/providers/twilio/rules/await-or-catch-rest-calls-in-event-handlers.ts
|
|
8409
8431
|
var REST_RESOURCE_METHODS = /* @__PURE__ */ new Set(["create", "update", "remove", "fetch"]);
|
|
8410
8432
|
var EVENT_REGISTRATION_METHODS = /* @__PURE__ */ new Set(["onStart", "onStop", "onMedia", "onConnected", "on"]);
|
|
8411
|
-
var
|
|
8433
|
+
var rule95 = {
|
|
8412
8434
|
meta: {
|
|
8413
8435
|
type: "problem",
|
|
8414
8436
|
docs: {
|
|
@@ -8516,10 +8538,10 @@ var rule97 = {
|
|
|
8516
8538
|
};
|
|
8517
8539
|
}
|
|
8518
8540
|
};
|
|
8519
|
-
var twilioAwaitOrCatchRestCallsInEventHandlersRule =
|
|
8541
|
+
var twilioAwaitOrCatchRestCallsInEventHandlersRule = rule95;
|
|
8520
8542
|
|
|
8521
8543
|
// src/providers/twilio/rules/use-twiml-builder-not-string-templates.ts
|
|
8522
|
-
var
|
|
8544
|
+
var rule96 = {
|
|
8523
8545
|
meta: {
|
|
8524
8546
|
type: "suggestion",
|
|
8525
8547
|
docs: {
|
|
@@ -8548,10 +8570,10 @@ var rule98 = {
|
|
|
8548
8570
|
};
|
|
8549
8571
|
}
|
|
8550
8572
|
};
|
|
8551
|
-
var twilioUseTwimlBuilderNotStringTemplatesRule =
|
|
8573
|
+
var twilioUseTwimlBuilderNotStringTemplatesRule = rule96;
|
|
8552
8574
|
|
|
8553
8575
|
// src/providers/twilio/rules/media-streams-mark-pacing.ts
|
|
8554
|
-
var
|
|
8576
|
+
var rule97 = {
|
|
8555
8577
|
meta: {
|
|
8556
8578
|
type: "suggestion",
|
|
8557
8579
|
docs: {
|
|
@@ -8627,10 +8649,10 @@ var rule99 = {
|
|
|
8627
8649
|
};
|
|
8628
8650
|
}
|
|
8629
8651
|
};
|
|
8630
|
-
var twilioMediaStreamsMarkPacingRule =
|
|
8652
|
+
var twilioMediaStreamsMarkPacingRule = rule97;
|
|
8631
8653
|
|
|
8632
8654
|
// src/providers/twilio/rules/validate-all-request-inputs.ts
|
|
8633
|
-
var
|
|
8655
|
+
var rule98 = {
|
|
8634
8656
|
meta: {
|
|
8635
8657
|
type: "suggestion",
|
|
8636
8658
|
docs: {
|
|
@@ -8705,10 +8727,10 @@ var rule100 = {
|
|
|
8705
8727
|
};
|
|
8706
8728
|
}
|
|
8707
8729
|
};
|
|
8708
|
-
var twilioValidateAllRequestInputsRule =
|
|
8730
|
+
var twilioValidateAllRequestInputsRule = rule98;
|
|
8709
8731
|
|
|
8710
8732
|
// src/providers/twilio/rules/media-streams-mark-name-string.ts
|
|
8711
|
-
var
|
|
8733
|
+
var rule99 = {
|
|
8712
8734
|
meta: {
|
|
8713
8735
|
type: "suggestion",
|
|
8714
8736
|
docs: {
|
|
@@ -8758,7 +8780,7 @@ var rule101 = {
|
|
|
8758
8780
|
};
|
|
8759
8781
|
}
|
|
8760
8782
|
};
|
|
8761
|
-
var twilioMediaStreamsMarkNameStringRule =
|
|
8783
|
+
var twilioMediaStreamsMarkNameStringRule = rule99;
|
|
8762
8784
|
|
|
8763
8785
|
// src/providers/openai-realtime/utils.ts
|
|
8764
8786
|
function isOpenAIRealtimeUrlArg(node) {
|
|
@@ -8898,7 +8920,7 @@ function resolveStringValue(node, stringVarValues) {
|
|
|
8898
8920
|
}
|
|
8899
8921
|
|
|
8900
8922
|
// src/providers/openai-realtime/rules/migrate-beta-to-ga.ts
|
|
8901
|
-
var
|
|
8923
|
+
var rule100 = {
|
|
8902
8924
|
meta: {
|
|
8903
8925
|
type: "problem",
|
|
8904
8926
|
docs: {
|
|
@@ -8935,7 +8957,7 @@ var rule102 = {
|
|
|
8935
8957
|
};
|
|
8936
8958
|
}
|
|
8937
8959
|
};
|
|
8938
|
-
var openaiRealtimeMigrateBetaToGaRule =
|
|
8960
|
+
var openaiRealtimeMigrateBetaToGaRule = rule100;
|
|
8939
8961
|
|
|
8940
8962
|
// src/providers/openai-realtime/rules/no-log-raw-message-payloads.ts
|
|
8941
8963
|
var LOG_METHOD_NAMES = /* @__PURE__ */ new Set(["info", "warn", "error", "log"]);
|
|
@@ -8971,7 +8993,7 @@ function findCallExpressions(node, out, depth = 0) {
|
|
|
8971
8993
|
if (val && typeof val === "object") findCallExpressions(val, out, depth + 1);
|
|
8972
8994
|
}
|
|
8973
8995
|
}
|
|
8974
|
-
var
|
|
8996
|
+
var rule101 = {
|
|
8975
8997
|
meta: {
|
|
8976
8998
|
type: "problem",
|
|
8977
8999
|
docs: {
|
|
@@ -9015,7 +9037,7 @@ var rule103 = {
|
|
|
9015
9037
|
};
|
|
9016
9038
|
}
|
|
9017
9039
|
};
|
|
9018
|
-
var openaiRealtimeNoLogRawMessagePayloadsRule =
|
|
9040
|
+
var openaiRealtimeNoLogRawMessagePayloadsRule = rule101;
|
|
9019
9041
|
|
|
9020
9042
|
// src/providers/openai-realtime/rules/handle-error-server-event.ts
|
|
9021
9043
|
function isTypeMemberExpression(node) {
|
|
@@ -9046,13 +9068,13 @@ function collectTypeComparisonLiterals(node, out, depth = 0) {
|
|
|
9046
9068
|
if (val && typeof val === "object") collectTypeComparisonLiterals(val, out, depth + 1);
|
|
9047
9069
|
}
|
|
9048
9070
|
}
|
|
9049
|
-
var
|
|
9071
|
+
var rule102 = {
|
|
9050
9072
|
meta: {
|
|
9051
9073
|
type: "problem",
|
|
9052
9074
|
docs: {
|
|
9053
9075
|
description: 'OpenAI Realtime message handlers must check for the API-level "error" server event',
|
|
9054
9076
|
category: "reliability",
|
|
9055
|
-
docsUrl: "https://developers.openai.com/api/
|
|
9077
|
+
docsUrl: "https://developers.openai.com/api/reference/resources/realtime/server-events",
|
|
9056
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.",
|
|
9057
9079
|
recommended: true
|
|
9058
9080
|
},
|
|
@@ -9082,7 +9104,7 @@ var rule104 = {
|
|
|
9082
9104
|
};
|
|
9083
9105
|
}
|
|
9084
9106
|
};
|
|
9085
|
-
var openaiRealtimeHandleErrorServerEventRule =
|
|
9107
|
+
var openaiRealtimeHandleErrorServerEventRule = rule102;
|
|
9086
9108
|
|
|
9087
9109
|
// src/providers/openai-realtime/rules/reconnect-on-drop.ts
|
|
9088
9110
|
var RECONNECT_NAME_PATTERN = /reconnect/i;
|
|
@@ -9106,7 +9128,7 @@ function hasReconnectAttempt(node, depth = 0) {
|
|
|
9106
9128
|
}
|
|
9107
9129
|
return false;
|
|
9108
9130
|
}
|
|
9109
|
-
var
|
|
9131
|
+
var rule103 = {
|
|
9110
9132
|
meta: {
|
|
9111
9133
|
type: "problem",
|
|
9112
9134
|
docs: {
|
|
@@ -9139,17 +9161,17 @@ var rule105 = {
|
|
|
9139
9161
|
};
|
|
9140
9162
|
}
|
|
9141
9163
|
};
|
|
9142
|
-
var openaiRealtimeReconnectOnDropRule =
|
|
9164
|
+
var openaiRealtimeReconnectOnDropRule = rule103;
|
|
9143
9165
|
|
|
9144
9166
|
// src/providers/openai-realtime/rules/avoid-dated-preview-snapshots.ts
|
|
9145
9167
|
var DATED_PREVIEW_MODEL_PATTERN = /model=[^&'"`]*-preview-\d{4}-\d{2}-\d{2}/;
|
|
9146
|
-
var
|
|
9168
|
+
var rule104 = {
|
|
9147
9169
|
meta: {
|
|
9148
9170
|
type: "suggestion",
|
|
9149
9171
|
docs: {
|
|
9150
9172
|
description: "OpenAI Realtime connections should not pin to a dated preview model snapshot",
|
|
9151
9173
|
category: "correctness",
|
|
9152
|
-
docsUrl: "https://developers.openai.com/api/
|
|
9174
|
+
docsUrl: "https://developers.openai.com/api/reference/resources/realtime/subresources/client_secrets",
|
|
9153
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.",
|
|
9154
9176
|
recommended: true
|
|
9155
9177
|
},
|
|
@@ -9178,16 +9200,16 @@ var rule106 = {
|
|
|
9178
9200
|
};
|
|
9179
9201
|
}
|
|
9180
9202
|
};
|
|
9181
|
-
var openaiRealtimeAvoidDatedPreviewSnapshotsRule =
|
|
9203
|
+
var openaiRealtimeAvoidDatedPreviewSnapshotsRule = rule104;
|
|
9182
9204
|
|
|
9183
9205
|
// src/providers/openai-realtime/rules/verify-deprecated-session-fields.ts
|
|
9184
|
-
var
|
|
9206
|
+
var rule105 = {
|
|
9185
9207
|
meta: {
|
|
9186
9208
|
type: "suggestion",
|
|
9187
9209
|
docs: {
|
|
9188
9210
|
description: "OpenAI Realtime session.update payloads should not rely on an unverified 'temperature' field",
|
|
9189
9211
|
category: "correctness",
|
|
9190
|
-
docsUrl: "https://developers.openai.com/api/
|
|
9212
|
+
docsUrl: "https://developers.openai.com/api/reference/resources/realtime/subresources/client_secrets",
|
|
9191
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.",
|
|
9192
9214
|
recommended: true
|
|
9193
9215
|
},
|
|
@@ -9212,15 +9234,20 @@ var rule107 = {
|
|
|
9212
9234
|
};
|
|
9213
9235
|
}
|
|
9214
9236
|
};
|
|
9215
|
-
var openaiRealtimeVerifyDeprecatedSessionFieldsRule =
|
|
9237
|
+
var openaiRealtimeVerifyDeprecatedSessionFieldsRule = rule105;
|
|
9216
9238
|
|
|
9217
9239
|
// src/providers/openai-realtime/rules/buffer-audio-until-session-ready.ts
|
|
9218
9240
|
var QUEUE_CALL_NAME_PATTERN = /^(push|unshift|enqueue|queue)$/i;
|
|
9219
|
-
function
|
|
9220
|
-
if (test?.type !== "BinaryExpression"
|
|
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;
|
|
9221
9246
|
const isReadyStateMember = (n) => n?.type === "MemberExpression" && n.property?.type === "Identifier" && n.property.name === "readyState";
|
|
9222
9247
|
const isOpenValue = (n) => n?.type === "MemberExpression" && n.property?.type === "Identifier" && n.property.name === "OPEN" || n?.type === "Literal" && n.value === 1;
|
|
9223
|
-
|
|
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";
|
|
9224
9251
|
}
|
|
9225
9252
|
function hasQueueCall(node, depth = 0) {
|
|
9226
9253
|
if (!node || typeof node !== "object" || depth > 40) return false;
|
|
@@ -9237,13 +9264,13 @@ function hasQueueCall(node, depth = 0) {
|
|
|
9237
9264
|
}
|
|
9238
9265
|
return false;
|
|
9239
9266
|
}
|
|
9240
|
-
var
|
|
9267
|
+
var rule106 = {
|
|
9241
9268
|
meta: {
|
|
9242
9269
|
type: "suggestion",
|
|
9243
9270
|
docs: {
|
|
9244
9271
|
description: "Audio sent before an OpenAI Realtime socket is open must be buffered, not dropped",
|
|
9245
9272
|
category: "reliability",
|
|
9246
|
-
docsUrl: "https://developers.openai.com/api/
|
|
9273
|
+
docsUrl: "https://developers.openai.com/api/reference/resources/realtime/client-events",
|
|
9247
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.",
|
|
9248
9275
|
recommended: true
|
|
9249
9276
|
},
|
|
@@ -9255,18 +9282,20 @@ var rule108 = {
|
|
|
9255
9282
|
create(context) {
|
|
9256
9283
|
return {
|
|
9257
9284
|
IfStatement(node) {
|
|
9258
|
-
|
|
9259
|
-
if (!
|
|
9260
|
-
|
|
9261
|
-
|
|
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" });
|
|
9262
9291
|
}
|
|
9263
9292
|
};
|
|
9264
9293
|
}
|
|
9265
9294
|
};
|
|
9266
|
-
var openaiRealtimeBufferAudioUntilSessionReadyRule =
|
|
9295
|
+
var openaiRealtimeBufferAudioUntilSessionReadyRule = rule106;
|
|
9267
9296
|
|
|
9268
9297
|
// src/providers/openai-realtime/rules/send-safety-identifier.ts
|
|
9269
|
-
var
|
|
9298
|
+
var rule107 = {
|
|
9270
9299
|
meta: {
|
|
9271
9300
|
type: "suggestion",
|
|
9272
9301
|
docs: {
|
|
@@ -9299,10 +9328,10 @@ var rule109 = {
|
|
|
9299
9328
|
};
|
|
9300
9329
|
}
|
|
9301
9330
|
};
|
|
9302
|
-
var openaiRealtimeSendSafetyIdentifierRule =
|
|
9331
|
+
var openaiRealtimeSendSafetyIdentifierRule = rule107;
|
|
9303
9332
|
|
|
9304
9333
|
// src/providers/openai-realtime/rules/transcription-model-choice.ts
|
|
9305
|
-
var
|
|
9334
|
+
var rule108 = {
|
|
9306
9335
|
meta: {
|
|
9307
9336
|
type: "suggestion",
|
|
9308
9337
|
docs: {
|
|
@@ -9313,7 +9342,7 @@ var rule110 = {
|
|
|
9313
9342
|
recommended: true
|
|
9314
9343
|
},
|
|
9315
9344
|
messages: {
|
|
9316
|
-
nonStreamingTranscriptionModel: "
|
|
9345
|
+
nonStreamingTranscriptionModel: "This session's input transcription is configured with 'whisper-1', which is not natively streaming and not optimized for realtime sessions."
|
|
9317
9346
|
},
|
|
9318
9347
|
schema: []
|
|
9319
9348
|
},
|
|
@@ -9326,7 +9355,12 @@ var rule110 = {
|
|
|
9326
9355
|
if (!isSessionUpdate) return;
|
|
9327
9356
|
const sessionProp = findProperty4(node, "session");
|
|
9328
9357
|
if (sessionProp?.value?.type !== "ObjectExpression") return;
|
|
9329
|
-
|
|
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
|
+
}
|
|
9330
9364
|
if (transcriptionProp?.value?.type !== "ObjectExpression") return;
|
|
9331
9365
|
const modelProp = findProperty4(transcriptionProp.value, "model");
|
|
9332
9366
|
const modelValue = modelProp?.value;
|
|
@@ -9337,7 +9371,7 @@ var rule110 = {
|
|
|
9337
9371
|
};
|
|
9338
9372
|
}
|
|
9339
9373
|
};
|
|
9340
|
-
var openaiRealtimeTranscriptionModelChoiceRule =
|
|
9374
|
+
var openaiRealtimeTranscriptionModelChoiceRule = rule108;
|
|
9341
9375
|
|
|
9342
9376
|
// src/plugin/index.ts
|
|
9343
9377
|
var plugin = {
|
|
@@ -9382,7 +9416,6 @@ var plugin = {
|
|
|
9382
9416
|
"firebase-rtdb-write-promise-not-handled": firebaseRtdbWritePromiseNotHandledRule,
|
|
9383
9417
|
"firebase-firestore-rules-expired": firebaseFirestoreRulesExpiredRule,
|
|
9384
9418
|
"firebase-id-token-cookie-flags": firebaseIdTokenCookieFlagsRule,
|
|
9385
|
-
"firebase-middleware-token-not-verified": firebaseMiddlewareTokenNotVerifiedRule,
|
|
9386
9419
|
"firebase-hardcoded-user-id": firebaseHardcodedUserIdRule,
|
|
9387
9420
|
"firebase-auth-user-not-found-disclosure": firebaseAuthUserNotFoundDisclosureRule,
|
|
9388
9421
|
"firebase-signup-password-confirm": firebaseSignupPasswordConfirmRule,
|
|
@@ -9421,7 +9454,6 @@ var plugin = {
|
|
|
9421
9454
|
"tiptap-appendTransaction-add-to-history": tiptapAppendTransactionAddToHistoryRule,
|
|
9422
9455
|
"tiptap-appendTransaction-full-scan": tiptapAppendTransactionFullScanRule,
|
|
9423
9456
|
"tiptap-atom-node-wrap-in": tiptapAtomNodeWrapInRule,
|
|
9424
|
-
"tiptap-twitter-url-regex": tiptapTwitterUrlRegexRule,
|
|
9425
9457
|
"tiptap-drop-handler-pos-precedence": tiptapDropHandlerPosPrecedenceRule,
|
|
9426
9458
|
"tiptap-prefer-table-kit": tiptapPreferTableKitRule,
|
|
9427
9459
|
"tiptap-tiptap-markdown-missing-node-spec": tiptapTiptapMarkdownMissingNodeSpecRule,
|
|
@@ -9459,8 +9491,8 @@ var plugin = {
|
|
|
9459
9491
|
// src/plugin/rule-registry.ts
|
|
9460
9492
|
function buildRegistry() {
|
|
9461
9493
|
const registry2 = /* @__PURE__ */ new Map();
|
|
9462
|
-
for (const [key,
|
|
9463
|
-
const docs =
|
|
9494
|
+
for (const [key, rule109] of Object.entries(plugin.rules)) {
|
|
9495
|
+
const docs = rule109?.meta?.docs ?? {};
|
|
9464
9496
|
registry2.set(key, {
|
|
9465
9497
|
category: docs.category,
|
|
9466
9498
|
description: docs.description ?? "",
|
|
@@ -9654,9 +9686,10 @@ async function detectProviders(directory, filesContent) {
|
|
|
9654
9686
|
// src/scanner.ts
|
|
9655
9687
|
var SKIP_DIRS = /* @__PURE__ */ new Set(["node_modules", "dist", "build", ".next"]);
|
|
9656
9688
|
var SOURCE_EXT = /\.(tsx?|jsx?)$/;
|
|
9689
|
+
var NPX_CMD = process.platform === "win32" ? "npx.cmd" : "npx";
|
|
9657
9690
|
function runOxlint(args, cwd) {
|
|
9658
9691
|
return new Promise((resolveRun) => {
|
|
9659
|
-
const child = (0, import_node_child_process.spawn)(
|
|
9692
|
+
const child = (0, import_node_child_process.spawn)(NPX_CMD, args, { cwd });
|
|
9660
9693
|
let stdout = "";
|
|
9661
9694
|
let stderr = "";
|
|
9662
9695
|
child.stdout?.on("data", (chunk) => {
|
|
@@ -9695,9 +9728,9 @@ function buildOxlintConfig(detectedNames) {
|
|
|
9695
9728
|
const ruleMetaByKey = /* @__PURE__ */ new Map();
|
|
9696
9729
|
for (const provider of providers) {
|
|
9697
9730
|
if (!detectedNames.has(provider.name)) continue;
|
|
9698
|
-
for (const
|
|
9699
|
-
oxlintRules[`${PLUGIN_NAME}/${
|
|
9700
|
-
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);
|
|
9701
9734
|
}
|
|
9702
9735
|
}
|
|
9703
9736
|
return { oxlintRules, ruleMetaByKey };
|
|
@@ -9815,9 +9848,9 @@ var import_node_path5 = require("path");
|
|
|
9815
9848
|
function rationaleByRule() {
|
|
9816
9849
|
const map = /* @__PURE__ */ new Map();
|
|
9817
9850
|
for (const provider of providers) {
|
|
9818
|
-
for (const
|
|
9819
|
-
const docs = getRuleDocsMeta(
|
|
9820
|
-
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);
|
|
9821
9854
|
}
|
|
9822
9855
|
}
|
|
9823
9856
|
return map;
|