@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/plugin.js CHANGED
@@ -11,7 +11,7 @@ var rule = {
11
11
  cwe: "CWE-345",
12
12
  owasp: "API2:2023 Broken Authentication",
13
13
  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.",
14
- docsUrl: "https://resend.com/docs/dashboard/webhooks/introduction#verify-webhook-signatures",
14
+ docsUrl: "https://resend.com/docs/webhooks/verify-webhooks-requests",
15
15
  recommended: true
16
16
  },
17
17
  messages: {
@@ -88,6 +88,15 @@ var rule = {
88
88
  const obj = callee.object;
89
89
  return obj?.type === "Identifier" && (obj.name === "req" || obj.name === "request");
90
90
  }
91
+ function isReqTextCall(n) {
92
+ if (n?.type !== "CallExpression") return false;
93
+ const callee = n.callee;
94
+ if (callee?.type !== "MemberExpression") return false;
95
+ const prop = callee.property;
96
+ if (prop?.type !== "Identifier" || prop.name !== "text") return false;
97
+ const obj = callee.object;
98
+ return obj?.type === "Identifier" && (obj.name === "req" || obj.name === "request");
99
+ }
91
100
  function isBodyMember(n) {
92
101
  if (n?.type !== "MemberExpression") return false;
93
102
  const prop = n.property;
@@ -112,6 +121,17 @@ var rule = {
112
121
  const obj = callee.object;
113
122
  return obj?.type === "Identifier" && svixImports.has(obj.name);
114
123
  }
124
+ function isResendWebhooksVerifyCall(n) {
125
+ if (n?.type !== "CallExpression") return false;
126
+ const callee = n.callee;
127
+ if (callee?.type !== "MemberExpression") return false;
128
+ const prop = callee.property;
129
+ if (prop?.type !== "Identifier" || prop.name !== "verify") return false;
130
+ const obj = callee.object;
131
+ if (obj?.type !== "MemberExpression") return false;
132
+ const webhooksProp = obj.property;
133
+ return webhooksProp?.type === "Identifier" && webhooksProp.name === "webhooks";
134
+ }
115
135
  function recordFirst(posKey, handler, pos) {
116
136
  const existing = handler[posKey];
117
137
  if (!existing) {
@@ -126,6 +146,11 @@ var rule = {
126
146
  ImportDeclaration(node) {
127
147
  const importSource = node?.source?.value;
128
148
  if (importSource === "resend") importsResend = true;
149
+ for (const s of node.specifiers ?? []) {
150
+ if ((s?.type === "ImportSpecifier" || s?.type === "ImportDefaultSpecifier") && s.local?.type === "Identifier" && /^resend$/i.test(s.local.name)) {
151
+ importsResend = true;
152
+ }
153
+ }
129
154
  if (importSource === "svix") {
130
155
  for (const s of node.specifiers ?? []) {
131
156
  if (s?.type === "ImportSpecifier" && s.local?.type === "Identifier") {
@@ -147,8 +172,10 @@ var rule = {
147
172
  for (const handler of postHandlers) {
148
173
  if (!within(handler, node)) continue;
149
174
  if (isReqJsonCall(node)) recordFirst("firstBodyPos", handler, pos);
175
+ if (isReqTextCall(node)) recordFirst("firstRawBodyPos", handler, pos);
150
176
  if (isSvixVerifyCall(node)) recordFirst("firstVerifyPos", handler, pos);
151
177
  if (isCryptoCreateHmacCall(node)) recordFirst("firstVerifyPos", handler, pos);
178
+ if (isResendWebhooksVerifyCall(node)) recordFirst("firstVerifyPos", handler, pos);
152
179
  }
153
180
  },
154
181
  MemberExpression(node) {
@@ -163,6 +190,8 @@ var rule = {
163
190
  "Program:exit"() {
164
191
  if (!importsResend) return;
165
192
  for (const handler of postHandlers) {
193
+ const consumesRequest = handler.firstBodyPos || handler.firstRawBodyPos;
194
+ if (!consumesRequest) continue;
166
195
  if (!handler.firstVerifyPos) {
167
196
  context.report({ node: handler.node, messageId: "missingVerification" });
168
197
  continue;
@@ -734,7 +763,7 @@ var rule11 = {
734
763
  description: "Resend webhook handlers should deduplicate retried events",
735
764
  category: "reliability",
736
765
  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.",
737
- docsUrl: "https://resend.com/docs/dashboard/webhooks/introduction",
766
+ docsUrl: "https://resend.com/docs/webhooks/introduction",
738
767
  recommended: true
739
768
  },
740
769
  messages: {
@@ -1036,12 +1065,12 @@ var rule14 = {
1036
1065
  docs: {
1037
1066
  description: "Supabase queries that select a tenant column must filter by it",
1038
1067
  category: "correctness",
1039
- 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.",
1068
+ 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.",
1040
1069
  docsUrl: "https://supabase.com/docs/reference/javascript/eq",
1041
1070
  recommended: true
1042
1071
  },
1043
1072
  messages: {
1044
- missingTenantFilter: 'This query selects "{{column}}" but never filters by it. Add .eq("{{column}}", ...) (or .match()/.filter()) to scope results to the caller.'
1073
+ 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.`
1045
1074
  },
1046
1075
  schema: []
1047
1076
  },
@@ -1313,7 +1342,8 @@ function objectHasIdempotencyKey(objectExpression) {
1313
1342
  return (objectExpression.properties ?? []).some((p) => {
1314
1343
  if (p?.type !== "Property") return false;
1315
1344
  const name = p.key?.type === "Identifier" ? p.key.name : p.key?.type === "Literal" ? p.key.value : void 0;
1316
- return typeof name === "string" && /idempot|dedupe/i.test(name);
1345
+ if (typeof name !== "string") return false;
1346
+ return /idempot|dedupe/i.test(name) || /^(id|uuid)$/i.test(name) || /_(key|uuid)$/i.test(name);
1317
1347
  });
1318
1348
  }
1319
1349
  function insertPayloadHasIdempotencyKey(arg) {
@@ -1329,12 +1359,12 @@ var rule18 = {
1329
1359
  docs: {
1330
1360
  description: "Supabase insert calls should be retry-safe via an idempotency key",
1331
1361
  category: "reliability",
1332
- 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>" }).',
1362
+ 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.',
1333
1363
  docsUrl: "https://supabase.com/docs/reference/javascript/upsert",
1334
1364
  recommended: true
1335
1365
  },
1336
1366
  messages: {
1337
- 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>" }).'
1367
+ 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>" }).'
1338
1368
  },
1339
1369
  schema: []
1340
1370
  },
@@ -1383,7 +1413,7 @@ var rule19 = {
1383
1413
  docs: {
1384
1414
  description: "createClient must fail fast when required env vars are missing",
1385
1415
  category: "reliability",
1386
- 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.",
1416
+ 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.`,
1387
1417
  docsUrl: "https://supabase.com/docs/reference/javascript/initializing",
1388
1418
  recommended: true
1389
1419
  },
@@ -1393,7 +1423,11 @@ var rule19 = {
1393
1423
  schema: []
1394
1424
  },
1395
1425
  create(context) {
1396
- let createClientLocalName;
1426
+ const FACTORY_IMPORTS = {
1427
+ "@supabase/supabase-js": ["createClient"],
1428
+ "@supabase/ssr": ["createBrowserClient", "createServerClient"]
1429
+ };
1430
+ const factoryLocalNames = /* @__PURE__ */ new Set();
1397
1431
  const envVarOfVariable = /* @__PURE__ */ new Map();
1398
1432
  const validatedVarNames = /* @__PURE__ */ new Set();
1399
1433
  const validatedEnvNames = /* @__PURE__ */ new Set();
@@ -1427,10 +1461,11 @@ var rule19 = {
1427
1461
  }
1428
1462
  return {
1429
1463
  ImportDeclaration(node) {
1430
- if (node.source?.value !== "@supabase/supabase-js") return;
1464
+ const factories = FACTORY_IMPORTS[node.source?.value];
1465
+ if (!factories) return;
1431
1466
  for (const s of node.specifiers ?? []) {
1432
- if (s?.type === "ImportSpecifier" && s.imported?.type === "Identifier" && s.imported.name === "createClient" && s.local?.type === "Identifier") {
1433
- createClientLocalName = s.local.name;
1467
+ if (s?.type === "ImportSpecifier" && s.imported?.type === "Identifier" && factories.includes(s.imported.name) && s.local?.type === "Identifier") {
1468
+ factoryLocalNames.add(s.local.name);
1434
1469
  }
1435
1470
  }
1436
1471
  },
@@ -1444,8 +1479,8 @@ var rule19 = {
1444
1479
  collectGuardTargets(node.test);
1445
1480
  },
1446
1481
  CallExpression(node) {
1447
- if (!createClientLocalName) return;
1448
- if (node.callee?.type !== "Identifier" || node.callee.name !== createClientLocalName) return;
1482
+ if (factoryLocalNames.size === 0) return;
1483
+ if (node.callee?.type !== "Identifier" || !factoryLocalNames.has(node.callee.name)) return;
1449
1484
  const missing = [];
1450
1485
  for (const rawArg of node.arguments ?? []) {
1451
1486
  const arg = unwrapNonNull(rawArg);
@@ -1513,8 +1548,8 @@ var rule20 = {
1513
1548
  category: "security",
1514
1549
  cwe: "CWE-285",
1515
1550
  owasp: "A01:2021 Broken Access Control",
1516
- 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.",
1517
- docsUrl: "https://supabase.com/docs/guides/database/postgres/row-level-security",
1551
+ 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.',
1552
+ docsUrl: "https://supabase.com/docs/guides/auth/users",
1518
1553
  recommended: true
1519
1554
  },
1520
1555
  messages: {
@@ -1550,7 +1585,7 @@ var rule21 = {
1550
1585
  docs: {
1551
1586
  description: "Supabase .single() calls must inspect the returned error field",
1552
1587
  category: "correctness",
1553
- 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.",
1588
+ 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.",
1554
1589
  docsUrl: "https://supabase.com/docs/reference/javascript/single",
1555
1590
  recommended: true
1556
1591
  },
@@ -1560,14 +1595,29 @@ var rule21 = {
1560
1595
  schema: []
1561
1596
  },
1562
1597
  create(context) {
1598
+ const deferredBindings = [];
1599
+ const errorReadNames = /* @__PURE__ */ new Set();
1563
1600
  function checkAwaitBinding(node, pattern, awaitExpr) {
1564
1601
  if (!isSingleSupabaseQuery(awaitExpr.argument)) return;
1602
+ if (chainHasMethod(awaitExpr.argument, "throwOnError")) return;
1603
+ if (pattern?.type === "Identifier") {
1604
+ deferredBindings.push({ node, name: pattern.name });
1605
+ return;
1606
+ }
1565
1607
  const names = destructuredNames(pattern);
1566
1608
  if (names.has("error")) return;
1567
1609
  context.report({ node, messageId: "missingErrorCheck" });
1568
1610
  }
1569
1611
  return {
1612
+ MemberExpression(node) {
1613
+ if (!node.computed && node.object?.type === "Identifier" && node.property?.type === "Identifier" && node.property.name === "error") {
1614
+ errorReadNames.add(node.object.name);
1615
+ }
1616
+ },
1570
1617
  VariableDeclarator(node) {
1618
+ if (node.init?.type === "Identifier" && node.id?.type === "ObjectPattern") {
1619
+ if (destructuredNames(node.id).has("error")) errorReadNames.add(node.init.name);
1620
+ }
1571
1621
  if (node.init?.type !== "AwaitExpression") return;
1572
1622
  checkAwaitBinding(node, node.id, node.init);
1573
1623
  },
@@ -1579,9 +1629,17 @@ var rule21 = {
1579
1629
  const expr = node.expression;
1580
1630
  if (expr?.type !== "AwaitExpression") return;
1581
1631
  if (!isSingleSupabaseQuery(expr.argument)) return;
1632
+ if (chainHasMethod(expr.argument, "throwOnError")) return;
1582
1633
  if (memberPropName(expr.argument) === "single") {
1583
1634
  context.report({ node, messageId: "missingErrorCheck" });
1584
1635
  }
1636
+ },
1637
+ "Program:exit"() {
1638
+ for (const { node, name } of deferredBindings) {
1639
+ if (!errorReadNames.has(name)) {
1640
+ context.report({ node, messageId: "missingErrorCheck" });
1641
+ }
1642
+ }
1585
1643
  }
1586
1644
  };
1587
1645
  }
@@ -1690,7 +1748,7 @@ var rule23 = {
1690
1748
  docs: {
1691
1749
  description: "Supabase mutations must check the returned error field",
1692
1750
  category: "correctness",
1693
- 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.",
1751
+ 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.",
1694
1752
  docsUrl: "https://supabase.com/docs/reference/javascript/insert",
1695
1753
  recommended: true
1696
1754
  },
@@ -1700,30 +1758,52 @@ var rule23 = {
1700
1758
  schema: []
1701
1759
  },
1702
1760
  create(context) {
1761
+ const deferredBindings = [];
1762
+ const errorReadNames = /* @__PURE__ */ new Set();
1703
1763
  function checkMutationAwait(node, pattern, awaitExpr) {
1704
1764
  if (!isSupabaseMutationCall(awaitExpr.argument)) return;
1765
+ if (chainHasMethod(awaitExpr.argument, "throwOnError")) return;
1705
1766
  if (!pattern) {
1706
1767
  context.report({ node, messageId: "uncheckedMutation" });
1707
1768
  return;
1708
1769
  }
1770
+ if (pattern.type === "Identifier") {
1771
+ deferredBindings.push({ node, name: pattern.name });
1772
+ return;
1773
+ }
1709
1774
  const names = destructuredNames(pattern);
1710
1775
  if (!names.has("error")) {
1711
1776
  context.report({ node, messageId: "uncheckedMutation" });
1712
1777
  }
1713
1778
  }
1714
1779
  return {
1780
+ MemberExpression(node) {
1781
+ if (!node.computed && node.object?.type === "Identifier" && node.property?.type === "Identifier" && node.property.name === "error") {
1782
+ errorReadNames.add(node.object.name);
1783
+ }
1784
+ },
1715
1785
  ExpressionStatement(node) {
1716
1786
  const expr = node.expression;
1717
1787
  if (expr?.type !== "AwaitExpression") return;
1718
1788
  checkMutationAwait(node, void 0, expr);
1719
1789
  },
1720
1790
  VariableDeclarator(node) {
1791
+ if (node.init?.type === "Identifier" && node.id?.type === "ObjectPattern") {
1792
+ if (destructuredNames(node.id).has("error")) errorReadNames.add(node.init.name);
1793
+ }
1721
1794
  if (node.init?.type !== "AwaitExpression") return;
1722
1795
  checkMutationAwait(node, node.id, node.init);
1723
1796
  },
1724
1797
  AssignmentExpression(node) {
1725
1798
  if (node.right?.type !== "AwaitExpression") return;
1726
1799
  checkMutationAwait(node, node.left, node.right);
1800
+ },
1801
+ "Program:exit"() {
1802
+ for (const { node, name } of deferredBindings) {
1803
+ if (!errorReadNames.has(name)) {
1804
+ context.report({ node, messageId: "uncheckedMutation" });
1805
+ }
1806
+ }
1727
1807
  }
1728
1808
  };
1729
1809
  }
@@ -1773,11 +1853,20 @@ function isStorageUploadCall(node) {
1773
1853
  let current = node;
1774
1854
  let sawStorage = false;
1775
1855
  let sawUpload = false;
1776
- while (current?.type === "CallExpression") {
1777
- const prop = memberPropName(current);
1778
- if (prop === "storage") sawStorage = true;
1779
- if (prop === "upload") sawUpload = true;
1780
- current = chainObjectCall(current);
1856
+ while (current) {
1857
+ if (current.type === "CallExpression") {
1858
+ const prop = memberPropName(current);
1859
+ if (prop === "storage") sawStorage = true;
1860
+ if (prop === "upload") sawUpload = true;
1861
+ current = current.callee?.object;
1862
+ } else if (current.type === "MemberExpression") {
1863
+ const p = current.property;
1864
+ const name = !current.computed && p?.type === "Identifier" ? p.name : p?.type === "Literal" && typeof p.value === "string" ? p.value : void 0;
1865
+ if (name === "storage") sawStorage = true;
1866
+ current = current.object;
1867
+ } else {
1868
+ break;
1869
+ }
1781
1870
  }
1782
1871
  return sawStorage && sawUpload;
1783
1872
  }
@@ -2358,8 +2447,8 @@ var rule30 = {
2358
2447
  category: "security",
2359
2448
  cwe: "CWE-285",
2360
2449
  owasp: "A04:2021 Insecure Design",
2361
- 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.",
2362
- docsUrl: "https://firebase.google.com/docs/database/web/start",
2450
+ 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.",
2451
+ docsUrl: "https://firebase.google.com/docs/app-check/web/recaptcha-provider",
2363
2452
  recommended: true
2364
2453
  },
2365
2454
  messages: {
@@ -2533,7 +2622,13 @@ function isValidationLikeCall(node) {
2533
2622
  const callee = node.callee;
2534
2623
  const name = callee?.type === "Identifier" ? callee.name : callee?.type === "MemberExpression" && !callee.computed && callee.property?.type === "Identifier" ? callee.property.name : void 0;
2535
2624
  if (!name) return false;
2536
- return /valid|^test$|check|assert|schema/i.test(name);
2625
+ if (/valid|^test$|check|assert|schema/i.test(name)) return true;
2626
+ if (callee?.type === "MemberExpression") {
2627
+ if (/^(safeParse|safeParseAsync|parseAsync)$/.test(name)) return true;
2628
+ const objName = callee.object?.type === "Identifier" ? callee.object.name : void 0;
2629
+ if (name === "parse" && objName !== "JSON") return true;
2630
+ }
2631
+ return false;
2537
2632
  }
2538
2633
  var rule33 = {
2539
2634
  meta: {
@@ -2722,18 +2817,24 @@ var firebaseRtdbListenerErrorNotHandledRule = rule35;
2722
2817
  function isUseEffectCall(node) {
2723
2818
  return node?.type === "CallExpression" && node.callee?.type === "Identifier" && node.callee.name === "useEffect";
2724
2819
  }
2820
+ function isNonUidMemberAccess(node, objName) {
2821
+ if (node?.type !== "MemberExpression") return false;
2822
+ if (node.object?.type !== "Identifier" || node.object.name !== objName) return false;
2823
+ if (node.computed) return true;
2824
+ return node.property?.type === "Identifier" && node.property.name !== "uid";
2825
+ }
2725
2826
  var rule36 = {
2726
2827
  meta: {
2727
2828
  type: "suggestion",
2728
2829
  docs: {
2729
2830
  description: "useEffect should depend on user?.uid, not the whole user object",
2730
2831
  category: "reliability",
2731
- 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.",
2832
+ 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.",
2732
2833
  docsUrl: "https://firebase.google.com/docs/reference/js/auth.md#onauthstatechanged",
2733
2834
  recommended: true
2734
2835
  },
2735
2836
  messages: {
2736
- 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."
2837
+ 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."
2737
2838
  },
2738
2839
  schema: []
2739
2840
  },
@@ -2746,9 +2847,9 @@ var rule36 = {
2746
2847
  for (const el of depsArg.elements ?? []) {
2747
2848
  if (el?.type !== "Identifier") continue;
2748
2849
  const name = el.name;
2749
- if (someDescendant(callback, (n) => isUidMemberAccess(n, name))) {
2750
- context.report({ node, messageId: "wholeUserObjectDep", data: { name } });
2751
- }
2850
+ if (!someDescendant(callback, (n) => isUidMemberAccess(n, name))) continue;
2851
+ if (someDescendant(callback, (n) => isNonUidMemberAccess(n, name))) continue;
2852
+ context.report({ node, messageId: "wholeUserObjectDep", data: { name } });
2752
2853
  }
2753
2854
  }
2754
2855
  };
@@ -2844,13 +2945,13 @@ var rule38 = {
2844
2945
  schema: []
2845
2946
  },
2846
2947
  create(context) {
2847
- const EXPIRED_YEAR = 2025;
2848
2948
  function checkStringForExpiredDate(value, reportNode) {
2849
- const re = /timestamp\.date\(\s*(\d{4})\s*,/g;
2949
+ const re = /timestamp\.date\(\s*(\d{4})\s*,\s*(\d{1,2})\s*,\s*(\d{1,2})\s*\)/g;
2850
2950
  let match;
2851
2951
  while ((match = re.exec(value)) !== null) {
2852
- const year = parseInt(match[1], 10);
2853
- if (year <= EXPIRED_YEAR) {
2952
+ const [, year, month, day] = match;
2953
+ const expiry = new Date(parseInt(year, 10), parseInt(month, 10) - 1, parseInt(day, 10));
2954
+ if (expiry.getTime() <= Date.now()) {
2854
2955
  context.report({ node: reportNode, messageId: "firestoreRulesExpired" });
2855
2956
  return;
2856
2957
  }
@@ -2882,6 +2983,18 @@ function findProp(obj, name) {
2882
2983
  (p) => p?.type === "Property" && (p.key?.type === "Identifier" && p.key.name === name || p.key?.type === "Literal" && p.key.value === name)
2883
2984
  );
2884
2985
  }
2986
+ var TOKEN_NAME = /token|session/i;
2987
+ function isTokenLiteral(node) {
2988
+ return node?.type === "Literal" && typeof node.value === "string" && TOKEN_NAME.test(node.value);
2989
+ }
2990
+ function isCookieReceiver(obj) {
2991
+ if (obj?.type === "Identifier") return /cookie/i.test(obj.name);
2992
+ if (obj?.type === "CallExpression" && obj.callee?.type === "Identifier") return /^cookies$/i.test(obj.callee.name);
2993
+ if (obj?.type === "MemberExpression" && !obj.computed && obj.property?.type === "Identifier") {
2994
+ return /cookie/i.test(obj.property.name);
2995
+ }
2996
+ return false;
2997
+ }
2885
2998
  var rule39 = {
2886
2999
  meta: {
2887
3000
  type: "problem",
@@ -2889,110 +3002,71 @@ var rule39 = {
2889
3002
  description: "Firebase ID token stored in a cookie without httpOnly flag",
2890
3003
  category: "security",
2891
3004
  cwe: "CWE-1004",
2892
- owasp: "A02:2021 Cryptographic Failures",
2893
- 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.",
3005
+ owasp: "A05:2021 Security Misconfiguration",
3006
+ 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.",
2894
3007
  docsUrl: "https://firebase.google.com/docs/auth/admin/manage-cookies",
2895
3008
  recommended: true
2896
3009
  },
2897
3010
  messages: {
2898
- 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."
3011
+ idTokenCookieMissingHttpOnly: "Auth token stored in cookie without httpOnly: true. Any XSS vulnerability can steal the token. Use the Firebase Admin SDK createSessionCookie flow instead."
2899
3012
  },
2900
3013
  schema: []
2901
3014
  },
2902
3015
  create(context) {
2903
- function isTokenName(val) {
2904
- return /token/i.test(val);
2905
- }
2906
3016
  function hasHttpOnlyTrue(optsNode) {
2907
- if (optsNode?.type !== "ObjectExpression") return false;
2908
3017
  const prop = findProp(optsNode, "httpOnly");
2909
- if (!prop) return false;
2910
- return prop.value?.type === "Literal" && prop.value.value === true;
2911
- }
2912
- return {
2913
- CallExpression(node) {
2914
- const callee = node.callee;
2915
- const isCookieSet = callee?.type === "Identifier" && callee.name === "setCookie";
2916
- if (!isCookieSet) return;
2917
- const args = node.arguments ?? [];
2918
- const nameArg = args[0];
2919
- if (nameArg?.type !== "Literal" || typeof nameArg.value !== "string") return;
2920
- if (!isTokenName(nameArg.value)) return;
2921
- const optsArg = args.length >= 3 ? args[2] : null;
2922
- if (!optsArg || optsArg.type !== "ObjectExpression") {
2923
- context.report({ node, messageId: "idTokenCookieMissingHttpOnly" });
2924
- return;
2925
- }
2926
- if (!hasHttpOnlyTrue(optsArg)) {
3018
+ return prop?.value?.type === "Literal" && prop.value.value === true;
3019
+ }
3020
+ function checkNameValueOptsCall(node) {
3021
+ const args = node.arguments ?? [];
3022
+ if (args.length === 1 && args[0]?.type === "ObjectExpression") {
3023
+ const nameProp = findProp(args[0], "name");
3024
+ if (!isTokenLiteral(nameProp?.value)) return;
3025
+ if (!hasHttpOnlyTrue(args[0])) {
2927
3026
  context.report({ node, messageId: "idTokenCookieMissingHttpOnly" });
2928
3027
  }
3028
+ return;
3029
+ }
3030
+ if (!isTokenLiteral(args[0])) return;
3031
+ const optsArg = args.length >= 3 ? args[2] : null;
3032
+ if (!optsArg || optsArg.type !== "ObjectExpression" || !hasHttpOnlyTrue(optsArg)) {
3033
+ context.report({ node, messageId: "idTokenCookieMissingHttpOnly" });
2929
3034
  }
2930
- };
2931
- }
2932
- };
2933
- var firebaseIdTokenCookieFlagsRule = rule39;
2934
-
2935
- // src/providers/firebase/rules/middleware-token-not-verified.ts
2936
- var rule40 = {
2937
- meta: {
2938
- type: "problem",
2939
- docs: {
2940
- description: "Next.js middleware reads auth cookie but never verifies it",
2941
- category: "security",
2942
- cwe: "CWE-345",
2943
- owasp: "A07:2021 Identification and Authentication Failures",
2944
- 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.",
2945
- docsUrl: "https://firebase.google.com/docs/auth/admin/manage-cookies#verify_session_cookies_and_check_permissions",
2946
- recommended: true
2947
- },
2948
- messages: {
2949
- 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."
2950
- },
2951
- schema: []
2952
- },
2953
- create(context) {
2954
- const AUTH_COOKIE_NAMES = /token|session/i;
2955
- const VERIFY_METHOD_NAMES = /* @__PURE__ */ new Set(["verifySessionCookie", "verifyIdToken", "verify"]);
2956
- let readsCookieWithAuthName = false;
2957
- let hasVerifyCall = false;
2958
- const cookieReadNodes = [];
2959
- function isAuthCookieGet(node) {
2960
- if (node?.type !== "CallExpression") return false;
2961
- const callee = node.callee;
2962
- if (callee?.type !== "MemberExpression") return false;
2963
- if (callee.property?.type !== "Identifier" || callee.property.name !== "get") return false;
2964
- const arg = node.arguments?.[0];
2965
- if (arg?.type !== "Literal" || typeof arg.value !== "string") return false;
2966
- return AUTH_COOKIE_NAMES.test(arg.value);
2967
3035
  }
2968
3036
  return {
2969
3037
  CallExpression(node) {
2970
- if (isAuthCookieGet(node)) {
2971
- readsCookieWithAuthName = true;
2972
- cookieReadNodes.push(node);
2973
- }
2974
3038
  const callee = node.callee;
2975
- if (callee?.type === "Identifier" && VERIFY_METHOD_NAMES.has(callee.name)) {
2976
- hasVerifyCall = true;
3039
+ if (callee?.type === "Identifier" && callee.name === "setCookie") {
3040
+ checkNameValueOptsCall(node);
3041
+ return;
3042
+ }
3043
+ if (callee?.type !== "MemberExpression" || callee.computed || callee.property?.type !== "Identifier") return;
3044
+ if (callee.property.name === "cookie") {
3045
+ checkNameValueOptsCall(node);
3046
+ return;
2977
3047
  }
2978
- if (callee?.type === "MemberExpression" && callee.property?.type === "Identifier" && VERIFY_METHOD_NAMES.has(callee.property.name)) {
2979
- hasVerifyCall = true;
3048
+ if (callee.property.name === "set" && isCookieReceiver(callee.object)) {
3049
+ checkNameValueOptsCall(node);
2980
3050
  }
2981
3051
  },
2982
- "Program:exit"() {
2983
- if (readsCookieWithAuthName && !hasVerifyCall) {
2984
- for (const node of cookieReadNodes) {
2985
- context.report({ node, messageId: "tokenNotVerified" });
2986
- }
3052
+ // document.cookie = `token=${idToken}` — can never be httpOnly
3053
+ AssignmentExpression(node) {
3054
+ const left = node.left;
3055
+ const isDocumentCookie = left?.type === "MemberExpression" && !left.computed && left.object?.type === "Identifier" && left.object.name === "document" && left.property?.type === "Identifier" && left.property.name === "cookie";
3056
+ if (!isDocumentCookie) return;
3057
+ const right = node.right;
3058
+ const text = right?.type === "Literal" && typeof right.value === "string" ? right.value : right?.type === "TemplateLiteral" ? (right.quasis ?? []).map((q) => q?.value?.cooked ?? "").join("") : "";
3059
+ if (/(?:^|;\s*)[^=;]*(?:token|session)[^=;]*=/i.test(text)) {
3060
+ context.report({ node, messageId: "idTokenCookieMissingHttpOnly" });
2987
3061
  }
2988
3062
  }
2989
3063
  };
2990
3064
  }
2991
3065
  };
2992
- var firebaseMiddlewareTokenNotVerifiedRule = rule40;
3066
+ var firebaseIdTokenCookieFlagsRule = rule39;
2993
3067
 
2994
3068
  // src/providers/firebase/rules/hardcoded-user-id.ts
2995
- var rule41 = {
3069
+ var rule40 = {
2996
3070
  meta: {
2997
3071
  type: "problem",
2998
3072
  docs: {
@@ -3010,6 +3084,9 @@ var rule41 = {
3010
3084
  schema: []
3011
3085
  },
3012
3086
  create(context) {
3087
+ if (isInsideTestFile2(String(context.filename ?? ""))) {
3088
+ return {};
3089
+ }
3013
3090
  const USER_ID_NAMES = /* @__PURE__ */ new Set(["userId", "uid", "userID", "user_id"]);
3014
3091
  return {
3015
3092
  VariableDeclarator(node) {
@@ -3024,10 +3101,10 @@ var rule41 = {
3024
3101
  };
3025
3102
  }
3026
3103
  };
3027
- var firebaseHardcodedUserIdRule = rule41;
3104
+ var firebaseHardcodedUserIdRule = rule40;
3028
3105
 
3029
3106
  // src/providers/firebase/rules/auth-user-not-found-disclosure.ts
3030
- var rule42 = {
3107
+ var rule41 = {
3031
3108
  meta: {
3032
3109
  type: "suggestion",
3033
3110
  docs: {
@@ -3066,10 +3143,10 @@ var rule42 = {
3066
3143
  };
3067
3144
  }
3068
3145
  };
3069
- var firebaseAuthUserNotFoundDisclosureRule = rule42;
3146
+ var firebaseAuthUserNotFoundDisclosureRule = rule41;
3070
3147
 
3071
3148
  // src/providers/firebase/rules/signup-password-confirm.ts
3072
- var rule43 = {
3149
+ var rule42 = {
3073
3150
  meta: {
3074
3151
  type: "suggestion",
3075
3152
  docs: {
@@ -3103,7 +3180,7 @@ var rule43 = {
3103
3180
  },
3104
3181
  BinaryExpression(node) {
3105
3182
  if (node.operator !== "===" && node.operator !== "==" && node.operator !== "!==" && node.operator !== "!=") return;
3106
- const mentionsConfirm = (n) => n?.type === "Identifier" && n.name === "confirmPassword";
3183
+ const mentionsConfirm = (n) => n?.type === "Identifier" && n.name === "confirmPassword" || n?.type === "MemberExpression" && !n.computed && n.property?.type === "Identifier" && n.property.name === "confirmPassword";
3107
3184
  if (mentionsConfirm(node.left) || mentionsConfirm(node.right)) {
3108
3185
  hasPasswordComparison = true;
3109
3186
  }
@@ -3126,16 +3203,16 @@ var rule43 = {
3126
3203
  };
3127
3204
  }
3128
3205
  };
3129
- var firebaseSignupPasswordConfirmRule = rule43;
3206
+ var firebaseSignupPasswordConfirmRule = rule42;
3130
3207
 
3131
3208
  // src/providers/firebase/rules/use-array-union-remove.ts
3132
- var rule44 = {
3209
+ var rule43 = {
3133
3210
  meta: {
3134
3211
  type: "suggestion",
3135
3212
  docs: {
3136
3213
  description: "Firestore array field updated with read-modify-write spread/filter instead of arrayUnion/arrayRemove",
3137
3214
  category: "correctness",
3138
- 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.",
3215
+ 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.",
3139
3216
  docsUrl: "https://firebase.google.com/docs/firestore/manage-data/add-data#update_elements_in_an_array",
3140
3217
  recommended: true
3141
3218
  },
@@ -3175,10 +3252,10 @@ var rule44 = {
3175
3252
  };
3176
3253
  }
3177
3254
  };
3178
- var firebaseUseArrayUnionRemoveRule = rule44;
3255
+ var firebaseUseArrayUnionRemoveRule = rule43;
3179
3256
 
3180
3257
  // src/providers/firebase/rules/duplicate-initialize-app.ts
3181
- var rule45 = {
3258
+ var rule44 = {
3182
3259
  meta: {
3183
3260
  type: "suggestion",
3184
3261
  docs: {
@@ -3211,9 +3288,11 @@ var rule45 = {
3211
3288
  const callee = node.callee;
3212
3289
  if (callee?.type !== "Identifier") return;
3213
3290
  if (initializeAppLocalName && callee.name === initializeAppLocalName) {
3214
- initializeAppCalls.push(node);
3291
+ if ((node.arguments ?? []).length < 2) {
3292
+ initializeAppCalls.push(node);
3293
+ }
3215
3294
  }
3216
- if (callee.name === "getApps") {
3295
+ if (callee.name === "getApps" || callee.name === "getApp") {
3217
3296
  hasGetAppsCall = true;
3218
3297
  }
3219
3298
  },
@@ -3226,21 +3305,21 @@ var rule45 = {
3226
3305
  };
3227
3306
  }
3228
3307
  };
3229
- var firebaseDuplicateInitializeAppRule = rule45;
3308
+ var firebaseDuplicateInitializeAppRule = rule44;
3230
3309
 
3231
3310
  // src/providers/firebase/rules/onSnapshot-async-throw.ts
3232
- var rule46 = {
3311
+ var rule45 = {
3233
3312
  meta: {
3234
3313
  type: "suggestion",
3235
3314
  docs: {
3236
3315
  description: "throw inside async onSnapshot callback creates an unhandled promise rejection",
3237
3316
  category: "reliability",
3238
- 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.",
3317
+ 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.",
3239
3318
  docsUrl: "https://firebase.google.com/docs/firestore/query-data/listen#handle_listen_errors",
3240
3319
  recommended: true
3241
3320
  },
3242
3321
  messages: {
3243
- 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."
3322
+ 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."
3244
3323
  },
3245
3324
  schema: []
3246
3325
  },
@@ -3280,10 +3359,10 @@ var rule46 = {
3280
3359
  };
3281
3360
  }
3282
3361
  };
3283
- var firebaseOnSnapshotAsyncThrowRule = rule46;
3362
+ var firebaseOnSnapshotAsyncThrowRule = rule45;
3284
3363
 
3285
3364
  // src/providers/firebase/rules/onSnapshot-missing-error-callback.ts
3286
- var rule47 = {
3365
+ var rule46 = {
3287
3366
  meta: {
3288
3367
  type: "suggestion",
3289
3368
  docs: {
@@ -3321,21 +3400,21 @@ var rule47 = {
3321
3400
  };
3322
3401
  }
3323
3402
  };
3324
- var firebaseOnSnapshotMissingErrorCallbackRule = rule47;
3403
+ var firebaseOnSnapshotMissingErrorCallbackRule = rule46;
3325
3404
 
3326
3405
  // src/providers/firebase/rules/firestore-document-size-guard.ts
3327
- var rule48 = {
3406
+ var rule47 = {
3328
3407
  meta: {
3329
3408
  type: "suggestion",
3330
3409
  docs: {
3331
3410
  description: "Firestore write includes editor.getJSON() without a document size guard",
3332
3411
  category: "reliability",
3333
- 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.",
3412
+ 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.",
3334
3413
  docsUrl: "https://firebase.google.com/docs/firestore/quotas#limits",
3335
3414
  recommended: true
3336
3415
  },
3337
3416
  messages: {
3338
- 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."
3417
+ 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."
3339
3418
  },
3340
3419
  schema: []
3341
3420
  },
@@ -3375,21 +3454,21 @@ var rule48 = {
3375
3454
  };
3376
3455
  }
3377
3456
  };
3378
- var firebaseFirestoreDocumentSizeGuardRule = rule48;
3457
+ var firebaseFirestoreDocumentSizeGuardRule = rule47;
3379
3458
 
3380
3459
  // src/providers/firebase/rules/use-timestamp-now.ts
3381
- var rule49 = {
3460
+ var rule48 = {
3382
3461
  meta: {
3383
3462
  type: "suggestion",
3384
3463
  docs: {
3385
- description: "new Date() used for Firestore timestamp instead of Timestamp.now() or serverTimestamp()",
3464
+ description: "new Date() used for Firestore timestamp instead of serverTimestamp()",
3386
3465
  category: "correctness",
3387
- 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.",
3466
+ 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().",
3388
3467
  docsUrl: "https://firebase.google.com/docs/reference/js/firestore_.timestamp",
3389
3468
  recommended: true
3390
3469
  },
3391
3470
  messages: {
3392
- 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."
3471
+ 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."
3393
3472
  },
3394
3473
  schema: []
3395
3474
  },
@@ -3398,7 +3477,7 @@ var rule49 = {
3398
3477
  return {
3399
3478
  ImportDeclaration(node) {
3400
3479
  const src = node.source?.value;
3401
- if (typeof src === "string" && (src.startsWith("firebase/") || src === "firebase-admin/firestore")) {
3480
+ if (typeof src === "string" && (src.startsWith("firebase/firestore") || src === "firebase-admin/firestore")) {
3402
3481
  importsFromFirestore = true;
3403
3482
  }
3404
3483
  },
@@ -3411,7 +3490,7 @@ var rule49 = {
3411
3490
  };
3412
3491
  }
3413
3492
  };
3414
- var firebaseUseTimestampNowRule = rule49;
3493
+ var firebaseUseTimestampNowRule = rule48;
3415
3494
 
3416
3495
  // src/providers/lovable/utils.ts
3417
3496
  var LLM_HOST_PATTERN = /api\.anthropic\.com|api\.openai\.com/;
@@ -3426,7 +3505,7 @@ function containsKnownLlmHost(node) {
3426
3505
  }
3427
3506
 
3428
3507
  // src/providers/lovable/rules/no-client-side-secret-fetch.ts
3429
- var rule50 = {
3508
+ var rule49 = {
3430
3509
  meta: {
3431
3510
  type: "problem",
3432
3511
  docs: {
@@ -3499,13 +3578,13 @@ var rule50 = {
3499
3578
  };
3500
3579
  }
3501
3580
  };
3502
- var lovableNoClientSideSecretFetchRule = rule50;
3581
+ var lovableNoClientSideSecretFetchRule = rule49;
3503
3582
 
3504
3583
  // src/providers/lovable/rules/paid-flag-without-edge-function.ts
3505
3584
  var PRICE_PATTERN = /\$\s?\d/;
3506
3585
  var FLAG_PROPERTY_PATTERN = /^is_[a-z0-9_]+$/i;
3507
3586
  var FLAG_SUFFIX_PATTERN = /_(unlocked|active|enabled)$/i;
3508
- var rule51 = {
3587
+ var rule50 = {
3509
3588
  meta: {
3510
3589
  type: "problem",
3511
3590
  docs: {
@@ -3514,7 +3593,7 @@ var rule51 = {
3514
3593
  cwe: "CWE-840",
3515
3594
  owasp: "A04:2021 \u2013 Insecure Design",
3516
3595
  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.",
3517
- docsUrl: "https://docs.lovable.dev/features/cloud",
3596
+ docsUrl: "https://docs.lovable.dev/integrations/stripe",
3518
3597
  recommended: true
3519
3598
  },
3520
3599
  messages: {
@@ -3647,20 +3726,20 @@ var rule51 = {
3647
3726
  };
3648
3727
  }
3649
3728
  };
3650
- var lovablePaidFlagWithoutEdgeFunctionRule = rule51;
3729
+ var lovablePaidFlagWithoutEdgeFunctionRule = rule50;
3651
3730
 
3652
3731
  // src/providers/lovable/rules/expiry-column-never-checked.ts
3653
3732
  var EXPIRY_COLUMN_PATTERN = /_(until|expires?_at|expiry)$/i;
3654
3733
  var DATE_COMPARISON_OPERATORS = /* @__PURE__ */ new Set([">", "<", ">=", "<="]);
3655
3734
  var FILTER_METHODS = /* @__PURE__ */ new Set(["gt", "gte", "lt", "lte"]);
3656
- var rule52 = {
3735
+ var rule51 = {
3657
3736
  meta: {
3658
3737
  type: "suggestion",
3659
3738
  docs: {
3660
3739
  description: "An expiry column must be checked against the current time somewhere",
3661
3740
  category: "correctness",
3662
3741
  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.",
3663
- docsUrl: "https://docs.lovable.dev/features/cloud",
3742
+ docsUrl: "https://docs.lovable.dev/integrations/stripe",
3664
3743
  recommended: true
3665
3744
  },
3666
3745
  messages: {
@@ -3745,18 +3824,18 @@ var rule52 = {
3745
3824
  };
3746
3825
  }
3747
3826
  };
3748
- var lovableExpiryColumnNeverCheckedRule = rule52;
3827
+ var lovableExpiryColumnNeverCheckedRule = rule51;
3749
3828
 
3750
3829
  // src/providers/lovable/rules/silent-catch-on-provider-call.ts
3751
3830
  var LOGGING_CONSOLE_METHODS = /* @__PURE__ */ new Set(["error", "warn", "log", "info"]);
3752
- var rule53 = {
3831
+ var rule52 = {
3753
3832
  meta: {
3754
3833
  type: "suggestion",
3755
3834
  docs: {
3756
3835
  description: "A catch block around an LLM provider call must log the failure",
3757
3836
  category: "correctness",
3758
3837
  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.`,
3759
- docsUrl: "https://docs.lovable.dev/features/cloud",
3838
+ docsUrl: "https://docs.lovable.dev/integrations/cloud",
3760
3839
  recommended: true
3761
3840
  },
3762
3841
  messages: {
@@ -3822,7 +3901,7 @@ var rule53 = {
3822
3901
  };
3823
3902
  }
3824
3903
  };
3825
- var lovableSilentCatchOnProviderCallRule = rule53;
3904
+ var lovableSilentCatchOnProviderCallRule = rule52;
3826
3905
 
3827
3906
  // src/providers/browserbase/utils.ts
3828
3907
  function memberPropName3(node) {
@@ -3935,7 +4014,7 @@ function isTruthyGateTest(test) {
3935
4014
  }
3936
4015
  return false;
3937
4016
  }
3938
- var rule54 = {
4017
+ var rule53 = {
3939
4018
  meta: {
3940
4019
  type: "problem",
3941
4020
  docs: {
@@ -4007,10 +4086,10 @@ var rule54 = {
4007
4086
  };
4008
4087
  }
4009
4088
  };
4010
- var browserbaseNoConditionalAuthzOnAnonymousUserRule = rule54;
4089
+ var browserbaseNoConditionalAuthzOnAnonymousUserRule = rule53;
4011
4090
 
4012
4091
  // src/providers/browserbase/rules/no-connect-url-in-api-response.ts
4013
- var rule55 = {
4092
+ var rule54 = {
4014
4093
  meta: {
4015
4094
  type: "problem",
4016
4095
  docs: {
@@ -4040,7 +4119,7 @@ var rule55 = {
4040
4119
  };
4041
4120
  }
4042
4121
  };
4043
- var browserbaseNoConnectUrlInApiResponseRule = rule55;
4122
+ var browserbaseNoConnectUrlInApiResponseRule = rule54;
4044
4123
 
4045
4124
  // src/providers/browserbase/rules/session-id-requires-ownership-check.ts
4046
4125
  function isOwnershipCheckCall(node) {
@@ -4057,7 +4136,7 @@ function isSessionIdLikeArg(node) {
4057
4136
  }
4058
4137
  return false;
4059
4138
  }
4060
- var rule56 = {
4139
+ var rule55 = {
4061
4140
  meta: {
4062
4141
  type: "suggestion",
4063
4142
  docs: {
@@ -4065,7 +4144,7 @@ var rule56 = {
4065
4144
  category: "security",
4066
4145
  cwe: "CWE-862",
4067
4146
  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.",
4068
- docsUrl: "https://docs.browserbase.com/features/session-live-view",
4147
+ docsUrl: "https://docs.browserbase.com/platform/browser/observability/session-live-view",
4069
4148
  recommended: true
4070
4149
  },
4071
4150
  messages: {
@@ -4121,7 +4200,7 @@ var rule56 = {
4121
4200
  };
4122
4201
  }
4123
4202
  };
4124
- var browserbaseSessionIdRequiresOwnershipCheckRule = rule56;
4203
+ var browserbaseSessionIdRequiresOwnershipCheckRule = rule55;
4125
4204
 
4126
4205
  // src/providers/browserbase/rules/no-concurrent-shared-context.ts
4127
4206
  function isPromiseAllCall2(node) {
@@ -4178,14 +4257,14 @@ function findSharedContextCreateCall(callback) {
4178
4257
  visit(body);
4179
4258
  return found;
4180
4259
  }
4181
- var rule57 = {
4260
+ var rule56 = {
4182
4261
  meta: {
4183
4262
  type: "problem",
4184
4263
  docs: {
4185
4264
  description: "A Browserbase Context must not be attached to concurrent sessions",
4186
4265
  category: "correctness",
4187
4266
  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.`,
4188
- docsUrl: "https://docs.browserbase.com/features/contexts",
4267
+ docsUrl: "https://docs.browserbase.com/platform/browser/core-features/contexts",
4189
4268
  recommended: true
4190
4269
  },
4191
4270
  messages: {
@@ -4225,7 +4304,7 @@ var rule57 = {
4225
4304
  };
4226
4305
  }
4227
4306
  };
4228
- var browserbaseNoConcurrentSharedContextRule = rule57;
4307
+ var browserbaseNoConcurrentSharedContextRule = rule56;
4229
4308
 
4230
4309
  // src/providers/browserbase/rules/mobile-device-requires-os-setting.ts
4231
4310
  function isMobileLiteral(node) {
@@ -4258,14 +4337,14 @@ function isOsMobileSetting(node) {
4258
4337
  }
4259
4338
  return false;
4260
4339
  }
4261
- var rule58 = {
4340
+ var rule57 = {
4262
4341
  meta: {
4263
4342
  type: "problem",
4264
4343
  docs: {
4265
4344
  description: "Mobile device combos must set browserSettings.os, not just resize the viewport",
4266
4345
  category: "correctness",
4267
4346
  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.`,
4268
- docsUrl: "https://docs.browserbase.com/features/stealth-mode",
4347
+ docsUrl: "https://docs.browserbase.com/platform/identity/verified-customization",
4269
4348
  recommended: true
4270
4349
  },
4271
4350
  messages: {
@@ -4297,7 +4376,7 @@ var rule58 = {
4297
4376
  };
4298
4377
  }
4299
4378
  };
4300
- var browserbaseMobileDeviceRequiresOsSettingRule = rule58;
4379
+ var browserbaseMobileDeviceRequiresOsSettingRule = rule57;
4301
4380
 
4302
4381
  // src/providers/browserbase/rules/use-typed-exception-status-not-substring.ts
4303
4382
  function isStringifiedErrorSource(node, errName) {
@@ -4359,7 +4438,7 @@ function collectStringifiedVars(body, errName) {
4359
4438
  });
4360
4439
  return names;
4361
4440
  }
4362
- var rule59 = {
4441
+ var rule58 = {
4363
4442
  meta: {
4364
4443
  type: "suggestion",
4365
4444
  docs: {
@@ -4389,7 +4468,7 @@ var rule59 = {
4389
4468
  };
4390
4469
  }
4391
4470
  };
4392
- var browserbaseUseTypedExceptionStatusNotSubstringRule = rule59;
4471
+ var browserbaseUseTypedExceptionStatusNotSubstringRule = rule58;
4393
4472
 
4394
4473
  // src/providers/browserbase/rules/release-session-on-connect-failure.ts
4395
4474
  function isSessionsCreateAwait(node) {
@@ -4412,7 +4491,7 @@ function isReleaseCall(node) {
4412
4491
  const name = callee?.type === "Identifier" ? callee.name : memberPropName3(node);
4413
4492
  return !!name && /requeststop|releasesession|release_session/i.test(name);
4414
4493
  }
4415
- var rule60 = {
4494
+ var rule59 = {
4416
4495
  meta: {
4417
4496
  type: "problem",
4418
4497
  docs: {
@@ -4454,10 +4533,10 @@ var rule60 = {
4454
4533
  };
4455
4534
  }
4456
4535
  };
4457
- var browserbaseReleaseSessionOnConnectFailureRule = rule60;
4536
+ var browserbaseReleaseSessionOnConnectFailureRule = rule59;
4458
4537
 
4459
4538
  // src/providers/browserbase/rules/dont-stack-custom-retry-on-sdk-retry.ts
4460
- var rule61 = {
4539
+ var rule60 = {
4461
4540
  meta: {
4462
4541
  type: "suggestion",
4463
4542
  docs: {
@@ -4502,7 +4581,7 @@ var rule61 = {
4502
4581
  };
4503
4582
  }
4504
4583
  };
4505
- var browserbaseDontStackCustomRetryOnSdkRetryRule = rule61;
4584
+ var browserbaseDontStackCustomRetryOnSdkRetryRule = rule60;
4506
4585
 
4507
4586
  // src/providers/browserbase/rules/no-overbroad-error-substring-match.ts
4508
4587
  var OVERBROAD_TERMS = /* @__PURE__ */ new Set(["session", "context", "browser", "browserbase"]);
@@ -4524,7 +4603,7 @@ function isCleanupCall(node) {
4524
4603
  const name = callee?.type === "Identifier" ? callee.name : memberPropName3(node);
4525
4604
  return !!name && /release|remove|cleanup|stop|teardown/i.test(name);
4526
4605
  }
4527
- var rule62 = {
4606
+ var rule61 = {
4528
4607
  meta: {
4529
4608
  type: "problem",
4530
4609
  docs: {
@@ -4554,7 +4633,7 @@ var rule62 = {
4554
4633
  };
4555
4634
  }
4556
4635
  };
4557
- var browserbaseNoOverbroadErrorSubstringMatchRule = rule62;
4636
+ var browserbaseNoOverbroadErrorSubstringMatchRule = rule61;
4558
4637
 
4559
4638
  // src/providers/browserbase/rules/use-sdk-not-raw-requests.ts
4560
4639
  var BROWSERBASE_HOST = "api.browserbase.com";
@@ -4594,7 +4673,7 @@ function isRawHttpCall(node) {
4594
4673
  }
4595
4674
  return { isCall: false, urlArg: void 0 };
4596
4675
  }
4597
- var rule63 = {
4676
+ var rule62 = {
4598
4677
  meta: {
4599
4678
  type: "suggestion",
4600
4679
  docs: {
@@ -4628,10 +4707,10 @@ var rule63 = {
4628
4707
  };
4629
4708
  }
4630
4709
  };
4631
- var browserbaseUseSdkNotRawRequestsRule = rule63;
4710
+ var browserbaseUseSdkNotRawRequestsRule = rule62;
4632
4711
 
4633
4712
  // src/providers/browserbase/rules/centralize-request-release.ts
4634
- var rule64 = {
4713
+ var rule63 = {
4635
4714
  meta: {
4636
4715
  type: "suggestion",
4637
4716
  docs: {
@@ -4664,11 +4743,11 @@ var rule64 = {
4664
4743
  };
4665
4744
  }
4666
4745
  };
4667
- var browserbaseCentralizeRequestReleaseRule = rule64;
4746
+ var browserbaseCentralizeRequestReleaseRule = rule63;
4668
4747
 
4669
4748
  // src/providers/openai-cua/rules/no-domain-allowlist.ts
4670
4749
  var ACTION_METHOD_NAMES = /* @__PURE__ */ new Set(["click", "type", "press", "move", "goto", "fill", "dblclick", "dragAndDrop"]);
4671
- var rule65 = {
4750
+ var rule64 = {
4672
4751
  meta: {
4673
4752
  type: "problem",
4674
4753
  docs: {
@@ -4794,11 +4873,11 @@ var rule65 = {
4794
4873
  };
4795
4874
  }
4796
4875
  };
4797
- var openaiCuaNoDomainAllowlistRule = rule65;
4876
+ var openaiCuaNoDomainAllowlistRule = rule64;
4798
4877
 
4799
4878
  // src/providers/openai-cua/rules/scroll-delta-default-zero.ts
4800
4879
  var VERTICAL_DELTA_NAME_PATTERN = /^(dy|delta_?y|scroll_?y)$/i;
4801
- var rule66 = {
4880
+ var rule65 = {
4802
4881
  meta: {
4803
4882
  type: "problem",
4804
4883
  docs: {
@@ -4874,12 +4953,12 @@ var rule66 = {
4874
4953
  };
4875
4954
  }
4876
4955
  };
4877
- var openaiCuaScrollDeltaDefaultZeroRule = rule66;
4956
+ var openaiCuaScrollDeltaDefaultZeroRule = rule65;
4878
4957
 
4879
4958
  // src/providers/openai-cua/rules/structured-step-metadata-not-text-json.ts
4880
4959
  var BRACE_SEARCH_METHODS = /* @__PURE__ */ new Set(["indexOf", "lastIndexOf"]);
4881
4960
  var SLICE_METHODS = /* @__PURE__ */ new Set(["slice", "substring", "substr"]);
4882
- var rule67 = {
4961
+ var rule66 = {
4883
4962
  meta: {
4884
4963
  type: "suggestion",
4885
4964
  docs: {
@@ -4978,10 +5057,10 @@ var rule67 = {
4978
5057
  };
4979
5058
  }
4980
5059
  };
4981
- var openaiCuaStructuredStepMetadataNotTextJsonRule = rule67;
5060
+ var openaiCuaStructuredStepMetadataNotTextJsonRule = rule66;
4982
5061
 
4983
5062
  // src/providers/openai-cua/rules/no-blind-safety-check-ack.ts
4984
- var rule68 = {
5063
+ var rule67 = {
4985
5064
  meta: {
4986
5065
  type: "suggestion",
4987
5066
  docs: {
@@ -5044,7 +5123,7 @@ var rule68 = {
5044
5123
  };
5045
5124
  }
5046
5125
  };
5047
- var openaiCuaNoBlindSafetyCheckAckRule = rule68;
5126
+ var openaiCuaNoBlindSafetyCheckAckRule = rule67;
5048
5127
 
5049
5128
  // src/providers/openai-cua/utils.ts
5050
5129
  function propName(node) {
@@ -5090,7 +5169,7 @@ function findResponsesCreateCall(node, depth = 0) {
5090
5169
  }
5091
5170
 
5092
5171
  // src/providers/openai-cua/rules/retry-transient-turn-errors.ts
5093
- var rule69 = {
5172
+ var rule68 = {
5094
5173
  meta: {
5095
5174
  type: "problem",
5096
5175
  docs: {
@@ -5172,10 +5251,10 @@ var rule69 = {
5172
5251
  };
5173
5252
  }
5174
5253
  };
5175
- var openaiCuaRetryTransientTurnErrorsRule = rule69;
5254
+ var openaiCuaRetryTransientTurnErrorsRule = rule68;
5176
5255
 
5177
5256
  // src/providers/openai-cua/rules/check-response-status-incomplete.ts
5178
- var rule70 = {
5257
+ var rule69 = {
5179
5258
  meta: {
5180
5259
  type: "problem",
5181
5260
  docs: {
@@ -5284,17 +5363,17 @@ var rule70 = {
5284
5363
  };
5285
5364
  }
5286
5365
  };
5287
- var openaiCuaCheckResponseStatusIncompleteRule = rule70;
5366
+ var openaiCuaCheckResponseStatusIncompleteRule = rule69;
5288
5367
 
5289
5368
  // src/providers/openai-cua/rules/set-safety-identifier.ts
5290
- var rule71 = {
5369
+ var rule70 = {
5291
5370
  meta: {
5292
5371
  type: "problem",
5293
5372
  docs: {
5294
5373
  description: "responses.create() must set safety_identifier for per-end-user policy attribution",
5295
5374
  category: "integration",
5296
5375
  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.",
5297
- docsUrl: "https://help.openai.com/en/articles/5428082-how-to-incorporate-a-safety-identifier",
5376
+ docsUrl: "https://developers.openai.com/api/docs/guides/safety-best-practices",
5298
5377
  recommended: true
5299
5378
  },
5300
5379
  messages: {
@@ -5325,10 +5404,10 @@ var rule71 = {
5325
5404
  };
5326
5405
  }
5327
5406
  };
5328
- var openaiCuaSetSafetyIdentifierRule = rule71;
5407
+ var openaiCuaSetSafetyIdentifierRule = rule70;
5329
5408
 
5330
5409
  // src/providers/tiptap/rules/upload-validate-fn-void.ts
5331
- var rule72 = {
5410
+ var rule71 = {
5332
5411
  meta: {
5333
5412
  type: "problem",
5334
5413
  docs: {
@@ -5392,10 +5471,10 @@ var rule72 = {
5392
5471
  };
5393
5472
  }
5394
5473
  };
5395
- var tiptapUploadValidateFnVoidRule = rule72;
5474
+ var tiptapUploadValidateFnVoidRule = rule71;
5396
5475
 
5397
5476
  // src/providers/tiptap/rules/script-src-hardcoded-api-key.ts
5398
- var rule73 = {
5477
+ var rule72 = {
5399
5478
  meta: {
5400
5479
  type: "problem",
5401
5480
  docs: {
@@ -5453,10 +5532,10 @@ var rule73 = {
5453
5532
  };
5454
5533
  }
5455
5534
  };
5456
- var tiptapScriptSrcHardcodedApiKeyRule = rule73;
5535
+ var tiptapScriptSrcHardcodedApiKeyRule = rule72;
5457
5536
 
5458
5537
  // src/providers/tiptap/rules/dynamic-script-no-sri.ts
5459
- var rule74 = {
5538
+ var rule73 = {
5460
5539
  meta: {
5461
5540
  type: "problem",
5462
5541
  docs: {
@@ -5465,7 +5544,7 @@ var rule74 = {
5465
5544
  cwe: "CWE-829",
5466
5545
  owasp: "API8:2023 Security Misconfiguration",
5467
5546
  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.',
5468
- docsUrl: "https://developer.mozilla.org/en-US/docs/Web/Security/Subresource_Integrity",
5547
+ docsUrl: "https://developer.mozilla.org/en-US/docs/Web/Security/Defenses/Subresource_Integrity",
5469
5548
  recommended: true
5470
5549
  },
5471
5550
  messages: {
@@ -5552,7 +5631,7 @@ var rule74 = {
5552
5631
  };
5553
5632
  }
5554
5633
  };
5555
- var tiptapDynamicScriptNoSriRule = rule74;
5634
+ var tiptapDynamicScriptNoSriRule = rule73;
5556
5635
 
5557
5636
  // src/providers/tiptap/utils.ts
5558
5637
  function findProperty3(objectExpression, name) {
@@ -5577,7 +5656,7 @@ function walk(node, visit) {
5577
5656
  }
5578
5657
 
5579
5658
  // src/providers/tiptap/rules/addAttributes-missing-renderHTML.ts
5580
- var rule75 = {
5659
+ var rule74 = {
5581
5660
  meta: {
5582
5661
  type: "problem",
5583
5662
  docs: {
@@ -5630,10 +5709,10 @@ var rule75 = {
5630
5709
  };
5631
5710
  }
5632
5711
  };
5633
- var tiptapAddAttributesMissingRenderHTMLRule = rule75;
5712
+ var tiptapAddAttributesMissingRenderHTMLRule = rule74;
5634
5713
 
5635
5714
  // src/providers/tiptap/rules/appendTransaction-add-to-history.ts
5636
- var rule76 = {
5715
+ var rule75 = {
5637
5716
  meta: {
5638
5717
  type: "suggestion",
5639
5718
  docs: {
@@ -5712,10 +5791,10 @@ var rule76 = {
5712
5791
  };
5713
5792
  }
5714
5793
  };
5715
- var tiptapAppendTransactionAddToHistoryRule = rule76;
5794
+ var tiptapAppendTransactionAddToHistoryRule = rule75;
5716
5795
 
5717
5796
  // src/providers/tiptap/rules/appendTransaction-full-scan.ts
5718
- var rule77 = {
5797
+ var rule76 = {
5719
5798
  meta: {
5720
5799
  type: "suggestion",
5721
5800
  docs: {
@@ -5775,10 +5854,10 @@ var rule77 = {
5775
5854
  };
5776
5855
  }
5777
5856
  };
5778
- var tiptapAppendTransactionFullScanRule = rule77;
5857
+ var tiptapAppendTransactionFullScanRule = rule76;
5779
5858
 
5780
5859
  // src/providers/tiptap/rules/atom-node-wrap-in.ts
5781
- var rule78 = {
5860
+ var rule77 = {
5782
5861
  meta: {
5783
5862
  type: "problem",
5784
5863
  docs: {
@@ -5841,57 +5920,16 @@ var rule78 = {
5841
5920
  };
5842
5921
  }
5843
5922
  };
5844
- var tiptapAtomNodeWrapInRule = rule78;
5845
-
5846
- // src/providers/tiptap/rules/twitter-url-regex.ts
5847
- var rule79 = {
5848
- meta: {
5849
- type: "suggestion",
5850
- docs: {
5851
- description: "Twitter/X URL regex must match both x.com and twitter.com",
5852
- category: "integration",
5853
- 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.",
5854
- docsUrl: "https://tiptap.dev/docs/editor/extensions/nodes",
5855
- recommended: true
5856
- },
5857
- messages: {
5858
- 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."
5859
- },
5860
- schema: []
5861
- },
5862
- create(context) {
5863
- function checkPattern(pattern, node) {
5864
- if (!pattern.includes("x\\.com") && !pattern.includes("x.com")) return;
5865
- if (pattern.includes("twitter\\.com") || pattern.includes("twitter.com")) return;
5866
- context.report({ node, messageId: "twitterRegexMissingLegacyDomain" });
5867
- }
5868
- return {
5869
- Literal(node) {
5870
- if (node.regex?.pattern) {
5871
- checkPattern(node.regex.pattern, node);
5872
- }
5873
- },
5874
- NewExpression(node) {
5875
- if (node.callee?.type === "Identifier" && node.callee.name === "RegExp") {
5876
- const firstArg = node.arguments?.[0];
5877
- if (firstArg?.type === "Literal" && typeof firstArg.value === "string") {
5878
- checkPattern(firstArg.value, node);
5879
- }
5880
- }
5881
- }
5882
- };
5883
- }
5884
- };
5885
- var tiptapTwitterUrlRegexRule = rule79;
5923
+ var tiptapAtomNodeWrapInRule = rule77;
5886
5924
 
5887
5925
  // src/providers/tiptap/rules/drop-handler-pos-precedence.ts
5888
- var rule80 = {
5926
+ var rule78 = {
5889
5927
  meta: {
5890
5928
  type: "problem",
5891
5929
  docs: {
5892
5930
  description: "x ?? 0 - 1 is parsed as x ?? (0 - 1) = x ?? -1 due to operator precedence",
5893
5931
  category: "correctness",
5894
- 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.",
5932
+ 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.",
5895
5933
  docsUrl: "https://prosemirror.net/docs/ref/#view.EditorView.posAtCoords",
5896
5934
  recommended: true
5897
5935
  },
@@ -5912,7 +5950,7 @@ var rule80 = {
5912
5950
  };
5913
5951
  }
5914
5952
  };
5915
- var tiptapDropHandlerPosPrecedenceRule = rule80;
5953
+ var tiptapDropHandlerPosPrecedenceRule = rule78;
5916
5954
 
5917
5955
  // src/providers/tiptap/rules/prefer-table-kit.ts
5918
5956
  var INDIVIDUAL_TABLE_PACKAGES = /* @__PURE__ */ new Set([
@@ -5920,18 +5958,18 @@ var INDIVIDUAL_TABLE_PACKAGES = /* @__PURE__ */ new Set([
5920
5958
  "@tiptap/extension-table-cell",
5921
5959
  "@tiptap/extension-table-header"
5922
5960
  ]);
5923
- var rule81 = {
5961
+ var rule79 = {
5924
5962
  meta: {
5925
5963
  type: "suggestion",
5926
5964
  docs: {
5927
5965
  description: "Use TableKit from @tiptap/extension-table instead of individual table packages",
5928
5966
  category: "integration",
5929
- 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.",
5967
+ 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.",
5930
5968
  docsUrl: "https://tiptap.dev/docs/editor/extensions/functionality/table-kit",
5931
5969
  recommended: true
5932
5970
  },
5933
5971
  messages: {
5934
- preferTableKit: "Individual TipTap table extension imported. Use TableKit from @tiptap/extension-table for coordinated configuration and the mergeOrSplit command."
5972
+ preferTableKit: "Individual TipTap table extension imported. Use TableKit from @tiptap/extension-table to configure all table elements together."
5935
5973
  },
5936
5974
  schema: []
5937
5975
  },
@@ -5954,21 +5992,21 @@ var rule81 = {
5954
5992
  };
5955
5993
  }
5956
5994
  };
5957
- var tiptapPreferTableKitRule = rule81;
5995
+ var tiptapPreferTableKitRule = rule79;
5958
5996
 
5959
5997
  // src/providers/tiptap/rules/tiptap-markdown-missing-node-spec.ts
5960
- var rule82 = {
5998
+ var rule80 = {
5961
5999
  meta: {
5962
6000
  type: "suggestion",
5963
6001
  docs: {
5964
6002
  description: "TipTap nodes used with tiptap-markdown must define a markdown serialization spec",
5965
6003
  category: "integration",
5966
- 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.",
5967
- docsUrl: "https://github.com/ueberdosis/tiptap-markdown",
6004
+ 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.",
6005
+ docsUrl: "https://github.com/aguingand/tiptap-markdown",
5968
6006
  recommended: true
5969
6007
  },
5970
6008
  messages: {
5971
- 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."
6009
+ 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."
5972
6010
  },
5973
6011
  schema: []
5974
6012
  },
@@ -6023,7 +6061,7 @@ var rule82 = {
6023
6061
  };
6024
6062
  }
6025
6063
  };
6026
- var tiptapTiptapMarkdownMissingNodeSpecRule = rule82;
6064
+ var tiptapTiptapMarkdownMissingNodeSpecRule = rule80;
6027
6065
 
6028
6066
  // src/providers/elevenlabs/utils.ts
6029
6067
  function isElevenLabsUrlArg(node) {
@@ -6084,7 +6122,7 @@ function posOf(n) {
6084
6122
  }
6085
6123
 
6086
6124
  // src/providers/elevenlabs/rules/validate-signed-url-response.ts
6087
- var rule83 = {
6125
+ var rule81 = {
6088
6126
  meta: {
6089
6127
  type: "problem",
6090
6128
  docs: {
@@ -6183,11 +6221,11 @@ var rule83 = {
6183
6221
  };
6184
6222
  }
6185
6223
  };
6186
- var elevenlabsValidateSignedUrlResponseRule = rule83;
6224
+ var elevenlabsValidateSignedUrlResponseRule = rule81;
6187
6225
 
6188
6226
  // src/providers/elevenlabs/rules/no-error-object-logging.ts
6189
6227
  var LOGGING_METHODS = /* @__PURE__ */ new Set(["error", "warn", "log"]);
6190
- var rule84 = {
6228
+ var rule82 = {
6191
6229
  meta: {
6192
6230
  type: "problem",
6193
6231
  docs: {
@@ -6255,10 +6293,10 @@ var rule84 = {
6255
6293
  };
6256
6294
  }
6257
6295
  };
6258
- var elevenlabsNoErrorObjectLoggingRule = rule84;
6296
+ var elevenlabsNoErrorObjectLoggingRule = rule82;
6259
6297
 
6260
6298
  // src/providers/elevenlabs/rules/fetch-timeout-required.ts
6261
- var rule85 = {
6299
+ var rule83 = {
6262
6300
  meta: {
6263
6301
  type: "suggestion",
6264
6302
  docs: {
@@ -6291,10 +6329,10 @@ var rule85 = {
6291
6329
  };
6292
6330
  }
6293
6331
  };
6294
- var elevenlabsFetchTimeoutRequiredRule = rule85;
6332
+ var elevenlabsFetchTimeoutRequiredRule = rule83;
6295
6333
 
6296
6334
  // src/providers/elevenlabs/rules/validate-agent-id-format.ts
6297
- var rule86 = {
6335
+ var rule84 = {
6298
6336
  meta: {
6299
6337
  type: "problem",
6300
6338
  docs: {
@@ -6400,10 +6438,10 @@ var rule86 = {
6400
6438
  };
6401
6439
  }
6402
6440
  };
6403
- var elevenlabsValidateAgentIdFormatRule = rule86;
6441
+ var elevenlabsValidateAgentIdFormatRule = rule84;
6404
6442
 
6405
6443
  // src/providers/elevenlabs/rules/secure-session-id-generation.ts
6406
- var rule87 = {
6444
+ var rule85 = {
6407
6445
  meta: {
6408
6446
  type: "problem",
6409
6447
  docs: {
@@ -6468,10 +6506,10 @@ var rule87 = {
6468
6506
  };
6469
6507
  }
6470
6508
  };
6471
- var elevenlabsSecureSessionIdGenerationRule = rule87;
6509
+ var elevenlabsSecureSessionIdGenerationRule = rule85;
6472
6510
 
6473
6511
  // src/providers/elevenlabs/rules/conversation-error-recovery.ts
6474
- var rule88 = {
6512
+ var rule86 = {
6475
6513
  meta: {
6476
6514
  type: "suggestion",
6477
6515
  docs: {
@@ -6571,10 +6609,10 @@ var rule88 = {
6571
6609
  };
6572
6610
  }
6573
6611
  };
6574
- var elevenlabsConversationErrorRecoveryRule = rule88;
6612
+ var elevenlabsConversationErrorRecoveryRule = rule86;
6575
6613
 
6576
6614
  // src/providers/elevenlabs/rules/api-version-pinning.ts
6577
- var rule89 = {
6615
+ var rule87 = {
6578
6616
  meta: {
6579
6617
  type: "suggestion",
6580
6618
  docs: {
@@ -6614,10 +6652,10 @@ var rule89 = {
6614
6652
  };
6615
6653
  }
6616
6654
  };
6617
- var elevenlabsApiVersionPinningRule = rule89;
6655
+ var elevenlabsApiVersionPinningRule = rule87;
6618
6656
 
6619
6657
  // src/providers/elevenlabs/rules/check-http-status-before-json.ts
6620
- var rule90 = {
6658
+ var rule88 = {
6621
6659
  meta: {
6622
6660
  type: "problem",
6623
6661
  docs: {
@@ -6713,11 +6751,11 @@ var rule90 = {
6713
6751
  };
6714
6752
  }
6715
6753
  };
6716
- var elevenlabsCheckHttpStatusBeforeJsonRule = rule90;
6754
+ var elevenlabsCheckHttpStatusBeforeJsonRule = rule88;
6717
6755
 
6718
6756
  // src/providers/elevenlabs/rules/conversation-cleanup-on-error.ts
6719
6757
  var END_METHODS = /* @__PURE__ */ new Set(["endSession", "endConversation"]);
6720
- var rule91 = {
6758
+ var rule89 = {
6721
6759
  meta: {
6722
6760
  type: "suggestion",
6723
6761
  docs: {
@@ -6778,11 +6816,11 @@ var rule91 = {
6778
6816
  };
6779
6817
  }
6780
6818
  };
6781
- var elevenlabsConversationCleanupOnErrorRule = rule91;
6819
+ var elevenlabsConversationCleanupOnErrorRule = rule89;
6782
6820
 
6783
6821
  // src/providers/elevenlabs/rules/env-var-validation.ts
6784
6822
  var ELEVENLABS_ENV_VAR_PATTERN = /^(XI_API_KEY|ELEVENLABS_API_KEY)$/;
6785
- var rule92 = {
6823
+ var rule90 = {
6786
6824
  meta: {
6787
6825
  type: "suggestion",
6788
6826
  docs: {
@@ -6843,7 +6881,7 @@ var rule92 = {
6843
6881
  };
6844
6882
  }
6845
6883
  };
6846
- var elevenlabsEnvVarValidationRule = rule92;
6884
+ var elevenlabsEnvVarValidationRule = rule90;
6847
6885
 
6848
6886
  // src/providers/twilio/utils.ts
6849
6887
  var POST_METHOD_NAMES = /* @__PURE__ */ new Set(["post"]);
@@ -6897,7 +6935,7 @@ function collectVarDeclarators2(node, out, depth = 0) {
6897
6935
  }
6898
6936
 
6899
6937
  // src/providers/twilio/rules/validate-webhook-signature.ts
6900
- var rule93 = {
6938
+ var rule91 = {
6901
6939
  meta: {
6902
6940
  type: "problem",
6903
6941
  docs: {
@@ -6939,10 +6977,10 @@ var rule93 = {
6939
6977
  };
6940
6978
  }
6941
6979
  };
6942
- var twilioValidateWebhookSignatureRule = rule93;
6980
+ var twilioValidateWebhookSignatureRule = rule91;
6943
6981
 
6944
6982
  // src/providers/twilio/rules/taskrouter-attributes-match-consumer.ts
6945
- var rule94 = {
6983
+ var rule92 = {
6946
6984
  meta: {
6947
6985
  type: "problem",
6948
6986
  docs: {
@@ -7039,10 +7077,10 @@ var rule94 = {
7039
7077
  };
7040
7078
  }
7041
7079
  };
7042
- var twilioTaskrouterAttributesMatchConsumerRule = rule94;
7080
+ var twilioTaskrouterAttributesMatchConsumerRule = rule92;
7043
7081
 
7044
7082
  // src/providers/twilio/rules/enqueue-task-json-stringify.ts
7045
- var rule95 = {
7083
+ var rule93 = {
7046
7084
  meta: {
7047
7085
  type: "problem",
7048
7086
  docs: {
@@ -7084,10 +7122,10 @@ var rule95 = {
7084
7122
  };
7085
7123
  }
7086
7124
  };
7087
- var twilioEnqueueTaskJsonStringifyRule = rule95;
7125
+ var twilioEnqueueTaskJsonStringifyRule = rule93;
7088
7126
 
7089
7127
  // src/providers/twilio/rules/media-streams-key-by-call-sid.ts
7090
- var rule96 = {
7128
+ var rule94 = {
7091
7129
  meta: {
7092
7130
  type: "problem",
7093
7131
  docs: {
@@ -7163,12 +7201,12 @@ var rule96 = {
7163
7201
  };
7164
7202
  }
7165
7203
  };
7166
- var twilioMediaStreamsKeyByCallSidRule = rule96;
7204
+ var twilioMediaStreamsKeyByCallSidRule = rule94;
7167
7205
 
7168
7206
  // src/providers/twilio/rules/await-or-catch-rest-calls-in-event-handlers.ts
7169
7207
  var REST_RESOURCE_METHODS = /* @__PURE__ */ new Set(["create", "update", "remove", "fetch"]);
7170
7208
  var EVENT_REGISTRATION_METHODS = /* @__PURE__ */ new Set(["onStart", "onStop", "onMedia", "onConnected", "on"]);
7171
- var rule97 = {
7209
+ var rule95 = {
7172
7210
  meta: {
7173
7211
  type: "problem",
7174
7212
  docs: {
@@ -7276,10 +7314,10 @@ var rule97 = {
7276
7314
  };
7277
7315
  }
7278
7316
  };
7279
- var twilioAwaitOrCatchRestCallsInEventHandlersRule = rule97;
7317
+ var twilioAwaitOrCatchRestCallsInEventHandlersRule = rule95;
7280
7318
 
7281
7319
  // src/providers/twilio/rules/use-twiml-builder-not-string-templates.ts
7282
- var rule98 = {
7320
+ var rule96 = {
7283
7321
  meta: {
7284
7322
  type: "suggestion",
7285
7323
  docs: {
@@ -7308,10 +7346,10 @@ var rule98 = {
7308
7346
  };
7309
7347
  }
7310
7348
  };
7311
- var twilioUseTwimlBuilderNotStringTemplatesRule = rule98;
7349
+ var twilioUseTwimlBuilderNotStringTemplatesRule = rule96;
7312
7350
 
7313
7351
  // src/providers/twilio/rules/media-streams-mark-pacing.ts
7314
- var rule99 = {
7352
+ var rule97 = {
7315
7353
  meta: {
7316
7354
  type: "suggestion",
7317
7355
  docs: {
@@ -7387,10 +7425,10 @@ var rule99 = {
7387
7425
  };
7388
7426
  }
7389
7427
  };
7390
- var twilioMediaStreamsMarkPacingRule = rule99;
7428
+ var twilioMediaStreamsMarkPacingRule = rule97;
7391
7429
 
7392
7430
  // src/providers/twilio/rules/validate-all-request-inputs.ts
7393
- var rule100 = {
7431
+ var rule98 = {
7394
7432
  meta: {
7395
7433
  type: "suggestion",
7396
7434
  docs: {
@@ -7465,10 +7503,10 @@ var rule100 = {
7465
7503
  };
7466
7504
  }
7467
7505
  };
7468
- var twilioValidateAllRequestInputsRule = rule100;
7506
+ var twilioValidateAllRequestInputsRule = rule98;
7469
7507
 
7470
7508
  // src/providers/twilio/rules/media-streams-mark-name-string.ts
7471
- var rule101 = {
7509
+ var rule99 = {
7472
7510
  meta: {
7473
7511
  type: "suggestion",
7474
7512
  docs: {
@@ -7518,7 +7556,7 @@ var rule101 = {
7518
7556
  };
7519
7557
  }
7520
7558
  };
7521
- var twilioMediaStreamsMarkNameStringRule = rule101;
7559
+ var twilioMediaStreamsMarkNameStringRule = rule99;
7522
7560
 
7523
7561
  // src/providers/openai-realtime/utils.ts
7524
7562
  function isOpenAIRealtimeUrlArg(node) {
@@ -7658,7 +7696,7 @@ function resolveStringValue(node, stringVarValues) {
7658
7696
  }
7659
7697
 
7660
7698
  // src/providers/openai-realtime/rules/migrate-beta-to-ga.ts
7661
- var rule102 = {
7699
+ var rule100 = {
7662
7700
  meta: {
7663
7701
  type: "problem",
7664
7702
  docs: {
@@ -7695,7 +7733,7 @@ var rule102 = {
7695
7733
  };
7696
7734
  }
7697
7735
  };
7698
- var openaiRealtimeMigrateBetaToGaRule = rule102;
7736
+ var openaiRealtimeMigrateBetaToGaRule = rule100;
7699
7737
 
7700
7738
  // src/providers/openai-realtime/rules/no-log-raw-message-payloads.ts
7701
7739
  var LOG_METHOD_NAMES = /* @__PURE__ */ new Set(["info", "warn", "error", "log"]);
@@ -7731,7 +7769,7 @@ function findCallExpressions(node, out, depth = 0) {
7731
7769
  if (val && typeof val === "object") findCallExpressions(val, out, depth + 1);
7732
7770
  }
7733
7771
  }
7734
- var rule103 = {
7772
+ var rule101 = {
7735
7773
  meta: {
7736
7774
  type: "problem",
7737
7775
  docs: {
@@ -7775,7 +7813,7 @@ var rule103 = {
7775
7813
  };
7776
7814
  }
7777
7815
  };
7778
- var openaiRealtimeNoLogRawMessagePayloadsRule = rule103;
7816
+ var openaiRealtimeNoLogRawMessagePayloadsRule = rule101;
7779
7817
 
7780
7818
  // src/providers/openai-realtime/rules/handle-error-server-event.ts
7781
7819
  function isTypeMemberExpression(node) {
@@ -7806,13 +7844,13 @@ function collectTypeComparisonLiterals(node, out, depth = 0) {
7806
7844
  if (val && typeof val === "object") collectTypeComparisonLiterals(val, out, depth + 1);
7807
7845
  }
7808
7846
  }
7809
- var rule104 = {
7847
+ var rule102 = {
7810
7848
  meta: {
7811
7849
  type: "problem",
7812
7850
  docs: {
7813
7851
  description: 'OpenAI Realtime message handlers must check for the API-level "error" server event',
7814
7852
  category: "reliability",
7815
- docsUrl: "https://developers.openai.com/api/docs/api-reference/realtime_server_events",
7853
+ docsUrl: "https://developers.openai.com/api/reference/resources/realtime/server-events",
7816
7854
  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.",
7817
7855
  recommended: true
7818
7856
  },
@@ -7842,7 +7880,7 @@ var rule104 = {
7842
7880
  };
7843
7881
  }
7844
7882
  };
7845
- var openaiRealtimeHandleErrorServerEventRule = rule104;
7883
+ var openaiRealtimeHandleErrorServerEventRule = rule102;
7846
7884
 
7847
7885
  // src/providers/openai-realtime/rules/reconnect-on-drop.ts
7848
7886
  var RECONNECT_NAME_PATTERN = /reconnect/i;
@@ -7866,7 +7904,7 @@ function hasReconnectAttempt(node, depth = 0) {
7866
7904
  }
7867
7905
  return false;
7868
7906
  }
7869
- var rule105 = {
7907
+ var rule103 = {
7870
7908
  meta: {
7871
7909
  type: "problem",
7872
7910
  docs: {
@@ -7899,17 +7937,17 @@ var rule105 = {
7899
7937
  };
7900
7938
  }
7901
7939
  };
7902
- var openaiRealtimeReconnectOnDropRule = rule105;
7940
+ var openaiRealtimeReconnectOnDropRule = rule103;
7903
7941
 
7904
7942
  // src/providers/openai-realtime/rules/avoid-dated-preview-snapshots.ts
7905
7943
  var DATED_PREVIEW_MODEL_PATTERN = /model=[^&'"`]*-preview-\d{4}-\d{2}-\d{2}/;
7906
- var rule106 = {
7944
+ var rule104 = {
7907
7945
  meta: {
7908
7946
  type: "suggestion",
7909
7947
  docs: {
7910
7948
  description: "OpenAI Realtime connections should not pin to a dated preview model snapshot",
7911
7949
  category: "correctness",
7912
- docsUrl: "https://developers.openai.com/api/docs/api-reference/realtime-sessions",
7950
+ docsUrl: "https://developers.openai.com/api/reference/resources/realtime/subresources/client_secrets",
7913
7951
  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.",
7914
7952
  recommended: true
7915
7953
  },
@@ -7938,16 +7976,16 @@ var rule106 = {
7938
7976
  };
7939
7977
  }
7940
7978
  };
7941
- var openaiRealtimeAvoidDatedPreviewSnapshotsRule = rule106;
7979
+ var openaiRealtimeAvoidDatedPreviewSnapshotsRule = rule104;
7942
7980
 
7943
7981
  // src/providers/openai-realtime/rules/verify-deprecated-session-fields.ts
7944
- var rule107 = {
7982
+ var rule105 = {
7945
7983
  meta: {
7946
7984
  type: "suggestion",
7947
7985
  docs: {
7948
7986
  description: "OpenAI Realtime session.update payloads should not rely on an unverified 'temperature' field",
7949
7987
  category: "correctness",
7950
- docsUrl: "https://developers.openai.com/api/docs/api-reference/realtime-sessions",
7988
+ docsUrl: "https://developers.openai.com/api/reference/resources/realtime/subresources/client_secrets",
7951
7989
  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.",
7952
7990
  recommended: true
7953
7991
  },
@@ -7972,15 +8010,20 @@ var rule107 = {
7972
8010
  };
7973
8011
  }
7974
8012
  };
7975
- var openaiRealtimeVerifyDeprecatedSessionFieldsRule = rule107;
8013
+ var openaiRealtimeVerifyDeprecatedSessionFieldsRule = rule105;
7976
8014
 
7977
8015
  // src/providers/openai-realtime/rules/buffer-audio-until-session-ready.ts
7978
8016
  var QUEUE_CALL_NAME_PATTERN = /^(push|unshift|enqueue|queue)$/i;
7979
- function isReadyStateOpenCheck(test) {
7980
- if (test?.type !== "BinaryExpression" || test.operator !== "===" && test.operator !== "==") return false;
8017
+ function readyStateOpenCheckKind(test) {
8018
+ if (test?.type !== "BinaryExpression") return null;
8019
+ const isEq = test.operator === "===" || test.operator === "==";
8020
+ const isNeq = test.operator === "!==" || test.operator === "!=";
8021
+ if (!isEq && !isNeq) return null;
7981
8022
  const isReadyStateMember = (n) => n?.type === "MemberExpression" && n.property?.type === "Identifier" && n.property.name === "readyState";
7982
8023
  const isOpenValue = (n) => n?.type === "MemberExpression" && n.property?.type === "Identifier" && n.property.name === "OPEN" || n?.type === "Literal" && n.value === 1;
7983
- return isReadyStateMember(test.left) && isOpenValue(test.right) || isReadyStateMember(test.right) && isOpenValue(test.left);
8024
+ const matches = isReadyStateMember(test.left) && isOpenValue(test.right) || isReadyStateMember(test.right) && isOpenValue(test.left);
8025
+ if (!matches) return null;
8026
+ return isEq ? "is-open" : "is-not-open";
7984
8027
  }
7985
8028
  function hasQueueCall(node, depth = 0) {
7986
8029
  if (!node || typeof node !== "object" || depth > 40) return false;
@@ -7997,13 +8040,13 @@ function hasQueueCall(node, depth = 0) {
7997
8040
  }
7998
8041
  return false;
7999
8042
  }
8000
- var rule108 = {
8043
+ var rule106 = {
8001
8044
  meta: {
8002
8045
  type: "suggestion",
8003
8046
  docs: {
8004
8047
  description: "Audio sent before an OpenAI Realtime socket is open must be buffered, not dropped",
8005
8048
  category: "reliability",
8006
- docsUrl: "https://developers.openai.com/api/docs/voice/media-streams/websocket-messages",
8049
+ docsUrl: "https://developers.openai.com/api/reference/resources/realtime/client-events",
8007
8050
  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.",
8008
8051
  recommended: true
8009
8052
  },
@@ -8015,18 +8058,20 @@ var rule108 = {
8015
8058
  create(context) {
8016
8059
  return {
8017
8060
  IfStatement(node) {
8018
- if (!isReadyStateOpenCheck(node.test)) return;
8019
- if (!node.alternate) return;
8020
- if (hasQueueCall(node.alternate)) return;
8021
- context.report({ node: node.alternate, messageId: "audioDroppedNotBuffered" });
8061
+ const kind = readyStateOpenCheckKind(node.test);
8062
+ if (!kind) return;
8063
+ const notOpenBranch = kind === "is-open" ? node.alternate : node.consequent;
8064
+ if (!notOpenBranch) return;
8065
+ if (hasQueueCall(notOpenBranch)) return;
8066
+ context.report({ node: notOpenBranch, messageId: "audioDroppedNotBuffered" });
8022
8067
  }
8023
8068
  };
8024
8069
  }
8025
8070
  };
8026
- var openaiRealtimeBufferAudioUntilSessionReadyRule = rule108;
8071
+ var openaiRealtimeBufferAudioUntilSessionReadyRule = rule106;
8027
8072
 
8028
8073
  // src/providers/openai-realtime/rules/send-safety-identifier.ts
8029
- var rule109 = {
8074
+ var rule107 = {
8030
8075
  meta: {
8031
8076
  type: "suggestion",
8032
8077
  docs: {
@@ -8059,10 +8104,10 @@ var rule109 = {
8059
8104
  };
8060
8105
  }
8061
8106
  };
8062
- var openaiRealtimeSendSafetyIdentifierRule = rule109;
8107
+ var openaiRealtimeSendSafetyIdentifierRule = rule107;
8063
8108
 
8064
8109
  // src/providers/openai-realtime/rules/transcription-model-choice.ts
8065
- var rule110 = {
8110
+ var rule108 = {
8066
8111
  meta: {
8067
8112
  type: "suggestion",
8068
8113
  docs: {
@@ -8073,7 +8118,7 @@ var rule110 = {
8073
8118
  recommended: true
8074
8119
  },
8075
8120
  messages: {
8076
- nonStreamingTranscriptionModel: "input_audio_transcription is configured with 'whisper-1', which is not natively streaming and not optimized for realtime sessions."
8121
+ nonStreamingTranscriptionModel: "This session's input transcription is configured with 'whisper-1', which is not natively streaming and not optimized for realtime sessions."
8077
8122
  },
8078
8123
  schema: []
8079
8124
  },
@@ -8086,7 +8131,12 @@ var rule110 = {
8086
8131
  if (!isSessionUpdate) return;
8087
8132
  const sessionProp = findProperty4(node, "session");
8088
8133
  if (sessionProp?.value?.type !== "ObjectExpression") return;
8089
- const transcriptionProp = findProperty4(sessionProp.value, "input_audio_transcription");
8134
+ let transcriptionProp = findProperty4(sessionProp.value, "input_audio_transcription");
8135
+ if (!transcriptionProp) {
8136
+ const audioProp = findProperty4(sessionProp.value, "audio");
8137
+ const inputProp = audioProp?.value?.type === "ObjectExpression" ? findProperty4(audioProp.value, "input") : null;
8138
+ transcriptionProp = inputProp?.value?.type === "ObjectExpression" ? findProperty4(inputProp.value, "transcription") : null;
8139
+ }
8090
8140
  if (transcriptionProp?.value?.type !== "ObjectExpression") return;
8091
8141
  const modelProp = findProperty4(transcriptionProp.value, "model");
8092
8142
  const modelValue = modelProp?.value;
@@ -8097,7 +8147,7 @@ var rule110 = {
8097
8147
  };
8098
8148
  }
8099
8149
  };
8100
- var openaiRealtimeTranscriptionModelChoiceRule = rule110;
8150
+ var openaiRealtimeTranscriptionModelChoiceRule = rule108;
8101
8151
 
8102
8152
  // src/plugin/index.ts
8103
8153
  var plugin = {
@@ -8142,7 +8192,6 @@ var plugin = {
8142
8192
  "firebase-rtdb-write-promise-not-handled": firebaseRtdbWritePromiseNotHandledRule,
8143
8193
  "firebase-firestore-rules-expired": firebaseFirestoreRulesExpiredRule,
8144
8194
  "firebase-id-token-cookie-flags": firebaseIdTokenCookieFlagsRule,
8145
- "firebase-middleware-token-not-verified": firebaseMiddlewareTokenNotVerifiedRule,
8146
8195
  "firebase-hardcoded-user-id": firebaseHardcodedUserIdRule,
8147
8196
  "firebase-auth-user-not-found-disclosure": firebaseAuthUserNotFoundDisclosureRule,
8148
8197
  "firebase-signup-password-confirm": firebaseSignupPasswordConfirmRule,
@@ -8181,7 +8230,6 @@ var plugin = {
8181
8230
  "tiptap-appendTransaction-add-to-history": tiptapAppendTransactionAddToHistoryRule,
8182
8231
  "tiptap-appendTransaction-full-scan": tiptapAppendTransactionFullScanRule,
8183
8232
  "tiptap-atom-node-wrap-in": tiptapAtomNodeWrapInRule,
8184
- "tiptap-twitter-url-regex": tiptapTwitterUrlRegexRule,
8185
8233
  "tiptap-drop-handler-pos-precedence": tiptapDropHandlerPosPrecedenceRule,
8186
8234
  "tiptap-prefer-table-kit": tiptapPreferTableKitRule,
8187
8235
  "tiptap-tiptap-markdown-missing-node-spec": tiptapTiptapMarkdownMissingNodeSpecRule,