@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.cjs CHANGED
@@ -160,7 +160,7 @@ var resendManifest = {
160
160
  resultRule: "resend/webhook-signature-missing",
161
161
  message: "This webhook handler appears to process Resend events without verifying the webhook signature.",
162
162
  fix: "Verify incoming webhooks with Svix (Resend uses Svix signatures). Validate headers and payload before handling events.",
163
- docsUrl: "https://resend.com/docs/dashboard/webhooks/introduction#verify-webhook-signatures",
163
+ docsUrl: "https://resend.com/docs/webhooks/verify-webhooks-requests",
164
164
  severity: "error"
165
165
  },
166
166
  {
@@ -240,7 +240,7 @@ var resendManifest = {
240
240
  resultRule: "resend/reliability/webhook-no-idempotency",
241
241
  message: "Resend webhook handler does not deduplicate retried events.",
242
242
  fix: "Track processed event ids (e.g. event.data.email_id) in a store or set, since Resend retries for 24h.",
243
- docsUrl: "https://resend.com/docs/dashboard/webhooks/introduction",
243
+ docsUrl: "https://resend.com/docs/webhooks/introduction",
244
244
  severity: "warning"
245
245
  },
246
246
  {
@@ -268,7 +268,7 @@ var supabaseManifest = {
268
268
  displayName: "Supabase",
269
269
  detect: {
270
270
  packages: ["@supabase/supabase-js"],
271
- imports: ["@supabase/supabase-js"],
271
+ imports: ["@supabase/supabase-js", "@supabase/ssr"],
272
272
  urlPatterns: ["supabase.co"]
273
273
  },
274
274
  oxlintRules: [
@@ -276,9 +276,9 @@ var supabaseManifest = {
276
276
  key: "supabase-scope-queries-by-tenant-column",
277
277
  resultRule: "supabase/correctness/scope-queries-by-tenant-column",
278
278
  message: "Query selects a tenant column but never filters by it.",
279
- fix: 'Add .eq("<column>", value) (or .match()/.filter()) to scope results to the caller.',
279
+ fix: 'Add .eq("<column>", value) (or .match()/.filter()) to scope results to the caller. If RLS scopes this table the filter is still worth adding \u2014 defense-in-depth, and it avoids overfetching.',
280
280
  docsUrl: "https://supabase.com/docs/reference/javascript/eq",
281
- severity: "error"
281
+ severity: "warning"
282
282
  },
283
283
  {
284
284
  key: "supabase-validate-uuid-columns",
@@ -307,25 +307,25 @@ var supabaseManifest = {
307
307
  {
308
308
  key: "supabase-idempotent-mutations",
309
309
  resultRule: "supabase/reliability/idempotent-mutations",
310
- message: "Insert has no idempotency/dedupe key, so a retry can create a duplicate row.",
311
- fix: 'Add a client-generated idempotency key field backed by a unique constraint, or use .upsert(..., { onConflict: "<key column>" }).',
310
+ message: "Insert payload has no unique/idempotency key field, so a retried request can create a duplicate row.",
311
+ fix: 'Include a client-generated unique key (e.g. an id or *_key field backed by a unique constraint), or use .upsert(..., { onConflict: "<key column>" }).',
312
312
  docsUrl: "https://supabase.com/docs/reference/javascript/upsert",
313
- severity: "warning"
313
+ severity: "info"
314
314
  },
315
315
  {
316
316
  key: "supabase-fail-fast-env-validation",
317
317
  resultRule: "supabase/reliability/fail-fast-env-validation",
318
318
  message: "createClient is called with env vars that have no presence check.",
319
- fix: "Throw a clear error (e.g. if (!url || !key) throw new Error(...)) before calling createClient.",
319
+ fix: `Throw an error naming the env var (e.g. if (!url || !key) throw new Error("SUPABASE_URL/KEY must be set")) before creating the client \u2014 the SDK's own error ("supabaseKey is required.") does not say which variable to fix.`,
320
320
  docsUrl: "https://supabase.com/docs/reference/javascript/initializing",
321
- severity: "warning"
321
+ severity: "info"
322
322
  },
323
323
  {
324
324
  key: "supabase-no-user-metadata-authz",
325
325
  resultRule: "supabase/security/no-user-metadata-authz",
326
326
  message: "Authorization data is read from or written to user_metadata, which clients can modify.",
327
327
  fix: "Store roles in app_metadata via a trusted server path, or in an RLS-protected profiles table \u2014 never user_metadata.",
328
- docsUrl: "https://supabase.com/docs/guides/database/postgres/row-level-security",
328
+ docsUrl: "https://supabase.com/docs/guides/auth/users",
329
329
  severity: "error"
330
330
  },
331
331
  {
@@ -430,8 +430,8 @@ var firebaseManifest = {
430
430
  key: "firebase-missing-app-check",
431
431
  resultRule: "firebase/security/missing-app-check",
432
432
  message: "Firebase app is initialized with no App Check configured.",
433
- fix: "Call initializeAppCheck(app, { provider: new ReCaptchaV3Provider(SITE_KEY), isTokenAutoRefreshEnabled: true }) alongside initializeApp.",
434
- docsUrl: "https://firebase.google.com/docs/database/web/start",
433
+ fix: "Call initializeAppCheck(app, { provider: new ReCaptchaV3Provider(SITE_KEY), isTokenAutoRefreshEnabled: true }) alongside initializeApp, then turn on enforcement in the Firebase console \u2014 App Check does nothing until enforced.",
434
+ docsUrl: "https://firebase.google.com/docs/app-check/web/recaptcha-provider",
435
435
  severity: "warning"
436
436
  },
437
437
  {
@@ -454,7 +454,7 @@ var firebaseManifest = {
454
454
  key: "firebase-unvalidated-external-data-to-rtdb",
455
455
  resultRule: "firebase/correctness/unvalidated-external-data-to-rtdb",
456
456
  message: "Parsed external data is written to the Realtime Database with no validation.",
457
- fix: "Validate shape/values (e.g. regex-check date fields) before calling set()/update()/push(), and surface a parse-quality warning for skipped items.",
457
+ fix: "Validate shape/values (e.g. with a zod schema.parse/safeParse, or regex-check date fields) before calling set()/update()/push(), and surface a parse-quality warning for skipped items.",
458
458
  docsUrl: "https://firebase.google.com/docs/database/web/read-and-write",
459
459
  severity: "error"
460
460
  },
@@ -488,7 +488,7 @@ var firebaseManifest = {
488
488
  message: "A Realtime Database write promise is neither awaited in a try/catch nor .catch-handled.",
489
489
  fix: "Wrap state-changing writes in try/catch and surface failures; do not navigate away on a write whose result was never checked.",
490
490
  docsUrl: "https://firebase.google.com/docs/database/web/read-and-write",
491
- severity: "error"
491
+ severity: "warning"
492
492
  },
493
493
  {
494
494
  key: "firebase-firestore-rules-expired",
@@ -506,14 +506,6 @@ var firebaseManifest = {
506
506
  docsUrl: "https://firebase.google.com/docs/auth/admin/manage-cookies",
507
507
  severity: "error"
508
508
  },
509
- {
510
- key: "firebase-middleware-token-not-verified",
511
- resultRule: "firebase/security/middleware-token-not-verified",
512
- message: "Middleware reads the auth cookie but never verifies it. Any non-empty cookie value bypasses the guard.",
513
- fix: "Call adminAuth.verifySessionCookie(cookie, true) in the middleware and redirect on failure.",
514
- docsUrl: "https://firebase.google.com/docs/auth/admin/manage-cookies#verify_session_cookies_and_check_permissions",
515
- severity: "error"
516
- },
517
509
  {
518
510
  key: "firebase-hardcoded-user-id",
519
511
  resultRule: "firebase/security/hardcoded-user-id",
@@ -542,7 +534,7 @@ var firebaseManifest = {
542
534
  key: "firebase-use-array-union-remove",
543
535
  resultRule: "firebase/correctness/use-array-union-remove",
544
536
  message: "Firestore array updated with read-modify-write spread/filter instead of atomic arrayUnion/arrayRemove.",
545
- fix: "Use arrayUnion(item) or arrayRemove(item) from firebase/firestore for atomic array operations.",
537
+ fix: "Use arrayUnion(item) or arrayRemove(item) from firebase/firestore for atomic array operations. Note: arrayRemove needs the exact element value \u2014 to remove object items by predicate, use a transaction instead.",
546
538
  docsUrl: "https://firebase.google.com/docs/firestore/manage-data/add-data#update_elements_in_an_array",
547
539
  severity: "warning"
548
540
  },
@@ -574,15 +566,15 @@ var firebaseManifest = {
574
566
  key: "firebase-firestore-document-size-guard",
575
567
  resultRule: "firebase/reliability/firestore-document-size-guard",
576
568
  message: "Firestore write includes editor.getJSON() without a document size check.",
577
- fix: 'Check document size before writing: if (new Blob([payload]).size > 900_000) { setSaveStatus("Document too large"); return; }',
569
+ fix: 'Check document size before writing: if (new Blob([JSON.stringify(payload)]).size > 900_000) { setSaveStatus("Document too large"); return; }',
578
570
  docsUrl: "https://firebase.google.com/docs/firestore/quotas#limits",
579
571
  severity: "warning"
580
572
  },
581
573
  {
582
574
  key: "firebase-use-timestamp-now",
583
575
  resultRule: "firebase/correctness/use-timestamp-now",
584
- message: "new Date() used for a Firestore timestamp field instead of Timestamp.now() or serverTimestamp().",
585
- fix: 'Import { Timestamp } from "firebase/firestore" and use Timestamp.now() for consistency with Firestore security rules.',
576
+ message: "new Date() used for a Firestore timestamp field instead of serverTimestamp().",
577
+ fix: 'Import { serverTimestamp } from "firebase/firestore" and use it for created/updated fields \u2014 it is server-authoritative, immune to client clock skew, and satisfies rules that compare against request.time.',
586
578
  docsUrl: "https://firebase.google.com/docs/reference/js/firestore_.timestamp",
587
579
  severity: "info"
588
580
  }
@@ -612,7 +604,7 @@ var lovableManifest = {
612
604
  resultRule: "lovable/security/paid-flag-without-edge-function",
613
605
  message: "A paid-looking flag is set via a direct database update with no payment-provider or Edge Function call.",
614
606
  fix: "Move the purchase behind an Edge Function that creates a payment-provider checkout session, and only flip the flag from a server-side webhook after payment is confirmed.",
615
- docsUrl: "https://docs.lovable.dev/features/cloud",
607
+ docsUrl: "https://docs.lovable.dev/integrations/stripe",
616
608
  severity: "error"
617
609
  },
618
610
  {
@@ -620,7 +612,7 @@ var lovableManifest = {
620
612
  resultRule: "lovable/correctness/expiry-column-never-checked",
621
613
  message: "An expiry column is written but never compared against the current time anywhere in this file.",
622
614
  fix: "Filter or sort by the expiry column against the current time, or add a scheduled Edge Function / pg_cron job that clears the flag once it passes.",
623
- docsUrl: "https://docs.lovable.dev/features/cloud",
615
+ docsUrl: "https://docs.lovable.dev/integrations/stripe",
624
616
  severity: "warning"
625
617
  },
626
618
  {
@@ -628,7 +620,7 @@ var lovableManifest = {
628
620
  resultRule: "lovable/correctness/silent-catch-on-provider-call",
629
621
  message: 'A catch block around an LLM provider call has no logging, so failures look like "no key configured."',
630
622
  fix: "console.error (or log to your error tracker) the failure reason \u2014 status code and error body \u2014 before falling through.",
631
- docsUrl: "https://docs.lovable.dev/features/cloud",
623
+ docsUrl: "https://docs.lovable.dev/integrations/cloud",
632
624
  severity: "warning"
633
625
  }
634
626
  ]
@@ -665,7 +657,7 @@ var browserbaseManifest = {
665
657
  resultRule: "browserbase/security/session-id-requires-ownership-check",
666
658
  message: "sessionId flows into a live-view/recording call with no ownership check.",
667
659
  fix: "Look up the owning project/org by sessionId and verify the requesting user has access before calling sessions.debug()/recording.retrieve().",
668
- docsUrl: "https://docs.browserbase.com/features/session-live-view",
660
+ docsUrl: "https://docs.browserbase.com/platform/browser/observability/session-live-view",
669
661
  severity: "warning"
670
662
  },
671
663
  {
@@ -673,7 +665,7 @@ var browserbaseManifest = {
673
665
  resultRule: "browserbase/correctness/no-concurrent-shared-context",
674
666
  message: "The same Context id is passed into every session in a concurrent Promise.all batch.",
675
667
  fix: "Serialize sessions that share a context id, or create a fresh Context per concurrent run.",
676
- docsUrl: "https://docs.browserbase.com/features/contexts",
668
+ docsUrl: "https://docs.browserbase.com/platform/browser/core-features/contexts",
677
669
  severity: "error"
678
670
  },
679
671
  {
@@ -681,7 +673,7 @@ var browserbaseManifest = {
681
673
  resultRule: "browserbase/correctness/mobile-device-requires-os-setting",
682
674
  message: 'A "mobile" device branch resizes the viewport but never sets browserSettings.os.',
683
675
  fix: "Set browserSettings.os = 'mobile' (or 'tablet') alongside the viewport resize \u2014 the Node SDK's device-emulation lever is `os`, not a fingerprint object.",
684
- docsUrl: "https://docs.browserbase.com/features/stealth-mode",
676
+ docsUrl: "https://docs.browserbase.com/platform/identity/verified-customization",
685
677
  severity: "error"
686
678
  },
687
679
  {
@@ -798,7 +790,7 @@ var openaiCuaManifest = {
798
790
  resultRule: "openai-cua/integration/set-safety-identifier",
799
791
  message: "responses.create() call has no safety_identifier (or user) parameter.",
800
792
  fix: "Thread a stable, hashed per-customer identifier through to every responses.create() call as safety_identifier.",
801
- docsUrl: "https://help.openai.com/en/articles/5428082-how-to-incorporate-a-safety-identifier",
793
+ docsUrl: "https://developers.openai.com/api/docs/guides/safety-best-practices",
802
794
  severity: "warning"
803
795
  }
804
796
  ]
@@ -835,7 +827,7 @@ var tiptapManifest = {
835
827
  resultRule: "tiptap/security/dynamic-script-no-sri",
836
828
  message: "Dynamically injected script appended without SRI integrity attribute.",
837
829
  fix: 'Add script.setAttribute("integrity", "sha384-...") before appending the script.',
838
- docsUrl: "https://developer.mozilla.org/en-US/docs/Web/Security/Subresource_Integrity",
830
+ docsUrl: "https://developer.mozilla.org/en-US/docs/Web/Security/Defenses/Subresource_Integrity",
839
831
  severity: "warning"
840
832
  },
841
833
  {
@@ -870,14 +862,6 @@ var tiptapManifest = {
870
862
  docsUrl: "https://tiptap.dev/docs/editor/extensions/custom-extensions/create-new/node",
871
863
  severity: "warning"
872
864
  },
873
- {
874
- key: "tiptap-twitter-url-regex",
875
- resultRule: "tiptap/integration/twitter-url-regex",
876
- message: "Twitter/X regex matches x.com but not twitter.com \u2014 legacy URLs silently rejected.",
877
- fix: "Update pattern to (x\\.com|twitter\\.com) to support both domains.",
878
- docsUrl: "https://tiptap.dev/docs/editor/extensions/nodes",
879
- severity: "warning"
880
- },
881
865
  {
882
866
  key: "tiptap-drop-handler-pos-precedence",
883
867
  resultRule: "tiptap/correctness/drop-handler-pos-precedence",
@@ -898,8 +882,8 @@ var tiptapManifest = {
898
882
  key: "tiptap-tiptap-markdown-missing-node-spec",
899
883
  resultRule: "tiptap/integration/tiptap-markdown-missing-node-spec",
900
884
  message: "TipTap node used with tiptap-markdown has no markdown serialization spec \u2014 content lost on export.",
901
- fix: "Add a markdown serializer to addStorage or addOptions using MarkdownNodeSpec.",
902
- docsUrl: "https://github.com/ueberdosis/tiptap-markdown",
885
+ fix: "Return a markdown serialize/parse spec (MarkdownNodeSpec) from addStorage \u2014 tiptap-markdown reads only extension.storage.markdown.",
886
+ docsUrl: "https://github.com/aguingand/tiptap-markdown",
903
887
  severity: "warning"
904
888
  }
905
889
  ]
@@ -1112,7 +1096,7 @@ var openaiRealtimeManifest = {
1112
1096
  resultRule: "openai-realtime/reliability/handle-error-server-event",
1113
1097
  message: 'This Realtime message handler branches on event types but never checks for the API-level "error" event.',
1114
1098
  fix: "Add an explicit branch for message.type === 'error' that logs the error and surfaces/fails over, instead of letting it fall through silently.",
1115
- docsUrl: "https://developers.openai.com/api/docs/api-reference/realtime_server_events",
1099
+ docsUrl: "https://developers.openai.com/api/reference/resources/realtime/server-events",
1116
1100
  severity: "error"
1117
1101
  },
1118
1102
  {
@@ -1128,7 +1112,7 @@ var openaiRealtimeManifest = {
1128
1112
  resultRule: "openai-realtime/correctness/avoid-dated-preview-snapshots",
1129
1113
  message: "The Realtime connection is pinned to a dated preview model snapshot instead of the GA alias.",
1130
1114
  fix: "Use the GA model id (e.g. 'gpt-realtime') or a current dated snapshot tracked against OpenAI's deprecation notices.",
1131
- docsUrl: "https://developers.openai.com/api/docs/api-reference/realtime-sessions",
1115
+ docsUrl: "https://developers.openai.com/api/reference/resources/realtime/subresources/client_secrets",
1132
1116
  severity: "warning"
1133
1117
  },
1134
1118
  {
@@ -1136,7 +1120,7 @@ var openaiRealtimeManifest = {
1136
1120
  resultRule: "openai-realtime/correctness/verify-deprecated-session-fields",
1137
1121
  message: "The session config sets 'temperature', a field not documented in the current GA Realtime sessions schema.",
1138
1122
  fix: "Re-verify this field against the current sessions reference before relying on it, or drop it and control determinism via turn_detection/instructions instead.",
1139
- docsUrl: "https://developers.openai.com/api/docs/api-reference/realtime-sessions",
1123
+ docsUrl: "https://developers.openai.com/api/reference/resources/realtime/subresources/client_secrets",
1140
1124
  severity: "warning"
1141
1125
  },
1142
1126
  {
@@ -1144,7 +1128,7 @@ var openaiRealtimeManifest = {
1144
1128
  resultRule: "openai-realtime/reliability/buffer-audio-until-session-ready",
1145
1129
  message: "Audio sent before the Realtime socket reaches the open state is dropped instead of buffered.",
1146
1130
  fix: "Queue outbound input_audio_buffer.append messages until the open event fires, then flush them in order.",
1147
- docsUrl: "https://developers.openai.com/api/docs/voice/media-streams/websocket-messages",
1131
+ docsUrl: "https://developers.openai.com/api/reference/resources/realtime/client-events",
1148
1132
  severity: "warning"
1149
1133
  },
1150
1134
  {
@@ -1158,7 +1142,7 @@ var openaiRealtimeManifest = {
1158
1142
  {
1159
1143
  key: "openai-realtime-transcription-model-choice",
1160
1144
  resultRule: "openai-realtime/correctness/transcription-model-choice",
1161
- message: "input_audio_transcription is configured with 'whisper-1', which is not natively streaming and not optimized for realtime sessions.",
1145
+ message: "The session's input transcription is configured with 'whisper-1', which is not natively streaming and not optimized for realtime sessions.",
1162
1146
  fix: "Switch to 'gpt-realtime-whisper' if transcription output is consumed, or drop input_audio_transcription entirely if it isn't.",
1163
1147
  docsUrl: "https://developers.openai.com/api/docs/guides/realtime-transcription",
1164
1148
  severity: "info"
@@ -1251,7 +1235,7 @@ var rule = {
1251
1235
  cwe: "CWE-345",
1252
1236
  owasp: "API2:2023 Broken Authentication",
1253
1237
  rationale: "Webhook endpoints are public URLs, so anyone who learns the path can POST a forged payload. Without verifying the Svix signature first, an attacker can fake delivery, bounce, or complaint events and drive your application into the wrong state. Validating the signature against your webhook secret before reading the body ensures the event genuinely came from Resend.",
1254
- docsUrl: "https://resend.com/docs/dashboard/webhooks/introduction#verify-webhook-signatures",
1238
+ docsUrl: "https://resend.com/docs/webhooks/verify-webhooks-requests",
1255
1239
  recommended: true
1256
1240
  },
1257
1241
  messages: {
@@ -1328,6 +1312,15 @@ var rule = {
1328
1312
  const obj = callee.object;
1329
1313
  return obj?.type === "Identifier" && (obj.name === "req" || obj.name === "request");
1330
1314
  }
1315
+ function isReqTextCall(n) {
1316
+ if (n?.type !== "CallExpression") return false;
1317
+ const callee = n.callee;
1318
+ if (callee?.type !== "MemberExpression") return false;
1319
+ const prop = callee.property;
1320
+ if (prop?.type !== "Identifier" || prop.name !== "text") return false;
1321
+ const obj = callee.object;
1322
+ return obj?.type === "Identifier" && (obj.name === "req" || obj.name === "request");
1323
+ }
1331
1324
  function isBodyMember(n) {
1332
1325
  if (n?.type !== "MemberExpression") return false;
1333
1326
  const prop = n.property;
@@ -1352,6 +1345,17 @@ var rule = {
1352
1345
  const obj = callee.object;
1353
1346
  return obj?.type === "Identifier" && svixImports.has(obj.name);
1354
1347
  }
1348
+ function isResendWebhooksVerifyCall(n) {
1349
+ if (n?.type !== "CallExpression") return false;
1350
+ const callee = n.callee;
1351
+ if (callee?.type !== "MemberExpression") return false;
1352
+ const prop = callee.property;
1353
+ if (prop?.type !== "Identifier" || prop.name !== "verify") return false;
1354
+ const obj = callee.object;
1355
+ if (obj?.type !== "MemberExpression") return false;
1356
+ const webhooksProp = obj.property;
1357
+ return webhooksProp?.type === "Identifier" && webhooksProp.name === "webhooks";
1358
+ }
1355
1359
  function recordFirst(posKey, handler, pos) {
1356
1360
  const existing = handler[posKey];
1357
1361
  if (!existing) {
@@ -1366,6 +1370,11 @@ var rule = {
1366
1370
  ImportDeclaration(node) {
1367
1371
  const importSource = node?.source?.value;
1368
1372
  if (importSource === "resend") importsResend = true;
1373
+ for (const s of node.specifiers ?? []) {
1374
+ if ((s?.type === "ImportSpecifier" || s?.type === "ImportDefaultSpecifier") && s.local?.type === "Identifier" && /^resend$/i.test(s.local.name)) {
1375
+ importsResend = true;
1376
+ }
1377
+ }
1369
1378
  if (importSource === "svix") {
1370
1379
  for (const s of node.specifiers ?? []) {
1371
1380
  if (s?.type === "ImportSpecifier" && s.local?.type === "Identifier") {
@@ -1387,8 +1396,10 @@ var rule = {
1387
1396
  for (const handler of postHandlers) {
1388
1397
  if (!within(handler, node)) continue;
1389
1398
  if (isReqJsonCall(node)) recordFirst("firstBodyPos", handler, pos);
1399
+ if (isReqTextCall(node)) recordFirst("firstRawBodyPos", handler, pos);
1390
1400
  if (isSvixVerifyCall(node)) recordFirst("firstVerifyPos", handler, pos);
1391
1401
  if (isCryptoCreateHmacCall(node)) recordFirst("firstVerifyPos", handler, pos);
1402
+ if (isResendWebhooksVerifyCall(node)) recordFirst("firstVerifyPos", handler, pos);
1392
1403
  }
1393
1404
  },
1394
1405
  MemberExpression(node) {
@@ -1403,6 +1414,8 @@ var rule = {
1403
1414
  "Program:exit"() {
1404
1415
  if (!importsResend) return;
1405
1416
  for (const handler of postHandlers) {
1417
+ const consumesRequest = handler.firstBodyPos || handler.firstRawBodyPos;
1418
+ if (!consumesRequest) continue;
1406
1419
  if (!handler.firstVerifyPos) {
1407
1420
  context.report({ node: handler.node, messageId: "missingVerification" });
1408
1421
  continue;
@@ -1974,7 +1987,7 @@ var rule11 = {
1974
1987
  description: "Resend webhook handlers should deduplicate retried events",
1975
1988
  category: "reliability",
1976
1989
  rationale: "Resend retries failed webhook deliveries for up to 24 hours, so the same event can legitimately arrive more than once. A handler that acts on every delivery without deduplication will double-process events \u2014 sending duplicate downstream notifications, double-counting metrics, or corrupting state. Tracking processed event ids (e.g. event.data.email_id) in a store or set and skipping ones already seen makes the handler safely idempotent.",
1977
- docsUrl: "https://resend.com/docs/dashboard/webhooks/introduction",
1990
+ docsUrl: "https://resend.com/docs/webhooks/introduction",
1978
1991
  recommended: true
1979
1992
  },
1980
1993
  messages: {
@@ -2276,12 +2289,12 @@ var rule14 = {
2276
2289
  docs: {
2277
2290
  description: "Supabase queries that select a tenant column must filter by it",
2278
2291
  category: "correctness",
2279
- rationale: "A column like session_id or user_id existing in the schema (and being selected) signals intent to scope rows to one caller, but selecting it is not the same as filtering by it. Without an .eq()/.match()/.filter() on that column, the query returns every row for every tenant, turning a per-user feed into a single shared, cross-user one.",
2292
+ rationale: "A column like session_id or user_id being selected signals intent to scope rows to one caller, but selecting it is not the same as filtering by it. If RLS is not enabled on the table, the unfiltered query returns every row for every tenant \u2014 a per-user feed becomes a shared, cross-user one. Even when RLS scopes the rows server-side, an explicit .eq()/.match()/.filter() is defense-in-depth (a dropped policy no longer leaks data) and avoids fetching rows the page never uses.",
2280
2293
  docsUrl: "https://supabase.com/docs/reference/javascript/eq",
2281
2294
  recommended: true
2282
2295
  },
2283
2296
  messages: {
2284
- missingTenantFilter: 'This query selects "{{column}}" but never filters by it. Add .eq("{{column}}", ...) (or .match()/.filter()) to scope results to the caller.'
2297
+ missingTenantFilter: `This query selects "{{column}}" but never filters by it \u2014 without RLS on this table that is every tenant's rows. Add .eq("{{column}}", ...) (or .match()/.filter()) to scope results to the caller.`
2285
2298
  },
2286
2299
  schema: []
2287
2300
  },
@@ -2553,7 +2566,8 @@ function objectHasIdempotencyKey(objectExpression) {
2553
2566
  return (objectExpression.properties ?? []).some((p) => {
2554
2567
  if (p?.type !== "Property") return false;
2555
2568
  const name = p.key?.type === "Identifier" ? p.key.name : p.key?.type === "Literal" ? p.key.value : void 0;
2556
- return typeof name === "string" && /idempot|dedupe/i.test(name);
2569
+ if (typeof name !== "string") return false;
2570
+ return /idempot|dedupe/i.test(name) || /^(id|uuid)$/i.test(name) || /_(key|uuid)$/i.test(name);
2557
2571
  });
2558
2572
  }
2559
2573
  function insertPayloadHasIdempotencyKey(arg) {
@@ -2569,12 +2583,12 @@ var rule18 = {
2569
2583
  docs: {
2570
2584
  description: "Supabase insert calls should be retry-safe via an idempotency key",
2571
2585
  category: "reliability",
2572
- rationale: 'Nothing prevents a duplicate row if the client fetch behind an insert is retried (flaky network, double-click, browser replay) \u2014 there is no unique constraint or dedupe key visible in the payload, and no upsert semantics. Generate a client-side idempotency key per logical action and either include it as a field guarded by a unique constraint, or use .upsert(..., { onConflict: "<key column>" }).',
2586
+ rationale: 'Nothing prevents a duplicate row if the client fetch behind an insert is retried (flaky network, double-click, browser replay) \u2014 no unique key is visible in the payload, and no upsert semantics. Include a client-generated unique key (an id, or a *_key field backed by a unique constraint), or use .upsert(..., { onConflict: "<key column>" }). This is general retry-safety practice rather than a documented Supabase requirement, hence info severity.',
2573
2587
  docsUrl: "https://supabase.com/docs/reference/javascript/upsert",
2574
2588
  recommended: true
2575
2589
  },
2576
2590
  messages: {
2577
- missingIdempotencyKey: 'This insert has no idempotency/dedupe key field, so a retried request can create a duplicate row. Add one, or use .upsert(..., { onConflict: "<key column>" }).'
2591
+ missingIdempotencyKey: 'This insert payload has no unique/idempotency key field, so a retried request can create a duplicate row. Include a client-generated key, or use .upsert(..., { onConflict: "<key column>" }).'
2578
2592
  },
2579
2593
  schema: []
2580
2594
  },
@@ -2623,7 +2637,7 @@ var rule19 = {
2623
2637
  docs: {
2624
2638
  description: "createClient must fail fast when required env vars are missing",
2625
2639
  category: "reliability",
2626
- rationale: "createClient does not throw on undefined arguments \u2014 a missing env var surfaces later as an opaque error deep in a fetch call rather than a clear message at startup. Checking presence before calling createClient turns a confusing runtime failure (e.g. on a misconfigured second service) into an immediate, actionable one.",
2640
+ rationale: `createClient throws immediately when an argument is missing, but with the SDK's message ("supabaseKey is required.") \u2014 it names the SDK parameter, not your env var. An explicit presence check produces an error naming the exact variable to set, which matters most with Next.js NEXT_PUBLIC_* inlining where the fix is a rebuild with the var present, not a server restart.`,
2627
2641
  docsUrl: "https://supabase.com/docs/reference/javascript/initializing",
2628
2642
  recommended: true
2629
2643
  },
@@ -2633,7 +2647,11 @@ var rule19 = {
2633
2647
  schema: []
2634
2648
  },
2635
2649
  create(context) {
2636
- let createClientLocalName;
2650
+ const FACTORY_IMPORTS = {
2651
+ "@supabase/supabase-js": ["createClient"],
2652
+ "@supabase/ssr": ["createBrowserClient", "createServerClient"]
2653
+ };
2654
+ const factoryLocalNames = /* @__PURE__ */ new Set();
2637
2655
  const envVarOfVariable = /* @__PURE__ */ new Map();
2638
2656
  const validatedVarNames = /* @__PURE__ */ new Set();
2639
2657
  const validatedEnvNames = /* @__PURE__ */ new Set();
@@ -2667,10 +2685,11 @@ var rule19 = {
2667
2685
  }
2668
2686
  return {
2669
2687
  ImportDeclaration(node) {
2670
- if (node.source?.value !== "@supabase/supabase-js") return;
2688
+ const factories = FACTORY_IMPORTS[node.source?.value];
2689
+ if (!factories) return;
2671
2690
  for (const s of node.specifiers ?? []) {
2672
- if (s?.type === "ImportSpecifier" && s.imported?.type === "Identifier" && s.imported.name === "createClient" && s.local?.type === "Identifier") {
2673
- createClientLocalName = s.local.name;
2691
+ if (s?.type === "ImportSpecifier" && s.imported?.type === "Identifier" && factories.includes(s.imported.name) && s.local?.type === "Identifier") {
2692
+ factoryLocalNames.add(s.local.name);
2674
2693
  }
2675
2694
  }
2676
2695
  },
@@ -2684,8 +2703,8 @@ var rule19 = {
2684
2703
  collectGuardTargets(node.test);
2685
2704
  },
2686
2705
  CallExpression(node) {
2687
- if (!createClientLocalName) return;
2688
- if (node.callee?.type !== "Identifier" || node.callee.name !== createClientLocalName) return;
2706
+ if (factoryLocalNames.size === 0) return;
2707
+ if (node.callee?.type !== "Identifier" || !factoryLocalNames.has(node.callee.name)) return;
2689
2708
  const missing = [];
2690
2709
  for (const rawArg of node.arguments ?? []) {
2691
2710
  const arg = unwrapNonNull(rawArg);
@@ -2753,8 +2772,8 @@ var rule20 = {
2753
2772
  category: "security",
2754
2773
  cwe: "CWE-285",
2755
2774
  owasp: "A01:2021 Broken Access Control",
2756
- rationale: "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.",
2757
- docsUrl: "https://supabase.com/docs/guides/database/postgres/row-level-security",
2775
+ rationale: 'Supabase documents user_metadata as user-editable: "Do not use it in security sensitive context (such as in RLS policies or authorization logic), as this value is editable by the user without any checks." Reading user_metadata.role (or writing role into signUp/updateUser data) lets any signed-in user self-assign privileged roles from the browser. Store roles in app_metadata via a trusted server path, or in an RLS-protected profiles table.',
2776
+ docsUrl: "https://supabase.com/docs/guides/auth/users",
2758
2777
  recommended: true
2759
2778
  },
2760
2779
  messages: {
@@ -2790,7 +2809,7 @@ var rule21 = {
2790
2809
  docs: {
2791
2810
  description: "Supabase .single() calls must inspect the returned error field",
2792
2811
  category: "correctness",
2793
- rationale: ".single() signals zero-or-one-row intent via the error field (PGRST116), not by leaving data undefined silently. Destructuring only data and never reading error produces infinite spinners on deleted rows, bad IDs, or RLS-denied reads. Prefer .maybeSingle() or branch on error before rendering.",
2812
+ rationale: ".single() signals zero-or-one-row intent via the error field (PGRST116), not by leaving data undefined silently. Destructuring only data and never reading error produces infinite spinners on deleted rows, bad IDs, or RLS-denied reads. Prefer .maybeSingle() or branch on error before rendering. Chains ending in .throwOnError() opt into exceptions instead and are exempt.",
2794
2813
  docsUrl: "https://supabase.com/docs/reference/javascript/single",
2795
2814
  recommended: true
2796
2815
  },
@@ -2800,14 +2819,29 @@ var rule21 = {
2800
2819
  schema: []
2801
2820
  },
2802
2821
  create(context) {
2822
+ const deferredBindings = [];
2823
+ const errorReadNames = /* @__PURE__ */ new Set();
2803
2824
  function checkAwaitBinding(node, pattern, awaitExpr) {
2804
2825
  if (!isSingleSupabaseQuery(awaitExpr.argument)) return;
2826
+ if (chainHasMethod(awaitExpr.argument, "throwOnError")) return;
2827
+ if (pattern?.type === "Identifier") {
2828
+ deferredBindings.push({ node, name: pattern.name });
2829
+ return;
2830
+ }
2805
2831
  const names = destructuredNames(pattern);
2806
2832
  if (names.has("error")) return;
2807
2833
  context.report({ node, messageId: "missingErrorCheck" });
2808
2834
  }
2809
2835
  return {
2836
+ MemberExpression(node) {
2837
+ if (!node.computed && node.object?.type === "Identifier" && node.property?.type === "Identifier" && node.property.name === "error") {
2838
+ errorReadNames.add(node.object.name);
2839
+ }
2840
+ },
2810
2841
  VariableDeclarator(node) {
2842
+ if (node.init?.type === "Identifier" && node.id?.type === "ObjectPattern") {
2843
+ if (destructuredNames(node.id).has("error")) errorReadNames.add(node.init.name);
2844
+ }
2811
2845
  if (node.init?.type !== "AwaitExpression") return;
2812
2846
  checkAwaitBinding(node, node.id, node.init);
2813
2847
  },
@@ -2819,9 +2853,17 @@ var rule21 = {
2819
2853
  const expr = node.expression;
2820
2854
  if (expr?.type !== "AwaitExpression") return;
2821
2855
  if (!isSingleSupabaseQuery(expr.argument)) return;
2856
+ if (chainHasMethod(expr.argument, "throwOnError")) return;
2822
2857
  if (memberPropName(expr.argument) === "single") {
2823
2858
  context.report({ node, messageId: "missingErrorCheck" });
2824
2859
  }
2860
+ },
2861
+ "Program:exit"() {
2862
+ for (const { node, name } of deferredBindings) {
2863
+ if (!errorReadNames.has(name)) {
2864
+ context.report({ node, messageId: "missingErrorCheck" });
2865
+ }
2866
+ }
2825
2867
  }
2826
2868
  };
2827
2869
  }
@@ -2930,7 +2972,7 @@ var rule23 = {
2930
2972
  docs: {
2931
2973
  description: "Supabase mutations must check the returned error field",
2932
2974
  category: "correctness",
2933
- rationale: "Unlike fetch(), Supabase client mutations return { data, error } and resolve even when RLS denies the write or a constraint fails. Fire-and-forget awaits or destructuring only data lets optimistic UI state diverge from the database with no toast or rollback.",
2975
+ rationale: "Unlike fetch(), Supabase client mutations return { data, error } and resolve even when RLS denies the write or a constraint fails. Fire-and-forget awaits or destructuring only data lets optimistic UI state diverge from the database with no toast or rollback. Chains ending in .throwOnError() opt into exceptions instead and are exempt.",
2934
2976
  docsUrl: "https://supabase.com/docs/reference/javascript/insert",
2935
2977
  recommended: true
2936
2978
  },
@@ -2940,30 +2982,52 @@ var rule23 = {
2940
2982
  schema: []
2941
2983
  },
2942
2984
  create(context) {
2985
+ const deferredBindings = [];
2986
+ const errorReadNames = /* @__PURE__ */ new Set();
2943
2987
  function checkMutationAwait(node, pattern, awaitExpr) {
2944
2988
  if (!isSupabaseMutationCall(awaitExpr.argument)) return;
2989
+ if (chainHasMethod(awaitExpr.argument, "throwOnError")) return;
2945
2990
  if (!pattern) {
2946
2991
  context.report({ node, messageId: "uncheckedMutation" });
2947
2992
  return;
2948
2993
  }
2994
+ if (pattern.type === "Identifier") {
2995
+ deferredBindings.push({ node, name: pattern.name });
2996
+ return;
2997
+ }
2949
2998
  const names = destructuredNames(pattern);
2950
2999
  if (!names.has("error")) {
2951
3000
  context.report({ node, messageId: "uncheckedMutation" });
2952
3001
  }
2953
3002
  }
2954
3003
  return {
3004
+ MemberExpression(node) {
3005
+ if (!node.computed && node.object?.type === "Identifier" && node.property?.type === "Identifier" && node.property.name === "error") {
3006
+ errorReadNames.add(node.object.name);
3007
+ }
3008
+ },
2955
3009
  ExpressionStatement(node) {
2956
3010
  const expr = node.expression;
2957
3011
  if (expr?.type !== "AwaitExpression") return;
2958
3012
  checkMutationAwait(node, void 0, expr);
2959
3013
  },
2960
3014
  VariableDeclarator(node) {
3015
+ if (node.init?.type === "Identifier" && node.id?.type === "ObjectPattern") {
3016
+ if (destructuredNames(node.id).has("error")) errorReadNames.add(node.init.name);
3017
+ }
2961
3018
  if (node.init?.type !== "AwaitExpression") return;
2962
3019
  checkMutationAwait(node, node.id, node.init);
2963
3020
  },
2964
3021
  AssignmentExpression(node) {
2965
3022
  if (node.right?.type !== "AwaitExpression") return;
2966
3023
  checkMutationAwait(node, node.left, node.right);
3024
+ },
3025
+ "Program:exit"() {
3026
+ for (const { node, name } of deferredBindings) {
3027
+ if (!errorReadNames.has(name)) {
3028
+ context.report({ node, messageId: "uncheckedMutation" });
3029
+ }
3030
+ }
2967
3031
  }
2968
3032
  };
2969
3033
  }
@@ -3013,11 +3077,20 @@ function isStorageUploadCall(node) {
3013
3077
  let current = node;
3014
3078
  let sawStorage = false;
3015
3079
  let sawUpload = false;
3016
- while (current?.type === "CallExpression") {
3017
- const prop = memberPropName(current);
3018
- if (prop === "storage") sawStorage = true;
3019
- if (prop === "upload") sawUpload = true;
3020
- current = chainObjectCall(current);
3080
+ while (current) {
3081
+ if (current.type === "CallExpression") {
3082
+ const prop = memberPropName(current);
3083
+ if (prop === "storage") sawStorage = true;
3084
+ if (prop === "upload") sawUpload = true;
3085
+ current = current.callee?.object;
3086
+ } else if (current.type === "MemberExpression") {
3087
+ const p = current.property;
3088
+ const name = !current.computed && p?.type === "Identifier" ? p.name : p?.type === "Literal" && typeof p.value === "string" ? p.value : void 0;
3089
+ if (name === "storage") sawStorage = true;
3090
+ current = current.object;
3091
+ } else {
3092
+ break;
3093
+ }
3021
3094
  }
3022
3095
  return sawStorage && sawUpload;
3023
3096
  }
@@ -3598,8 +3671,8 @@ var rule30 = {
3598
3671
  category: "security",
3599
3672
  cwe: "CWE-285",
3600
3673
  owasp: "A04:2021 Insecure Design",
3601
- rationale: "Firebase web API keys are public by design and only identify the project to Google's backend \u2014 they are not a secret. The only things standing between an attacker who clones the client config and your database/auth quota are Security Rules and App Check. A file that calls initializeApp but never initializeAppCheck leaves App Check unconfigured, so any client presenting a valid project config (not necessarily yours) can reach Firebase Auth and, subject to rules, the database.",
3602
- docsUrl: "https://firebase.google.com/docs/database/web/start",
3674
+ rationale: "Firebase web API keys are public by design and only identify the project to Google's backend \u2014 they are not a secret. The only things standing between an attacker who clones the client config and your database/auth quota are Security Rules and App Check. A file that calls initializeApp but never initializeAppCheck leaves App Check unconfigured, so any client presenting a valid project config (not necessarily yours) can reach Firebase Auth and, subject to rules, the database. Note the client-side call alone is not enough: App Check only blocks traffic once enforcement is turned on per-product in the Firebase console.",
3675
+ docsUrl: "https://firebase.google.com/docs/app-check/web/recaptcha-provider",
3603
3676
  recommended: true
3604
3677
  },
3605
3678
  messages: {
@@ -3773,7 +3846,13 @@ function isValidationLikeCall(node) {
3773
3846
  const callee = node.callee;
3774
3847
  const name = callee?.type === "Identifier" ? callee.name : callee?.type === "MemberExpression" && !callee.computed && callee.property?.type === "Identifier" ? callee.property.name : void 0;
3775
3848
  if (!name) return false;
3776
- return /valid|^test$|check|assert|schema/i.test(name);
3849
+ if (/valid|^test$|check|assert|schema/i.test(name)) return true;
3850
+ if (callee?.type === "MemberExpression") {
3851
+ if (/^(safeParse|safeParseAsync|parseAsync)$/.test(name)) return true;
3852
+ const objName = callee.object?.type === "Identifier" ? callee.object.name : void 0;
3853
+ if (name === "parse" && objName !== "JSON") return true;
3854
+ }
3855
+ return false;
3777
3856
  }
3778
3857
  var rule33 = {
3779
3858
  meta: {
@@ -3962,18 +4041,24 @@ var firebaseRtdbListenerErrorNotHandledRule = rule35;
3962
4041
  function isUseEffectCall(node) {
3963
4042
  return node?.type === "CallExpression" && node.callee?.type === "Identifier" && node.callee.name === "useEffect";
3964
4043
  }
4044
+ function isNonUidMemberAccess(node, objName) {
4045
+ if (node?.type !== "MemberExpression") return false;
4046
+ if (node.object?.type !== "Identifier" || node.object.name !== objName) return false;
4047
+ if (node.computed) return true;
4048
+ return node.property?.type === "Identifier" && node.property.name !== "uid";
4049
+ }
3965
4050
  var rule36 = {
3966
4051
  meta: {
3967
4052
  type: "suggestion",
3968
4053
  docs: {
3969
4054
  description: "useEffect should depend on user?.uid, not the whole user object",
3970
4055
  category: "reliability",
3971
- rationale: "onAuthStateChanged delivers a new user object reference on every internal token refresh (roughly hourly) even when uid is unchanged. An effect that only reads user.uid but lists the whole user object in its dependency array tears down and re-establishes its RTDB listener on every refresh \u2014 an avoidable unsubscribe/resubscribe cycle that briefly clears local state.",
4056
+ rationale: "Auth context providers commonly hand out a new user object reference on every token refresh (roughly hourly) \u2014 providers subscribed to onIdTokenChanged, or ones that clone the user into React state, re-render with a fresh reference even when uid is unchanged. An effect that only reads user.uid but lists the whole user object in its dependency array tears down and re-establishes its RTDB listener on every refresh \u2014 an avoidable unsubscribe/resubscribe cycle that briefly clears local state.",
3972
4057
  docsUrl: "https://firebase.google.com/docs/reference/js/auth.md#onauthstatechanged",
3973
4058
  recommended: true
3974
4059
  },
3975
4060
  messages: {
3976
- wholeUserObjectDep: "This effect only reads {{name}}.uid but depends on the whole {{name}} object, which gets a new reference on every token refresh. Depend on {{name}}?.uid instead."
4061
+ wholeUserObjectDep: "This effect only reads {{name}}.uid but depends on the whole {{name}} object, which can get a new reference on every token refresh. Depend on {{name}}?.uid instead."
3977
4062
  },
3978
4063
  schema: []
3979
4064
  },
@@ -3986,9 +4071,9 @@ var rule36 = {
3986
4071
  for (const el of depsArg.elements ?? []) {
3987
4072
  if (el?.type !== "Identifier") continue;
3988
4073
  const name = el.name;
3989
- if (someDescendant(callback, (n) => isUidMemberAccess(n, name))) {
3990
- context.report({ node, messageId: "wholeUserObjectDep", data: { name } });
3991
- }
4074
+ if (!someDescendant(callback, (n) => isUidMemberAccess(n, name))) continue;
4075
+ if (someDescendant(callback, (n) => isNonUidMemberAccess(n, name))) continue;
4076
+ context.report({ node, messageId: "wholeUserObjectDep", data: { name } });
3992
4077
  }
3993
4078
  }
3994
4079
  };
@@ -4084,13 +4169,13 @@ var rule38 = {
4084
4169
  schema: []
4085
4170
  },
4086
4171
  create(context) {
4087
- const EXPIRED_YEAR = 2025;
4088
4172
  function checkStringForExpiredDate(value, reportNode) {
4089
- const re = /timestamp\.date\(\s*(\d{4})\s*,/g;
4173
+ const re = /timestamp\.date\(\s*(\d{4})\s*,\s*(\d{1,2})\s*,\s*(\d{1,2})\s*\)/g;
4090
4174
  let match;
4091
4175
  while ((match = re.exec(value)) !== null) {
4092
- const year = parseInt(match[1], 10);
4093
- if (year <= EXPIRED_YEAR) {
4176
+ const [, year, month, day] = match;
4177
+ const expiry = new Date(parseInt(year, 10), parseInt(month, 10) - 1, parseInt(day, 10));
4178
+ if (expiry.getTime() <= Date.now()) {
4094
4179
  context.report({ node: reportNode, messageId: "firestoreRulesExpired" });
4095
4180
  return;
4096
4181
  }
@@ -4122,6 +4207,18 @@ function findProp(obj, name) {
4122
4207
  (p) => p?.type === "Property" && (p.key?.type === "Identifier" && p.key.name === name || p.key?.type === "Literal" && p.key.value === name)
4123
4208
  );
4124
4209
  }
4210
+ var TOKEN_NAME = /token|session/i;
4211
+ function isTokenLiteral(node) {
4212
+ return node?.type === "Literal" && typeof node.value === "string" && TOKEN_NAME.test(node.value);
4213
+ }
4214
+ function isCookieReceiver(obj) {
4215
+ if (obj?.type === "Identifier") return /cookie/i.test(obj.name);
4216
+ if (obj?.type === "CallExpression" && obj.callee?.type === "Identifier") return /^cookies$/i.test(obj.callee.name);
4217
+ if (obj?.type === "MemberExpression" && !obj.computed && obj.property?.type === "Identifier") {
4218
+ return /cookie/i.test(obj.property.name);
4219
+ }
4220
+ return false;
4221
+ }
4125
4222
  var rule39 = {
4126
4223
  meta: {
4127
4224
  type: "problem",
@@ -4129,110 +4226,71 @@ var rule39 = {
4129
4226
  description: "Firebase ID token stored in a cookie without httpOnly flag",
4130
4227
  category: "security",
4131
4228
  cwe: "CWE-1004",
4132
- owasp: "A02:2021 Cryptographic Failures",
4133
- rationale: "Storing a Firebase ID token in a non-httpOnly cookie makes it readable by any JavaScript on the page. An XSS vulnerability can steal the token and impersonate the user. Use the Firebase Admin SDK createSessionCookie flow to issue a proper httpOnly session cookie instead.",
4229
+ owasp: "A05:2021 Security Misconfiguration",
4230
+ rationale: "Storing a Firebase ID token in a non-httpOnly cookie makes it readable by any JavaScript on the page. An XSS vulnerability can steal the token and impersonate the user. Use the Firebase Admin SDK createSessionCookie flow to issue a proper httpOnly session cookie instead \u2014 note httpOnly can only be set from server-side code, never from client-side JavaScript.",
4134
4231
  docsUrl: "https://firebase.google.com/docs/auth/admin/manage-cookies",
4135
4232
  recommended: true
4136
4233
  },
4137
4234
  messages: {
4138
- idTokenCookieMissingHttpOnly: "Firebase ID token stored in cookie without httpOnly: true. Any XSS vulnerability can steal the token. Use the Firebase Admin SDK createSessionCookie flow instead."
4235
+ idTokenCookieMissingHttpOnly: "Auth token stored in cookie without httpOnly: true. Any XSS vulnerability can steal the token. Use the Firebase Admin SDK createSessionCookie flow instead."
4139
4236
  },
4140
4237
  schema: []
4141
4238
  },
4142
4239
  create(context) {
4143
- function isTokenName(val) {
4144
- return /token/i.test(val);
4145
- }
4146
4240
  function hasHttpOnlyTrue(optsNode) {
4147
- if (optsNode?.type !== "ObjectExpression") return false;
4148
4241
  const prop = findProp(optsNode, "httpOnly");
4149
- if (!prop) return false;
4150
- return prop.value?.type === "Literal" && prop.value.value === true;
4151
- }
4152
- return {
4153
- CallExpression(node) {
4154
- const callee = node.callee;
4155
- const isCookieSet = callee?.type === "Identifier" && callee.name === "setCookie";
4156
- if (!isCookieSet) return;
4157
- const args = node.arguments ?? [];
4158
- const nameArg = args[0];
4159
- if (nameArg?.type !== "Literal" || typeof nameArg.value !== "string") return;
4160
- if (!isTokenName(nameArg.value)) return;
4161
- const optsArg = args.length >= 3 ? args[2] : null;
4162
- if (!optsArg || optsArg.type !== "ObjectExpression") {
4163
- context.report({ node, messageId: "idTokenCookieMissingHttpOnly" });
4164
- return;
4165
- }
4166
- if (!hasHttpOnlyTrue(optsArg)) {
4242
+ return prop?.value?.type === "Literal" && prop.value.value === true;
4243
+ }
4244
+ function checkNameValueOptsCall(node) {
4245
+ const args = node.arguments ?? [];
4246
+ if (args.length === 1 && args[0]?.type === "ObjectExpression") {
4247
+ const nameProp = findProp(args[0], "name");
4248
+ if (!isTokenLiteral(nameProp?.value)) return;
4249
+ if (!hasHttpOnlyTrue(args[0])) {
4167
4250
  context.report({ node, messageId: "idTokenCookieMissingHttpOnly" });
4168
4251
  }
4252
+ return;
4253
+ }
4254
+ if (!isTokenLiteral(args[0])) return;
4255
+ const optsArg = args.length >= 3 ? args[2] : null;
4256
+ if (!optsArg || optsArg.type !== "ObjectExpression" || !hasHttpOnlyTrue(optsArg)) {
4257
+ context.report({ node, messageId: "idTokenCookieMissingHttpOnly" });
4169
4258
  }
4170
- };
4171
- }
4172
- };
4173
- var firebaseIdTokenCookieFlagsRule = rule39;
4174
-
4175
- // src/providers/firebase/rules/middleware-token-not-verified.ts
4176
- var rule40 = {
4177
- meta: {
4178
- type: "problem",
4179
- docs: {
4180
- description: "Next.js middleware reads auth cookie but never verifies it",
4181
- category: "security",
4182
- cwe: "CWE-345",
4183
- owasp: "A07:2021 Identification and Authentication Failures",
4184
- rationale: "Checking only that an auth cookie is non-empty does not verify the token. An attacker who sets any non-empty cookie value will bypass the guard entirely. The middleware must call verifySessionCookie or verifyIdToken to confirm the token is valid and unexpired.",
4185
- docsUrl: "https://firebase.google.com/docs/auth/admin/manage-cookies#verify_session_cookies_and_check_permissions",
4186
- recommended: true
4187
- },
4188
- messages: {
4189
- tokenNotVerified: "Middleware reads the auth cookie but never verifies it. Any non-empty cookie value bypasses the guard. Call adminAuth.verifySessionCookie() or verifyIdToken() before allowing access."
4190
- },
4191
- schema: []
4192
- },
4193
- create(context) {
4194
- const AUTH_COOKIE_NAMES = /token|session/i;
4195
- const VERIFY_METHOD_NAMES = /* @__PURE__ */ new Set(["verifySessionCookie", "verifyIdToken", "verify"]);
4196
- let readsCookieWithAuthName = false;
4197
- let hasVerifyCall = false;
4198
- const cookieReadNodes = [];
4199
- function isAuthCookieGet(node) {
4200
- if (node?.type !== "CallExpression") return false;
4201
- const callee = node.callee;
4202
- if (callee?.type !== "MemberExpression") return false;
4203
- if (callee.property?.type !== "Identifier" || callee.property.name !== "get") return false;
4204
- const arg = node.arguments?.[0];
4205
- if (arg?.type !== "Literal" || typeof arg.value !== "string") return false;
4206
- return AUTH_COOKIE_NAMES.test(arg.value);
4207
4259
  }
4208
4260
  return {
4209
4261
  CallExpression(node) {
4210
- if (isAuthCookieGet(node)) {
4211
- readsCookieWithAuthName = true;
4212
- cookieReadNodes.push(node);
4213
- }
4214
4262
  const callee = node.callee;
4215
- if (callee?.type === "Identifier" && VERIFY_METHOD_NAMES.has(callee.name)) {
4216
- hasVerifyCall = true;
4263
+ if (callee?.type === "Identifier" && callee.name === "setCookie") {
4264
+ checkNameValueOptsCall(node);
4265
+ return;
4266
+ }
4267
+ if (callee?.type !== "MemberExpression" || callee.computed || callee.property?.type !== "Identifier") return;
4268
+ if (callee.property.name === "cookie") {
4269
+ checkNameValueOptsCall(node);
4270
+ return;
4217
4271
  }
4218
- if (callee?.type === "MemberExpression" && callee.property?.type === "Identifier" && VERIFY_METHOD_NAMES.has(callee.property.name)) {
4219
- hasVerifyCall = true;
4272
+ if (callee.property.name === "set" && isCookieReceiver(callee.object)) {
4273
+ checkNameValueOptsCall(node);
4220
4274
  }
4221
4275
  },
4222
- "Program:exit"() {
4223
- if (readsCookieWithAuthName && !hasVerifyCall) {
4224
- for (const node of cookieReadNodes) {
4225
- context.report({ node, messageId: "tokenNotVerified" });
4226
- }
4276
+ // document.cookie = `token=${idToken}` — can never be httpOnly
4277
+ AssignmentExpression(node) {
4278
+ const left = node.left;
4279
+ const isDocumentCookie = left?.type === "MemberExpression" && !left.computed && left.object?.type === "Identifier" && left.object.name === "document" && left.property?.type === "Identifier" && left.property.name === "cookie";
4280
+ if (!isDocumentCookie) return;
4281
+ const right = node.right;
4282
+ const text = right?.type === "Literal" && typeof right.value === "string" ? right.value : right?.type === "TemplateLiteral" ? (right.quasis ?? []).map((q) => q?.value?.cooked ?? "").join("") : "";
4283
+ if (/(?:^|;\s*)[^=;]*(?:token|session)[^=;]*=/i.test(text)) {
4284
+ context.report({ node, messageId: "idTokenCookieMissingHttpOnly" });
4227
4285
  }
4228
4286
  }
4229
4287
  };
4230
4288
  }
4231
4289
  };
4232
- var firebaseMiddlewareTokenNotVerifiedRule = rule40;
4290
+ var firebaseIdTokenCookieFlagsRule = rule39;
4233
4291
 
4234
4292
  // src/providers/firebase/rules/hardcoded-user-id.ts
4235
- var rule41 = {
4293
+ var rule40 = {
4236
4294
  meta: {
4237
4295
  type: "problem",
4238
4296
  docs: {
@@ -4250,6 +4308,9 @@ var rule41 = {
4250
4308
  schema: []
4251
4309
  },
4252
4310
  create(context) {
4311
+ if (isInsideTestFile2(String(context.filename ?? ""))) {
4312
+ return {};
4313
+ }
4253
4314
  const USER_ID_NAMES = /* @__PURE__ */ new Set(["userId", "uid", "userID", "user_id"]);
4254
4315
  return {
4255
4316
  VariableDeclarator(node) {
@@ -4264,10 +4325,10 @@ var rule41 = {
4264
4325
  };
4265
4326
  }
4266
4327
  };
4267
- var firebaseHardcodedUserIdRule = rule41;
4328
+ var firebaseHardcodedUserIdRule = rule40;
4268
4329
 
4269
4330
  // src/providers/firebase/rules/auth-user-not-found-disclosure.ts
4270
- var rule42 = {
4331
+ var rule41 = {
4271
4332
  meta: {
4272
4333
  type: "suggestion",
4273
4334
  docs: {
@@ -4306,10 +4367,10 @@ var rule42 = {
4306
4367
  };
4307
4368
  }
4308
4369
  };
4309
- var firebaseAuthUserNotFoundDisclosureRule = rule42;
4370
+ var firebaseAuthUserNotFoundDisclosureRule = rule41;
4310
4371
 
4311
4372
  // src/providers/firebase/rules/signup-password-confirm.ts
4312
- var rule43 = {
4373
+ var rule42 = {
4313
4374
  meta: {
4314
4375
  type: "suggestion",
4315
4376
  docs: {
@@ -4343,7 +4404,7 @@ var rule43 = {
4343
4404
  },
4344
4405
  BinaryExpression(node) {
4345
4406
  if (node.operator !== "===" && node.operator !== "==" && node.operator !== "!==" && node.operator !== "!=") return;
4346
- const mentionsConfirm = (n) => n?.type === "Identifier" && n.name === "confirmPassword";
4407
+ const mentionsConfirm = (n) => n?.type === "Identifier" && n.name === "confirmPassword" || n?.type === "MemberExpression" && !n.computed && n.property?.type === "Identifier" && n.property.name === "confirmPassword";
4347
4408
  if (mentionsConfirm(node.left) || mentionsConfirm(node.right)) {
4348
4409
  hasPasswordComparison = true;
4349
4410
  }
@@ -4366,16 +4427,16 @@ var rule43 = {
4366
4427
  };
4367
4428
  }
4368
4429
  };
4369
- var firebaseSignupPasswordConfirmRule = rule43;
4430
+ var firebaseSignupPasswordConfirmRule = rule42;
4370
4431
 
4371
4432
  // src/providers/firebase/rules/use-array-union-remove.ts
4372
- var rule44 = {
4433
+ var rule43 = {
4373
4434
  meta: {
4374
4435
  type: "suggestion",
4375
4436
  docs: {
4376
4437
  description: "Firestore array field updated with read-modify-write spread/filter instead of arrayUnion/arrayRemove",
4377
4438
  category: "correctness",
4378
- rationale: "Spreading an array or using filter() inside updateDoc() performs a non-atomic read-modify-write that loses concurrent updates. Firestore provides arrayUnion() and arrayRemove() specifically for atomic array updates that do not require reading the document first.",
4439
+ rationale: "Spreading an array or using filter() inside updateDoc() performs a non-atomic read-modify-write that loses concurrent updates. Firestore provides arrayUnion() and arrayRemove() specifically for atomic array updates that do not require reading the document first. Caveat: arrayRemove() matches whole element values \u2014 to remove object items by a predicate (e.g. by id), use runTransaction() instead so the read-modify-write is atomic.",
4379
4440
  docsUrl: "https://firebase.google.com/docs/firestore/manage-data/add-data#update_elements_in_an_array",
4380
4441
  recommended: true
4381
4442
  },
@@ -4415,10 +4476,10 @@ var rule44 = {
4415
4476
  };
4416
4477
  }
4417
4478
  };
4418
- var firebaseUseArrayUnionRemoveRule = rule44;
4479
+ var firebaseUseArrayUnionRemoveRule = rule43;
4419
4480
 
4420
4481
  // src/providers/firebase/rules/duplicate-initialize-app.ts
4421
- var rule45 = {
4482
+ var rule44 = {
4422
4483
  meta: {
4423
4484
  type: "suggestion",
4424
4485
  docs: {
@@ -4451,9 +4512,11 @@ var rule45 = {
4451
4512
  const callee = node.callee;
4452
4513
  if (callee?.type !== "Identifier") return;
4453
4514
  if (initializeAppLocalName && callee.name === initializeAppLocalName) {
4454
- initializeAppCalls.push(node);
4515
+ if ((node.arguments ?? []).length < 2) {
4516
+ initializeAppCalls.push(node);
4517
+ }
4455
4518
  }
4456
- if (callee.name === "getApps") {
4519
+ if (callee.name === "getApps" || callee.name === "getApp") {
4457
4520
  hasGetAppsCall = true;
4458
4521
  }
4459
4522
  },
@@ -4466,21 +4529,21 @@ var rule45 = {
4466
4529
  };
4467
4530
  }
4468
4531
  };
4469
- var firebaseDuplicateInitializeAppRule = rule45;
4532
+ var firebaseDuplicateInitializeAppRule = rule44;
4470
4533
 
4471
4534
  // src/providers/firebase/rules/onSnapshot-async-throw.ts
4472
- var rule46 = {
4535
+ var rule45 = {
4473
4536
  meta: {
4474
4537
  type: "suggestion",
4475
4538
  docs: {
4476
4539
  description: "throw inside async onSnapshot callback creates an unhandled promise rejection",
4477
4540
  category: "reliability",
4478
- rationale: "onSnapshot does not handle Promise rejections from its callback. A throw inside an async callback becomes an unhandled rejection, silently terminating the listener and leaving the UI in a broken state with no error feedback.",
4541
+ rationale: "onSnapshot ignores the promise returned by an async callback, so a throw inside one becomes an unhandled promise rejection: the error never reaches the UI or the onSnapshot error callback (which only fires for stream errors), and in Node it can crash the process. The listener keeps delivering snapshots, but every one that hits the throw fails invisibly.",
4479
4542
  docsUrl: "https://firebase.google.com/docs/firestore/query-data/listen#handle_listen_errors",
4480
4543
  recommended: true
4481
4544
  },
4482
4545
  messages: {
4483
- asyncThrowInSnapshot: "throw inside an async onSnapshot callback creates an unhandled promise rejection. The listener silently stops. Use return with error logging or the onSnapshot error callback instead."
4546
+ asyncThrowInSnapshot: "throw inside an async onSnapshot callback creates an unhandled promise rejection \u2014 the error surfaces nowhere. Catch it in the callback and set error state instead of throwing."
4484
4547
  },
4485
4548
  schema: []
4486
4549
  },
@@ -4520,10 +4583,10 @@ var rule46 = {
4520
4583
  };
4521
4584
  }
4522
4585
  };
4523
- var firebaseOnSnapshotAsyncThrowRule = rule46;
4586
+ var firebaseOnSnapshotAsyncThrowRule = rule45;
4524
4587
 
4525
4588
  // src/providers/firebase/rules/onSnapshot-missing-error-callback.ts
4526
- var rule47 = {
4589
+ var rule46 = {
4527
4590
  meta: {
4528
4591
  type: "suggestion",
4529
4592
  docs: {
@@ -4561,21 +4624,21 @@ var rule47 = {
4561
4624
  };
4562
4625
  }
4563
4626
  };
4564
- var firebaseOnSnapshotMissingErrorCallbackRule = rule47;
4627
+ var firebaseOnSnapshotMissingErrorCallbackRule = rule46;
4565
4628
 
4566
4629
  // src/providers/firebase/rules/firestore-document-size-guard.ts
4567
- var rule48 = {
4630
+ var rule47 = {
4568
4631
  meta: {
4569
4632
  type: "suggestion",
4570
4633
  docs: {
4571
4634
  description: "Firestore write includes editor.getJSON() without a document size guard",
4572
4635
  category: "reliability",
4573
- rationale: "Firestore documents have a 1 MiB limit. When a rich-text editor stores base64 images inline, a single paste can push the document over the limit. The write silently fails with INVALID_ARGUMENT. Always check document size before calling updateDoc/setDoc when the payload includes editor JSON.",
4636
+ rationale: "Firestore documents have a 1 MiB limit. When a rich-text editor stores base64 images inline, a single paste can push the document over the limit. The write is rejected with INVALID_ARGUMENT \u2014 and if that rejection is not handled, nothing surfaces to the user. Check document size (e.g. new Blob([JSON.stringify(payload)]).size) before calling updateDoc/setDoc when the payload includes editor JSON.",
4574
4637
  docsUrl: "https://firebase.google.com/docs/firestore/quotas#limits",
4575
4638
  recommended: true
4576
4639
  },
4577
4640
  messages: {
4578
- missingDocumentSizeGuard: "Firestore write includes editor.getJSON() without a document size check. Base64 images can exceed the 1 MiB Firestore limit, silently failing the write. Add a Blob size guard before calling updateDoc/setDoc."
4641
+ missingDocumentSizeGuard: "Firestore write includes editor.getJSON() without a document size check. Base64 images can exceed the 1 MiB Firestore limit and the write is rejected. Add a size guard (new Blob([JSON.stringify(payload)]).size) before calling updateDoc/setDoc."
4579
4642
  },
4580
4643
  schema: []
4581
4644
  },
@@ -4615,21 +4678,21 @@ var rule48 = {
4615
4678
  };
4616
4679
  }
4617
4680
  };
4618
- var firebaseFirestoreDocumentSizeGuardRule = rule48;
4681
+ var firebaseFirestoreDocumentSizeGuardRule = rule47;
4619
4682
 
4620
4683
  // src/providers/firebase/rules/use-timestamp-now.ts
4621
- var rule49 = {
4684
+ var rule48 = {
4622
4685
  meta: {
4623
4686
  type: "suggestion",
4624
4687
  docs: {
4625
- description: "new Date() used for Firestore timestamp instead of Timestamp.now() or serverTimestamp()",
4688
+ description: "new Date() used for Firestore timestamp instead of serverTimestamp()",
4626
4689
  category: "correctness",
4627
- rationale: "While Firestore auto-converts Date objects on write, mixing new Date() with Timestamp.now() creates type inconsistencies. Firestore security rules that compare request.resource.data.createdAt against request.time expect a Timestamp on both sides; a bare Date object can cause silent rule evaluation failures.",
4690
+ rationale: "Firestore auto-converts Date objects to Timestamps on write, so new Date() and Timestamp.now() are equivalent \u2014 both use the client clock, which cannot be trusted (skewed or deliberately changed clocks produce wrong orderings). serverTimestamp() stamps the value on the server instead, and it is the only option that satisfies security rules comparing a field against request.time (e.g. createdAt == request.time), which reject both new Date() and Timestamp.now().",
4628
4691
  docsUrl: "https://firebase.google.com/docs/reference/js/firestore_.timestamp",
4629
4692
  recommended: true
4630
4693
  },
4631
4694
  messages: {
4632
- useTimestampNow: "Use Timestamp.now() or serverTimestamp() instead of new Date() for Firestore timestamp fields. new Date() creates type inconsistencies with security rules that compare against request.time."
4695
+ useTimestampNow: "Use serverTimestamp() instead of new Date() for Firestore timestamp fields. new Date() uses the untrusted client clock and fails rules that compare against request.time."
4633
4696
  },
4634
4697
  schema: []
4635
4698
  },
@@ -4638,7 +4701,7 @@ var rule49 = {
4638
4701
  return {
4639
4702
  ImportDeclaration(node) {
4640
4703
  const src = node.source?.value;
4641
- if (typeof src === "string" && (src.startsWith("firebase/") || src === "firebase-admin/firestore")) {
4704
+ if (typeof src === "string" && (src.startsWith("firebase/firestore") || src === "firebase-admin/firestore")) {
4642
4705
  importsFromFirestore = true;
4643
4706
  }
4644
4707
  },
@@ -4651,7 +4714,7 @@ var rule49 = {
4651
4714
  };
4652
4715
  }
4653
4716
  };
4654
- var firebaseUseTimestampNowRule = rule49;
4717
+ var firebaseUseTimestampNowRule = rule48;
4655
4718
 
4656
4719
  // src/providers/lovable/utils.ts
4657
4720
  var LLM_HOST_PATTERN = /api\.anthropic\.com|api\.openai\.com/;
@@ -4666,7 +4729,7 @@ function containsKnownLlmHost(node) {
4666
4729
  }
4667
4730
 
4668
4731
  // src/providers/lovable/rules/no-client-side-secret-fetch.ts
4669
- var rule50 = {
4732
+ var rule49 = {
4670
4733
  meta: {
4671
4734
  type: "problem",
4672
4735
  docs: {
@@ -4739,13 +4802,13 @@ var rule50 = {
4739
4802
  };
4740
4803
  }
4741
4804
  };
4742
- var lovableNoClientSideSecretFetchRule = rule50;
4805
+ var lovableNoClientSideSecretFetchRule = rule49;
4743
4806
 
4744
4807
  // src/providers/lovable/rules/paid-flag-without-edge-function.ts
4745
4808
  var PRICE_PATTERN = /\$\s?\d/;
4746
4809
  var FLAG_PROPERTY_PATTERN = /^is_[a-z0-9_]+$/i;
4747
4810
  var FLAG_SUFFIX_PATTERN = /_(unlocked|active|enabled)$/i;
4748
- var rule51 = {
4811
+ var rule50 = {
4749
4812
  meta: {
4750
4813
  type: "problem",
4751
4814
  docs: {
@@ -4754,7 +4817,7 @@ var rule51 = {
4754
4817
  cwe: "CWE-840",
4755
4818
  owasp: "A04:2021 \u2013 Insecure Design",
4756
4819
  rationale: "Lovable documents payment processing as Edge Function territory specifically so purchase/premium-access flags are only ever set after a payment provider confirms payment server-side. A handler that writes a paid-looking flag straight from the client with no payment call in between either never actually charges anyone, or \u2014 even if a charge happens elsewhere \u2014 leaves the flag itself freely settable by any caller with write access to the row.",
4757
- docsUrl: "https://docs.lovable.dev/features/cloud",
4820
+ docsUrl: "https://docs.lovable.dev/integrations/stripe",
4758
4821
  recommended: true
4759
4822
  },
4760
4823
  messages: {
@@ -4887,20 +4950,20 @@ var rule51 = {
4887
4950
  };
4888
4951
  }
4889
4952
  };
4890
- var lovablePaidFlagWithoutEdgeFunctionRule = rule51;
4953
+ var lovablePaidFlagWithoutEdgeFunctionRule = rule50;
4891
4954
 
4892
4955
  // src/providers/lovable/rules/expiry-column-never-checked.ts
4893
4956
  var EXPIRY_COLUMN_PATTERN = /_(until|expires?_at|expiry)$/i;
4894
4957
  var DATE_COMPARISON_OPERATORS = /* @__PURE__ */ new Set([">", "<", ">=", "<="]);
4895
4958
  var FILTER_METHODS = /* @__PURE__ */ new Set(["gt", "gte", "lt", "lte"]);
4896
- var rule52 = {
4959
+ var rule51 = {
4897
4960
  meta: {
4898
4961
  type: "suggestion",
4899
4962
  docs: {
4900
4963
  description: "An expiry column must be checked against the current time somewhere",
4901
4964
  category: "correctness",
4902
4965
  rationale: "Writing a *_until/*_expires_at column without ever comparing it to the current time anywhere in the codebase means the expiry is purely cosmetic \u2014 whatever it was meant to gate (a boost, a trial, a temporary unlock) never actually expires once granted, whether it was granted legitimately or through an unrelated bug.",
4903
- docsUrl: "https://docs.lovable.dev/features/cloud",
4966
+ docsUrl: "https://docs.lovable.dev/integrations/stripe",
4904
4967
  recommended: true
4905
4968
  },
4906
4969
  messages: {
@@ -4985,18 +5048,18 @@ var rule52 = {
4985
5048
  };
4986
5049
  }
4987
5050
  };
4988
- var lovableExpiryColumnNeverCheckedRule = rule52;
5051
+ var lovableExpiryColumnNeverCheckedRule = rule51;
4989
5052
 
4990
5053
  // src/providers/lovable/rules/silent-catch-on-provider-call.ts
4991
5054
  var LOGGING_CONSOLE_METHODS = /* @__PURE__ */ new Set(["error", "warn", "log", "info"]);
4992
- var rule53 = {
5055
+ var rule52 = {
4993
5056
  meta: {
4994
5057
  type: "suggestion",
4995
5058
  docs: {
4996
5059
  description: "A catch block around an LLM provider call must log the failure",
4997
5060
  category: "correctness",
4998
5061
  rationale: `Returning null/undefined from a bare catch with no logging makes every failure mode \u2014 missing key, expired key, rate limit, malformed response, network error \u2014 look identical to "no key configured." If a deployment's provider key starts failing in production, this is invisible: there is nothing in the browser or error-tracking logs to distinguish a real outage from an intentionally-unconfigured feature.`,
4999
- docsUrl: "https://docs.lovable.dev/features/cloud",
5062
+ docsUrl: "https://docs.lovable.dev/integrations/cloud",
5000
5063
  recommended: true
5001
5064
  },
5002
5065
  messages: {
@@ -5062,7 +5125,7 @@ var rule53 = {
5062
5125
  };
5063
5126
  }
5064
5127
  };
5065
- var lovableSilentCatchOnProviderCallRule = rule53;
5128
+ var lovableSilentCatchOnProviderCallRule = rule52;
5066
5129
 
5067
5130
  // src/providers/browserbase/utils.ts
5068
5131
  function memberPropName3(node) {
@@ -5175,7 +5238,7 @@ function isTruthyGateTest(test) {
5175
5238
  }
5176
5239
  return false;
5177
5240
  }
5178
- var rule54 = {
5241
+ var rule53 = {
5179
5242
  meta: {
5180
5243
  type: "problem",
5181
5244
  docs: {
@@ -5247,10 +5310,10 @@ var rule54 = {
5247
5310
  };
5248
5311
  }
5249
5312
  };
5250
- var browserbaseNoConditionalAuthzOnAnonymousUserRule = rule54;
5313
+ var browserbaseNoConditionalAuthzOnAnonymousUserRule = rule53;
5251
5314
 
5252
5315
  // src/providers/browserbase/rules/no-connect-url-in-api-response.ts
5253
- var rule55 = {
5316
+ var rule54 = {
5254
5317
  meta: {
5255
5318
  type: "problem",
5256
5319
  docs: {
@@ -5280,7 +5343,7 @@ var rule55 = {
5280
5343
  };
5281
5344
  }
5282
5345
  };
5283
- var browserbaseNoConnectUrlInApiResponseRule = rule55;
5346
+ var browserbaseNoConnectUrlInApiResponseRule = rule54;
5284
5347
 
5285
5348
  // src/providers/browserbase/rules/session-id-requires-ownership-check.ts
5286
5349
  function isOwnershipCheckCall(node) {
@@ -5297,7 +5360,7 @@ function isSessionIdLikeArg(node) {
5297
5360
  }
5298
5361
  return false;
5299
5362
  }
5300
- var rule56 = {
5363
+ var rule55 = {
5301
5364
  meta: {
5302
5365
  type: "suggestion",
5303
5366
  docs: {
@@ -5305,7 +5368,7 @@ var rule56 = {
5305
5368
  category: "security",
5306
5369
  cwe: "CWE-862",
5307
5370
  rationale: "A session id alone is not an authorization token \u2014 it is an opaque identifier that can leak via links, logs, screenshots, or support tickets. A handler that resolves a sessionId straight into sessions.debug()/sessions.recording.retrieve() with no ownership/org-membership check lets any authenticated caller view or replay a session that belongs to someone else.",
5308
- docsUrl: "https://docs.browserbase.com/features/session-live-view",
5371
+ docsUrl: "https://docs.browserbase.com/platform/browser/observability/session-live-view",
5309
5372
  recommended: true
5310
5373
  },
5311
5374
  messages: {
@@ -5361,7 +5424,7 @@ var rule56 = {
5361
5424
  };
5362
5425
  }
5363
5426
  };
5364
- var browserbaseSessionIdRequiresOwnershipCheckRule = rule56;
5427
+ var browserbaseSessionIdRequiresOwnershipCheckRule = rule55;
5365
5428
 
5366
5429
  // src/providers/browserbase/rules/no-concurrent-shared-context.ts
5367
5430
  function isPromiseAllCall2(node) {
@@ -5418,14 +5481,14 @@ function findSharedContextCreateCall(callback) {
5418
5481
  visit(body);
5419
5482
  return found;
5420
5483
  }
5421
- var rule57 = {
5484
+ var rule56 = {
5422
5485
  meta: {
5423
5486
  type: "problem",
5424
5487
  docs: {
5425
5488
  description: "A Browserbase Context must not be attached to concurrent sessions",
5426
5489
  category: "correctness",
5427
5490
  rationale: `Browserbase's docs state explicitly: "Avoid having multiple sessions using the same Context at once. Sites may force a log out." Passing the same browserSettings.context.id into every iteration of a Promise.all(items.map(...)) batch creates 2+ Browserbase sessions simultaneously attached to one Context, producing non-deterministic, flaky failures when a site force-logs-out one session because another concurrently authenticated against the same context.`,
5428
- docsUrl: "https://docs.browserbase.com/features/contexts",
5491
+ docsUrl: "https://docs.browserbase.com/platform/browser/core-features/contexts",
5429
5492
  recommended: true
5430
5493
  },
5431
5494
  messages: {
@@ -5465,7 +5528,7 @@ var rule57 = {
5465
5528
  };
5466
5529
  }
5467
5530
  };
5468
- var browserbaseNoConcurrentSharedContextRule = rule57;
5531
+ var browserbaseNoConcurrentSharedContextRule = rule56;
5469
5532
 
5470
5533
  // src/providers/browserbase/rules/mobile-device-requires-os-setting.ts
5471
5534
  function isMobileLiteral(node) {
@@ -5498,14 +5561,14 @@ function isOsMobileSetting(node) {
5498
5561
  }
5499
5562
  return false;
5500
5563
  }
5501
- var rule58 = {
5564
+ var rule57 = {
5502
5565
  meta: {
5503
5566
  type: "problem",
5504
5567
  docs: {
5505
5568
  description: "Mobile device combos must set browserSettings.os, not just resize the viewport",
5506
5569
  category: "correctness",
5507
5570
  rationale: `The Node SDK's device emulation lever is browserSettings.os: 'mobile' | 'tablet' \u2014 there is no fingerprint API in this SDK. A "mobile" session that only resizes the Playwright viewport is still a desktop Chrome browser with a desktop user-agent string in a small window. Sites that branch behavior on UA/touch capability (the majority of responsive sites) never actually exercise their mobile code path, silently undermining "test on mobile" results.`,
5508
- docsUrl: "https://docs.browserbase.com/features/stealth-mode",
5571
+ docsUrl: "https://docs.browserbase.com/platform/identity/verified-customization",
5509
5572
  recommended: true
5510
5573
  },
5511
5574
  messages: {
@@ -5537,7 +5600,7 @@ var rule58 = {
5537
5600
  };
5538
5601
  }
5539
5602
  };
5540
- var browserbaseMobileDeviceRequiresOsSettingRule = rule58;
5603
+ var browserbaseMobileDeviceRequiresOsSettingRule = rule57;
5541
5604
 
5542
5605
  // src/providers/browserbase/rules/use-typed-exception-status-not-substring.ts
5543
5606
  function isStringifiedErrorSource(node, errName) {
@@ -5599,7 +5662,7 @@ function collectStringifiedVars(body, errName) {
5599
5662
  });
5600
5663
  return names;
5601
5664
  }
5602
- var rule59 = {
5665
+ var rule58 = {
5603
5666
  meta: {
5604
5667
  type: "suggestion",
5605
5668
  docs: {
@@ -5629,7 +5692,7 @@ var rule59 = {
5629
5692
  };
5630
5693
  }
5631
5694
  };
5632
- var browserbaseUseTypedExceptionStatusNotSubstringRule = rule59;
5695
+ var browserbaseUseTypedExceptionStatusNotSubstringRule = rule58;
5633
5696
 
5634
5697
  // src/providers/browserbase/rules/release-session-on-connect-failure.ts
5635
5698
  function isSessionsCreateAwait(node) {
@@ -5652,7 +5715,7 @@ function isReleaseCall(node) {
5652
5715
  const name = callee?.type === "Identifier" ? callee.name : memberPropName3(node);
5653
5716
  return !!name && /requeststop|releasesession|release_session/i.test(name);
5654
5717
  }
5655
- var rule60 = {
5718
+ var rule59 = {
5656
5719
  meta: {
5657
5720
  type: "problem",
5658
5721
  docs: {
@@ -5694,10 +5757,10 @@ var rule60 = {
5694
5757
  };
5695
5758
  }
5696
5759
  };
5697
- var browserbaseReleaseSessionOnConnectFailureRule = rule60;
5760
+ var browserbaseReleaseSessionOnConnectFailureRule = rule59;
5698
5761
 
5699
5762
  // src/providers/browserbase/rules/dont-stack-custom-retry-on-sdk-retry.ts
5700
- var rule61 = {
5763
+ var rule60 = {
5701
5764
  meta: {
5702
5765
  type: "suggestion",
5703
5766
  docs: {
@@ -5742,7 +5805,7 @@ var rule61 = {
5742
5805
  };
5743
5806
  }
5744
5807
  };
5745
- var browserbaseDontStackCustomRetryOnSdkRetryRule = rule61;
5808
+ var browserbaseDontStackCustomRetryOnSdkRetryRule = rule60;
5746
5809
 
5747
5810
  // src/providers/browserbase/rules/no-overbroad-error-substring-match.ts
5748
5811
  var OVERBROAD_TERMS = /* @__PURE__ */ new Set(["session", "context", "browser", "browserbase"]);
@@ -5764,7 +5827,7 @@ function isCleanupCall(node) {
5764
5827
  const name = callee?.type === "Identifier" ? callee.name : memberPropName3(node);
5765
5828
  return !!name && /release|remove|cleanup|stop|teardown/i.test(name);
5766
5829
  }
5767
- var rule62 = {
5830
+ var rule61 = {
5768
5831
  meta: {
5769
5832
  type: "problem",
5770
5833
  docs: {
@@ -5794,7 +5857,7 @@ var rule62 = {
5794
5857
  };
5795
5858
  }
5796
5859
  };
5797
- var browserbaseNoOverbroadErrorSubstringMatchRule = rule62;
5860
+ var browserbaseNoOverbroadErrorSubstringMatchRule = rule61;
5798
5861
 
5799
5862
  // src/providers/browserbase/rules/use-sdk-not-raw-requests.ts
5800
5863
  var BROWSERBASE_HOST = "api.browserbase.com";
@@ -5834,7 +5897,7 @@ function isRawHttpCall(node) {
5834
5897
  }
5835
5898
  return { isCall: false, urlArg: void 0 };
5836
5899
  }
5837
- var rule63 = {
5900
+ var rule62 = {
5838
5901
  meta: {
5839
5902
  type: "suggestion",
5840
5903
  docs: {
@@ -5868,10 +5931,10 @@ var rule63 = {
5868
5931
  };
5869
5932
  }
5870
5933
  };
5871
- var browserbaseUseSdkNotRawRequestsRule = rule63;
5934
+ var browserbaseUseSdkNotRawRequestsRule = rule62;
5872
5935
 
5873
5936
  // src/providers/browserbase/rules/centralize-request-release.ts
5874
- var rule64 = {
5937
+ var rule63 = {
5875
5938
  meta: {
5876
5939
  type: "suggestion",
5877
5940
  docs: {
@@ -5904,11 +5967,11 @@ var rule64 = {
5904
5967
  };
5905
5968
  }
5906
5969
  };
5907
- var browserbaseCentralizeRequestReleaseRule = rule64;
5970
+ var browserbaseCentralizeRequestReleaseRule = rule63;
5908
5971
 
5909
5972
  // src/providers/openai-cua/rules/no-domain-allowlist.ts
5910
5973
  var ACTION_METHOD_NAMES = /* @__PURE__ */ new Set(["click", "type", "press", "move", "goto", "fill", "dblclick", "dragAndDrop"]);
5911
- var rule65 = {
5974
+ var rule64 = {
5912
5975
  meta: {
5913
5976
  type: "problem",
5914
5977
  docs: {
@@ -6034,11 +6097,11 @@ var rule65 = {
6034
6097
  };
6035
6098
  }
6036
6099
  };
6037
- var openaiCuaNoDomainAllowlistRule = rule65;
6100
+ var openaiCuaNoDomainAllowlistRule = rule64;
6038
6101
 
6039
6102
  // src/providers/openai-cua/rules/scroll-delta-default-zero.ts
6040
6103
  var VERTICAL_DELTA_NAME_PATTERN = /^(dy|delta_?y|scroll_?y)$/i;
6041
- var rule66 = {
6104
+ var rule65 = {
6042
6105
  meta: {
6043
6106
  type: "problem",
6044
6107
  docs: {
@@ -6114,12 +6177,12 @@ var rule66 = {
6114
6177
  };
6115
6178
  }
6116
6179
  };
6117
- var openaiCuaScrollDeltaDefaultZeroRule = rule66;
6180
+ var openaiCuaScrollDeltaDefaultZeroRule = rule65;
6118
6181
 
6119
6182
  // src/providers/openai-cua/rules/structured-step-metadata-not-text-json.ts
6120
6183
  var BRACE_SEARCH_METHODS = /* @__PURE__ */ new Set(["indexOf", "lastIndexOf"]);
6121
6184
  var SLICE_METHODS = /* @__PURE__ */ new Set(["slice", "substring", "substr"]);
6122
- var rule67 = {
6185
+ var rule66 = {
6123
6186
  meta: {
6124
6187
  type: "suggestion",
6125
6188
  docs: {
@@ -6218,10 +6281,10 @@ var rule67 = {
6218
6281
  };
6219
6282
  }
6220
6283
  };
6221
- var openaiCuaStructuredStepMetadataNotTextJsonRule = rule67;
6284
+ var openaiCuaStructuredStepMetadataNotTextJsonRule = rule66;
6222
6285
 
6223
6286
  // src/providers/openai-cua/rules/no-blind-safety-check-ack.ts
6224
- var rule68 = {
6287
+ var rule67 = {
6225
6288
  meta: {
6226
6289
  type: "suggestion",
6227
6290
  docs: {
@@ -6284,7 +6347,7 @@ var rule68 = {
6284
6347
  };
6285
6348
  }
6286
6349
  };
6287
- var openaiCuaNoBlindSafetyCheckAckRule = rule68;
6350
+ var openaiCuaNoBlindSafetyCheckAckRule = rule67;
6288
6351
 
6289
6352
  // src/providers/openai-cua/utils.ts
6290
6353
  function propName(node) {
@@ -6330,7 +6393,7 @@ function findResponsesCreateCall(node, depth = 0) {
6330
6393
  }
6331
6394
 
6332
6395
  // src/providers/openai-cua/rules/retry-transient-turn-errors.ts
6333
- var rule69 = {
6396
+ var rule68 = {
6334
6397
  meta: {
6335
6398
  type: "problem",
6336
6399
  docs: {
@@ -6412,10 +6475,10 @@ var rule69 = {
6412
6475
  };
6413
6476
  }
6414
6477
  };
6415
- var openaiCuaRetryTransientTurnErrorsRule = rule69;
6478
+ var openaiCuaRetryTransientTurnErrorsRule = rule68;
6416
6479
 
6417
6480
  // src/providers/openai-cua/rules/check-response-status-incomplete.ts
6418
- var rule70 = {
6481
+ var rule69 = {
6419
6482
  meta: {
6420
6483
  type: "problem",
6421
6484
  docs: {
@@ -6524,17 +6587,17 @@ var rule70 = {
6524
6587
  };
6525
6588
  }
6526
6589
  };
6527
- var openaiCuaCheckResponseStatusIncompleteRule = rule70;
6590
+ var openaiCuaCheckResponseStatusIncompleteRule = rule69;
6528
6591
 
6529
6592
  // src/providers/openai-cua/rules/set-safety-identifier.ts
6530
- var rule71 = {
6593
+ var rule70 = {
6531
6594
  meta: {
6532
6595
  type: "problem",
6533
6596
  docs: {
6534
6597
  description: "responses.create() must set safety_identifier for per-end-user policy attribution",
6535
6598
  category: "integration",
6536
6599
  rationale: "OpenAI documents that a stable per-end-user safety_identifier lets policy violations be attributed and acted on per end user. Without one on a multi-tenant integration routing many customers through a single shared API key, a single customer triggering a high-confidence policy-violation heuristic can result in access being temporarily revoked for the entire organization, not just the offending identifier.",
6537
- docsUrl: "https://help.openai.com/en/articles/5428082-how-to-incorporate-a-safety-identifier",
6600
+ docsUrl: "https://developers.openai.com/api/docs/guides/safety-best-practices",
6538
6601
  recommended: true
6539
6602
  },
6540
6603
  messages: {
@@ -6565,10 +6628,10 @@ var rule71 = {
6565
6628
  };
6566
6629
  }
6567
6630
  };
6568
- var openaiCuaSetSafetyIdentifierRule = rule71;
6631
+ var openaiCuaSetSafetyIdentifierRule = rule70;
6569
6632
 
6570
6633
  // src/providers/tiptap/rules/upload-validate-fn-void.ts
6571
- var rule72 = {
6634
+ var rule71 = {
6572
6635
  meta: {
6573
6636
  type: "problem",
6574
6637
  docs: {
@@ -6632,10 +6695,10 @@ var rule72 = {
6632
6695
  };
6633
6696
  }
6634
6697
  };
6635
- var tiptapUploadValidateFnVoidRule = rule72;
6698
+ var tiptapUploadValidateFnVoidRule = rule71;
6636
6699
 
6637
6700
  // src/providers/tiptap/rules/script-src-hardcoded-api-key.ts
6638
- var rule73 = {
6701
+ var rule72 = {
6639
6702
  meta: {
6640
6703
  type: "problem",
6641
6704
  docs: {
@@ -6693,10 +6756,10 @@ var rule73 = {
6693
6756
  };
6694
6757
  }
6695
6758
  };
6696
- var tiptapScriptSrcHardcodedApiKeyRule = rule73;
6759
+ var tiptapScriptSrcHardcodedApiKeyRule = rule72;
6697
6760
 
6698
6761
  // src/providers/tiptap/rules/dynamic-script-no-sri.ts
6699
- var rule74 = {
6762
+ var rule73 = {
6700
6763
  meta: {
6701
6764
  type: "problem",
6702
6765
  docs: {
@@ -6705,7 +6768,7 @@ var rule74 = {
6705
6768
  cwe: "CWE-829",
6706
6769
  owasp: "API8:2023 Security Misconfiguration",
6707
6770
  rationale: 'A script element injected without an integrity attribute will execute whatever the CDN returns, including malicious content if the CDN is compromised. Adding a Subresource Integrity hash (script.setAttribute("integrity", "sha384-...")) lets the browser verify the fetched script matches the expected bytes before executing it.',
6708
- docsUrl: "https://developer.mozilla.org/en-US/docs/Web/Security/Subresource_Integrity",
6771
+ docsUrl: "https://developer.mozilla.org/en-US/docs/Web/Security/Defenses/Subresource_Integrity",
6709
6772
  recommended: true
6710
6773
  },
6711
6774
  messages: {
@@ -6792,7 +6855,7 @@ var rule74 = {
6792
6855
  };
6793
6856
  }
6794
6857
  };
6795
- var tiptapDynamicScriptNoSriRule = rule74;
6858
+ var tiptapDynamicScriptNoSriRule = rule73;
6796
6859
 
6797
6860
  // src/providers/tiptap/utils.ts
6798
6861
  function findProperty3(objectExpression, name) {
@@ -6817,7 +6880,7 @@ function walk(node, visit) {
6817
6880
  }
6818
6881
 
6819
6882
  // src/providers/tiptap/rules/addAttributes-missing-renderHTML.ts
6820
- var rule75 = {
6883
+ var rule74 = {
6821
6884
  meta: {
6822
6885
  type: "problem",
6823
6886
  docs: {
@@ -6870,10 +6933,10 @@ var rule75 = {
6870
6933
  };
6871
6934
  }
6872
6935
  };
6873
- var tiptapAddAttributesMissingRenderHTMLRule = rule75;
6936
+ var tiptapAddAttributesMissingRenderHTMLRule = rule74;
6874
6937
 
6875
6938
  // src/providers/tiptap/rules/appendTransaction-add-to-history.ts
6876
- var rule76 = {
6939
+ var rule75 = {
6877
6940
  meta: {
6878
6941
  type: "suggestion",
6879
6942
  docs: {
@@ -6952,10 +7015,10 @@ var rule76 = {
6952
7015
  };
6953
7016
  }
6954
7017
  };
6955
- var tiptapAppendTransactionAddToHistoryRule = rule76;
7018
+ var tiptapAppendTransactionAddToHistoryRule = rule75;
6956
7019
 
6957
7020
  // src/providers/tiptap/rules/appendTransaction-full-scan.ts
6958
- var rule77 = {
7021
+ var rule76 = {
6959
7022
  meta: {
6960
7023
  type: "suggestion",
6961
7024
  docs: {
@@ -7015,10 +7078,10 @@ var rule77 = {
7015
7078
  };
7016
7079
  }
7017
7080
  };
7018
- var tiptapAppendTransactionFullScanRule = rule77;
7081
+ var tiptapAppendTransactionFullScanRule = rule76;
7019
7082
 
7020
7083
  // src/providers/tiptap/rules/atom-node-wrap-in.ts
7021
- var rule78 = {
7084
+ var rule77 = {
7022
7085
  meta: {
7023
7086
  type: "problem",
7024
7087
  docs: {
@@ -7081,57 +7144,16 @@ var rule78 = {
7081
7144
  };
7082
7145
  }
7083
7146
  };
7084
- var tiptapAtomNodeWrapInRule = rule78;
7085
-
7086
- // src/providers/tiptap/rules/twitter-url-regex.ts
7087
- var rule79 = {
7088
- meta: {
7089
- type: "suggestion",
7090
- docs: {
7091
- description: "Twitter/X URL regex must match both x.com and twitter.com",
7092
- category: "integration",
7093
- rationale: "Twitter rebranded to X but twitter.com URLs still resolve and embed correctly. A regex that only matches x.com silently rejects all legacy twitter.com links pasted by users. Update the pattern to (x\\.com|twitter\\.com) so both domains are handled.",
7094
- docsUrl: "https://tiptap.dev/docs/editor/extensions/nodes",
7095
- recommended: true
7096
- },
7097
- messages: {
7098
- twitterRegexMissingLegacyDomain: "This regex matches x.com but not twitter.com. Legacy twitter.com URLs are still valid and should be supported. Add (x\\.com|twitter\\.com) to the pattern."
7099
- },
7100
- schema: []
7101
- },
7102
- create(context) {
7103
- function checkPattern(pattern, node) {
7104
- if (!pattern.includes("x\\.com") && !pattern.includes("x.com")) return;
7105
- if (pattern.includes("twitter\\.com") || pattern.includes("twitter.com")) return;
7106
- context.report({ node, messageId: "twitterRegexMissingLegacyDomain" });
7107
- }
7108
- return {
7109
- Literal(node) {
7110
- if (node.regex?.pattern) {
7111
- checkPattern(node.regex.pattern, node);
7112
- }
7113
- },
7114
- NewExpression(node) {
7115
- if (node.callee?.type === "Identifier" && node.callee.name === "RegExp") {
7116
- const firstArg = node.arguments?.[0];
7117
- if (firstArg?.type === "Literal" && typeof firstArg.value === "string") {
7118
- checkPattern(firstArg.value, node);
7119
- }
7120
- }
7121
- }
7122
- };
7123
- }
7124
- };
7125
- var tiptapTwitterUrlRegexRule = rule79;
7147
+ var tiptapAtomNodeWrapInRule = rule77;
7126
7148
 
7127
7149
  // src/providers/tiptap/rules/drop-handler-pos-precedence.ts
7128
- var rule80 = {
7150
+ var rule78 = {
7129
7151
  meta: {
7130
7152
  type: "problem",
7131
7153
  docs: {
7132
7154
  description: "x ?? 0 - 1 is parsed as x ?? (0 - 1) = x ?? -1 due to operator precedence",
7133
7155
  category: "correctness",
7134
- rationale: "The nullish coalescing operator (??) has lower precedence than arithmetic operators. Writing `pos ?? 0 - 1` evaluates as `pos ?? -1`, not `(pos ?? 0) - 1`. When pos is null or undefined, the fallback is -1 instead of the intended -1 offset from 0. In ProseMirror drop handlers this produces a decoration at position -1, which corrupts the placeholder position map and can insert content at the document start.",
7156
+ rationale: "The nullish coalescing operator (??) has lower precedence than arithmetic operators. Writing `pos ?? 0 - 1` evaluates as `pos ?? -1`, not `(pos ?? 0) - 1`. Whenever pos is defined, the expression yields pos instead of the intended pos - 1 \u2014 an off-by-one on every real drop. In ProseMirror drop handlers this misplaces the decoration/insert position, and the -1 fallback (when pos is nullish) is an invalid document position.",
7135
7157
  docsUrl: "https://prosemirror.net/docs/ref/#view.EditorView.posAtCoords",
7136
7158
  recommended: true
7137
7159
  },
@@ -7152,7 +7174,7 @@ var rule80 = {
7152
7174
  };
7153
7175
  }
7154
7176
  };
7155
- var tiptapDropHandlerPosPrecedenceRule = rule80;
7177
+ var tiptapDropHandlerPosPrecedenceRule = rule78;
7156
7178
 
7157
7179
  // src/providers/tiptap/rules/prefer-table-kit.ts
7158
7180
  var INDIVIDUAL_TABLE_PACKAGES = /* @__PURE__ */ new Set([
@@ -7160,18 +7182,18 @@ var INDIVIDUAL_TABLE_PACKAGES = /* @__PURE__ */ new Set([
7160
7182
  "@tiptap/extension-table-cell",
7161
7183
  "@tiptap/extension-table-header"
7162
7184
  ]);
7163
- var rule81 = {
7185
+ var rule79 = {
7164
7186
  meta: {
7165
7187
  type: "suggestion",
7166
7188
  docs: {
7167
7189
  description: "Use TableKit from @tiptap/extension-table instead of individual table packages",
7168
7190
  category: "integration",
7169
- rationale: "Importing table extensions individually from their separate packages bypasses the coordinated wiring that TableKit provides: shared configuration, the mergeOrSplit command, and consistent HTMLAttributes across all table elements. Individual imports also leave TableCell and TableHeader unconfigured by default.",
7191
+ rationale: "Importing table extensions individually from their separate packages bypasses the coordinated wiring that TableKit provides: shared configuration and consistent HTMLAttributes across all table elements, configured in one place. TableKit from @tiptap/extension-table is the documented way to register the full table feature set.",
7170
7192
  docsUrl: "https://tiptap.dev/docs/editor/extensions/functionality/table-kit",
7171
7193
  recommended: true
7172
7194
  },
7173
7195
  messages: {
7174
- preferTableKit: "Individual TipTap table extension imported. Use TableKit from @tiptap/extension-table for coordinated configuration and the mergeOrSplit command."
7196
+ preferTableKit: "Individual TipTap table extension imported. Use TableKit from @tiptap/extension-table to configure all table elements together."
7175
7197
  },
7176
7198
  schema: []
7177
7199
  },
@@ -7194,21 +7216,21 @@ var rule81 = {
7194
7216
  };
7195
7217
  }
7196
7218
  };
7197
- var tiptapPreferTableKitRule = rule81;
7219
+ var tiptapPreferTableKitRule = rule79;
7198
7220
 
7199
7221
  // src/providers/tiptap/rules/tiptap-markdown-missing-node-spec.ts
7200
- var rule82 = {
7222
+ var rule80 = {
7201
7223
  meta: {
7202
7224
  type: "suggestion",
7203
7225
  docs: {
7204
7226
  description: "TipTap nodes used with tiptap-markdown must define a markdown serialization spec",
7205
7227
  category: "integration",
7206
- rationale: "When tiptap-markdown serializes a document, nodes without a markdown spec are silently dropped or emitted as empty strings. Custom node types must register a serialize/parse pair via MarkdownNodeSpec (in addStorage or addOptions) so content survives markdown export/import cycles.",
7207
- docsUrl: "https://github.com/ueberdosis/tiptap-markdown",
7228
+ rationale: "When tiptap-markdown serializes a document, nodes without a markdown spec are silently dropped or emitted as empty strings. Custom node types must register a serialize/parse pair (MarkdownNodeSpec) under a `markdown` key returned from addStorage \u2014 the library reads only extension.storage.markdown \u2014 so content survives markdown export/import cycles.",
7229
+ docsUrl: "https://github.com/aguingand/tiptap-markdown",
7208
7230
  recommended: true
7209
7231
  },
7210
7232
  messages: {
7211
- missingMarkdownNodeSpec: "This TipTap node is used alongside tiptap-markdown but defines no markdown serialization. Nodes without a markdown spec are silently dropped on export. Add a MarkdownNodeSpec to addStorage or addOptions."
7233
+ missingMarkdownNodeSpec: "This TipTap node is used alongside tiptap-markdown but defines no markdown serialization. Nodes without a markdown spec are silently dropped on export. Return a markdown spec (MarkdownNodeSpec) from addStorage."
7212
7234
  },
7213
7235
  schema: []
7214
7236
  },
@@ -7263,7 +7285,7 @@ var rule82 = {
7263
7285
  };
7264
7286
  }
7265
7287
  };
7266
- var tiptapTiptapMarkdownMissingNodeSpecRule = rule82;
7288
+ var tiptapTiptapMarkdownMissingNodeSpecRule = rule80;
7267
7289
 
7268
7290
  // src/providers/elevenlabs/utils.ts
7269
7291
  function isElevenLabsUrlArg(node) {
@@ -7324,7 +7346,7 @@ function posOf(n) {
7324
7346
  }
7325
7347
 
7326
7348
  // src/providers/elevenlabs/rules/validate-signed-url-response.ts
7327
- var rule83 = {
7349
+ var rule81 = {
7328
7350
  meta: {
7329
7351
  type: "problem",
7330
7352
  docs: {
@@ -7423,11 +7445,11 @@ var rule83 = {
7423
7445
  };
7424
7446
  }
7425
7447
  };
7426
- var elevenlabsValidateSignedUrlResponseRule = rule83;
7448
+ var elevenlabsValidateSignedUrlResponseRule = rule81;
7427
7449
 
7428
7450
  // src/providers/elevenlabs/rules/no-error-object-logging.ts
7429
7451
  var LOGGING_METHODS = /* @__PURE__ */ new Set(["error", "warn", "log"]);
7430
- var rule84 = {
7452
+ var rule82 = {
7431
7453
  meta: {
7432
7454
  type: "problem",
7433
7455
  docs: {
@@ -7495,10 +7517,10 @@ var rule84 = {
7495
7517
  };
7496
7518
  }
7497
7519
  };
7498
- var elevenlabsNoErrorObjectLoggingRule = rule84;
7520
+ var elevenlabsNoErrorObjectLoggingRule = rule82;
7499
7521
 
7500
7522
  // src/providers/elevenlabs/rules/fetch-timeout-required.ts
7501
- var rule85 = {
7523
+ var rule83 = {
7502
7524
  meta: {
7503
7525
  type: "suggestion",
7504
7526
  docs: {
@@ -7531,10 +7553,10 @@ var rule85 = {
7531
7553
  };
7532
7554
  }
7533
7555
  };
7534
- var elevenlabsFetchTimeoutRequiredRule = rule85;
7556
+ var elevenlabsFetchTimeoutRequiredRule = rule83;
7535
7557
 
7536
7558
  // src/providers/elevenlabs/rules/validate-agent-id-format.ts
7537
- var rule86 = {
7559
+ var rule84 = {
7538
7560
  meta: {
7539
7561
  type: "problem",
7540
7562
  docs: {
@@ -7640,10 +7662,10 @@ var rule86 = {
7640
7662
  };
7641
7663
  }
7642
7664
  };
7643
- var elevenlabsValidateAgentIdFormatRule = rule86;
7665
+ var elevenlabsValidateAgentIdFormatRule = rule84;
7644
7666
 
7645
7667
  // src/providers/elevenlabs/rules/secure-session-id-generation.ts
7646
- var rule87 = {
7668
+ var rule85 = {
7647
7669
  meta: {
7648
7670
  type: "problem",
7649
7671
  docs: {
@@ -7708,10 +7730,10 @@ var rule87 = {
7708
7730
  };
7709
7731
  }
7710
7732
  };
7711
- var elevenlabsSecureSessionIdGenerationRule = rule87;
7733
+ var elevenlabsSecureSessionIdGenerationRule = rule85;
7712
7734
 
7713
7735
  // src/providers/elevenlabs/rules/conversation-error-recovery.ts
7714
- var rule88 = {
7736
+ var rule86 = {
7715
7737
  meta: {
7716
7738
  type: "suggestion",
7717
7739
  docs: {
@@ -7811,10 +7833,10 @@ var rule88 = {
7811
7833
  };
7812
7834
  }
7813
7835
  };
7814
- var elevenlabsConversationErrorRecoveryRule = rule88;
7836
+ var elevenlabsConversationErrorRecoveryRule = rule86;
7815
7837
 
7816
7838
  // src/providers/elevenlabs/rules/api-version-pinning.ts
7817
- var rule89 = {
7839
+ var rule87 = {
7818
7840
  meta: {
7819
7841
  type: "suggestion",
7820
7842
  docs: {
@@ -7854,10 +7876,10 @@ var rule89 = {
7854
7876
  };
7855
7877
  }
7856
7878
  };
7857
- var elevenlabsApiVersionPinningRule = rule89;
7879
+ var elevenlabsApiVersionPinningRule = rule87;
7858
7880
 
7859
7881
  // src/providers/elevenlabs/rules/check-http-status-before-json.ts
7860
- var rule90 = {
7882
+ var rule88 = {
7861
7883
  meta: {
7862
7884
  type: "problem",
7863
7885
  docs: {
@@ -7953,11 +7975,11 @@ var rule90 = {
7953
7975
  };
7954
7976
  }
7955
7977
  };
7956
- var elevenlabsCheckHttpStatusBeforeJsonRule = rule90;
7978
+ var elevenlabsCheckHttpStatusBeforeJsonRule = rule88;
7957
7979
 
7958
7980
  // src/providers/elevenlabs/rules/conversation-cleanup-on-error.ts
7959
7981
  var END_METHODS = /* @__PURE__ */ new Set(["endSession", "endConversation"]);
7960
- var rule91 = {
7982
+ var rule89 = {
7961
7983
  meta: {
7962
7984
  type: "suggestion",
7963
7985
  docs: {
@@ -8018,11 +8040,11 @@ var rule91 = {
8018
8040
  };
8019
8041
  }
8020
8042
  };
8021
- var elevenlabsConversationCleanupOnErrorRule = rule91;
8043
+ var elevenlabsConversationCleanupOnErrorRule = rule89;
8022
8044
 
8023
8045
  // src/providers/elevenlabs/rules/env-var-validation.ts
8024
8046
  var ELEVENLABS_ENV_VAR_PATTERN = /^(XI_API_KEY|ELEVENLABS_API_KEY)$/;
8025
- var rule92 = {
8047
+ var rule90 = {
8026
8048
  meta: {
8027
8049
  type: "suggestion",
8028
8050
  docs: {
@@ -8083,7 +8105,7 @@ var rule92 = {
8083
8105
  };
8084
8106
  }
8085
8107
  };
8086
- var elevenlabsEnvVarValidationRule = rule92;
8108
+ var elevenlabsEnvVarValidationRule = rule90;
8087
8109
 
8088
8110
  // src/providers/twilio/utils.ts
8089
8111
  var POST_METHOD_NAMES = /* @__PURE__ */ new Set(["post"]);
@@ -8137,7 +8159,7 @@ function collectVarDeclarators2(node, out, depth = 0) {
8137
8159
  }
8138
8160
 
8139
8161
  // src/providers/twilio/rules/validate-webhook-signature.ts
8140
- var rule93 = {
8162
+ var rule91 = {
8141
8163
  meta: {
8142
8164
  type: "problem",
8143
8165
  docs: {
@@ -8179,10 +8201,10 @@ var rule93 = {
8179
8201
  };
8180
8202
  }
8181
8203
  };
8182
- var twilioValidateWebhookSignatureRule = rule93;
8204
+ var twilioValidateWebhookSignatureRule = rule91;
8183
8205
 
8184
8206
  // src/providers/twilio/rules/taskrouter-attributes-match-consumer.ts
8185
- var rule94 = {
8207
+ var rule92 = {
8186
8208
  meta: {
8187
8209
  type: "problem",
8188
8210
  docs: {
@@ -8279,10 +8301,10 @@ var rule94 = {
8279
8301
  };
8280
8302
  }
8281
8303
  };
8282
- var twilioTaskrouterAttributesMatchConsumerRule = rule94;
8304
+ var twilioTaskrouterAttributesMatchConsumerRule = rule92;
8283
8305
 
8284
8306
  // src/providers/twilio/rules/enqueue-task-json-stringify.ts
8285
- var rule95 = {
8307
+ var rule93 = {
8286
8308
  meta: {
8287
8309
  type: "problem",
8288
8310
  docs: {
@@ -8324,10 +8346,10 @@ var rule95 = {
8324
8346
  };
8325
8347
  }
8326
8348
  };
8327
- var twilioEnqueueTaskJsonStringifyRule = rule95;
8349
+ var twilioEnqueueTaskJsonStringifyRule = rule93;
8328
8350
 
8329
8351
  // src/providers/twilio/rules/media-streams-key-by-call-sid.ts
8330
- var rule96 = {
8352
+ var rule94 = {
8331
8353
  meta: {
8332
8354
  type: "problem",
8333
8355
  docs: {
@@ -8403,12 +8425,12 @@ var rule96 = {
8403
8425
  };
8404
8426
  }
8405
8427
  };
8406
- var twilioMediaStreamsKeyByCallSidRule = rule96;
8428
+ var twilioMediaStreamsKeyByCallSidRule = rule94;
8407
8429
 
8408
8430
  // src/providers/twilio/rules/await-or-catch-rest-calls-in-event-handlers.ts
8409
8431
  var REST_RESOURCE_METHODS = /* @__PURE__ */ new Set(["create", "update", "remove", "fetch"]);
8410
8432
  var EVENT_REGISTRATION_METHODS = /* @__PURE__ */ new Set(["onStart", "onStop", "onMedia", "onConnected", "on"]);
8411
- var rule97 = {
8433
+ var rule95 = {
8412
8434
  meta: {
8413
8435
  type: "problem",
8414
8436
  docs: {
@@ -8516,10 +8538,10 @@ var rule97 = {
8516
8538
  };
8517
8539
  }
8518
8540
  };
8519
- var twilioAwaitOrCatchRestCallsInEventHandlersRule = rule97;
8541
+ var twilioAwaitOrCatchRestCallsInEventHandlersRule = rule95;
8520
8542
 
8521
8543
  // src/providers/twilio/rules/use-twiml-builder-not-string-templates.ts
8522
- var rule98 = {
8544
+ var rule96 = {
8523
8545
  meta: {
8524
8546
  type: "suggestion",
8525
8547
  docs: {
@@ -8548,10 +8570,10 @@ var rule98 = {
8548
8570
  };
8549
8571
  }
8550
8572
  };
8551
- var twilioUseTwimlBuilderNotStringTemplatesRule = rule98;
8573
+ var twilioUseTwimlBuilderNotStringTemplatesRule = rule96;
8552
8574
 
8553
8575
  // src/providers/twilio/rules/media-streams-mark-pacing.ts
8554
- var rule99 = {
8576
+ var rule97 = {
8555
8577
  meta: {
8556
8578
  type: "suggestion",
8557
8579
  docs: {
@@ -8627,10 +8649,10 @@ var rule99 = {
8627
8649
  };
8628
8650
  }
8629
8651
  };
8630
- var twilioMediaStreamsMarkPacingRule = rule99;
8652
+ var twilioMediaStreamsMarkPacingRule = rule97;
8631
8653
 
8632
8654
  // src/providers/twilio/rules/validate-all-request-inputs.ts
8633
- var rule100 = {
8655
+ var rule98 = {
8634
8656
  meta: {
8635
8657
  type: "suggestion",
8636
8658
  docs: {
@@ -8705,10 +8727,10 @@ var rule100 = {
8705
8727
  };
8706
8728
  }
8707
8729
  };
8708
- var twilioValidateAllRequestInputsRule = rule100;
8730
+ var twilioValidateAllRequestInputsRule = rule98;
8709
8731
 
8710
8732
  // src/providers/twilio/rules/media-streams-mark-name-string.ts
8711
- var rule101 = {
8733
+ var rule99 = {
8712
8734
  meta: {
8713
8735
  type: "suggestion",
8714
8736
  docs: {
@@ -8758,7 +8780,7 @@ var rule101 = {
8758
8780
  };
8759
8781
  }
8760
8782
  };
8761
- var twilioMediaStreamsMarkNameStringRule = rule101;
8783
+ var twilioMediaStreamsMarkNameStringRule = rule99;
8762
8784
 
8763
8785
  // src/providers/openai-realtime/utils.ts
8764
8786
  function isOpenAIRealtimeUrlArg(node) {
@@ -8898,7 +8920,7 @@ function resolveStringValue(node, stringVarValues) {
8898
8920
  }
8899
8921
 
8900
8922
  // src/providers/openai-realtime/rules/migrate-beta-to-ga.ts
8901
- var rule102 = {
8923
+ var rule100 = {
8902
8924
  meta: {
8903
8925
  type: "problem",
8904
8926
  docs: {
@@ -8935,7 +8957,7 @@ var rule102 = {
8935
8957
  };
8936
8958
  }
8937
8959
  };
8938
- var openaiRealtimeMigrateBetaToGaRule = rule102;
8960
+ var openaiRealtimeMigrateBetaToGaRule = rule100;
8939
8961
 
8940
8962
  // src/providers/openai-realtime/rules/no-log-raw-message-payloads.ts
8941
8963
  var LOG_METHOD_NAMES = /* @__PURE__ */ new Set(["info", "warn", "error", "log"]);
@@ -8971,7 +8993,7 @@ function findCallExpressions(node, out, depth = 0) {
8971
8993
  if (val && typeof val === "object") findCallExpressions(val, out, depth + 1);
8972
8994
  }
8973
8995
  }
8974
- var rule103 = {
8996
+ var rule101 = {
8975
8997
  meta: {
8976
8998
  type: "problem",
8977
8999
  docs: {
@@ -9015,7 +9037,7 @@ var rule103 = {
9015
9037
  };
9016
9038
  }
9017
9039
  };
9018
- var openaiRealtimeNoLogRawMessagePayloadsRule = rule103;
9040
+ var openaiRealtimeNoLogRawMessagePayloadsRule = rule101;
9019
9041
 
9020
9042
  // src/providers/openai-realtime/rules/handle-error-server-event.ts
9021
9043
  function isTypeMemberExpression(node) {
@@ -9046,13 +9068,13 @@ function collectTypeComparisonLiterals(node, out, depth = 0) {
9046
9068
  if (val && typeof val === "object") collectTypeComparisonLiterals(val, out, depth + 1);
9047
9069
  }
9048
9070
  }
9049
- var rule104 = {
9071
+ var rule102 = {
9050
9072
  meta: {
9051
9073
  type: "problem",
9052
9074
  docs: {
9053
9075
  description: 'OpenAI Realtime message handlers must check for the API-level "error" server event',
9054
9076
  category: "reliability",
9055
- docsUrl: "https://developers.openai.com/api/docs/api-reference/realtime_server_events",
9077
+ docsUrl: "https://developers.openai.com/api/reference/resources/realtime/server-events",
9056
9078
  rationale: "The Realtime API's own error server event \u2014 distinct from the WebSocket transport-level error event \u2014 covers rate limits, invalid session.update payloads, content-moderation blocks, and retired/invalid model ids. A message handler that branches on event types but never checks for 'error' lets the call continue silently with translation/processing permanently dead for that leg, with no operator-visible signal of why.",
9057
9079
  recommended: true
9058
9080
  },
@@ -9082,7 +9104,7 @@ var rule104 = {
9082
9104
  };
9083
9105
  }
9084
9106
  };
9085
- var openaiRealtimeHandleErrorServerEventRule = rule104;
9107
+ var openaiRealtimeHandleErrorServerEventRule = rule102;
9086
9108
 
9087
9109
  // src/providers/openai-realtime/rules/reconnect-on-drop.ts
9088
9110
  var RECONNECT_NAME_PATTERN = /reconnect/i;
@@ -9106,7 +9128,7 @@ function hasReconnectAttempt(node, depth = 0) {
9106
9128
  }
9107
9129
  return false;
9108
9130
  }
9109
- var rule105 = {
9131
+ var rule103 = {
9110
9132
  meta: {
9111
9133
  type: "problem",
9112
9134
  docs: {
@@ -9139,17 +9161,17 @@ var rule105 = {
9139
9161
  };
9140
9162
  }
9141
9163
  };
9142
- var openaiRealtimeReconnectOnDropRule = rule105;
9164
+ var openaiRealtimeReconnectOnDropRule = rule103;
9143
9165
 
9144
9166
  // src/providers/openai-realtime/rules/avoid-dated-preview-snapshots.ts
9145
9167
  var DATED_PREVIEW_MODEL_PATTERN = /model=[^&'"`]*-preview-\d{4}-\d{2}-\d{2}/;
9146
- var rule106 = {
9168
+ var rule104 = {
9147
9169
  meta: {
9148
9170
  type: "suggestion",
9149
9171
  docs: {
9150
9172
  description: "OpenAI Realtime connections should not pin to a dated preview model snapshot",
9151
9173
  category: "correctness",
9152
- docsUrl: "https://developers.openai.com/api/docs/api-reference/realtime-sessions",
9174
+ docsUrl: "https://developers.openai.com/api/reference/resources/realtime/subresources/client_secrets",
9153
9175
  rationale: "Dated preview snapshots are the category of model id OpenAI has historically retired with advance-notice sunset windows. Pinning to a dated preview snapshot rather than the floating GA alias (or a current dated GA snapshot) maximizes exposure to an eventual retirement, with no code path to detect or gracefully react to a model_not_found-style error if/when that happens.",
9154
9176
  recommended: true
9155
9177
  },
@@ -9178,16 +9200,16 @@ var rule106 = {
9178
9200
  };
9179
9201
  }
9180
9202
  };
9181
- var openaiRealtimeAvoidDatedPreviewSnapshotsRule = rule106;
9203
+ var openaiRealtimeAvoidDatedPreviewSnapshotsRule = rule104;
9182
9204
 
9183
9205
  // src/providers/openai-realtime/rules/verify-deprecated-session-fields.ts
9184
- var rule107 = {
9206
+ var rule105 = {
9185
9207
  meta: {
9186
9208
  type: "suggestion",
9187
9209
  docs: {
9188
9210
  description: "OpenAI Realtime session.update payloads should not rely on an unverified 'temperature' field",
9189
9211
  category: "correctness",
9190
- docsUrl: "https://developers.openai.com/api/docs/api-reference/realtime-sessions",
9212
+ docsUrl: "https://developers.openai.com/api/reference/resources/realtime/subresources/client_secrets",
9191
9213
  rationale: "Two independent fetches of the current Realtime sessions API reference did not surface 'temperature' as a documented session field. Sending an undocumented field is typically harmless (silently ignored), but code that depends on an assumed constraint (e.g. 'a hard floor of 0.6') for behavior like deterministic output is relying on a claim that is no longer traceable to any current, citable source.",
9192
9214
  recommended: true
9193
9215
  },
@@ -9212,15 +9234,20 @@ var rule107 = {
9212
9234
  };
9213
9235
  }
9214
9236
  };
9215
- var openaiRealtimeVerifyDeprecatedSessionFieldsRule = rule107;
9237
+ var openaiRealtimeVerifyDeprecatedSessionFieldsRule = rule105;
9216
9238
 
9217
9239
  // src/providers/openai-realtime/rules/buffer-audio-until-session-ready.ts
9218
9240
  var QUEUE_CALL_NAME_PATTERN = /^(push|unshift|enqueue|queue)$/i;
9219
- function isReadyStateOpenCheck(test) {
9220
- if (test?.type !== "BinaryExpression" || test.operator !== "===" && test.operator !== "==") return false;
9241
+ function readyStateOpenCheckKind(test) {
9242
+ if (test?.type !== "BinaryExpression") return null;
9243
+ const isEq = test.operator === "===" || test.operator === "==";
9244
+ const isNeq = test.operator === "!==" || test.operator === "!=";
9245
+ if (!isEq && !isNeq) return null;
9221
9246
  const isReadyStateMember = (n) => n?.type === "MemberExpression" && n.property?.type === "Identifier" && n.property.name === "readyState";
9222
9247
  const isOpenValue = (n) => n?.type === "MemberExpression" && n.property?.type === "Identifier" && n.property.name === "OPEN" || n?.type === "Literal" && n.value === 1;
9223
- return isReadyStateMember(test.left) && isOpenValue(test.right) || isReadyStateMember(test.right) && isOpenValue(test.left);
9248
+ const matches = isReadyStateMember(test.left) && isOpenValue(test.right) || isReadyStateMember(test.right) && isOpenValue(test.left);
9249
+ if (!matches) return null;
9250
+ return isEq ? "is-open" : "is-not-open";
9224
9251
  }
9225
9252
  function hasQueueCall(node, depth = 0) {
9226
9253
  if (!node || typeof node !== "object" || depth > 40) return false;
@@ -9237,13 +9264,13 @@ function hasQueueCall(node, depth = 0) {
9237
9264
  }
9238
9265
  return false;
9239
9266
  }
9240
- var rule108 = {
9267
+ var rule106 = {
9241
9268
  meta: {
9242
9269
  type: "suggestion",
9243
9270
  docs: {
9244
9271
  description: "Audio sent before an OpenAI Realtime socket is open must be buffered, not dropped",
9245
9272
  category: "reliability",
9246
- docsUrl: "https://developers.openai.com/api/docs/voice/media-streams/websocket-messages",
9273
+ docsUrl: "https://developers.openai.com/api/reference/resources/realtime/client-events",
9247
9274
  rationale: "Caller audio can arrive before the Realtime WebSocket finishes its connect + session.update round-trip. A readyState !== OPEN branch that only logs and returns silently drops every audio chunk that arrives in that window, meaning the first fragment of speech is lost to translation/processing on every call.",
9248
9275
  recommended: true
9249
9276
  },
@@ -9255,18 +9282,20 @@ var rule108 = {
9255
9282
  create(context) {
9256
9283
  return {
9257
9284
  IfStatement(node) {
9258
- if (!isReadyStateOpenCheck(node.test)) return;
9259
- if (!node.alternate) return;
9260
- if (hasQueueCall(node.alternate)) return;
9261
- context.report({ node: node.alternate, messageId: "audioDroppedNotBuffered" });
9285
+ const kind = readyStateOpenCheckKind(node.test);
9286
+ if (!kind) return;
9287
+ const notOpenBranch = kind === "is-open" ? node.alternate : node.consequent;
9288
+ if (!notOpenBranch) return;
9289
+ if (hasQueueCall(notOpenBranch)) return;
9290
+ context.report({ node: notOpenBranch, messageId: "audioDroppedNotBuffered" });
9262
9291
  }
9263
9292
  };
9264
9293
  }
9265
9294
  };
9266
- var openaiRealtimeBufferAudioUntilSessionReadyRule = rule108;
9295
+ var openaiRealtimeBufferAudioUntilSessionReadyRule = rule106;
9267
9296
 
9268
9297
  // src/providers/openai-realtime/rules/send-safety-identifier.ts
9269
- var rule109 = {
9298
+ var rule107 = {
9270
9299
  meta: {
9271
9300
  type: "suggestion",
9272
9301
  docs: {
@@ -9299,10 +9328,10 @@ var rule109 = {
9299
9328
  };
9300
9329
  }
9301
9330
  };
9302
- var openaiRealtimeSendSafetyIdentifierRule = rule109;
9331
+ var openaiRealtimeSendSafetyIdentifierRule = rule107;
9303
9332
 
9304
9333
  // src/providers/openai-realtime/rules/transcription-model-choice.ts
9305
- var rule110 = {
9334
+ var rule108 = {
9306
9335
  meta: {
9307
9336
  type: "suggestion",
9308
9337
  docs: {
@@ -9313,7 +9342,7 @@ var rule110 = {
9313
9342
  recommended: true
9314
9343
  },
9315
9344
  messages: {
9316
- nonStreamingTranscriptionModel: "input_audio_transcription is configured with 'whisper-1', which is not natively streaming and not optimized for realtime sessions."
9345
+ nonStreamingTranscriptionModel: "This session's input transcription is configured with 'whisper-1', which is not natively streaming and not optimized for realtime sessions."
9317
9346
  },
9318
9347
  schema: []
9319
9348
  },
@@ -9326,7 +9355,12 @@ var rule110 = {
9326
9355
  if (!isSessionUpdate) return;
9327
9356
  const sessionProp = findProperty4(node, "session");
9328
9357
  if (sessionProp?.value?.type !== "ObjectExpression") return;
9329
- const transcriptionProp = findProperty4(sessionProp.value, "input_audio_transcription");
9358
+ let transcriptionProp = findProperty4(sessionProp.value, "input_audio_transcription");
9359
+ if (!transcriptionProp) {
9360
+ const audioProp = findProperty4(sessionProp.value, "audio");
9361
+ const inputProp = audioProp?.value?.type === "ObjectExpression" ? findProperty4(audioProp.value, "input") : null;
9362
+ transcriptionProp = inputProp?.value?.type === "ObjectExpression" ? findProperty4(inputProp.value, "transcription") : null;
9363
+ }
9330
9364
  if (transcriptionProp?.value?.type !== "ObjectExpression") return;
9331
9365
  const modelProp = findProperty4(transcriptionProp.value, "model");
9332
9366
  const modelValue = modelProp?.value;
@@ -9337,7 +9371,7 @@ var rule110 = {
9337
9371
  };
9338
9372
  }
9339
9373
  };
9340
- var openaiRealtimeTranscriptionModelChoiceRule = rule110;
9374
+ var openaiRealtimeTranscriptionModelChoiceRule = rule108;
9341
9375
 
9342
9376
  // src/plugin/index.ts
9343
9377
  var plugin = {
@@ -9382,7 +9416,6 @@ var plugin = {
9382
9416
  "firebase-rtdb-write-promise-not-handled": firebaseRtdbWritePromiseNotHandledRule,
9383
9417
  "firebase-firestore-rules-expired": firebaseFirestoreRulesExpiredRule,
9384
9418
  "firebase-id-token-cookie-flags": firebaseIdTokenCookieFlagsRule,
9385
- "firebase-middleware-token-not-verified": firebaseMiddlewareTokenNotVerifiedRule,
9386
9419
  "firebase-hardcoded-user-id": firebaseHardcodedUserIdRule,
9387
9420
  "firebase-auth-user-not-found-disclosure": firebaseAuthUserNotFoundDisclosureRule,
9388
9421
  "firebase-signup-password-confirm": firebaseSignupPasswordConfirmRule,
@@ -9421,7 +9454,6 @@ var plugin = {
9421
9454
  "tiptap-appendTransaction-add-to-history": tiptapAppendTransactionAddToHistoryRule,
9422
9455
  "tiptap-appendTransaction-full-scan": tiptapAppendTransactionFullScanRule,
9423
9456
  "tiptap-atom-node-wrap-in": tiptapAtomNodeWrapInRule,
9424
- "tiptap-twitter-url-regex": tiptapTwitterUrlRegexRule,
9425
9457
  "tiptap-drop-handler-pos-precedence": tiptapDropHandlerPosPrecedenceRule,
9426
9458
  "tiptap-prefer-table-kit": tiptapPreferTableKitRule,
9427
9459
  "tiptap-tiptap-markdown-missing-node-spec": tiptapTiptapMarkdownMissingNodeSpecRule,
@@ -9459,8 +9491,8 @@ var plugin = {
9459
9491
  // src/plugin/rule-registry.ts
9460
9492
  function buildRegistry() {
9461
9493
  const registry2 = /* @__PURE__ */ new Map();
9462
- for (const [key, rule111] of Object.entries(plugin.rules)) {
9463
- const docs = rule111?.meta?.docs ?? {};
9494
+ for (const [key, rule109] of Object.entries(plugin.rules)) {
9495
+ const docs = rule109?.meta?.docs ?? {};
9464
9496
  registry2.set(key, {
9465
9497
  category: docs.category,
9466
9498
  description: docs.description ?? "",
@@ -9654,9 +9686,10 @@ async function detectProviders(directory, filesContent) {
9654
9686
  // src/scanner.ts
9655
9687
  var SKIP_DIRS = /* @__PURE__ */ new Set(["node_modules", "dist", "build", ".next"]);
9656
9688
  var SOURCE_EXT = /\.(tsx?|jsx?)$/;
9689
+ var NPX_CMD = process.platform === "win32" ? "npx.cmd" : "npx";
9657
9690
  function runOxlint(args, cwd) {
9658
9691
  return new Promise((resolveRun) => {
9659
- const child = (0, import_node_child_process.spawn)("npx", args, { cwd });
9692
+ const child = (0, import_node_child_process.spawn)(NPX_CMD, args, { cwd });
9660
9693
  let stdout = "";
9661
9694
  let stderr = "";
9662
9695
  child.stdout?.on("data", (chunk) => {
@@ -9695,9 +9728,9 @@ function buildOxlintConfig(detectedNames) {
9695
9728
  const ruleMetaByKey = /* @__PURE__ */ new Map();
9696
9729
  for (const provider of providers) {
9697
9730
  if (!detectedNames.has(provider.name)) continue;
9698
- for (const rule111 of provider.oxlintRules) {
9699
- oxlintRules[`${PLUGIN_NAME}/${rule111.key}`] = rule111.severity === "error" || rule111.severity === void 0 ? "error" : "warn";
9700
- ruleMetaByKey.set(rule111.key, rule111);
9731
+ for (const rule109 of provider.oxlintRules) {
9732
+ oxlintRules[`${PLUGIN_NAME}/${rule109.key}`] = rule109.severity === "error" || rule109.severity === void 0 ? "error" : "warn";
9733
+ ruleMetaByKey.set(rule109.key, rule109);
9701
9734
  }
9702
9735
  }
9703
9736
  return { oxlintRules, ruleMetaByKey };
@@ -9815,9 +9848,9 @@ var import_node_path5 = require("path");
9815
9848
  function rationaleByRule() {
9816
9849
  const map = /* @__PURE__ */ new Map();
9817
9850
  for (const provider of providers) {
9818
- for (const rule111 of provider.oxlintRules) {
9819
- const docs = getRuleDocsMeta(rule111.key);
9820
- if (docs?.rationale) map.set(rule111.resultRule, docs.rationale);
9851
+ for (const rule109 of provider.oxlintRules) {
9852
+ const docs = getRuleDocsMeta(rule109.key);
9853
+ if (docs?.rationale) map.set(rule109.resultRule, docs.rationale);
9821
9854
  }
9822
9855
  }
9823
9856
  return map;