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