@api-doctor/cli 0.0.4 → 0.0.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/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;
2977
3042
  }
2978
- if (callee?.type === "MemberExpression" && callee.property?.type === "Identifier" && VERIFY_METHOD_NAMES.has(callee.property.name)) {
2979
- hasVerifyCall = true;
3043
+ if (callee?.type !== "MemberExpression" || callee.computed || callee.property?.type !== "Identifier") return;
3044
+ if (callee.property.name === "cookie") {
3045
+ checkNameValueOptsCall(node);
3046
+ return;
3047
+ }
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,2093 @@ var rule82 = {
6023
6061
  };
6024
6062
  }
6025
6063
  };
6026
- var tiptapTiptapMarkdownMissingNodeSpecRule = rule82;
6064
+ var tiptapTiptapMarkdownMissingNodeSpecRule = rule80;
6065
+
6066
+ // src/providers/elevenlabs/utils.ts
6067
+ function isElevenLabsUrlArg(node) {
6068
+ if (!node) return false;
6069
+ if (node.type === "Literal" && typeof node.value === "string") {
6070
+ return node.value.includes("elevenlabs.io");
6071
+ }
6072
+ if (node.type === "TemplateLiteral") {
6073
+ return (node.quasis ?? []).some(
6074
+ (q) => typeof q?.value?.raw === "string" && q.value.raw.includes("elevenlabs.io")
6075
+ );
6076
+ }
6077
+ return false;
6078
+ }
6079
+ function findElevenLabsFetchCall(node, depth = 0) {
6080
+ if (!node || typeof node !== "object" || depth > 20) return null;
6081
+ if (Array.isArray(node)) {
6082
+ for (const n of node) {
6083
+ const found = findElevenLabsFetchCall(n, depth + 1);
6084
+ if (found) return found;
6085
+ }
6086
+ return null;
6087
+ }
6088
+ if (node.type === "CallExpression" && node.callee?.type === "Identifier" && node.callee.name === "fetch") {
6089
+ if (isElevenLabsUrlArg(node.arguments?.[0])) return node;
6090
+ }
6091
+ for (const key of Object.keys(node)) {
6092
+ if (key === "parent" || key === "loc" || key === "range") continue;
6093
+ const val = node[key];
6094
+ if (val && typeof val === "object") {
6095
+ const found = findElevenLabsFetchCall(val, depth + 1);
6096
+ if (found) return found;
6097
+ }
6098
+ }
6099
+ return null;
6100
+ }
6101
+ function unwrapAwait(node) {
6102
+ return node?.type === "AwaitExpression" ? node.argument : node;
6103
+ }
6104
+ function collectVarDeclarators(node, out, depth = 0) {
6105
+ if (!node || typeof node !== "object" || depth > 24) return;
6106
+ if (Array.isArray(node)) {
6107
+ for (const n of node) collectVarDeclarators(n, out, depth + 1);
6108
+ return;
6109
+ }
6110
+ if (node.type === "VariableDeclarator") out.push(node);
6111
+ for (const key of Object.keys(node)) {
6112
+ if (key === "parent" || key === "loc" || key === "range") continue;
6113
+ const val = node[key];
6114
+ if (val && typeof val === "object") collectVarDeclarators(val, out, depth + 1);
6115
+ }
6116
+ }
6117
+ function posOf(n) {
6118
+ if (typeof n?.range?.[0] === "number") return n.range[0];
6119
+ const line = n?.loc?.start?.line ?? 0;
6120
+ const column = n?.loc?.start?.column ?? 0;
6121
+ return line * 1e6 + column;
6122
+ }
6123
+
6124
+ // src/providers/elevenlabs/rules/validate-signed-url-response.ts
6125
+ var rule81 = {
6126
+ meta: {
6127
+ type: "problem",
6128
+ docs: {
6129
+ description: "ElevenLabs signed URL responses must be validated before the signed_url field is used",
6130
+ category: "correctness",
6131
+ cwe: "CWE-252",
6132
+ rationale: "The signed-url endpoint assumes the ElevenLabs API always returns a well-formed body. If the API ever returns an unexpected shape (error payload, empty body, schema change), `data.signed_url` is silently `undefined` and the failure only surfaces downstream when the client tries to connect with an invalid URL.",
6133
+ docsUrl: "https://elevenlabs.io/docs/eleven-agents/api-reference/conversations/get-signed-url",
6134
+ recommended: true
6135
+ },
6136
+ messages: {
6137
+ missingValidation: "This code reads signed_url from the ElevenLabs API response without checking that the field exists. A malformed or unexpected response will silently produce an undefined signed URL."
6138
+ }
6139
+ },
6140
+ create(context) {
6141
+ function analyzeFunction(fnNode) {
6142
+ const declarators = [];
6143
+ collectVarDeclarators(fnNode.body, declarators);
6144
+ const responseVarNames = [];
6145
+ for (const d of declarators) {
6146
+ const init = unwrapAwait(d.init);
6147
+ if (init?.type === "CallExpression" && init.callee?.type === "Identifier" && init.callee.name === "fetch" && isElevenLabsUrlArg(init.arguments?.[0]) && d.id?.type === "Identifier") {
6148
+ responseVarNames.push(d.id.name);
6149
+ }
6150
+ }
6151
+ if (responseVarNames.length === 0) return;
6152
+ for (const responseVarName of responseVarNames) {
6153
+ for (const d of declarators) {
6154
+ const init = unwrapAwait(d.init);
6155
+ if (init?.type === "CallExpression" && init.callee?.type === "MemberExpression" && init.callee.property?.type === "Identifier" && init.callee.property.name === "json" && init.callee.object?.type === "Identifier" && init.callee.object.name === responseVarName && d.id?.type === "Identifier") {
6156
+ analyzeDataVar(fnNode, d.id.name, d);
6157
+ }
6158
+ }
6159
+ }
6160
+ }
6161
+ function analyzeDataVar(fnNode, dataVarName, dataDeclaratorNode) {
6162
+ function isSignedUrlMember(n) {
6163
+ if (n?.type !== "MemberExpression") return false;
6164
+ if (n.object?.type !== "Identifier" || n.object.name !== dataVarName) return false;
6165
+ if (!n.computed) return n.property?.type === "Identifier" && n.property.name === "signed_url";
6166
+ return n.property?.type === "Literal" && n.property.value === "signed_url";
6167
+ }
6168
+ function isNullishLiteral(n) {
6169
+ return n?.type === "Identifier" && n.name === "undefined" || n?.type === "Literal" && n.value === null;
6170
+ }
6171
+ function isFalsyGuardOnSignedUrl(test) {
6172
+ if (!test) return false;
6173
+ if (test.type === "UnaryExpression" && test.operator === "!") {
6174
+ return isSignedUrlMember(test.argument);
6175
+ }
6176
+ if (test.type === "BinaryExpression" && (test.operator === "==" || test.operator === "===")) {
6177
+ return isSignedUrlMember(test.left) && isNullishLiteral(test.right) || isSignedUrlMember(test.right) && isNullishLiteral(test.left);
6178
+ }
6179
+ if (test.type === "LogicalExpression") {
6180
+ return isFalsyGuardOnSignedUrl(test.left) || isFalsyGuardOnSignedUrl(test.right);
6181
+ }
6182
+ return false;
6183
+ }
6184
+ let guardPos = null;
6185
+ let usagePos = null;
6186
+ function walk2(n, depth = 0) {
6187
+ if (!n || typeof n !== "object" || depth > 40) return;
6188
+ if (Array.isArray(n)) {
6189
+ for (const item of n) walk2(item, depth + 1);
6190
+ return;
6191
+ }
6192
+ if (n.type === "IfStatement" && isFalsyGuardOnSignedUrl(n.test)) {
6193
+ const p = posOf(n);
6194
+ if (guardPos === null || p < guardPos) guardPos = p;
6195
+ }
6196
+ if (isSignedUrlMember(n) && n !== dataDeclaratorNode) {
6197
+ const p = posOf(n);
6198
+ if (usagePos === null || p < usagePos) usagePos = p;
6199
+ }
6200
+ for (const key of Object.keys(n)) {
6201
+ if (key === "parent" || key === "loc" || key === "range") continue;
6202
+ const val = n[key];
6203
+ if (val && typeof val === "object") walk2(val, depth + 1);
6204
+ }
6205
+ }
6206
+ walk2(fnNode.body);
6207
+ if (usagePos === null) return;
6208
+ if (guardPos !== null && guardPos < usagePos) return;
6209
+ context.report({ node: dataDeclaratorNode, messageId: "missingValidation" });
6210
+ }
6211
+ return {
6212
+ FunctionDeclaration(node) {
6213
+ analyzeFunction(node);
6214
+ },
6215
+ FunctionExpression(node) {
6216
+ analyzeFunction(node);
6217
+ },
6218
+ ArrowFunctionExpression(node) {
6219
+ analyzeFunction(node);
6220
+ }
6221
+ };
6222
+ }
6223
+ };
6224
+ var elevenlabsValidateSignedUrlResponseRule = rule81;
6225
+
6226
+ // src/providers/elevenlabs/rules/no-error-object-logging.ts
6227
+ var LOGGING_METHODS = /* @__PURE__ */ new Set(["error", "warn", "log"]);
6228
+ var rule82 = {
6229
+ meta: {
6230
+ type: "problem",
6231
+ docs: {
6232
+ description: "Catch blocks must not log the raw caught error object",
6233
+ category: "security",
6234
+ cwe: "CWE-532",
6235
+ rationale: "Error objects thrown by fetch/SDK calls can carry response bodies, headers, or internal state. Logging them directly with console.error(error) writes that data verbatim into server logs, which may be shipped to third-party log aggregators or be readable by anyone with log access.",
6236
+ docsUrl: "https://elevenlabs.io/docs/api-reference/authentication",
6237
+ recommended: true
6238
+ },
6239
+ messages: {
6240
+ rawErrorLogged: "This catch block logs the raw error object instead of a sanitized message \u2014 error responses, headers, or internal state may leak into server logs."
6241
+ }
6242
+ },
6243
+ create(context) {
6244
+ function isConsoleLogCall(node) {
6245
+ if (node?.type !== "CallExpression") return false;
6246
+ const callee = node.callee;
6247
+ if (callee?.type !== "MemberExpression") return false;
6248
+ if (callee.object?.type !== "Identifier" || callee.object.name !== "console") return false;
6249
+ return callee.property?.type === "Identifier" && LOGGING_METHODS.has(callee.property.name);
6250
+ }
6251
+ function referencesRawIdentifier(node, paramName, depth = 0) {
6252
+ if (!node || typeof node !== "object" || depth > 10) return false;
6253
+ if (Array.isArray(node)) return node.some((n) => referencesRawIdentifier(n, paramName, depth + 1));
6254
+ if (node.type === "Identifier" && node.name === paramName) return true;
6255
+ if (node.type === "ObjectExpression") {
6256
+ return (node.properties ?? []).some((p) => {
6257
+ if (p.type !== "Property") return false;
6258
+ return referencesRawIdentifier(p.value, paramName, depth + 1);
6259
+ });
6260
+ }
6261
+ if (node.type === "ArrayExpression") {
6262
+ return (node.elements ?? []).some((el) => referencesRawIdentifier(el, paramName, depth + 1));
6263
+ }
6264
+ return false;
6265
+ }
6266
+ return {
6267
+ TryStatement(node) {
6268
+ if (!findElevenLabsFetchCall(node.block)) return;
6269
+ const handler = node.handler;
6270
+ const paramName = handler?.param?.type === "Identifier" ? handler.param.name : null;
6271
+ if (!paramName) return;
6272
+ function walk2(n, depth = 0) {
6273
+ if (!n || typeof n !== "object" || depth > 30) return;
6274
+ if (Array.isArray(n)) {
6275
+ for (const item of n) walk2(item, depth + 1);
6276
+ return;
6277
+ }
6278
+ if (isConsoleLogCall(n)) {
6279
+ const args = n.arguments ?? [];
6280
+ const loggedRaw = args.some((a) => referencesRawIdentifier(a, paramName));
6281
+ if (loggedRaw) {
6282
+ context.report({ node: n, messageId: "rawErrorLogged" });
6283
+ }
6284
+ }
6285
+ for (const key of Object.keys(n)) {
6286
+ if (key === "parent" || key === "loc" || key === "range") continue;
6287
+ const val = n[key];
6288
+ if (val && typeof val === "object") walk2(val, depth + 1);
6289
+ }
6290
+ }
6291
+ walk2(handler.body);
6292
+ }
6293
+ };
6294
+ }
6295
+ };
6296
+ var elevenlabsNoErrorObjectLoggingRule = rule82;
6297
+
6298
+ // src/providers/elevenlabs/rules/fetch-timeout-required.ts
6299
+ var rule83 = {
6300
+ meta: {
6301
+ type: "suggestion",
6302
+ docs: {
6303
+ description: "Fetch calls to the ElevenLabs API must set an abort timeout",
6304
+ category: "reliability",
6305
+ rationale: "Native fetch has no default timeout. If the ElevenLabs API becomes slow or hangs, a request with no AbortController/signal will wait indefinitely, tying up the request handler and starving the server of available connections.",
6306
+ docsUrl: "https://elevenlabs.io/docs/eleven-agents/api-reference/conversations/get-signed-url",
6307
+ recommended: true
6308
+ },
6309
+ messages: {
6310
+ missingTimeout: "This fetch call to the ElevenLabs API has no abort signal/timeout \u2014 a slow or unresponsive API will hang the request indefinitely."
6311
+ }
6312
+ },
6313
+ create(context) {
6314
+ function hasSignalOption(optionsArg) {
6315
+ if (optionsArg?.type !== "ObjectExpression") return false;
6316
+ return (optionsArg.properties ?? []).some((p) => {
6317
+ if (p.type === "SpreadElement") return true;
6318
+ return p.type === "Property" && p.key?.type === "Identifier" && p.key.name === "signal";
6319
+ });
6320
+ }
6321
+ return {
6322
+ CallExpression(node) {
6323
+ if (node.callee?.type !== "Identifier" || node.callee.name !== "fetch") return;
6324
+ if (!isElevenLabsUrlArg(node.arguments?.[0])) return;
6325
+ const optionsArg = node.arguments?.[1];
6326
+ if (hasSignalOption(optionsArg)) return;
6327
+ context.report({ node, messageId: "missingTimeout" });
6328
+ }
6329
+ };
6330
+ }
6331
+ };
6332
+ var elevenlabsFetchTimeoutRequiredRule = rule83;
6333
+
6334
+ // src/providers/elevenlabs/rules/validate-agent-id-format.ts
6335
+ var rule84 = {
6336
+ meta: {
6337
+ type: "problem",
6338
+ docs: {
6339
+ description: "agentId must be format-validated, not just checked for existence, before use",
6340
+ category: "correctness",
6341
+ cwe: "CWE-20",
6342
+ rationale: "A query-param agentId that is only checked with `if (!agentId)` accepts any non-empty string. An attacker can pass arbitrary values \u2014 oversized strings, path-traversal-like sequences, or characters the ElevenLabs API was never designed to receive \u2014 directly into the request URL, producing undefined behavior instead of a clean 400.",
6343
+ docsUrl: "https://elevenlabs.io/docs/eleven-agents/customization/authentication",
6344
+ recommended: true
6345
+ },
6346
+ messages: {
6347
+ missingFormatValidation: "agentId is checked for existence but never validated against an expected format before being used in an ElevenLabs API request."
6348
+ }
6349
+ },
6350
+ create(context) {
6351
+ function isSearchParamsGetAgentId(node) {
6352
+ if (node?.type !== "CallExpression") return false;
6353
+ const callee = node.callee;
6354
+ if (callee?.type !== "MemberExpression") return false;
6355
+ if (callee.property?.type !== "Identifier" || callee.property.name !== "get") return false;
6356
+ const arg = node.arguments?.[0];
6357
+ return arg?.type === "Literal" && arg.value === "agentId";
6358
+ }
6359
+ function analyzeFunction(fnNode) {
6360
+ const declarators = [];
6361
+ collectVarDeclarators(fnNode.body, declarators);
6362
+ let agentIdVarName = null;
6363
+ let agentIdDeclaratorNode = null;
6364
+ for (const d of declarators) {
6365
+ const init = unwrapAwait(d.init);
6366
+ if (isSearchParamsGetAgentId(init) && d.id?.type === "Identifier") {
6367
+ agentIdVarName = d.id.name;
6368
+ agentIdDeclaratorNode = d;
6369
+ }
6370
+ }
6371
+ if (!agentIdVarName) {
6372
+ for (const param of fnNode.params ?? []) {
6373
+ if (param?.type === "Identifier" && param.name === "agentId") {
6374
+ agentIdVarName = param.name;
6375
+ agentIdDeclaratorNode = param;
6376
+ }
6377
+ }
6378
+ }
6379
+ if (!agentIdVarName) return;
6380
+ function isAgentIdMember(n) {
6381
+ return n?.type === "Identifier" && n.name === agentIdVarName;
6382
+ }
6383
+ function isFormatCheck(n, depth = 0) {
6384
+ if (!n || typeof n !== "object" || depth > 10) return false;
6385
+ if (Array.isArray(n)) return n.some((x) => isFormatCheck(x, depth + 1));
6386
+ if (n.type === "CallExpression" && n.callee?.type === "MemberExpression") {
6387
+ const propName2 = n.callee.property?.type === "Identifier" ? n.callee.property.name : null;
6388
+ if (propName2 === "test" && isAgentIdMember(n.arguments?.[0])) return true;
6389
+ if ((propName2 === "match" || propName2 === "matchAll") && isAgentIdMember(n.callee.object)) return true;
6390
+ }
6391
+ if (n.type === "LogicalExpression") {
6392
+ return isFormatCheck(n.left, depth + 1) || isFormatCheck(n.right, depth + 1);
6393
+ }
6394
+ if (n.type === "UnaryExpression") return isFormatCheck(n.argument, depth + 1);
6395
+ return false;
6396
+ }
6397
+ let formatGuardPos = null;
6398
+ let usagePos = null;
6399
+ function walk2(n, depth = 0) {
6400
+ if (!n || typeof n !== "object" || depth > 40) return;
6401
+ if (Array.isArray(n)) {
6402
+ for (const item of n) walk2(item, depth + 1);
6403
+ return;
6404
+ }
6405
+ if (n.type === "IfStatement" && isFormatCheck(n.test)) {
6406
+ const p = posOf(n);
6407
+ if (formatGuardPos === null || p < formatGuardPos) formatGuardPos = p;
6408
+ }
6409
+ if (n.type === "CallExpression" && n.callee?.type === "Identifier" && n.callee.name === "fetch" && isElevenLabsUrlArg(n.arguments?.[0])) {
6410
+ const urlArg = n.arguments[0];
6411
+ const containsAgentId = urlArg.type === "TemplateLiteral" && (urlArg.expressions ?? []).some((e) => isAgentIdMember(e));
6412
+ if (containsAgentId) {
6413
+ const p = posOf(n);
6414
+ if (usagePos === null || p < usagePos) usagePos = p;
6415
+ }
6416
+ }
6417
+ for (const key of Object.keys(n)) {
6418
+ if (key === "parent" || key === "loc" || key === "range") continue;
6419
+ const val = n[key];
6420
+ if (val && typeof val === "object") walk2(val, depth + 1);
6421
+ }
6422
+ }
6423
+ walk2(fnNode.body);
6424
+ if (usagePos === null) return;
6425
+ if (formatGuardPos !== null && formatGuardPos < usagePos) return;
6426
+ context.report({ node: agentIdDeclaratorNode, messageId: "missingFormatValidation" });
6427
+ }
6428
+ return {
6429
+ FunctionDeclaration(node) {
6430
+ analyzeFunction(node);
6431
+ },
6432
+ FunctionExpression(node) {
6433
+ analyzeFunction(node);
6434
+ },
6435
+ ArrowFunctionExpression(node) {
6436
+ analyzeFunction(node);
6437
+ }
6438
+ };
6439
+ }
6440
+ };
6441
+ var elevenlabsValidateAgentIdFormatRule = rule84;
6442
+
6443
+ // src/providers/elevenlabs/rules/secure-session-id-generation.ts
6444
+ var rule85 = {
6445
+ meta: {
6446
+ type: "problem",
6447
+ docs: {
6448
+ description: "Session ids must be generated with a cryptographically secure RNG",
6449
+ category: "security",
6450
+ cwe: "CWE-338",
6451
+ rationale: 'Math.random() is a non-cryptographic PRNG \u2014 its output can be predicted by an attacker who observes enough samples or knows the engine implementation. If a "session_*" value derived from it is later trusted for routing, deduplication, or any access-control-adjacent decision, that predictability can be exploited.',
6452
+ docsUrl: "https://elevenlabs.io/docs/eleven-api/guides/how-to/best-practices/security",
6453
+ recommended: true
6454
+ },
6455
+ messages: {
6456
+ insecureSessionId: "This session id is generated with Math.random(), which is not cryptographically secure and can be predicted. Use crypto.getRandomValues() instead."
6457
+ }
6458
+ },
6459
+ create(context) {
6460
+ function containsMathRandomCall(node, depth = 0) {
6461
+ if (!node || typeof node !== "object" || depth > 20) return false;
6462
+ if (Array.isArray(node)) return node.some((n) => containsMathRandomCall(n, depth + 1));
6463
+ if (node.type === "CallExpression" && node.callee?.type === "MemberExpression" && node.callee.object?.type === "Identifier" && node.callee.object.name === "Math" && node.callee.property?.type === "Identifier" && node.callee.property.name === "random") {
6464
+ return true;
6465
+ }
6466
+ for (const key of Object.keys(node)) {
6467
+ if (key === "parent" || key === "loc" || key === "range") continue;
6468
+ const val = node[key];
6469
+ if (val && typeof val === "object" && containsMathRandomCall(val, depth + 1)) return true;
6470
+ }
6471
+ return false;
6472
+ }
6473
+ function looksLikeSessionId(name) {
6474
+ return typeof name === "string" && /session/i.test(name);
6475
+ }
6476
+ function isSessionIdValue(init) {
6477
+ if (init?.type === "TemplateLiteral") {
6478
+ const firstQuasi = init.quasis?.[0]?.value?.raw ?? "";
6479
+ if (/session[-_]?/i.test(firstQuasi) && containsMathRandomCall(init)) return true;
6480
+ }
6481
+ if (init?.type === "BinaryExpression" && init.operator === "+") {
6482
+ const leftLiteral = init.left?.type === "Literal" && typeof init.left.value === "string";
6483
+ if (leftLiteral && /session[-_]?/i.test(init.left.value) && containsMathRandomCall(init)) return true;
6484
+ }
6485
+ return false;
6486
+ }
6487
+ return {
6488
+ VariableDeclarator(node) {
6489
+ const init = node.init;
6490
+ if (!init) return;
6491
+ const declaredName = node.id?.type === "Identifier" ? node.id.name : node.id?.type === "ArrayPattern" && node.id.elements?.[0]?.type === "Identifier" ? node.id.elements[0].name : void 0;
6492
+ let valueNode = init;
6493
+ if (init.type === "CallExpression" && init.callee?.type === "Identifier" && init.callee.name === "useState" && init.arguments?.[0]?.type === "ArrowFunctionExpression") {
6494
+ const body = init.arguments[0].body;
6495
+ valueNode = body?.type === "BlockStatement" ? null : body;
6496
+ if (body?.type === "BlockStatement") {
6497
+ const returnStmt = (body.body ?? []).find((s) => s.type === "ReturnStatement");
6498
+ valueNode = returnStmt?.argument ?? null;
6499
+ }
6500
+ }
6501
+ if (!valueNode) return;
6502
+ if (!looksLikeSessionId(declaredName) && !isSessionIdValue(valueNode)) return;
6503
+ if (!containsMathRandomCall(valueNode)) return;
6504
+ context.report({ node, messageId: "insecureSessionId" });
6505
+ }
6506
+ };
6507
+ }
6508
+ };
6509
+ var elevenlabsSecureSessionIdGenerationRule = rule85;
6510
+
6511
+ // src/providers/elevenlabs/rules/conversation-error-recovery.ts
6512
+ var rule86 = {
6513
+ meta: {
6514
+ type: "suggestion",
6515
+ docs: {
6516
+ description: "A loading flag set before starting a conversation must be reset on failure",
6517
+ category: "reliability",
6518
+ rationale: 'If startConversation() sets isLoading true and getSignedUrl()/Conversation.startSession() throws, the loading flag must be reset in the catch or finally block. Otherwise the UI is stuck "loading" forever and the user can trigger overlapping conversation attempts by clicking again.',
6519
+ docsUrl: "https://elevenlabs.io/docs/eleven-agents/libraries/java-script",
6520
+ recommended: true
6521
+ },
6522
+ messages: {
6523
+ missingLoadingReset: "This function sets a loading flag true before a try block, but neither the catch nor a finally block resets it to false on failure."
6524
+ }
6525
+ },
6526
+ create(context) {
6527
+ function isSetterCallWithBoolean(n, setterName, expected) {
6528
+ if (n?.type !== "CallExpression") return false;
6529
+ if (n.callee?.type !== "Identifier" || n.callee.name !== setterName) return false;
6530
+ const arg = n.arguments?.[0];
6531
+ return arg?.type === "Literal" && arg.value === expected;
6532
+ }
6533
+ function containsCall(node, predicate, depth = 0) {
6534
+ if (!node || typeof node !== "object" || depth > 20) return false;
6535
+ if (Array.isArray(node)) return node.some((n) => containsCall(n, predicate, depth + 1));
6536
+ if (predicate(node)) return true;
6537
+ for (const key of Object.keys(node)) {
6538
+ if (key === "parent" || key === "loc" || key === "range") continue;
6539
+ const val = node[key];
6540
+ if (val && typeof val === "object" && containsCall(val, predicate, depth + 1)) return true;
6541
+ }
6542
+ return false;
6543
+ }
6544
+ function findLoadingSetterFromUseState(fnNode) {
6545
+ let setterName = null;
6546
+ function collect(n, depth = 0) {
6547
+ if (setterName || !n || typeof n !== "object" || depth > 20) return;
6548
+ if (Array.isArray(n)) {
6549
+ for (const item of n) collect(item, depth + 1);
6550
+ return;
6551
+ }
6552
+ if (n.type === "VariableDeclarator" && n.id?.type === "ArrayPattern" && n.id.elements?.length === 2 && n.id.elements[0]?.type === "Identifier" && /loading/i.test(n.id.elements[0].name) && n.id.elements[1]?.type === "Identifier" && n.init?.type === "CallExpression" && n.init.callee?.type === "Identifier" && n.init.callee.name === "useState") {
6553
+ setterName = n.id.elements[1].name;
6554
+ return;
6555
+ }
6556
+ for (const key of Object.keys(n)) {
6557
+ if (key === "parent" || key === "loc" || key === "range") continue;
6558
+ const val = n[key];
6559
+ if (val && typeof val === "object") collect(val, depth + 1);
6560
+ }
6561
+ }
6562
+ collect(fnNode);
6563
+ return setterName;
6564
+ }
6565
+ function isLoadingTrueStatement(stmt, setterName) {
6566
+ return stmt?.type === "ExpressionStatement" && isSetterCallWithBoolean(stmt.expression, setterName, true);
6567
+ }
6568
+ function analyzeFunction(fnNode) {
6569
+ const setterName = findLoadingSetterFromUseState(fnNode);
6570
+ if (!setterName) return;
6571
+ function walk2(n, depth = 0) {
6572
+ if (!n || typeof n !== "object" || depth > 30) return;
6573
+ if (Array.isArray(n)) {
6574
+ for (const item of n) walk2(item, depth + 1);
6575
+ return;
6576
+ }
6577
+ if (n.type === "BlockStatement" && Array.isArray(n.body)) {
6578
+ for (let i = 0; i < n.body.length - 1; i++) {
6579
+ if (!isLoadingTrueStatement(n.body[i], setterName)) continue;
6580
+ const tryNode = n.body[i + 1];
6581
+ if (tryNode?.type !== "TryStatement") continue;
6582
+ const handler = tryNode.handler;
6583
+ const finalizer = tryNode.finalizer;
6584
+ const finallyResets = finalizer && containsCall(finalizer, (x) => isSetterCallWithBoolean(x, setterName, false));
6585
+ const catchResets = handler && containsCall(handler.body, (x) => isSetterCallWithBoolean(x, setterName, false));
6586
+ if (!finallyResets && !catchResets) {
6587
+ context.report({ node: tryNode, messageId: "missingLoadingReset" });
6588
+ }
6589
+ }
6590
+ }
6591
+ for (const key of Object.keys(n)) {
6592
+ if (key === "parent" || key === "loc" || key === "range") continue;
6593
+ const val = n[key];
6594
+ if (val && typeof val === "object") walk2(val, depth + 1);
6595
+ }
6596
+ }
6597
+ walk2(fnNode.body);
6598
+ }
6599
+ return {
6600
+ FunctionDeclaration(node) {
6601
+ analyzeFunction(node);
6602
+ },
6603
+ FunctionExpression(node) {
6604
+ analyzeFunction(node);
6605
+ },
6606
+ ArrowFunctionExpression(node) {
6607
+ analyzeFunction(node);
6608
+ }
6609
+ };
6610
+ }
6611
+ };
6612
+ var elevenlabsConversationErrorRecoveryRule = rule86;
6613
+
6614
+ // src/providers/elevenlabs/rules/api-version-pinning.ts
6615
+ var rule87 = {
6616
+ meta: {
6617
+ type: "suggestion",
6618
+ docs: {
6619
+ description: "ElevenLabs API requests should pin an explicit API version header",
6620
+ category: "reliability",
6621
+ rationale: "The endpoint is hardcoded to /v1 in the URL path with no explicit version header. If ElevenLabs ships a v2 API and changes default response behavior for unversioned clients, requests with no version header silently pick up the new behavior instead of failing loudly or staying pinned.",
6622
+ docsUrl: "https://elevenlabs.io/docs/eleven-api/resources/breaking-changes-policy",
6623
+ recommended: true
6624
+ },
6625
+ messages: {
6626
+ missingVersionHeader: "This fetch call to the ElevenLabs API has no explicit API version header \u2014 a future API change could silently alter behavior."
6627
+ }
6628
+ },
6629
+ create(context) {
6630
+ function hasVersionHeader(optionsArg) {
6631
+ if (optionsArg?.type !== "ObjectExpression") return false;
6632
+ const headersProp = (optionsArg.properties ?? []).find(
6633
+ (p) => p.type === "Property" && p.key?.type === "Identifier" && p.key.name === "headers"
6634
+ );
6635
+ if (!headersProp) return false;
6636
+ if (headersProp.value?.type !== "ObjectExpression") return true;
6637
+ return (headersProp.value.properties ?? []).some((p) => {
6638
+ if (p.type === "SpreadElement") return true;
6639
+ if (p.type !== "Property") return false;
6640
+ const keyName = p.key?.type === "Identifier" ? p.key.name : p.key?.type === "Literal" ? p.key.value : null;
6641
+ return typeof keyName === "string" && /version/i.test(keyName);
6642
+ });
6643
+ }
6644
+ return {
6645
+ CallExpression(node) {
6646
+ if (node.callee?.type !== "Identifier" || node.callee.name !== "fetch") return;
6647
+ if (!isElevenLabsUrlArg(node.arguments?.[0])) return;
6648
+ const optionsArg = node.arguments?.[1];
6649
+ if (hasVersionHeader(optionsArg)) return;
6650
+ context.report({ node, messageId: "missingVersionHeader" });
6651
+ }
6652
+ };
6653
+ }
6654
+ };
6655
+ var elevenlabsApiVersionPinningRule = rule87;
6656
+
6657
+ // src/providers/elevenlabs/rules/check-http-status-before-json.ts
6658
+ var rule88 = {
6659
+ meta: {
6660
+ type: "problem",
6661
+ docs: {
6662
+ description: "response.json() must not be called before checking the HTTP status of an ElevenLabs response",
6663
+ category: "correctness",
6664
+ rationale: "Calling response.json() unconditionally parses whatever body the server returned, including error payloads, as if it were a successful response. Without checking response.ok (or response.status) first, a 4xx/5xx error from the ElevenLabs API is silently treated as valid data.",
6665
+ docsUrl: "https://elevenlabs.io/docs/eleven-api/resources/errors",
6666
+ recommended: true
6667
+ },
6668
+ messages: {
6669
+ missingStatusCheck: "response.json() is called on this ElevenLabs API response without first checking response.ok/status \u2014 an error response will be parsed as if it were valid data."
6670
+ }
6671
+ },
6672
+ create(context) {
6673
+ function analyzeFunction(fnNode) {
6674
+ const declarators = [];
6675
+ collectVarDeclarators(fnNode.body, declarators);
6676
+ const responseVarNames = [];
6677
+ for (const d of declarators) {
6678
+ const init = unwrapAwait(d.init);
6679
+ if (init?.type === "CallExpression" && init.callee?.type === "Identifier" && init.callee.name === "fetch" && isElevenLabsUrlArg(init.arguments?.[0]) && d.id?.type === "Identifier") {
6680
+ responseVarNames.push(d.id.name);
6681
+ }
6682
+ }
6683
+ for (const responseVarName of responseVarNames) {
6684
+ analyzeResponseVar(fnNode, responseVarName);
6685
+ }
6686
+ }
6687
+ function analyzeResponseVar(fnNode, responseVarName) {
6688
+ function isResponseStatusCheck(test) {
6689
+ if (!test) return false;
6690
+ if (test.type === "UnaryExpression" && test.operator === "!") return isResponseStatusCheck(test.argument);
6691
+ if (test.type === "MemberExpression" && test.object?.type === "Identifier" && test.object.name === responseVarName && test.property?.type === "Identifier" && test.property.name === "ok") {
6692
+ return true;
6693
+ }
6694
+ if (test.type === "BinaryExpression") {
6695
+ const sideIsStatus = (n) => n?.type === "MemberExpression" && n.object?.type === "Identifier" && n.object.name === responseVarName && n.property?.type === "Identifier" && n.property.name === "status";
6696
+ if (sideIsStatus(test.left) || sideIsStatus(test.right)) return true;
6697
+ }
6698
+ if (test.type === "LogicalExpression") {
6699
+ return isResponseStatusCheck(test.left) || isResponseStatusCheck(test.right);
6700
+ }
6701
+ return false;
6702
+ }
6703
+ function isResponseJsonCall2(n) {
6704
+ if (n?.type !== "CallExpression") return false;
6705
+ const callee = n.callee;
6706
+ if (callee?.type !== "MemberExpression") return false;
6707
+ if (callee.property?.type !== "Identifier" || callee.property.name !== "json") return false;
6708
+ return callee.object?.type === "Identifier" && callee.object.name === responseVarName;
6709
+ }
6710
+ let guardPos = null;
6711
+ let jsonCallNode = null;
6712
+ let jsonCallPos = null;
6713
+ function walk2(n, depth = 0) {
6714
+ if (!n || typeof n !== "object" || depth > 40) return;
6715
+ if (Array.isArray(n)) {
6716
+ for (const item of n) walk2(item, depth + 1);
6717
+ return;
6718
+ }
6719
+ if (n.type === "IfStatement" && isResponseStatusCheck(n.test)) {
6720
+ const p = posOf(n);
6721
+ if (guardPos === null || p < guardPos) guardPos = p;
6722
+ }
6723
+ if (isResponseJsonCall2(n)) {
6724
+ const p = posOf(n);
6725
+ if (jsonCallPos === null || p < jsonCallPos) {
6726
+ jsonCallPos = p;
6727
+ jsonCallNode = n;
6728
+ }
6729
+ }
6730
+ for (const key of Object.keys(n)) {
6731
+ if (key === "parent" || key === "loc" || key === "range") continue;
6732
+ const val = n[key];
6733
+ if (val && typeof val === "object") walk2(val, depth + 1);
6734
+ }
6735
+ }
6736
+ walk2(fnNode.body);
6737
+ if (jsonCallPos === null) return;
6738
+ if (guardPos !== null && guardPos < jsonCallPos) return;
6739
+ context.report({ node: jsonCallNode, messageId: "missingStatusCheck" });
6740
+ }
6741
+ return {
6742
+ FunctionDeclaration(node) {
6743
+ analyzeFunction(node);
6744
+ },
6745
+ FunctionExpression(node) {
6746
+ analyzeFunction(node);
6747
+ },
6748
+ ArrowFunctionExpression(node) {
6749
+ analyzeFunction(node);
6750
+ }
6751
+ };
6752
+ }
6753
+ };
6754
+ var elevenlabsCheckHttpStatusBeforeJsonRule = rule88;
6755
+
6756
+ // src/providers/elevenlabs/rules/conversation-cleanup-on-error.ts
6757
+ var END_METHODS = /* @__PURE__ */ new Set(["endSession", "endConversation"]);
6758
+ var rule89 = {
6759
+ meta: {
6760
+ type: "suggestion",
6761
+ docs: {
6762
+ description: "Ending a conversation must be wrapped in try/catch so a rejected call is handled",
6763
+ category: "reliability",
6764
+ rationale: "If conversation.endSession()/endConversation() rejects (e.g. during a GL-mode transition) and the call site has no try/catch, the rejection becomes an unhandled promise rejection and the conversation state is never cleaned up, leaving stale connections in memory.",
6765
+ docsUrl: "https://elevenlabs.io/docs/eleven-agents/libraries/java-script",
6766
+ recommended: true
6767
+ },
6768
+ messages: {
6769
+ missingTryCatch: "This call to end the conversation has no surrounding try/catch \u2014 a rejection will go unhandled and the conversation state may never be cleaned up."
6770
+ }
6771
+ },
6772
+ create(context) {
6773
+ function posEnd(n) {
6774
+ if (typeof n?.range?.[1] === "number") return n.range[1];
6775
+ const line = n?.loc?.end?.line ?? n?.loc?.start?.line ?? 0;
6776
+ const column = n?.loc?.end?.column ?? n?.loc?.start?.column ?? 0;
6777
+ return line * 1e6 + column;
6778
+ }
6779
+ function isConversationEndCall(node) {
6780
+ if (node?.type !== "CallExpression") return false;
6781
+ const callee = node.callee;
6782
+ if (callee?.type === "MemberExpression") {
6783
+ return callee.property?.type === "Identifier" && END_METHODS.has(callee.property.name);
6784
+ }
6785
+ return callee?.type === "Identifier" && END_METHODS.has(callee.name);
6786
+ }
6787
+ return {
6788
+ "Program:exit"(program) {
6789
+ const tryRanges = [];
6790
+ const endCalls = [];
6791
+ function walk2(n, depth = 0) {
6792
+ if (!n || typeof n !== "object" || depth > 60) return;
6793
+ if (Array.isArray(n)) {
6794
+ for (const item of n) walk2(item, depth + 1);
6795
+ return;
6796
+ }
6797
+ if (n.type === "TryStatement" && n.block) {
6798
+ tryRanges.push([posOf(n.block), posEnd(n.block)]);
6799
+ }
6800
+ if (isConversationEndCall(n)) endCalls.push(n);
6801
+ for (const key of Object.keys(n)) {
6802
+ if (key === "parent" || key === "loc" || key === "range") continue;
6803
+ const val = n[key];
6804
+ if (val && typeof val === "object") walk2(val, depth + 1);
6805
+ }
6806
+ }
6807
+ walk2(program);
6808
+ for (const call of endCalls) {
6809
+ const p = posOf(call);
6810
+ const covered = tryRanges.some(([start, end]) => p >= start && p <= end);
6811
+ if (!covered) {
6812
+ context.report({ node: call, messageId: "missingTryCatch" });
6813
+ }
6814
+ }
6815
+ }
6816
+ };
6817
+ }
6818
+ };
6819
+ var elevenlabsConversationCleanupOnErrorRule = rule89;
6820
+
6821
+ // src/providers/elevenlabs/rules/env-var-validation.ts
6822
+ var ELEVENLABS_ENV_VAR_PATTERN = /^(XI_API_KEY|ELEVENLABS_API_KEY)$/;
6823
+ var rule90 = {
6824
+ meta: {
6825
+ type: "suggestion",
6826
+ docs: {
6827
+ description: "ElevenLabs API key environment variables should be validated at module load, not only at request time",
6828
+ category: "correctness",
6829
+ rationale: "Checking `if (!apiKey)` inside a request handler means a misconfigured deployment only fails the first time a user exercises the feature, instead of failing fast at startup where it would show up in deploy logs or a health check.",
6830
+ docsUrl: "https://elevenlabs.io/docs/api-reference/authentication",
6831
+ recommended: true
6832
+ },
6833
+ messages: {
6834
+ missingStartupValidation: "This module reads an ElevenLabs API key from process.env but only validates it inside a request handler \u2014 validate it at module scope so a missing key fails at startup, not at first use."
6835
+ }
6836
+ },
6837
+ create(context) {
6838
+ function isElevenLabsEnvAccess(node) {
6839
+ if (node?.type !== "MemberExpression") return false;
6840
+ if (node.object?.type !== "MemberExpression") return false;
6841
+ const inner = node.object;
6842
+ if (inner.object?.type !== "Identifier" || inner.object.name !== "process") return false;
6843
+ if (inner.property?.type !== "Identifier" || inner.property.name !== "env") return false;
6844
+ const propName2 = node.property?.type === "Identifier" ? node.property.name : node.property?.value;
6845
+ return typeof propName2 === "string" && ELEVENLABS_ENV_VAR_PATTERN.test(propName2);
6846
+ }
6847
+ function isInsideFunction(node, ancestors) {
6848
+ return ancestors.some(
6849
+ (a) => a?.type === "FunctionDeclaration" || a?.type === "FunctionExpression" || a?.type === "ArrowFunctionExpression"
6850
+ );
6851
+ }
6852
+ return {
6853
+ "Program:exit"(program) {
6854
+ let moduleScopeAccess = false;
6855
+ let handlerScopeAccess = false;
6856
+ function walk2(n, ancestors, depth = 0) {
6857
+ if (!n || typeof n !== "object" || depth > 60) return;
6858
+ if (Array.isArray(n)) {
6859
+ for (const item of n) walk2(item, ancestors, depth + 1);
6860
+ return;
6861
+ }
6862
+ if (isElevenLabsEnvAccess(n)) {
6863
+ if (isInsideFunction(n, ancestors)) {
6864
+ handlerScopeAccess = true;
6865
+ } else {
6866
+ moduleScopeAccess = true;
6867
+ }
6868
+ }
6869
+ const nextAncestors = n.type === "FunctionDeclaration" || n.type === "FunctionExpression" || n.type === "ArrowFunctionExpression" ? [...ancestors, n] : ancestors;
6870
+ for (const key of Object.keys(n)) {
6871
+ if (key === "parent" || key === "loc" || key === "range") continue;
6872
+ const val = n[key];
6873
+ if (val && typeof val === "object") walk2(val, nextAncestors, depth + 1);
6874
+ }
6875
+ }
6876
+ walk2(program, []);
6877
+ if (handlerScopeAccess && !moduleScopeAccess) {
6878
+ context.report({ node: program, messageId: "missingStartupValidation" });
6879
+ }
6880
+ }
6881
+ };
6882
+ }
6883
+ };
6884
+ var elevenlabsEnvVarValidationRule = rule90;
6885
+
6886
+ // src/providers/twilio/utils.ts
6887
+ var POST_METHOD_NAMES = /* @__PURE__ */ new Set(["post"]);
6888
+ var ROUTER_OBJECT_NAMES = /* @__PURE__ */ new Set(["server", "app", "router", "fastify"]);
6889
+ function isPostRouteRegistration(node) {
6890
+ if (node?.type !== "CallExpression") return false;
6891
+ const callee = node.callee;
6892
+ if (callee?.type !== "MemberExpression") return false;
6893
+ if (callee.property?.type !== "Identifier" || !POST_METHOD_NAMES.has(callee.property.name)) return false;
6894
+ return callee.object?.type === "Identifier" && ROUTER_OBJECT_NAMES.has(callee.object.name);
6895
+ }
6896
+ function findInSubtree(node, predicate, depth = 0) {
6897
+ if (!node || typeof node !== "object" || depth > 40) return null;
6898
+ if (Array.isArray(node)) {
6899
+ for (const n of node) {
6900
+ const found = findInSubtree(n, predicate, depth + 1);
6901
+ if (found) return found;
6902
+ }
6903
+ return null;
6904
+ }
6905
+ if (predicate(node)) return node;
6906
+ for (const key of Object.keys(node)) {
6907
+ if (key === "parent" || key === "loc" || key === "range") continue;
6908
+ const val = node[key];
6909
+ if (val && typeof val === "object") {
6910
+ const found = findInSubtree(val, predicate, depth + 1);
6911
+ if (found) return found;
6912
+ }
6913
+ }
6914
+ return null;
6915
+ }
6916
+ function referencesRequestBody(node) {
6917
+ return !!findInSubtree(node, (n) => {
6918
+ if (n?.type !== "MemberExpression") return false;
6919
+ if (n.property?.type !== "Identifier" || n.property.name !== "body") return false;
6920
+ return n.object?.type === "Identifier" && (n.object.name === "req" || n.object.name === "request");
6921
+ });
6922
+ }
6923
+ function collectVarDeclarators2(node, out, depth = 0) {
6924
+ if (!node || typeof node !== "object" || depth > 24) return;
6925
+ if (Array.isArray(node)) {
6926
+ for (const n of node) collectVarDeclarators2(n, out, depth + 1);
6927
+ return;
6928
+ }
6929
+ if (node.type === "VariableDeclarator") out.push(node);
6930
+ for (const key of Object.keys(node)) {
6931
+ if (key === "parent" || key === "loc" || key === "range") continue;
6932
+ const val = node[key];
6933
+ if (val && typeof val === "object") collectVarDeclarators2(val, out, depth + 1);
6934
+ }
6935
+ }
6936
+
6937
+ // src/providers/twilio/rules/validate-webhook-signature.ts
6938
+ var rule91 = {
6939
+ meta: {
6940
+ type: "problem",
6941
+ docs: {
6942
+ description: "Twilio webhook routes must validate the X-Twilio-Signature header before trusting the body",
6943
+ category: "security",
6944
+ cwe: "CWE-345",
6945
+ owasp: "A07:2021 Identification and Authentication Failures",
6946
+ rationale: "Webhook URLs are public. Anyone who learns or guesses the path can POST a forged body \u2014 fake CallSid/From/TaskAttributes values \u2014 and the handler has no way to tell it apart from a real Twilio request. Without validating the signature first, an attacker can trigger outbound calls (toll/billing impact) or falsely invoke reservation/task callbacks to manipulate call state for an arbitrary caller.",
6947
+ docsUrl: "https://www.twilio.com/docs/usage/webhooks/webhooks-security",
6948
+ recommended: true
6949
+ },
6950
+ messages: {
6951
+ missingSignatureValidation: "This POST webhook route reads req.body but the file never validates the X-Twilio-Signature header via twilio.validateRequest()/RequestValidator \u2014 a forged request body is indistinguishable from a real Twilio callback."
6952
+ }
6953
+ },
6954
+ create(context) {
6955
+ function isValidateRequestCall(n) {
6956
+ if (n?.type !== "CallExpression") return false;
6957
+ const callee = n.callee;
6958
+ if (callee?.type !== "MemberExpression") return false;
6959
+ return callee.property?.type === "Identifier" && callee.property.name === "validateRequest";
6960
+ }
6961
+ function isRequestValidatorConstruction(n) {
6962
+ return n?.type === "NewExpression" && n.callee?.type === "Identifier" && n.callee.name === "RequestValidator";
6963
+ }
6964
+ return {
6965
+ "Program:exit"(program) {
6966
+ const hasValidation = !!findInSubtree(program, isValidateRequestCall) || !!findInSubtree(program, isRequestValidatorConstruction);
6967
+ if (hasValidation) return;
6968
+ const postRoutes = [];
6969
+ findInSubtree(program, (n) => {
6970
+ if (isPostRouteRegistration(n) && referencesRequestBody(n)) postRoutes.push(n);
6971
+ return false;
6972
+ });
6973
+ for (const route of postRoutes) {
6974
+ context.report({ node: route, messageId: "missingSignatureValidation" });
6975
+ }
6976
+ }
6977
+ };
6978
+ }
6979
+ };
6980
+ var twilioValidateWebhookSignatureRule = rule91;
6981
+
6982
+ // src/providers/twilio/rules/taskrouter-attributes-match-consumer.ts
6983
+ var rule92 = {
6984
+ meta: {
6985
+ type: "problem",
6986
+ docs: {
6987
+ description: "TaskRouter Task attributes must include every field the reservation handler reads back out",
6988
+ category: "correctness",
6989
+ rationale: "A Task is created with attributes like `{ name, type }`, and later a reservation-accepted handler does `const { from } = JSON.parse(req.body.TaskAttributes)` to look up state by `from`. If the producer never set `from` on the Task attributes, that destructure is always `undefined`, the lookup always misses, and the handler returns 404 \u2014 even though every other part of the flow worked correctly.",
6990
+ docsUrl: "https://www.twilio.com/docs/taskrouter/twiml-queue-calls",
6991
+ recommended: true
6992
+ },
6993
+ messages: {
6994
+ attributeMismatch: 'This .task() attributes object does not set "{{field}}", but a reservation handler in this codebase destructures "{{field}}" from JSON.parse(TaskAttributes) \u2014 that lookup will always be undefined.'
6995
+ }
6996
+ },
6997
+ create(context) {
6998
+ function isTaskCall(n) {
6999
+ if (n?.type !== "CallExpression") return false;
7000
+ const callee = n.callee;
7001
+ if (callee?.type !== "MemberExpression") return false;
7002
+ return callee.property?.type === "Identifier" && callee.property.name === "task";
7003
+ }
7004
+ function extractAttributeKeys(taskCallNode) {
7005
+ const arg = taskCallNode.arguments?.[0];
7006
+ if (!arg) return null;
7007
+ if (arg.type === "CallExpression" && arg.callee?.type === "MemberExpression" && arg.callee.object?.type === "Identifier" && arg.callee.object.name === "JSON" && arg.callee.property?.type === "Identifier" && arg.callee.property.name === "stringify" && arg.arguments?.[0]?.type === "ObjectExpression") {
7008
+ return objectKeys(arg.arguments[0]);
7009
+ }
7010
+ if (arg.type === "TemplateLiteral") {
7011
+ const raw = arg.quasis.map((q) => q.value?.raw ?? "").join("");
7012
+ const keys = /* @__PURE__ */ new Set();
7013
+ for (const m of raw.matchAll(/"([a-zA-Z0-9_]+)"\s*:/g)) keys.add(m[1]);
7014
+ return keys;
7015
+ }
7016
+ if (arg.type === "Literal" && typeof arg.value === "string") {
7017
+ const keys = /* @__PURE__ */ new Set();
7018
+ for (const m of arg.value.matchAll(/"([a-zA-Z0-9_]+)"\s*:/g)) keys.add(m[1]);
7019
+ return keys;
7020
+ }
7021
+ return null;
7022
+ }
7023
+ function objectKeys(objExpr) {
7024
+ const keys = /* @__PURE__ */ new Set();
7025
+ for (const p of objExpr.properties ?? []) {
7026
+ if (p.type !== "Property") continue;
7027
+ const name = p.key?.type === "Identifier" ? p.key.name : p.key?.type === "Literal" ? p.key.value : null;
7028
+ if (typeof name === "string") keys.add(name);
7029
+ }
7030
+ return keys;
7031
+ }
7032
+ function isTaskAttributesJsonParse(n) {
7033
+ if (n?.type !== "CallExpression") return false;
7034
+ const callee = n.callee;
7035
+ if (callee?.type !== "MemberExpression") return false;
7036
+ if (callee.object?.type !== "Identifier" || callee.object.name !== "JSON") return false;
7037
+ if (callee.property?.type !== "Identifier" || callee.property.name !== "parse") return false;
7038
+ const arg = n.arguments?.[0];
7039
+ if (arg?.type !== "MemberExpression") return false;
7040
+ return arg.property?.type === "Identifier" && arg.property.name === "TaskAttributes";
7041
+ }
7042
+ function findConsumedFields(program) {
7043
+ const fields = /* @__PURE__ */ new Set();
7044
+ const declarators = [];
7045
+ collectVarDeclarators2(program, declarators);
7046
+ for (const d of declarators) {
7047
+ if (!isTaskAttributesJsonParse(d.init)) continue;
7048
+ if (d.id?.type === "ObjectPattern") {
7049
+ for (const p of d.id.properties ?? []) {
7050
+ if (p.type !== "Property") continue;
7051
+ const name = p.key?.type === "Identifier" ? p.key.name : null;
7052
+ if (name) fields.add(name);
7053
+ }
7054
+ }
7055
+ }
7056
+ return fields;
7057
+ }
7058
+ return {
7059
+ "Program:exit"(program) {
7060
+ const consumedFields = findConsumedFields(program);
7061
+ if (consumedFields.size === 0) return;
7062
+ const taskCalls = [];
7063
+ findInSubtree(program, (n) => {
7064
+ if (isTaskCall(n)) taskCalls.push(n);
7065
+ return false;
7066
+ });
7067
+ for (const taskCall of taskCalls) {
7068
+ const producedKeys = extractAttributeKeys(taskCall);
7069
+ if (!producedKeys) continue;
7070
+ for (const field of consumedFields) {
7071
+ if (!producedKeys.has(field)) {
7072
+ context.report({ node: taskCall, messageId: "attributeMismatch", data: { field } });
7073
+ }
7074
+ }
7075
+ }
7076
+ }
7077
+ };
7078
+ }
7079
+ };
7080
+ var twilioTaskrouterAttributesMatchConsumerRule = rule92;
7081
+
7082
+ // src/providers/twilio/rules/enqueue-task-json-stringify.ts
7083
+ var rule93 = {
7084
+ meta: {
7085
+ type: "problem",
7086
+ docs: {
7087
+ description: "TaskRouter .task() attributes must be built with JSON.stringify(), not raw string interpolation",
7088
+ category: "security",
7089
+ cwe: "CWE-116",
7090
+ owasp: "CWE-91 XML/JSON Injection",
7091
+ rationale: 'Hand-building the Task attributes JSON by interpolating an unvalidated request field directly into a string literal breaks the moment that field contains a `"` or `\\`. The resulting body is invalid JSON, and Twilio rejects the Enqueue with error 14221 ("Provided Attributes JSON was not valid") \u2014 turning a single unexpected character in a caller-controlled field (like a name with a quote in it) into a hard failure of the call flow.',
7092
+ docsUrl: "https://www.twilio.com/docs/api/errors/14221",
7093
+ recommended: true
7094
+ },
7095
+ messages: {
7096
+ rawJsonInterpolation: 'This .task() call builds JSON attributes via raw string interpolation instead of JSON.stringify() \u2014 a "/\\\\ character in the interpolated value produces invalid JSON and Twilio error 14221.'
7097
+ }
7098
+ },
7099
+ create(context) {
7100
+ function isTaskCall(n) {
7101
+ if (n?.type !== "CallExpression") return false;
7102
+ const callee = n.callee;
7103
+ if (callee?.type !== "MemberExpression") return false;
7104
+ return callee.property?.type === "Identifier" && callee.property.name === "task";
7105
+ }
7106
+ function hasInterpolatedExpression(templateLiteral) {
7107
+ return (templateLiteral.expressions ?? []).length > 0;
7108
+ }
7109
+ return {
7110
+ CallExpression(node) {
7111
+ if (!isTaskCall(node)) return;
7112
+ const arg = node.arguments?.[0];
7113
+ if (!arg) return;
7114
+ if (arg.type === "TemplateLiteral" && hasInterpolatedExpression(arg)) {
7115
+ context.report({ node, messageId: "rawJsonInterpolation" });
7116
+ return;
7117
+ }
7118
+ if (arg.type === "BinaryExpression" && arg.operator === "+") {
7119
+ context.report({ node, messageId: "rawJsonInterpolation" });
7120
+ }
7121
+ }
7122
+ };
7123
+ }
7124
+ };
7125
+ var twilioEnqueueTaskJsonStringifyRule = rule93;
7126
+
7127
+ // src/providers/twilio/rules/media-streams-key-by-call-sid.ts
7128
+ var rule94 = {
7129
+ meta: {
7130
+ type: "problem",
7131
+ docs: {
7132
+ description: "Media Streams session maps must be keyed by callSid, not phone number",
7133
+ category: "reliability",
7134
+ rationale: "A Map keyed on the caller's phone number breaks the moment the same number places two concurrent calls \u2014 a retry after a dropped call, two family members sharing a line, a misdial-and-redial. The second map.set() silently overwrites the first call's entry, and that first call's downstream events (reservation-accepted, outbound-leg wiring) end up pointed at the wrong session, crossing audio streams between two unrelated calls. Twilio provides a unique callSid on every start message specifically to avoid this collision.",
7135
+ docsUrl: "https://www.twilio.com/docs/voice/media-streams/websocket-messages",
7136
+ recommended: true
7137
+ },
7138
+ messages: {
7139
+ keyedByPhoneNumber: `This Map is keyed by "{{key}}" (a phone-number-like field) instead of callSid \u2014 two concurrent calls from the same number will silently overwrite each other's entry.`
7140
+ }
7141
+ },
7142
+ create(context) {
7143
+ function isFromLikeIdentifier(n) {
7144
+ if (n?.type === "Identifier" && n.name === "from") return "from";
7145
+ if (n?.type === "MemberExpression" && n.property?.type === "Identifier" && n.property.name === "from" && !n.computed) {
7146
+ return "from";
7147
+ }
7148
+ return null;
7149
+ }
7150
+ function isMapMutationCall(n, methodNames) {
7151
+ if (n?.type !== "CallExpression") return false;
7152
+ const callee = n.callee;
7153
+ if (callee?.type !== "MemberExpression") return false;
7154
+ return callee.property?.type === "Identifier" && methodNames.has(callee.property.name);
7155
+ }
7156
+ const SET_OR_GET = /* @__PURE__ */ new Set(["set", "get"]);
7157
+ return {
7158
+ "Program:exit"(program) {
7159
+ let callSidAvailable = false;
7160
+ function scanForCallSid(n, depth = 0) {
7161
+ if (callSidAvailable || !n || typeof n !== "object" || depth > 60) return;
7162
+ if (Array.isArray(n)) {
7163
+ for (const item of n) scanForCallSid(item, depth + 1);
7164
+ return;
7165
+ }
7166
+ if (n.type === "Identifier" && n.name === "callSid") {
7167
+ callSidAvailable = true;
7168
+ return;
7169
+ }
7170
+ if (n.type === "MemberExpression" && !n.computed && n.property?.type === "Identifier" && n.property.name === "callSid") {
7171
+ callSidAvailable = true;
7172
+ return;
7173
+ }
7174
+ for (const key of Object.keys(n)) {
7175
+ if (key === "parent" || key === "loc" || key === "range") continue;
7176
+ const val = n[key];
7177
+ if (val && typeof val === "object") scanForCallSid(val, depth + 1);
7178
+ }
7179
+ }
7180
+ scanForCallSid(program);
7181
+ if (!callSidAvailable) return;
7182
+ function walk2(n, depth = 0) {
7183
+ if (!n || typeof n !== "object" || depth > 60) return;
7184
+ if (Array.isArray(n)) {
7185
+ for (const item of n) walk2(item, depth + 1);
7186
+ return;
7187
+ }
7188
+ if (isMapMutationCall(n, SET_OR_GET)) {
7189
+ const firstArg = n.arguments?.[0];
7190
+ const key = isFromLikeIdentifier(firstArg);
7191
+ if (key) context.report({ node: n, messageId: "keyedByPhoneNumber", data: { key } });
7192
+ }
7193
+ for (const k of Object.keys(n)) {
7194
+ if (k === "parent" || k === "loc" || k === "range") continue;
7195
+ const val = n[k];
7196
+ if (val && typeof val === "object") walk2(val, depth + 1);
7197
+ }
7198
+ }
7199
+ walk2(program);
7200
+ }
7201
+ };
7202
+ }
7203
+ };
7204
+ var twilioMediaStreamsKeyByCallSidRule = rule94;
7205
+
7206
+ // src/providers/twilio/rules/await-or-catch-rest-calls-in-event-handlers.ts
7207
+ var REST_RESOURCE_METHODS = /* @__PURE__ */ new Set(["create", "update", "remove", "fetch"]);
7208
+ var EVENT_REGISTRATION_METHODS = /* @__PURE__ */ new Set(["onStart", "onStop", "onMedia", "onConnected", "on"]);
7209
+ var rule95 = {
7210
+ meta: {
7211
+ type: "problem",
7212
+ docs: {
7213
+ description: "Twilio REST calls inside event-handler callbacks must be wrapped in try/catch",
7214
+ category: "reliability",
7215
+ rationale: 'Event-emitter style callbacks (onStart, onMessage, on("message", ...)) are typically invoked via something like `callbacks.map((cb) => cb(event))`, which discards the returned promise. If the callback is `async` and calls `await twilio.calls.create(...)` with no try/catch, any Twilio REST error (invalid number, suspended account, rate limit) becomes an unhandled promise rejection. If the process has a global `unhandledRejection` handler that calls `process.exit()` (a common pattern), one failed outbound call takes down the entire server and every other in-progress call, not just the failing one.',
7216
+ docsUrl: "https://www.twilio.com/docs/usage/webhooks/webhooks-faq",
7217
+ recommended: true
7218
+ },
7219
+ messages: {
7220
+ missingTryCatch: "This await twilio.{{resource}}.{{method}}() call is inside an event-handler callback with no surrounding try/catch \u2014 a REST API error here becomes an unhandled promise rejection that can crash the whole process."
7221
+ }
7222
+ },
7223
+ create(context) {
7224
+ function isAwaitedTwilioRestCall(n) {
7225
+ if (n?.type !== "AwaitExpression") return null;
7226
+ const call = n.argument;
7227
+ if (call?.type !== "CallExpression") return null;
7228
+ const callee = call.callee;
7229
+ if (callee?.type !== "MemberExpression") return null;
7230
+ const method = callee.property?.type === "Identifier" ? callee.property.name : null;
7231
+ if (!method || !REST_RESOURCE_METHODS.has(method)) return null;
7232
+ const obj = callee.object;
7233
+ if (obj?.type !== "MemberExpression") return null;
7234
+ const resource = obj.property?.type === "Identifier" ? obj.property.name : null;
7235
+ if (!resource) return null;
7236
+ const base = obj.object;
7237
+ const baseName = base?.type === "Identifier" ? base.name : null;
7238
+ if (!baseName || !/^(twilio\w*|client)$/i.test(baseName)) return null;
7239
+ return { resource, method };
7240
+ }
7241
+ function isEventHandlerRegistration(n) {
7242
+ if (n?.type !== "CallExpression") return false;
7243
+ const callee = n.callee;
7244
+ if (callee?.type !== "MemberExpression") return false;
7245
+ return callee.property?.type === "Identifier" && EVENT_REGISTRATION_METHODS.has(callee.property.name);
7246
+ }
7247
+ function isAsyncCallback(n) {
7248
+ return (n?.type === "ArrowFunctionExpression" || n?.type === "FunctionExpression") && n.async === true;
7249
+ }
7250
+ function posOf2(n) {
7251
+ if (typeof n?.range?.[0] === "number") return n.range[0];
7252
+ const line = n?.loc?.start?.line ?? 0;
7253
+ const column = n?.loc?.start?.column ?? 0;
7254
+ return line * 1e6 + column;
7255
+ }
7256
+ function posEnd(n) {
7257
+ if (typeof n?.range?.[1] === "number") return n.range[1];
7258
+ const line = n?.loc?.end?.line ?? n?.loc?.start?.line ?? 0;
7259
+ const column = n?.loc?.end?.column ?? n?.loc?.start?.column ?? 0;
7260
+ return line * 1e6 + column;
7261
+ }
7262
+ function isWithinTryBlock(node, root) {
7263
+ const tryRanges = [];
7264
+ function collectTryRanges(n, depth = 0) {
7265
+ if (!n || typeof n !== "object" || depth > 60) return;
7266
+ if (Array.isArray(n)) {
7267
+ for (const item of n) collectTryRanges(item, depth + 1);
7268
+ return;
7269
+ }
7270
+ if (n.type === "TryStatement" && n.block) {
7271
+ tryRanges.push([posOf2(n.block), posEnd(n.block)]);
7272
+ }
7273
+ for (const key of Object.keys(n)) {
7274
+ if (key === "parent" || key === "loc" || key === "range") continue;
7275
+ const val = n[key];
7276
+ if (val && typeof val === "object") collectTryRanges(val, depth + 1);
7277
+ }
7278
+ }
7279
+ collectTryRanges(root);
7280
+ const p = posOf2(node);
7281
+ return tryRanges.some(([start, end]) => p >= start && p <= end);
7282
+ }
7283
+ return {
7284
+ CallExpression(node) {
7285
+ if (!isEventHandlerRegistration(node)) return;
7286
+ const callback = node.arguments?.find((a) => isAsyncCallback(a));
7287
+ if (!callback) return;
7288
+ const restCalls = [];
7289
+ function collect(n, depth = 0) {
7290
+ if (!n || typeof n !== "object" || depth > 40) return;
7291
+ if (Array.isArray(n)) {
7292
+ for (const item of n) collect(item, depth + 1);
7293
+ return;
7294
+ }
7295
+ const info = isAwaitedTwilioRestCall(n);
7296
+ if (info) restCalls.push({ node: n, ...info });
7297
+ for (const key of Object.keys(n)) {
7298
+ if (key === "parent" || key === "loc" || key === "range") continue;
7299
+ const val = n[key];
7300
+ if (val && typeof val === "object") collect(val, depth + 1);
7301
+ }
7302
+ }
7303
+ collect(callback.body);
7304
+ for (const call of restCalls) {
7305
+ if (!isWithinTryBlock(call.node, callback.body)) {
7306
+ context.report({
7307
+ node: call.node,
7308
+ messageId: "missingTryCatch",
7309
+ data: { resource: call.resource, method: call.method }
7310
+ });
7311
+ }
7312
+ }
7313
+ }
7314
+ };
7315
+ }
7316
+ };
7317
+ var twilioAwaitOrCatchRestCallsInEventHandlersRule = rule95;
7318
+
7319
+ // src/providers/twilio/rules/use-twiml-builder-not-string-templates.ts
7320
+ var rule96 = {
7321
+ meta: {
7322
+ type: "suggestion",
7323
+ docs: {
7324
+ description: "TwiML responses must be built with the VoiceResponse builder, not raw XML template strings",
7325
+ category: "security",
7326
+ cwe: "CWE-91",
7327
+ rationale: "The VoiceResponse builder XML-escapes every interpolated value automatically. A raw template literal that interpolates values directly into XML attribute or text positions has no such protection \u2014 if either value's source ever changes to something attacker-influenced (e.g. a SIP header instead of a Twilio-controlled CallSid), unescaped `<`/`\"`/`&` characters can break out of the intended XML structure.",
7328
+ docsUrl: "https://www.twilio.com/docs/voice/twiml/stream",
7329
+ recommended: true
7330
+ },
7331
+ messages: {
7332
+ rawTwimlTemplate: "TwiML is built here as a raw template literal with interpolated values instead of the VoiceResponse builder \u2014 interpolated values are not XML-escaped, unlike new VoiceResponse()."
7333
+ }
7334
+ },
7335
+ create(context) {
7336
+ function looksLikeTwimlXml(raw) {
7337
+ return /<Response>|<Connect>|<Say>|<Stream\b|<Enqueue\b|<Dial\b/.test(raw);
7338
+ }
7339
+ return {
7340
+ TemplateLiteral(node) {
7341
+ if ((node.expressions ?? []).length === 0) return;
7342
+ const raw = (node.quasis ?? []).map((q) => q.value?.raw ?? "").join("");
7343
+ if (!looksLikeTwimlXml(raw)) return;
7344
+ context.report({ node, messageId: "rawTwimlTemplate" });
7345
+ }
7346
+ };
7347
+ }
7348
+ };
7349
+ var twilioUseTwimlBuilderNotStringTemplatesRule = rule96;
7350
+
7351
+ // src/providers/twilio/rules/media-streams-mark-pacing.ts
7352
+ var rule97 = {
7353
+ meta: {
7354
+ type: "suggestion",
7355
+ docs: {
7356
+ description: "Media Streams audio forwarding should use mark-based pacing to avoid buffer overflow",
7357
+ category: "reliability",
7358
+ rationale: 'Twilio buffers at most 10 minutes of audio per bidirectional Stream. If audio chunks are forwarded with no mark events and no pacing, and the sender produces media faster than real-time (or playback stalls), Twilio stops accepting further audio for that Stream and emits warning 31931 ("Media Discarded") \u2014 silently dropping audio rather than erroring loudly.',
7359
+ docsUrl: "https://www.twilio.com/docs/api/errors/31931",
7360
+ recommended: true
7361
+ },
7362
+ messages: {
7363
+ noMarkPacing: "This file forwards media payloads via send() but never passes isLast/a mark argument anywhere \u2014 sustained high-throughput forwarding risks Twilio silently discarding buffered audio (warning 31931)."
7364
+ }
7365
+ },
7366
+ create(context) {
7367
+ function isMediaSendCall(n) {
7368
+ if (n?.type !== "CallExpression") return false;
7369
+ const callee = n.callee;
7370
+ if (callee?.type !== "MemberExpression") return false;
7371
+ if (callee.property?.type !== "Identifier" || callee.property.name !== "send") return false;
7372
+ const arg = n.arguments?.[0];
7373
+ const argText = arg?.type === "ArrayExpression" && (arg.elements ?? []).some(
7374
+ (el) => el?.type === "MemberExpression" && el.property?.type === "Identifier" && el.property.name === "delta"
7375
+ ) || arg?.type === "Identifier" && /delta|payload|media/i.test(arg.name);
7376
+ return !!argText;
7377
+ }
7378
+ function hasIsLastOrMarkSignal(program) {
7379
+ let found = false;
7380
+ function walk2(n, depth = 0) {
7381
+ if (found || !n || typeof n !== "object" || depth > 60) return;
7382
+ if (Array.isArray(n)) {
7383
+ for (const item of n) walk2(item, depth + 1);
7384
+ return;
7385
+ }
7386
+ if (typeof n.type === "string" && n.type.startsWith("TS")) return;
7387
+ if (n.type === "CallExpression") {
7388
+ const callee = n.callee;
7389
+ if (callee?.type === "MemberExpression" && callee.property?.type === "Identifier" && callee.property.name === "send") {
7390
+ const second = n.arguments?.[1];
7391
+ if (second?.type === "Literal" && second.value === true) {
7392
+ found = true;
7393
+ return;
7394
+ }
7395
+ if (second?.type === "Identifier" && /isLast/i.test(second.name)) {
7396
+ found = true;
7397
+ return;
7398
+ }
7399
+ }
7400
+ }
7401
+ if (n.type === "Identifier" && n.name === "isLast") {
7402
+ found = true;
7403
+ return;
7404
+ }
7405
+ for (const key of Object.keys(n)) {
7406
+ if (key === "parent" || key === "loc" || key === "range" || key === "typeAnnotation" || key === "returnType") continue;
7407
+ const val = n[key];
7408
+ if (val && typeof val === "object") walk2(val, depth + 1);
7409
+ }
7410
+ }
7411
+ walk2(program);
7412
+ return found;
7413
+ }
7414
+ return {
7415
+ "Program:exit"(program) {
7416
+ const sendCalls = [];
7417
+ findInSubtree(program, (n) => {
7418
+ if (isMediaSendCall(n)) sendCalls.push(n);
7419
+ return false;
7420
+ });
7421
+ if (sendCalls.length === 0) return;
7422
+ if (hasIsLastOrMarkSignal(program)) return;
7423
+ context.report({ node: sendCalls[0], messageId: "noMarkPacing" });
7424
+ }
7425
+ };
7426
+ }
7427
+ };
7428
+ var twilioMediaStreamsMarkPacingRule = rule97;
7429
+
7430
+ // src/providers/twilio/rules/validate-all-request-inputs.ts
7431
+ var rule98 = {
7432
+ meta: {
7433
+ type: "suggestion",
7434
+ docs: {
7435
+ description: "Fastify Twilio webhook routes must declare a schema for every request input they read",
7436
+ category: "correctness",
7437
+ rationale: "A route that validates `body` but not `querystring` (or declares no schema at all) lets unvalidated, possibly-undefined fields flow downstream. If a later call does `value.toString()` or similar on a field that never arrived, that throws instead of failing the request cleanly with a 400 \u2014 the failure surfaces far from the actual missing-validation root cause.",
7438
+ docsUrl: "https://www.twilio.com/docs/voice/twiml/stream",
7439
+ recommended: true
7440
+ },
7441
+ messages: {
7442
+ missingQuerystringSchema: "This route reads req.query.{{field}} but the route schema validates body without a matching querystring schema \u2014 a missing query param flows downstream as undefined instead of failing with a 400.",
7443
+ missingSchemaEntirely: "This route reads req.body.{{field}} but declares no request schema at all \u2014 req.body is untyped and unvalidated before use."
7444
+ }
7445
+ },
7446
+ create(context) {
7447
+ function getOptionsArg(routeCall) {
7448
+ return routeCall.arguments?.length === 3 ? routeCall.arguments[1] : null;
7449
+ }
7450
+ function getHandlerArg(routeCall) {
7451
+ const args = routeCall.arguments ?? [];
7452
+ return args[args.length - 1];
7453
+ }
7454
+ function hasSchemaProperty(optionsArg, propName2) {
7455
+ if (optionsArg?.type !== "ObjectExpression") return false;
7456
+ const schemaProp = (optionsArg.properties ?? []).find(
7457
+ (p) => p.type === "Property" && p.key?.type === "Identifier" && p.key.name === "schema"
7458
+ );
7459
+ if (!schemaProp || schemaProp.value?.type !== "ObjectExpression") return false;
7460
+ return (schemaProp.value.properties ?? []).some(
7461
+ (p) => p.type === "Property" && p.key?.type === "Identifier" && p.key.name === propName2
7462
+ );
7463
+ }
7464
+ function hasAnySchema(optionsArg) {
7465
+ if (optionsArg?.type !== "ObjectExpression") return false;
7466
+ return (optionsArg.properties ?? []).some(
7467
+ (p) => p.type === "Property" && p.key?.type === "Identifier" && p.key.name === "schema"
7468
+ );
7469
+ }
7470
+ function findReadFields(handlerBody, objectNames) {
7471
+ const fields = /* @__PURE__ */ new Set();
7472
+ findInSubtree(handlerBody, (n) => {
7473
+ if (n?.type !== "MemberExpression") return false;
7474
+ const obj = n.object;
7475
+ if (obj?.type !== "MemberExpression") return false;
7476
+ if (obj.object?.type !== "Identifier" || obj.object.name !== "req" && obj.object.name !== "request") return false;
7477
+ if (obj.property?.type !== "Identifier" || !objectNames.has(obj.property.name)) return false;
7478
+ const field = n.property?.type === "Identifier" ? n.property.name : null;
7479
+ if (field) fields.add(field);
7480
+ return false;
7481
+ });
7482
+ return fields;
7483
+ }
7484
+ return {
7485
+ CallExpression(node) {
7486
+ if (!isPostRouteRegistration(node)) return;
7487
+ const optionsArg = getOptionsArg(node);
7488
+ const handler = getHandlerArg(node);
7489
+ if (!handler || handler.type !== "ArrowFunctionExpression" && handler.type !== "FunctionExpression") return;
7490
+ const queryFields = findReadFields(handler.body, /* @__PURE__ */ new Set(["query"]));
7491
+ const bodyFields = findReadFields(handler.body, /* @__PURE__ */ new Set(["body"]));
7492
+ const hasBodySchema = hasSchemaProperty(optionsArg, "body");
7493
+ const hasQuerystringSchema = hasSchemaProperty(optionsArg, "querystring");
7494
+ if (queryFields.size > 0 && hasBodySchema && !hasQuerystringSchema) {
7495
+ const field = [...queryFields][0];
7496
+ context.report({ node, messageId: "missingQuerystringSchema", data: { field } });
7497
+ }
7498
+ if (bodyFields.size > 0 && !hasAnySchema(optionsArg)) {
7499
+ const field = [...bodyFields][0];
7500
+ context.report({ node, messageId: "missingSchemaEntirely", data: { field } });
7501
+ }
7502
+ }
7503
+ };
7504
+ }
7505
+ };
7506
+ var twilioValidateAllRequestInputsRule = rule98;
7507
+
7508
+ // src/providers/twilio/rules/media-streams-mark-name-string.ts
7509
+ var rule99 = {
7510
+ meta: {
7511
+ type: "suggestion",
7512
+ docs: {
7513
+ description: "Media Streams mark.name must be a string, not a number",
7514
+ category: "correctness",
7515
+ rationale: 'Twilio\'s documented Media Streams `mark` message schema and every example show `mark.name` as a string (e.g. "my label"). Setting it to a bare number \u2014 `Date.now()` is a common culprit \u2014 means JSON.stringify() serializes it as a numeric literal, not the documented string type, a latent type mismatch that surfaces once mark-based pacing actually depends on matching mark names.',
7516
+ docsUrl: "https://www.twilio.com/docs/voice/media-streams/websocket-messages",
7517
+ recommended: true
7518
+ },
7519
+ messages: {
7520
+ markNameNotString: "mark.name is set to a non-string value here \u2014 Twilio's documented schema requires mark.name to be a string (e.g. String(Date.now()))."
7521
+ }
7522
+ },
7523
+ create(context) {
7524
+ const NUMERIC_CALLS = /* @__PURE__ */ new Set(["Date.now", "performance.now", "Math.random", "Math.floor", "Math.round"]);
7525
+ function isNumericLooking(n) {
7526
+ if (!n) return false;
7527
+ if (n.type === "Literal" && typeof n.value === "number") return true;
7528
+ if (n.type === "CallExpression") {
7529
+ const callee = n.callee;
7530
+ if (callee?.type === "Identifier") return false;
7531
+ if (callee?.type === "MemberExpression" && callee.object?.type === "Identifier" && callee.property?.type === "Identifier") {
7532
+ return NUMERIC_CALLS.has(`${callee.object.name}.${callee.property.name}`);
7533
+ }
7534
+ return false;
7535
+ }
7536
+ return false;
7537
+ }
7538
+ function findMarkNameProperty(markObjExpr) {
7539
+ for (const p of markObjExpr.properties ?? []) {
7540
+ if (p.type !== "Property") continue;
7541
+ const keyName = p.key?.type === "Identifier" ? p.key.name : p.key?.type === "Literal" ? p.key.value : null;
7542
+ if (keyName === "name") return p;
7543
+ }
7544
+ return null;
7545
+ }
7546
+ return {
7547
+ Property(node) {
7548
+ const keyName = node.key?.type === "Identifier" ? node.key.name : node.key?.type === "Literal" ? node.key.value : null;
7549
+ if (keyName !== "mark") return;
7550
+ if (node.value?.type !== "ObjectExpression") return;
7551
+ const nameProp = findMarkNameProperty(node.value);
7552
+ if (!nameProp) return;
7553
+ if (!isNumericLooking(nameProp.value)) return;
7554
+ context.report({ node: nameProp, messageId: "markNameNotString" });
7555
+ }
7556
+ };
7557
+ }
7558
+ };
7559
+ var twilioMediaStreamsMarkNameStringRule = rule99;
7560
+
7561
+ // src/providers/openai-realtime/utils.ts
7562
+ function isOpenAIRealtimeUrlArg(node) {
7563
+ if (!node) return false;
7564
+ if (node.type === "Literal" && typeof node.value === "string") {
7565
+ return node.value.includes("api.openai.com/v1/realtime");
7566
+ }
7567
+ if (node.type === "TemplateLiteral") {
7568
+ return (node.quasis ?? []).some(
7569
+ (q) => typeof q?.value?.raw === "string" && q.value.raw.includes("api.openai.com/v1/realtime")
7570
+ );
7571
+ }
7572
+ return false;
7573
+ }
7574
+ function collectOpenAIRealtimeUrlVarNames(program) {
7575
+ const names = /* @__PURE__ */ new Set();
7576
+ visit(program);
7577
+ return names;
7578
+ function visit(node, depth = 0) {
7579
+ if (!node || typeof node !== "object" || depth > 40) return;
7580
+ if (Array.isArray(node)) {
7581
+ for (const n of node) visit(n, depth + 1);
7582
+ return;
7583
+ }
7584
+ if (node.type === "VariableDeclarator" && node.id?.type === "Identifier" && isOpenAIRealtimeUrlArg(node.init)) {
7585
+ names.add(node.id.name);
7586
+ }
7587
+ for (const key of Object.keys(node)) {
7588
+ if (key === "parent" || key === "loc" || key === "range") continue;
7589
+ const val = node[key];
7590
+ if (val && typeof val === "object") visit(val, depth + 1);
7591
+ }
7592
+ }
7593
+ }
7594
+ function isOpenAIRealtimeUrlNode(urlArgNode, urlVarNames) {
7595
+ if (isOpenAIRealtimeUrlArg(urlArgNode)) return true;
7596
+ return urlArgNode?.type === "Identifier" && urlVarNames.has(urlArgNode.name);
7597
+ }
7598
+ function isOpenAIRealtimeNewWebSocket(node, urlVarNames = /* @__PURE__ */ new Set()) {
7599
+ if (node?.type !== "NewExpression") return false;
7600
+ if (node.callee?.type !== "Identifier" || node.callee.name !== "WebSocket") return false;
7601
+ return isOpenAIRealtimeUrlNode(node.arguments?.[0], urlVarNames);
7602
+ }
7603
+ function findProperty4(objectExpression, propertyName) {
7604
+ if (objectExpression?.type !== "ObjectExpression") return null;
7605
+ for (const prop of objectExpression.properties ?? []) {
7606
+ if (prop?.type !== "Property") continue;
7607
+ const key = prop.key;
7608
+ const name = key?.type === "Identifier" ? key.name : key?.type === "Literal" ? key.value : null;
7609
+ if (name === propertyName) return prop;
7610
+ }
7611
+ return null;
7612
+ }
7613
+ function collectOpenAIRealtimeSocketVarNames(program) {
7614
+ const urlVarNames = collectOpenAIRealtimeUrlVarNames(program);
7615
+ const names = /* @__PURE__ */ new Set();
7616
+ visit(program);
7617
+ return names;
7618
+ function visit(node, depth = 0) {
7619
+ if (!node || typeof node !== "object" || depth > 40) return;
7620
+ if (Array.isArray(node)) {
7621
+ for (const n of node) visit(n, depth + 1);
7622
+ return;
7623
+ }
7624
+ if (node.type === "VariableDeclarator" && node.id?.type === "Identifier" && isOpenAIRealtimeNewWebSocket(node.init, urlVarNames)) {
7625
+ names.add(node.id.name);
7626
+ }
7627
+ if (node.type === "AssignmentExpression" && node.operator === "=" && isOpenAIRealtimeNewWebSocket(node.right, urlVarNames)) {
7628
+ const left = node.left;
7629
+ if (left?.type === "Identifier") {
7630
+ names.add(left.name);
7631
+ } else if (left?.type === "MemberExpression" && left.property) {
7632
+ const propName2 = left.property.name;
7633
+ if (typeof propName2 === "string") names.add(propName2);
7634
+ }
7635
+ }
7636
+ for (const key of Object.keys(node)) {
7637
+ if (key === "parent" || key === "loc" || key === "range") continue;
7638
+ const val = node[key];
7639
+ if (val && typeof val === "object") visit(val, depth + 1);
7640
+ }
7641
+ }
7642
+ }
7643
+ function isTrackedSocketRef(objNode, socketVarNames) {
7644
+ if (objNode?.type === "Identifier") return socketVarNames.has(objNode.name);
7645
+ if (objNode?.type === "MemberExpression" && objNode.property) {
7646
+ const propName2 = objNode.property.name;
7647
+ return typeof propName2 === "string" && socketVarNames.has(propName2);
7648
+ }
7649
+ return false;
7650
+ }
7651
+ function isSocketOnCall(node, socketVarNames, eventName) {
7652
+ if (node?.type !== "CallExpression") return false;
7653
+ const callee = node.callee;
7654
+ if (callee?.type !== "MemberExpression") return false;
7655
+ const prop = callee.property;
7656
+ if (prop?.type !== "Identifier" || prop.name !== "on") return false;
7657
+ if (!isTrackedSocketRef(callee.object, socketVarNames)) return false;
7658
+ const firstArg = node.arguments?.[0];
7659
+ return firstArg?.type === "Literal" && firstArg.value === eventName;
7660
+ }
7661
+ function collectStringVarValues(program) {
7662
+ const values = /* @__PURE__ */ new Map();
7663
+ visit(program);
7664
+ return values;
7665
+ function visit(node, depth = 0) {
7666
+ if (!node || typeof node !== "object" || depth > 40) return;
7667
+ if (Array.isArray(node)) {
7668
+ for (const n of node) visit(n, depth + 1);
7669
+ return;
7670
+ }
7671
+ if (node.type === "VariableDeclarator" && node.id?.type === "Identifier") {
7672
+ const init = node.init;
7673
+ if (init?.type === "Literal" && typeof init.value === "string") {
7674
+ values.set(node.id.name, init.value);
7675
+ } else if (init?.type === "TemplateLiteral" && (init.expressions ?? []).length === 0) {
7676
+ values.set(node.id.name, (init.quasis ?? []).map((q) => q.value?.raw ?? "").join(""));
7677
+ }
7678
+ }
7679
+ for (const key of Object.keys(node)) {
7680
+ if (key === "parent" || key === "loc" || key === "range") continue;
7681
+ const val = node[key];
7682
+ if (val && typeof val === "object") visit(val, depth + 1);
7683
+ }
7684
+ }
7685
+ }
7686
+ function resolveStringValue(node, stringVarValues) {
7687
+ if (!node) return null;
7688
+ if (node.type === "Literal" && typeof node.value === "string") return node.value;
7689
+ if (node.type === "TemplateLiteral" && (node.expressions ?? []).length === 0) {
7690
+ return (node.quasis ?? []).map((q) => q.value?.raw ?? "").join("");
7691
+ }
7692
+ if (node.type === "Identifier" && stringVarValues.has(node.name)) {
7693
+ return stringVarValues.get(node.name) ?? null;
7694
+ }
7695
+ return null;
7696
+ }
7697
+
7698
+ // src/providers/openai-realtime/rules/migrate-beta-to-ga.ts
7699
+ var rule100 = {
7700
+ meta: {
7701
+ type: "problem",
7702
+ docs: {
7703
+ description: "OpenAI Realtime connections must not pin to the deprecated beta interface",
7704
+ category: "correctness",
7705
+ docsUrl: "https://developers.openai.com/api/docs/guides/realtime",
7706
+ rationale: "OpenAI's current Realtime guide states that beta integrations must migrate to the GA interface before new work proceeds, and explicitly calls out removing the OpenAI-Beta: realtime=v1 header as a required step. Staying on the beta header keeps a connection locked to the legacy session shape and event names with no confirmed sunset date \u2014 a ticking liability rather than an active failure.",
7707
+ recommended: true
7708
+ },
7709
+ messages: {
7710
+ betaHeaderPresent: "This OpenAI Realtime connection sends the deprecated 'OpenAI-Beta: realtime=v1' header instead of using the GA interface."
7711
+ },
7712
+ schema: []
7713
+ },
7714
+ create(context) {
7715
+ let urlVarNames = /* @__PURE__ */ new Set();
7716
+ return {
7717
+ Program(node) {
7718
+ urlVarNames = collectOpenAIRealtimeUrlVarNames(node);
7719
+ },
7720
+ NewExpression(node) {
7721
+ if (!isOpenAIRealtimeNewWebSocket(node, urlVarNames)) return;
7722
+ const optionsArg = node.arguments?.[1];
7723
+ if (optionsArg?.type !== "ObjectExpression") return;
7724
+ const headersProp = findProperty4(optionsArg, "headers");
7725
+ if (headersProp?.value?.type !== "ObjectExpression") return;
7726
+ const betaProp = findProperty4(headersProp.value, "OpenAI-Beta");
7727
+ if (!betaProp) return;
7728
+ const value = betaProp.value;
7729
+ const isRealtimeV1 = value?.type === "Literal" && typeof value.value === "string" && value.value.includes("realtime=v1");
7730
+ if (!isRealtimeV1) return;
7731
+ context.report({ node: betaProp, messageId: "betaHeaderPresent" });
7732
+ }
7733
+ };
7734
+ }
7735
+ };
7736
+ var openaiRealtimeMigrateBetaToGaRule = rule100;
7737
+
7738
+ // src/providers/openai-realtime/rules/no-log-raw-message-payloads.ts
7739
+ var LOG_METHOD_NAMES = /* @__PURE__ */ new Set(["info", "warn", "error", "log"]);
7740
+ function isLogCall(node) {
7741
+ if (node?.type !== "CallExpression") return { matches: false };
7742
+ const callee = node.callee;
7743
+ if (callee?.type !== "MemberExpression") return { matches: false };
7744
+ const prop = callee.property;
7745
+ if (prop?.type !== "Identifier" || !LOG_METHOD_NAMES.has(prop.name)) return { matches: false };
7746
+ return { matches: true, calleeProp: prop };
7747
+ }
7748
+ function referencesRawParam(argNode, paramName) {
7749
+ if (!argNode) return false;
7750
+ if (argNode.type === "Identifier" && argNode.name === paramName) return true;
7751
+ if (argNode.type === "CallExpression" && argNode.callee?.type === "MemberExpression" && argNode.callee.object?.type === "Identifier" && argNode.callee.object.name === paramName && argNode.callee.property?.type === "Identifier" && argNode.callee.property.name === "toString") {
7752
+ return true;
7753
+ }
7754
+ if (argNode.type === "TemplateLiteral") {
7755
+ return (argNode.expressions ?? []).some((expr) => referencesRawParam(expr, paramName));
7756
+ }
7757
+ return false;
7758
+ }
7759
+ function findCallExpressions(node, out, depth = 0) {
7760
+ if (!node || typeof node !== "object" || depth > 40) return;
7761
+ if (Array.isArray(node)) {
7762
+ for (const n of node) findCallExpressions(n, out, depth + 1);
7763
+ return;
7764
+ }
7765
+ if (node.type === "CallExpression") out.push(node);
7766
+ for (const key of Object.keys(node)) {
7767
+ if (key === "parent" || key === "loc" || key === "range") continue;
7768
+ const val = node[key];
7769
+ if (val && typeof val === "object") findCallExpressions(val, out, depth + 1);
7770
+ }
7771
+ }
7772
+ var rule101 = {
7773
+ meta: {
7774
+ type: "problem",
7775
+ docs: {
7776
+ description: "Raw OpenAI Realtime message payloads must not be logged verbatim",
7777
+ category: "security",
7778
+ cwe: "CWE-532",
7779
+ docsUrl: "https://developers.openai.com/api/docs/guides/realtime",
7780
+ rationale: "Every inbound Realtime WebSocket message can include response.audio.delta payloads (base64-encoded live call audio) and, once transcription is wired up, transcript text. Logging the raw message object or string verbatim writes a durable, unredacted record of live conversation content into whatever log sink the application ships to.",
7781
+ recommended: true
7782
+ },
7783
+ messages: {
7784
+ rawPayloadLogged: "A raw OpenAI Realtime message is logged verbatim here, which can include live call audio or transcript content."
7785
+ },
7786
+ schema: []
7787
+ },
7788
+ create(context) {
7789
+ let socketVarNames = /* @__PURE__ */ new Set();
7790
+ return {
7791
+ Program(node) {
7792
+ socketVarNames = collectOpenAIRealtimeSocketVarNames(node);
7793
+ },
7794
+ CallExpression(node) {
7795
+ if (socketVarNames.size === 0) return;
7796
+ if (!isSocketOnCall(node, socketVarNames, "message")) return;
7797
+ const handler = node.arguments?.[1];
7798
+ if (handler?.type !== "ArrowFunctionExpression" && handler?.type !== "FunctionExpression") return;
7799
+ const param = handler.params?.[0];
7800
+ if (param?.type !== "Identifier") return;
7801
+ const paramName = param.name;
7802
+ const calls = [];
7803
+ findCallExpressions(handler.body, calls);
7804
+ for (const call of calls) {
7805
+ const { matches } = isLogCall(call);
7806
+ if (!matches) continue;
7807
+ const hasRawArg = (call.arguments ?? []).some((arg) => referencesRawParam(arg, paramName));
7808
+ if (hasRawArg) {
7809
+ context.report({ node: call, messageId: "rawPayloadLogged" });
7810
+ }
7811
+ }
7812
+ }
7813
+ };
7814
+ }
7815
+ };
7816
+ var openaiRealtimeNoLogRawMessagePayloadsRule = rule101;
7817
+
7818
+ // src/providers/openai-realtime/rules/handle-error-server-event.ts
7819
+ function isTypeMemberExpression(node) {
7820
+ return node?.type === "MemberExpression" && node.property?.type === "Identifier" && node.property.name === "type";
7821
+ }
7822
+ function collectTypeComparisonLiterals(node, out, depth = 0) {
7823
+ if (!node || typeof node !== "object" || depth > 60) return;
7824
+ if (Array.isArray(node)) {
7825
+ for (const n of node) collectTypeComparisonLiterals(n, out, depth + 1);
7826
+ return;
7827
+ }
7828
+ if (node.type === "BinaryExpression" && (node.operator === "===" || node.operator === "==")) {
7829
+ if (isTypeMemberExpression(node.left) && node.right?.type === "Literal" && typeof node.right.value === "string") {
7830
+ out.add(node.right.value);
7831
+ }
7832
+ if (isTypeMemberExpression(node.right) && node.left?.type === "Literal" && typeof node.left.value === "string") {
7833
+ out.add(node.left.value);
7834
+ }
7835
+ }
7836
+ if (node.type === "SwitchStatement" && isTypeMemberExpression(node.discriminant)) {
7837
+ for (const c of node.cases ?? []) {
7838
+ if (c?.test?.type === "Literal" && typeof c.test.value === "string") out.add(c.test.value);
7839
+ }
7840
+ }
7841
+ for (const key of Object.keys(node)) {
7842
+ if (key === "parent" || key === "loc" || key === "range") continue;
7843
+ const val = node[key];
7844
+ if (val && typeof val === "object") collectTypeComparisonLiterals(val, out, depth + 1);
7845
+ }
7846
+ }
7847
+ var rule102 = {
7848
+ meta: {
7849
+ type: "problem",
7850
+ docs: {
7851
+ description: 'OpenAI Realtime message handlers must check for the API-level "error" server event',
7852
+ category: "reliability",
7853
+ docsUrl: "https://developers.openai.com/api/reference/resources/realtime/server-events",
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.",
7855
+ recommended: true
7856
+ },
7857
+ messages: {
7858
+ missingErrorBranch: "This Realtime message handler branches on event types but never checks for message.type === 'error'."
7859
+ },
7860
+ schema: []
7861
+ },
7862
+ create(context) {
7863
+ let socketVarNames = /* @__PURE__ */ new Set();
7864
+ return {
7865
+ Program(node) {
7866
+ socketVarNames = collectOpenAIRealtimeSocketVarNames(node);
7867
+ },
7868
+ CallExpression(node) {
7869
+ if (socketVarNames.size === 0) return;
7870
+ if (!isSocketOnCall(node, socketVarNames, "message")) return;
7871
+ const handler = node.arguments?.[1];
7872
+ if (handler?.type !== "ArrowFunctionExpression" && handler?.type !== "FunctionExpression") return;
7873
+ const literals = /* @__PURE__ */ new Set();
7874
+ collectTypeComparisonLiterals(handler.body, literals);
7875
+ if (literals.size === 0) return;
7876
+ if (!literals.has("error")) {
7877
+ context.report({ node, messageId: "missingErrorBranch" });
7878
+ }
7879
+ }
7880
+ };
7881
+ }
7882
+ };
7883
+ var openaiRealtimeHandleErrorServerEventRule = rule102;
7884
+
7885
+ // src/providers/openai-realtime/rules/reconnect-on-drop.ts
7886
+ var RECONNECT_NAME_PATTERN = /reconnect/i;
7887
+ function hasReconnectAttempt(node, depth = 0) {
7888
+ if (!node || typeof node !== "object" || depth > 60) return false;
7889
+ if (Array.isArray(node)) {
7890
+ return node.some((n) => hasReconnectAttempt(n, depth + 1));
7891
+ }
7892
+ if (node.type === "NewExpression" && node.callee?.type === "Identifier" && node.callee.name === "WebSocket") {
7893
+ return true;
7894
+ }
7895
+ if (node.type === "CallExpression") {
7896
+ const callee = node.callee;
7897
+ const calleeName = callee?.type === "Identifier" ? callee.name : callee?.type === "MemberExpression" && callee.property?.type === "Identifier" ? callee.property.name : null;
7898
+ if (typeof calleeName === "string" && RECONNECT_NAME_PATTERN.test(calleeName)) return true;
7899
+ }
7900
+ for (const key of Object.keys(node)) {
7901
+ if (key === "parent" || key === "loc" || key === "range") continue;
7902
+ const val = node[key];
7903
+ if (val && typeof val === "object" && hasReconnectAttempt(val, depth + 1)) return true;
7904
+ }
7905
+ return false;
7906
+ }
7907
+ var rule103 = {
7908
+ meta: {
7909
+ type: "problem",
7910
+ docs: {
7911
+ description: "A dropped OpenAI Realtime connection must be retried, not just logged",
7912
+ category: "reliability",
7913
+ docsUrl: "https://developers.openai.com/api/docs/guides/realtime",
7914
+ rationale: "For a multi-minute phone call, a single transient OpenAI-side disconnect that is only logged on 'close' permanently kills translation/processing for the remainder of the call in that direction \u2014 the call itself stays connected and silently degraded, with neither party getting a signal that something stopped working.",
7915
+ recommended: true
7916
+ },
7917
+ messages: {
7918
+ noReconnectAttempt: "This Realtime socket's close handler only logs and never attempts to reconnect."
7919
+ },
7920
+ schema: []
7921
+ },
7922
+ create(context) {
7923
+ let socketVarNames = /* @__PURE__ */ new Set();
7924
+ return {
7925
+ Program(node) {
7926
+ socketVarNames = collectOpenAIRealtimeSocketVarNames(node);
7927
+ },
7928
+ CallExpression(node) {
7929
+ if (socketVarNames.size === 0) return;
7930
+ if (!isSocketOnCall(node, socketVarNames, "close")) return;
7931
+ const handler = node.arguments?.[1];
7932
+ if (handler?.type !== "ArrowFunctionExpression" && handler?.type !== "FunctionExpression") return;
7933
+ if (!hasReconnectAttempt(handler.body)) {
7934
+ context.report({ node, messageId: "noReconnectAttempt" });
7935
+ }
7936
+ }
7937
+ };
7938
+ }
7939
+ };
7940
+ var openaiRealtimeReconnectOnDropRule = rule103;
7941
+
7942
+ // src/providers/openai-realtime/rules/avoid-dated-preview-snapshots.ts
7943
+ var DATED_PREVIEW_MODEL_PATTERN = /model=[^&'"`]*-preview-\d{4}-\d{2}-\d{2}/;
7944
+ var rule104 = {
7945
+ meta: {
7946
+ type: "suggestion",
7947
+ docs: {
7948
+ description: "OpenAI Realtime connections should not pin to a dated preview model snapshot",
7949
+ category: "correctness",
7950
+ docsUrl: "https://developers.openai.com/api/reference/resources/realtime/subresources/client_secrets",
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.",
7952
+ recommended: true
7953
+ },
7954
+ messages: {
7955
+ datedPreviewSnapshot: "This Realtime connection is pinned to a dated preview model snapshot instead of the GA alias."
7956
+ },
7957
+ schema: []
7958
+ },
7959
+ create(context) {
7960
+ let stringVarValues = /* @__PURE__ */ new Map();
7961
+ let urlVarNames = /* @__PURE__ */ new Set();
7962
+ return {
7963
+ Program(node) {
7964
+ stringVarValues = collectStringVarValues(node);
7965
+ urlVarNames = collectOpenAIRealtimeUrlVarNames(node);
7966
+ },
7967
+ NewExpression(node) {
7968
+ if (!isOpenAIRealtimeNewWebSocket(node, urlVarNames)) return;
7969
+ const urlArg = node.arguments?.[0];
7970
+ const urlString = resolveStringValue(urlArg, stringVarValues);
7971
+ if (!urlString) return;
7972
+ if (DATED_PREVIEW_MODEL_PATTERN.test(urlString)) {
7973
+ context.report({ node: urlArg, messageId: "datedPreviewSnapshot" });
7974
+ }
7975
+ }
7976
+ };
7977
+ }
7978
+ };
7979
+ var openaiRealtimeAvoidDatedPreviewSnapshotsRule = rule104;
7980
+
7981
+ // src/providers/openai-realtime/rules/verify-deprecated-session-fields.ts
7982
+ var rule105 = {
7983
+ meta: {
7984
+ type: "suggestion",
7985
+ docs: {
7986
+ description: "OpenAI Realtime session.update payloads should not rely on an unverified 'temperature' field",
7987
+ category: "correctness",
7988
+ docsUrl: "https://developers.openai.com/api/reference/resources/realtime/subresources/client_secrets",
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.",
7990
+ recommended: true
7991
+ },
7992
+ messages: {
7993
+ unverifiedTemperatureField: "This session.update payload sets 'temperature', a field not documented in the current GA Realtime sessions schema."
7994
+ },
7995
+ schema: []
7996
+ },
7997
+ create(context) {
7998
+ return {
7999
+ ObjectExpression(node) {
8000
+ const typeProp = findProperty4(node, "type");
8001
+ const typeValue = typeProp?.value;
8002
+ const isSessionUpdate = typeValue?.type === "Literal" && typeof typeValue.value === "string" && typeValue.value === "session.update";
8003
+ if (!isSessionUpdate) return;
8004
+ const sessionProp = findProperty4(node, "session");
8005
+ if (sessionProp?.value?.type !== "ObjectExpression") return;
8006
+ const temperatureProp = findProperty4(sessionProp.value, "temperature");
8007
+ if (!temperatureProp) return;
8008
+ context.report({ node: temperatureProp, messageId: "unverifiedTemperatureField" });
8009
+ }
8010
+ };
8011
+ }
8012
+ };
8013
+ var openaiRealtimeVerifyDeprecatedSessionFieldsRule = rule105;
8014
+
8015
+ // src/providers/openai-realtime/rules/buffer-audio-until-session-ready.ts
8016
+ var QUEUE_CALL_NAME_PATTERN = /^(push|unshift|enqueue|queue)$/i;
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;
8022
+ const isReadyStateMember = (n) => n?.type === "MemberExpression" && n.property?.type === "Identifier" && n.property.name === "readyState";
8023
+ const isOpenValue = (n) => n?.type === "MemberExpression" && n.property?.type === "Identifier" && n.property.name === "OPEN" || n?.type === "Literal" && n.value === 1;
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";
8027
+ }
8028
+ function hasQueueCall(node, depth = 0) {
8029
+ if (!node || typeof node !== "object" || depth > 40) return false;
8030
+ if (Array.isArray(node)) return node.some((n) => hasQueueCall(n, depth + 1));
8031
+ if (node.type === "CallExpression") {
8032
+ const callee = node.callee;
8033
+ const calleeName = callee?.type === "MemberExpression" && callee.property?.type === "Identifier" ? callee.property.name : callee?.type === "Identifier" ? callee.name : null;
8034
+ if (typeof calleeName === "string" && QUEUE_CALL_NAME_PATTERN.test(calleeName)) return true;
8035
+ }
8036
+ for (const key of Object.keys(node)) {
8037
+ if (key === "parent" || key === "loc" || key === "range") continue;
8038
+ const val = node[key];
8039
+ if (val && typeof val === "object" && hasQueueCall(val, depth + 1)) return true;
8040
+ }
8041
+ return false;
8042
+ }
8043
+ var rule106 = {
8044
+ meta: {
8045
+ type: "suggestion",
8046
+ docs: {
8047
+ description: "Audio sent before an OpenAI Realtime socket is open must be buffered, not dropped",
8048
+ category: "reliability",
8049
+ docsUrl: "https://developers.openai.com/api/reference/resources/realtime/client-events",
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.",
8051
+ recommended: true
8052
+ },
8053
+ messages: {
8054
+ audioDroppedNotBuffered: "Audio sent while this Realtime socket is not yet open is dropped here instead of being buffered and flushed once open."
8055
+ },
8056
+ schema: []
8057
+ },
8058
+ create(context) {
8059
+ return {
8060
+ IfStatement(node) {
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" });
8067
+ }
8068
+ };
8069
+ }
8070
+ };
8071
+ var openaiRealtimeBufferAudioUntilSessionReadyRule = rule106;
8072
+
8073
+ // src/providers/openai-realtime/rules/send-safety-identifier.ts
8074
+ var rule107 = {
8075
+ meta: {
8076
+ type: "suggestion",
8077
+ docs: {
8078
+ description: "OpenAI Realtime connections should send an OpenAI-Safety-Identifier header",
8079
+ category: "security",
8080
+ docsUrl: "https://developers.openai.com/api/docs/guides/realtime",
8081
+ rationale: "OpenAI's Realtime guidance recommends including an OpenAI-Safety-Identifier header with a stable, privacy-preserving value (e.g. a hashed user/account id) on Realtime connections, to support OpenAI's abuse/safety monitoring for voice content. A connection that omits it gives OpenAI no way to correlate abusive sessions back to an account without per-connection metadata.",
8082
+ recommended: true
8083
+ },
8084
+ messages: {
8085
+ missingSafetyIdentifier: "This OpenAI Realtime WebSocket connection does not send an OpenAI-Safety-Identifier header."
8086
+ },
8087
+ schema: []
8088
+ },
8089
+ create(context) {
8090
+ let urlVarNames = /* @__PURE__ */ new Set();
8091
+ return {
8092
+ Program(node) {
8093
+ urlVarNames = collectOpenAIRealtimeUrlVarNames(node);
8094
+ },
8095
+ NewExpression(node) {
8096
+ if (!isOpenAIRealtimeNewWebSocket(node, urlVarNames)) return;
8097
+ const optionsArg = node.arguments?.[1];
8098
+ const headersProp = optionsArg?.type === "ObjectExpression" ? findProperty4(optionsArg, "headers") : null;
8099
+ const headersObj = headersProp?.value?.type === "ObjectExpression" ? headersProp.value : null;
8100
+ const safetyIdProp = headersObj ? findProperty4(headersObj, "OpenAI-Safety-Identifier") : null;
8101
+ if (safetyIdProp) return;
8102
+ context.report({ node: headersObj ?? optionsArg ?? node, messageId: "missingSafetyIdentifier" });
8103
+ }
8104
+ };
8105
+ }
8106
+ };
8107
+ var openaiRealtimeSendSafetyIdentifierRule = rule107;
8108
+
8109
+ // src/providers/openai-realtime/rules/transcription-model-choice.ts
8110
+ var rule108 = {
8111
+ meta: {
8112
+ type: "suggestion",
8113
+ docs: {
8114
+ description: "OpenAI Realtime sessions should not configure 'whisper-1' for input transcription",
8115
+ category: "correctness",
8116
+ docsUrl: "https://developers.openai.com/api/docs/guides/realtime-transcription",
8117
+ rationale: "OpenAI's transcription guidance describes 'whisper-1' as for existing Whisper integrations that are not natively streaming, while 'gpt-realtime-whisper' is the natively-streaming option designed for realtime sessions. Configuring input_audio_transcription with 'whisper-1' is the wrong tool for a realtime session, adding latency/cost to a streaming pipeline.",
8118
+ recommended: true
8119
+ },
8120
+ messages: {
8121
+ nonStreamingTranscriptionModel: "This session's input transcription is configured with 'whisper-1', which is not natively streaming and not optimized for realtime sessions."
8122
+ },
8123
+ schema: []
8124
+ },
8125
+ create(context) {
8126
+ return {
8127
+ ObjectExpression(node) {
8128
+ const typeProp = findProperty4(node, "type");
8129
+ const typeValue = typeProp?.value;
8130
+ const isSessionUpdate = typeValue?.type === "Literal" && typeof typeValue.value === "string" && typeValue.value === "session.update";
8131
+ if (!isSessionUpdate) return;
8132
+ const sessionProp = findProperty4(node, "session");
8133
+ if (sessionProp?.value?.type !== "ObjectExpression") return;
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
+ }
8140
+ if (transcriptionProp?.value?.type !== "ObjectExpression") return;
8141
+ const modelProp = findProperty4(transcriptionProp.value, "model");
8142
+ const modelValue = modelProp?.value;
8143
+ const isWhisper1 = modelValue?.type === "Literal" && typeof modelValue.value === "string" && modelValue.value === "whisper-1";
8144
+ if (!isWhisper1) return;
8145
+ context.report({ node: modelProp, messageId: "nonStreamingTranscriptionModel" });
8146
+ }
8147
+ };
8148
+ }
8149
+ };
8150
+ var openaiRealtimeTranscriptionModelChoiceRule = rule108;
6027
8151
 
6028
8152
  // src/plugin/index.ts
6029
8153
  var plugin = {
@@ -6068,7 +8192,6 @@ var plugin = {
6068
8192
  "firebase-rtdb-write-promise-not-handled": firebaseRtdbWritePromiseNotHandledRule,
6069
8193
  "firebase-firestore-rules-expired": firebaseFirestoreRulesExpiredRule,
6070
8194
  "firebase-id-token-cookie-flags": firebaseIdTokenCookieFlagsRule,
6071
- "firebase-middleware-token-not-verified": firebaseMiddlewareTokenNotVerifiedRule,
6072
8195
  "firebase-hardcoded-user-id": firebaseHardcodedUserIdRule,
6073
8196
  "firebase-auth-user-not-found-disclosure": firebaseAuthUserNotFoundDisclosureRule,
6074
8197
  "firebase-signup-password-confirm": firebaseSignupPasswordConfirmRule,
@@ -6107,10 +8230,37 @@ var plugin = {
6107
8230
  "tiptap-appendTransaction-add-to-history": tiptapAppendTransactionAddToHistoryRule,
6108
8231
  "tiptap-appendTransaction-full-scan": tiptapAppendTransactionFullScanRule,
6109
8232
  "tiptap-atom-node-wrap-in": tiptapAtomNodeWrapInRule,
6110
- "tiptap-twitter-url-regex": tiptapTwitterUrlRegexRule,
6111
8233
  "tiptap-drop-handler-pos-precedence": tiptapDropHandlerPosPrecedenceRule,
6112
8234
  "tiptap-prefer-table-kit": tiptapPreferTableKitRule,
6113
- "tiptap-tiptap-markdown-missing-node-spec": tiptapTiptapMarkdownMissingNodeSpecRule
8235
+ "tiptap-tiptap-markdown-missing-node-spec": tiptapTiptapMarkdownMissingNodeSpecRule,
8236
+ "elevenlabs-validate-signed-url-response": elevenlabsValidateSignedUrlResponseRule,
8237
+ "elevenlabs-no-error-object-logging": elevenlabsNoErrorObjectLoggingRule,
8238
+ "elevenlabs-fetch-timeout-required": elevenlabsFetchTimeoutRequiredRule,
8239
+ "elevenlabs-validate-agent-id-format": elevenlabsValidateAgentIdFormatRule,
8240
+ "elevenlabs-secure-session-id-generation": elevenlabsSecureSessionIdGenerationRule,
8241
+ "elevenlabs-conversation-error-recovery": elevenlabsConversationErrorRecoveryRule,
8242
+ "elevenlabs-api-version-pinning": elevenlabsApiVersionPinningRule,
8243
+ "elevenlabs-check-http-status-before-json": elevenlabsCheckHttpStatusBeforeJsonRule,
8244
+ "elevenlabs-conversation-cleanup-on-error": elevenlabsConversationCleanupOnErrorRule,
8245
+ "elevenlabs-env-var-validation": elevenlabsEnvVarValidationRule,
8246
+ "twilio-validate-webhook-signature": twilioValidateWebhookSignatureRule,
8247
+ "twilio-taskrouter-attributes-match-consumer": twilioTaskrouterAttributesMatchConsumerRule,
8248
+ "twilio-enqueue-task-json-stringify": twilioEnqueueTaskJsonStringifyRule,
8249
+ "twilio-media-streams-key-by-call-sid": twilioMediaStreamsKeyByCallSidRule,
8250
+ "twilio-await-or-catch-rest-calls-in-event-handlers": twilioAwaitOrCatchRestCallsInEventHandlersRule,
8251
+ "twilio-use-twiml-builder-not-string-templates": twilioUseTwimlBuilderNotStringTemplatesRule,
8252
+ "twilio-media-streams-mark-pacing": twilioMediaStreamsMarkPacingRule,
8253
+ "twilio-validate-all-request-inputs": twilioValidateAllRequestInputsRule,
8254
+ "twilio-media-streams-mark-name-string": twilioMediaStreamsMarkNameStringRule,
8255
+ "openai-realtime-migrate-beta-to-ga": openaiRealtimeMigrateBetaToGaRule,
8256
+ "openai-realtime-no-log-raw-message-payloads": openaiRealtimeNoLogRawMessagePayloadsRule,
8257
+ "openai-realtime-handle-error-server-event": openaiRealtimeHandleErrorServerEventRule,
8258
+ "openai-realtime-reconnect-on-drop": openaiRealtimeReconnectOnDropRule,
8259
+ "openai-realtime-avoid-dated-preview-snapshots": openaiRealtimeAvoidDatedPreviewSnapshotsRule,
8260
+ "openai-realtime-verify-deprecated-session-fields": openaiRealtimeVerifyDeprecatedSessionFieldsRule,
8261
+ "openai-realtime-buffer-audio-until-session-ready": openaiRealtimeBufferAudioUntilSessionReadyRule,
8262
+ "openai-realtime-send-safety-identifier": openaiRealtimeSendSafetyIdentifierRule,
8263
+ "openai-realtime-transcription-model-choice": openaiRealtimeTranscriptionModelChoiceRule
6114
8264
  }
6115
8265
  };
6116
8266
  var plugin_default = plugin;