@api-doctor/cli 0.0.5 → 0.0.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -2
- package/dist/cli.cjs +423 -390
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.mjs +423 -390
- package/dist/cli.mjs.map +1 -1
- package/dist/plugin.d.ts +7 -42
- package/dist/plugin.js +379 -331
- package/dist/plugin.js.map +1 -1
- package/package.json +2 -1
package/dist/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/
|
|
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/
|
|
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
|
|
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:
|
|
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
|
-
|
|
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
|
|
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
|
|
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:
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
1433
|
-
|
|
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 (
|
|
1448
|
-
if (node.callee?.type !== "Identifier" || node.callee.name
|
|
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:
|
|
1517
|
-
docsUrl: "https://supabase.com/docs/guides/
|
|
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
|
|
1777
|
-
|
|
1778
|
-
|
|
1779
|
-
|
|
1780
|
-
|
|
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/
|
|
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
|
-
|
|
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: "
|
|
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
|
|
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
|
-
|
|
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
|
|
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 =
|
|
2853
|
-
|
|
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: "
|
|
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: "
|
|
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
|
-
|
|
2910
|
-
|
|
2911
|
-
|
|
2912
|
-
|
|
2913
|
-
|
|
2914
|
-
const
|
|
2915
|
-
|
|
2916
|
-
if (!
|
|
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" &&
|
|
2976
|
-
|
|
3039
|
+
if (callee?.type === "Identifier" && callee.name === "setCookie") {
|
|
3040
|
+
checkNameValueOptsCall(node);
|
|
3041
|
+
return;
|
|
3042
|
+
}
|
|
3043
|
+
if (callee?.type !== "MemberExpression" || callee.computed || callee.property?.type !== "Identifier") return;
|
|
3044
|
+
if (callee.property.name === "cookie") {
|
|
3045
|
+
checkNameValueOptsCall(node);
|
|
3046
|
+
return;
|
|
2977
3047
|
}
|
|
2978
|
-
if (callee
|
|
2979
|
-
|
|
3048
|
+
if (callee.property.name === "set" && isCookieReceiver(callee.object)) {
|
|
3049
|
+
checkNameValueOptsCall(node);
|
|
2980
3050
|
}
|
|
2981
3051
|
},
|
|
2982
|
-
|
|
2983
|
-
|
|
2984
|
-
|
|
2985
|
-
|
|
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
|
|
3066
|
+
var firebaseIdTokenCookieFlagsRule = rule39;
|
|
2993
3067
|
|
|
2994
3068
|
// src/providers/firebase/rules/hardcoded-user-id.ts
|
|
2995
|
-
var
|
|
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 =
|
|
3104
|
+
var firebaseHardcodedUserIdRule = rule40;
|
|
3028
3105
|
|
|
3029
3106
|
// src/providers/firebase/rules/auth-user-not-found-disclosure.ts
|
|
3030
|
-
var
|
|
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 =
|
|
3146
|
+
var firebaseAuthUserNotFoundDisclosureRule = rule41;
|
|
3070
3147
|
|
|
3071
3148
|
// src/providers/firebase/rules/signup-password-confirm.ts
|
|
3072
|
-
var
|
|
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 =
|
|
3206
|
+
var firebaseSignupPasswordConfirmRule = rule42;
|
|
3130
3207
|
|
|
3131
3208
|
// src/providers/firebase/rules/use-array-union-remove.ts
|
|
3132
|
-
var
|
|
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 =
|
|
3255
|
+
var firebaseUseArrayUnionRemoveRule = rule43;
|
|
3179
3256
|
|
|
3180
3257
|
// src/providers/firebase/rules/duplicate-initialize-app.ts
|
|
3181
|
-
var
|
|
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
|
-
|
|
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 =
|
|
3308
|
+
var firebaseDuplicateInitializeAppRule = rule44;
|
|
3230
3309
|
|
|
3231
3310
|
// src/providers/firebase/rules/onSnapshot-async-throw.ts
|
|
3232
|
-
var
|
|
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
|
|
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
|
|
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 =
|
|
3362
|
+
var firebaseOnSnapshotAsyncThrowRule = rule45;
|
|
3284
3363
|
|
|
3285
3364
|
// src/providers/firebase/rules/onSnapshot-missing-error-callback.ts
|
|
3286
|
-
var
|
|
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 =
|
|
3403
|
+
var firebaseOnSnapshotMissingErrorCallbackRule = rule46;
|
|
3325
3404
|
|
|
3326
3405
|
// src/providers/firebase/rules/firestore-document-size-guard.ts
|
|
3327
|
-
var
|
|
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
|
|
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
|
|
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 =
|
|
3457
|
+
var firebaseFirestoreDocumentSizeGuardRule = rule47;
|
|
3379
3458
|
|
|
3380
3459
|
// src/providers/firebase/rules/use-timestamp-now.ts
|
|
3381
|
-
var
|
|
3460
|
+
var rule48 = {
|
|
3382
3461
|
meta: {
|
|
3383
3462
|
type: "suggestion",
|
|
3384
3463
|
docs: {
|
|
3385
|
-
description: "new Date() used for Firestore timestamp instead of
|
|
3464
|
+
description: "new Date() used for Firestore timestamp instead of serverTimestamp()",
|
|
3386
3465
|
category: "correctness",
|
|
3387
|
-
rationale: "
|
|
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
|
|
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 =
|
|
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
|
|
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 =
|
|
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
|
|
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/
|
|
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 =
|
|
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
|
|
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/
|
|
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 =
|
|
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
|
|
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/
|
|
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 =
|
|
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
|
|
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 =
|
|
4089
|
+
var browserbaseNoConditionalAuthzOnAnonymousUserRule = rule53;
|
|
4011
4090
|
|
|
4012
4091
|
// src/providers/browserbase/rules/no-connect-url-in-api-response.ts
|
|
4013
|
-
var
|
|
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 =
|
|
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
|
|
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/
|
|
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 =
|
|
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
|
|
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 =
|
|
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
|
|
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/
|
|
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 =
|
|
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
|
|
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 =
|
|
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
|
|
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 =
|
|
4536
|
+
var browserbaseReleaseSessionOnConnectFailureRule = rule59;
|
|
4458
4537
|
|
|
4459
4538
|
// src/providers/browserbase/rules/dont-stack-custom-retry-on-sdk-retry.ts
|
|
4460
|
-
var
|
|
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 =
|
|
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
|
|
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 =
|
|
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
|
|
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 =
|
|
4710
|
+
var browserbaseUseSdkNotRawRequestsRule = rule62;
|
|
4632
4711
|
|
|
4633
4712
|
// src/providers/browserbase/rules/centralize-request-release.ts
|
|
4634
|
-
var
|
|
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 =
|
|
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
|
|
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 =
|
|
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
|
|
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 =
|
|
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
|
|
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 =
|
|
5060
|
+
var openaiCuaStructuredStepMetadataNotTextJsonRule = rule66;
|
|
4982
5061
|
|
|
4983
5062
|
// src/providers/openai-cua/rules/no-blind-safety-check-ack.ts
|
|
4984
|
-
var
|
|
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 =
|
|
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
|
|
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 =
|
|
5254
|
+
var openaiCuaRetryTransientTurnErrorsRule = rule68;
|
|
5176
5255
|
|
|
5177
5256
|
// src/providers/openai-cua/rules/check-response-status-incomplete.ts
|
|
5178
|
-
var
|
|
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 =
|
|
5366
|
+
var openaiCuaCheckResponseStatusIncompleteRule = rule69;
|
|
5288
5367
|
|
|
5289
5368
|
// src/providers/openai-cua/rules/set-safety-identifier.ts
|
|
5290
|
-
var
|
|
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://
|
|
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 =
|
|
5407
|
+
var openaiCuaSetSafetyIdentifierRule = rule70;
|
|
5329
5408
|
|
|
5330
5409
|
// src/providers/tiptap/rules/upload-validate-fn-void.ts
|
|
5331
|
-
var
|
|
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 =
|
|
5474
|
+
var tiptapUploadValidateFnVoidRule = rule71;
|
|
5396
5475
|
|
|
5397
5476
|
// src/providers/tiptap/rules/script-src-hardcoded-api-key.ts
|
|
5398
|
-
var
|
|
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 =
|
|
5535
|
+
var tiptapScriptSrcHardcodedApiKeyRule = rule72;
|
|
5457
5536
|
|
|
5458
5537
|
// src/providers/tiptap/rules/dynamic-script-no-sri.ts
|
|
5459
|
-
var
|
|
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 =
|
|
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
|
|
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 =
|
|
5712
|
+
var tiptapAddAttributesMissingRenderHTMLRule = rule74;
|
|
5634
5713
|
|
|
5635
5714
|
// src/providers/tiptap/rules/appendTransaction-add-to-history.ts
|
|
5636
|
-
var
|
|
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 =
|
|
5794
|
+
var tiptapAppendTransactionAddToHistoryRule = rule75;
|
|
5716
5795
|
|
|
5717
5796
|
// src/providers/tiptap/rules/appendTransaction-full-scan.ts
|
|
5718
|
-
var
|
|
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 =
|
|
5857
|
+
var tiptapAppendTransactionFullScanRule = rule76;
|
|
5779
5858
|
|
|
5780
5859
|
// src/providers/tiptap/rules/atom-node-wrap-in.ts
|
|
5781
|
-
var
|
|
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 =
|
|
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
|
|
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`.
|
|
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 =
|
|
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
|
|
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
|
|
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
|
|
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 =
|
|
5995
|
+
var tiptapPreferTableKitRule = rule79;
|
|
5958
5996
|
|
|
5959
5997
|
// src/providers/tiptap/rules/tiptap-markdown-missing-node-spec.ts
|
|
5960
|
-
var
|
|
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
|
|
5967
|
-
docsUrl: "https://github.com/
|
|
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.
|
|
6009
|
+
missingMarkdownNodeSpec: "This TipTap node is used alongside tiptap-markdown but defines no markdown serialization. Nodes without a markdown spec are silently dropped on export. Return a markdown spec (MarkdownNodeSpec) from addStorage."
|
|
5972
6010
|
},
|
|
5973
6011
|
schema: []
|
|
5974
6012
|
},
|
|
@@ -6023,7 +6061,7 @@ var rule82 = {
|
|
|
6023
6061
|
};
|
|
6024
6062
|
}
|
|
6025
6063
|
};
|
|
6026
|
-
var tiptapTiptapMarkdownMissingNodeSpecRule =
|
|
6064
|
+
var tiptapTiptapMarkdownMissingNodeSpecRule = rule80;
|
|
6027
6065
|
|
|
6028
6066
|
// src/providers/elevenlabs/utils.ts
|
|
6029
6067
|
function isElevenLabsUrlArg(node) {
|
|
@@ -6084,7 +6122,7 @@ function posOf(n) {
|
|
|
6084
6122
|
}
|
|
6085
6123
|
|
|
6086
6124
|
// src/providers/elevenlabs/rules/validate-signed-url-response.ts
|
|
6087
|
-
var
|
|
6125
|
+
var rule81 = {
|
|
6088
6126
|
meta: {
|
|
6089
6127
|
type: "problem",
|
|
6090
6128
|
docs: {
|
|
@@ -6183,11 +6221,11 @@ var rule83 = {
|
|
|
6183
6221
|
};
|
|
6184
6222
|
}
|
|
6185
6223
|
};
|
|
6186
|
-
var elevenlabsValidateSignedUrlResponseRule =
|
|
6224
|
+
var elevenlabsValidateSignedUrlResponseRule = rule81;
|
|
6187
6225
|
|
|
6188
6226
|
// src/providers/elevenlabs/rules/no-error-object-logging.ts
|
|
6189
6227
|
var LOGGING_METHODS = /* @__PURE__ */ new Set(["error", "warn", "log"]);
|
|
6190
|
-
var
|
|
6228
|
+
var rule82 = {
|
|
6191
6229
|
meta: {
|
|
6192
6230
|
type: "problem",
|
|
6193
6231
|
docs: {
|
|
@@ -6255,10 +6293,10 @@ var rule84 = {
|
|
|
6255
6293
|
};
|
|
6256
6294
|
}
|
|
6257
6295
|
};
|
|
6258
|
-
var elevenlabsNoErrorObjectLoggingRule =
|
|
6296
|
+
var elevenlabsNoErrorObjectLoggingRule = rule82;
|
|
6259
6297
|
|
|
6260
6298
|
// src/providers/elevenlabs/rules/fetch-timeout-required.ts
|
|
6261
|
-
var
|
|
6299
|
+
var rule83 = {
|
|
6262
6300
|
meta: {
|
|
6263
6301
|
type: "suggestion",
|
|
6264
6302
|
docs: {
|
|
@@ -6291,10 +6329,10 @@ var rule85 = {
|
|
|
6291
6329
|
};
|
|
6292
6330
|
}
|
|
6293
6331
|
};
|
|
6294
|
-
var elevenlabsFetchTimeoutRequiredRule =
|
|
6332
|
+
var elevenlabsFetchTimeoutRequiredRule = rule83;
|
|
6295
6333
|
|
|
6296
6334
|
// src/providers/elevenlabs/rules/validate-agent-id-format.ts
|
|
6297
|
-
var
|
|
6335
|
+
var rule84 = {
|
|
6298
6336
|
meta: {
|
|
6299
6337
|
type: "problem",
|
|
6300
6338
|
docs: {
|
|
@@ -6400,10 +6438,10 @@ var rule86 = {
|
|
|
6400
6438
|
};
|
|
6401
6439
|
}
|
|
6402
6440
|
};
|
|
6403
|
-
var elevenlabsValidateAgentIdFormatRule =
|
|
6441
|
+
var elevenlabsValidateAgentIdFormatRule = rule84;
|
|
6404
6442
|
|
|
6405
6443
|
// src/providers/elevenlabs/rules/secure-session-id-generation.ts
|
|
6406
|
-
var
|
|
6444
|
+
var rule85 = {
|
|
6407
6445
|
meta: {
|
|
6408
6446
|
type: "problem",
|
|
6409
6447
|
docs: {
|
|
@@ -6468,10 +6506,10 @@ var rule87 = {
|
|
|
6468
6506
|
};
|
|
6469
6507
|
}
|
|
6470
6508
|
};
|
|
6471
|
-
var elevenlabsSecureSessionIdGenerationRule =
|
|
6509
|
+
var elevenlabsSecureSessionIdGenerationRule = rule85;
|
|
6472
6510
|
|
|
6473
6511
|
// src/providers/elevenlabs/rules/conversation-error-recovery.ts
|
|
6474
|
-
var
|
|
6512
|
+
var rule86 = {
|
|
6475
6513
|
meta: {
|
|
6476
6514
|
type: "suggestion",
|
|
6477
6515
|
docs: {
|
|
@@ -6571,10 +6609,10 @@ var rule88 = {
|
|
|
6571
6609
|
};
|
|
6572
6610
|
}
|
|
6573
6611
|
};
|
|
6574
|
-
var elevenlabsConversationErrorRecoveryRule =
|
|
6612
|
+
var elevenlabsConversationErrorRecoveryRule = rule86;
|
|
6575
6613
|
|
|
6576
6614
|
// src/providers/elevenlabs/rules/api-version-pinning.ts
|
|
6577
|
-
var
|
|
6615
|
+
var rule87 = {
|
|
6578
6616
|
meta: {
|
|
6579
6617
|
type: "suggestion",
|
|
6580
6618
|
docs: {
|
|
@@ -6614,10 +6652,10 @@ var rule89 = {
|
|
|
6614
6652
|
};
|
|
6615
6653
|
}
|
|
6616
6654
|
};
|
|
6617
|
-
var elevenlabsApiVersionPinningRule =
|
|
6655
|
+
var elevenlabsApiVersionPinningRule = rule87;
|
|
6618
6656
|
|
|
6619
6657
|
// src/providers/elevenlabs/rules/check-http-status-before-json.ts
|
|
6620
|
-
var
|
|
6658
|
+
var rule88 = {
|
|
6621
6659
|
meta: {
|
|
6622
6660
|
type: "problem",
|
|
6623
6661
|
docs: {
|
|
@@ -6713,11 +6751,11 @@ var rule90 = {
|
|
|
6713
6751
|
};
|
|
6714
6752
|
}
|
|
6715
6753
|
};
|
|
6716
|
-
var elevenlabsCheckHttpStatusBeforeJsonRule =
|
|
6754
|
+
var elevenlabsCheckHttpStatusBeforeJsonRule = rule88;
|
|
6717
6755
|
|
|
6718
6756
|
// src/providers/elevenlabs/rules/conversation-cleanup-on-error.ts
|
|
6719
6757
|
var END_METHODS = /* @__PURE__ */ new Set(["endSession", "endConversation"]);
|
|
6720
|
-
var
|
|
6758
|
+
var rule89 = {
|
|
6721
6759
|
meta: {
|
|
6722
6760
|
type: "suggestion",
|
|
6723
6761
|
docs: {
|
|
@@ -6778,11 +6816,11 @@ var rule91 = {
|
|
|
6778
6816
|
};
|
|
6779
6817
|
}
|
|
6780
6818
|
};
|
|
6781
|
-
var elevenlabsConversationCleanupOnErrorRule =
|
|
6819
|
+
var elevenlabsConversationCleanupOnErrorRule = rule89;
|
|
6782
6820
|
|
|
6783
6821
|
// src/providers/elevenlabs/rules/env-var-validation.ts
|
|
6784
6822
|
var ELEVENLABS_ENV_VAR_PATTERN = /^(XI_API_KEY|ELEVENLABS_API_KEY)$/;
|
|
6785
|
-
var
|
|
6823
|
+
var rule90 = {
|
|
6786
6824
|
meta: {
|
|
6787
6825
|
type: "suggestion",
|
|
6788
6826
|
docs: {
|
|
@@ -6843,7 +6881,7 @@ var rule92 = {
|
|
|
6843
6881
|
};
|
|
6844
6882
|
}
|
|
6845
6883
|
};
|
|
6846
|
-
var elevenlabsEnvVarValidationRule =
|
|
6884
|
+
var elevenlabsEnvVarValidationRule = rule90;
|
|
6847
6885
|
|
|
6848
6886
|
// src/providers/twilio/utils.ts
|
|
6849
6887
|
var POST_METHOD_NAMES = /* @__PURE__ */ new Set(["post"]);
|
|
@@ -6897,7 +6935,7 @@ function collectVarDeclarators2(node, out, depth = 0) {
|
|
|
6897
6935
|
}
|
|
6898
6936
|
|
|
6899
6937
|
// src/providers/twilio/rules/validate-webhook-signature.ts
|
|
6900
|
-
var
|
|
6938
|
+
var rule91 = {
|
|
6901
6939
|
meta: {
|
|
6902
6940
|
type: "problem",
|
|
6903
6941
|
docs: {
|
|
@@ -6939,10 +6977,10 @@ var rule93 = {
|
|
|
6939
6977
|
};
|
|
6940
6978
|
}
|
|
6941
6979
|
};
|
|
6942
|
-
var twilioValidateWebhookSignatureRule =
|
|
6980
|
+
var twilioValidateWebhookSignatureRule = rule91;
|
|
6943
6981
|
|
|
6944
6982
|
// src/providers/twilio/rules/taskrouter-attributes-match-consumer.ts
|
|
6945
|
-
var
|
|
6983
|
+
var rule92 = {
|
|
6946
6984
|
meta: {
|
|
6947
6985
|
type: "problem",
|
|
6948
6986
|
docs: {
|
|
@@ -7039,10 +7077,10 @@ var rule94 = {
|
|
|
7039
7077
|
};
|
|
7040
7078
|
}
|
|
7041
7079
|
};
|
|
7042
|
-
var twilioTaskrouterAttributesMatchConsumerRule =
|
|
7080
|
+
var twilioTaskrouterAttributesMatchConsumerRule = rule92;
|
|
7043
7081
|
|
|
7044
7082
|
// src/providers/twilio/rules/enqueue-task-json-stringify.ts
|
|
7045
|
-
var
|
|
7083
|
+
var rule93 = {
|
|
7046
7084
|
meta: {
|
|
7047
7085
|
type: "problem",
|
|
7048
7086
|
docs: {
|
|
@@ -7084,10 +7122,10 @@ var rule95 = {
|
|
|
7084
7122
|
};
|
|
7085
7123
|
}
|
|
7086
7124
|
};
|
|
7087
|
-
var twilioEnqueueTaskJsonStringifyRule =
|
|
7125
|
+
var twilioEnqueueTaskJsonStringifyRule = rule93;
|
|
7088
7126
|
|
|
7089
7127
|
// src/providers/twilio/rules/media-streams-key-by-call-sid.ts
|
|
7090
|
-
var
|
|
7128
|
+
var rule94 = {
|
|
7091
7129
|
meta: {
|
|
7092
7130
|
type: "problem",
|
|
7093
7131
|
docs: {
|
|
@@ -7163,12 +7201,12 @@ var rule96 = {
|
|
|
7163
7201
|
};
|
|
7164
7202
|
}
|
|
7165
7203
|
};
|
|
7166
|
-
var twilioMediaStreamsKeyByCallSidRule =
|
|
7204
|
+
var twilioMediaStreamsKeyByCallSidRule = rule94;
|
|
7167
7205
|
|
|
7168
7206
|
// src/providers/twilio/rules/await-or-catch-rest-calls-in-event-handlers.ts
|
|
7169
7207
|
var REST_RESOURCE_METHODS = /* @__PURE__ */ new Set(["create", "update", "remove", "fetch"]);
|
|
7170
7208
|
var EVENT_REGISTRATION_METHODS = /* @__PURE__ */ new Set(["onStart", "onStop", "onMedia", "onConnected", "on"]);
|
|
7171
|
-
var
|
|
7209
|
+
var rule95 = {
|
|
7172
7210
|
meta: {
|
|
7173
7211
|
type: "problem",
|
|
7174
7212
|
docs: {
|
|
@@ -7276,10 +7314,10 @@ var rule97 = {
|
|
|
7276
7314
|
};
|
|
7277
7315
|
}
|
|
7278
7316
|
};
|
|
7279
|
-
var twilioAwaitOrCatchRestCallsInEventHandlersRule =
|
|
7317
|
+
var twilioAwaitOrCatchRestCallsInEventHandlersRule = rule95;
|
|
7280
7318
|
|
|
7281
7319
|
// src/providers/twilio/rules/use-twiml-builder-not-string-templates.ts
|
|
7282
|
-
var
|
|
7320
|
+
var rule96 = {
|
|
7283
7321
|
meta: {
|
|
7284
7322
|
type: "suggestion",
|
|
7285
7323
|
docs: {
|
|
@@ -7308,10 +7346,10 @@ var rule98 = {
|
|
|
7308
7346
|
};
|
|
7309
7347
|
}
|
|
7310
7348
|
};
|
|
7311
|
-
var twilioUseTwimlBuilderNotStringTemplatesRule =
|
|
7349
|
+
var twilioUseTwimlBuilderNotStringTemplatesRule = rule96;
|
|
7312
7350
|
|
|
7313
7351
|
// src/providers/twilio/rules/media-streams-mark-pacing.ts
|
|
7314
|
-
var
|
|
7352
|
+
var rule97 = {
|
|
7315
7353
|
meta: {
|
|
7316
7354
|
type: "suggestion",
|
|
7317
7355
|
docs: {
|
|
@@ -7387,10 +7425,10 @@ var rule99 = {
|
|
|
7387
7425
|
};
|
|
7388
7426
|
}
|
|
7389
7427
|
};
|
|
7390
|
-
var twilioMediaStreamsMarkPacingRule =
|
|
7428
|
+
var twilioMediaStreamsMarkPacingRule = rule97;
|
|
7391
7429
|
|
|
7392
7430
|
// src/providers/twilio/rules/validate-all-request-inputs.ts
|
|
7393
|
-
var
|
|
7431
|
+
var rule98 = {
|
|
7394
7432
|
meta: {
|
|
7395
7433
|
type: "suggestion",
|
|
7396
7434
|
docs: {
|
|
@@ -7465,10 +7503,10 @@ var rule100 = {
|
|
|
7465
7503
|
};
|
|
7466
7504
|
}
|
|
7467
7505
|
};
|
|
7468
|
-
var twilioValidateAllRequestInputsRule =
|
|
7506
|
+
var twilioValidateAllRequestInputsRule = rule98;
|
|
7469
7507
|
|
|
7470
7508
|
// src/providers/twilio/rules/media-streams-mark-name-string.ts
|
|
7471
|
-
var
|
|
7509
|
+
var rule99 = {
|
|
7472
7510
|
meta: {
|
|
7473
7511
|
type: "suggestion",
|
|
7474
7512
|
docs: {
|
|
@@ -7518,7 +7556,7 @@ var rule101 = {
|
|
|
7518
7556
|
};
|
|
7519
7557
|
}
|
|
7520
7558
|
};
|
|
7521
|
-
var twilioMediaStreamsMarkNameStringRule =
|
|
7559
|
+
var twilioMediaStreamsMarkNameStringRule = rule99;
|
|
7522
7560
|
|
|
7523
7561
|
// src/providers/openai-realtime/utils.ts
|
|
7524
7562
|
function isOpenAIRealtimeUrlArg(node) {
|
|
@@ -7658,7 +7696,7 @@ function resolveStringValue(node, stringVarValues) {
|
|
|
7658
7696
|
}
|
|
7659
7697
|
|
|
7660
7698
|
// src/providers/openai-realtime/rules/migrate-beta-to-ga.ts
|
|
7661
|
-
var
|
|
7699
|
+
var rule100 = {
|
|
7662
7700
|
meta: {
|
|
7663
7701
|
type: "problem",
|
|
7664
7702
|
docs: {
|
|
@@ -7695,7 +7733,7 @@ var rule102 = {
|
|
|
7695
7733
|
};
|
|
7696
7734
|
}
|
|
7697
7735
|
};
|
|
7698
|
-
var openaiRealtimeMigrateBetaToGaRule =
|
|
7736
|
+
var openaiRealtimeMigrateBetaToGaRule = rule100;
|
|
7699
7737
|
|
|
7700
7738
|
// src/providers/openai-realtime/rules/no-log-raw-message-payloads.ts
|
|
7701
7739
|
var LOG_METHOD_NAMES = /* @__PURE__ */ new Set(["info", "warn", "error", "log"]);
|
|
@@ -7731,7 +7769,7 @@ function findCallExpressions(node, out, depth = 0) {
|
|
|
7731
7769
|
if (val && typeof val === "object") findCallExpressions(val, out, depth + 1);
|
|
7732
7770
|
}
|
|
7733
7771
|
}
|
|
7734
|
-
var
|
|
7772
|
+
var rule101 = {
|
|
7735
7773
|
meta: {
|
|
7736
7774
|
type: "problem",
|
|
7737
7775
|
docs: {
|
|
@@ -7775,7 +7813,7 @@ var rule103 = {
|
|
|
7775
7813
|
};
|
|
7776
7814
|
}
|
|
7777
7815
|
};
|
|
7778
|
-
var openaiRealtimeNoLogRawMessagePayloadsRule =
|
|
7816
|
+
var openaiRealtimeNoLogRawMessagePayloadsRule = rule101;
|
|
7779
7817
|
|
|
7780
7818
|
// src/providers/openai-realtime/rules/handle-error-server-event.ts
|
|
7781
7819
|
function isTypeMemberExpression(node) {
|
|
@@ -7806,13 +7844,13 @@ function collectTypeComparisonLiterals(node, out, depth = 0) {
|
|
|
7806
7844
|
if (val && typeof val === "object") collectTypeComparisonLiterals(val, out, depth + 1);
|
|
7807
7845
|
}
|
|
7808
7846
|
}
|
|
7809
|
-
var
|
|
7847
|
+
var rule102 = {
|
|
7810
7848
|
meta: {
|
|
7811
7849
|
type: "problem",
|
|
7812
7850
|
docs: {
|
|
7813
7851
|
description: 'OpenAI Realtime message handlers must check for the API-level "error" server event',
|
|
7814
7852
|
category: "reliability",
|
|
7815
|
-
docsUrl: "https://developers.openai.com/api/
|
|
7853
|
+
docsUrl: "https://developers.openai.com/api/reference/resources/realtime/server-events",
|
|
7816
7854
|
rationale: "The Realtime API's own error server event \u2014 distinct from the WebSocket transport-level error event \u2014 covers rate limits, invalid session.update payloads, content-moderation blocks, and retired/invalid model ids. A message handler that branches on event types but never checks for 'error' lets the call continue silently with translation/processing permanently dead for that leg, with no operator-visible signal of why.",
|
|
7817
7855
|
recommended: true
|
|
7818
7856
|
},
|
|
@@ -7842,7 +7880,7 @@ var rule104 = {
|
|
|
7842
7880
|
};
|
|
7843
7881
|
}
|
|
7844
7882
|
};
|
|
7845
|
-
var openaiRealtimeHandleErrorServerEventRule =
|
|
7883
|
+
var openaiRealtimeHandleErrorServerEventRule = rule102;
|
|
7846
7884
|
|
|
7847
7885
|
// src/providers/openai-realtime/rules/reconnect-on-drop.ts
|
|
7848
7886
|
var RECONNECT_NAME_PATTERN = /reconnect/i;
|
|
@@ -7866,7 +7904,7 @@ function hasReconnectAttempt(node, depth = 0) {
|
|
|
7866
7904
|
}
|
|
7867
7905
|
return false;
|
|
7868
7906
|
}
|
|
7869
|
-
var
|
|
7907
|
+
var rule103 = {
|
|
7870
7908
|
meta: {
|
|
7871
7909
|
type: "problem",
|
|
7872
7910
|
docs: {
|
|
@@ -7899,17 +7937,17 @@ var rule105 = {
|
|
|
7899
7937
|
};
|
|
7900
7938
|
}
|
|
7901
7939
|
};
|
|
7902
|
-
var openaiRealtimeReconnectOnDropRule =
|
|
7940
|
+
var openaiRealtimeReconnectOnDropRule = rule103;
|
|
7903
7941
|
|
|
7904
7942
|
// src/providers/openai-realtime/rules/avoid-dated-preview-snapshots.ts
|
|
7905
7943
|
var DATED_PREVIEW_MODEL_PATTERN = /model=[^&'"`]*-preview-\d{4}-\d{2}-\d{2}/;
|
|
7906
|
-
var
|
|
7944
|
+
var rule104 = {
|
|
7907
7945
|
meta: {
|
|
7908
7946
|
type: "suggestion",
|
|
7909
7947
|
docs: {
|
|
7910
7948
|
description: "OpenAI Realtime connections should not pin to a dated preview model snapshot",
|
|
7911
7949
|
category: "correctness",
|
|
7912
|
-
docsUrl: "https://developers.openai.com/api/
|
|
7950
|
+
docsUrl: "https://developers.openai.com/api/reference/resources/realtime/subresources/client_secrets",
|
|
7913
7951
|
rationale: "Dated preview snapshots are the category of model id OpenAI has historically retired with advance-notice sunset windows. Pinning to a dated preview snapshot rather than the floating GA alias (or a current dated GA snapshot) maximizes exposure to an eventual retirement, with no code path to detect or gracefully react to a model_not_found-style error if/when that happens.",
|
|
7914
7952
|
recommended: true
|
|
7915
7953
|
},
|
|
@@ -7938,16 +7976,16 @@ var rule106 = {
|
|
|
7938
7976
|
};
|
|
7939
7977
|
}
|
|
7940
7978
|
};
|
|
7941
|
-
var openaiRealtimeAvoidDatedPreviewSnapshotsRule =
|
|
7979
|
+
var openaiRealtimeAvoidDatedPreviewSnapshotsRule = rule104;
|
|
7942
7980
|
|
|
7943
7981
|
// src/providers/openai-realtime/rules/verify-deprecated-session-fields.ts
|
|
7944
|
-
var
|
|
7982
|
+
var rule105 = {
|
|
7945
7983
|
meta: {
|
|
7946
7984
|
type: "suggestion",
|
|
7947
7985
|
docs: {
|
|
7948
7986
|
description: "OpenAI Realtime session.update payloads should not rely on an unverified 'temperature' field",
|
|
7949
7987
|
category: "correctness",
|
|
7950
|
-
docsUrl: "https://developers.openai.com/api/
|
|
7988
|
+
docsUrl: "https://developers.openai.com/api/reference/resources/realtime/subresources/client_secrets",
|
|
7951
7989
|
rationale: "Two independent fetches of the current Realtime sessions API reference did not surface 'temperature' as a documented session field. Sending an undocumented field is typically harmless (silently ignored), but code that depends on an assumed constraint (e.g. 'a hard floor of 0.6') for behavior like deterministic output is relying on a claim that is no longer traceable to any current, citable source.",
|
|
7952
7990
|
recommended: true
|
|
7953
7991
|
},
|
|
@@ -7972,15 +8010,20 @@ var rule107 = {
|
|
|
7972
8010
|
};
|
|
7973
8011
|
}
|
|
7974
8012
|
};
|
|
7975
|
-
var openaiRealtimeVerifyDeprecatedSessionFieldsRule =
|
|
8013
|
+
var openaiRealtimeVerifyDeprecatedSessionFieldsRule = rule105;
|
|
7976
8014
|
|
|
7977
8015
|
// src/providers/openai-realtime/rules/buffer-audio-until-session-ready.ts
|
|
7978
8016
|
var QUEUE_CALL_NAME_PATTERN = /^(push|unshift|enqueue|queue)$/i;
|
|
7979
|
-
function
|
|
7980
|
-
if (test?.type !== "BinaryExpression"
|
|
8017
|
+
function readyStateOpenCheckKind(test) {
|
|
8018
|
+
if (test?.type !== "BinaryExpression") return null;
|
|
8019
|
+
const isEq = test.operator === "===" || test.operator === "==";
|
|
8020
|
+
const isNeq = test.operator === "!==" || test.operator === "!=";
|
|
8021
|
+
if (!isEq && !isNeq) return null;
|
|
7981
8022
|
const isReadyStateMember = (n) => n?.type === "MemberExpression" && n.property?.type === "Identifier" && n.property.name === "readyState";
|
|
7982
8023
|
const isOpenValue = (n) => n?.type === "MemberExpression" && n.property?.type === "Identifier" && n.property.name === "OPEN" || n?.type === "Literal" && n.value === 1;
|
|
7983
|
-
|
|
8024
|
+
const matches = isReadyStateMember(test.left) && isOpenValue(test.right) || isReadyStateMember(test.right) && isOpenValue(test.left);
|
|
8025
|
+
if (!matches) return null;
|
|
8026
|
+
return isEq ? "is-open" : "is-not-open";
|
|
7984
8027
|
}
|
|
7985
8028
|
function hasQueueCall(node, depth = 0) {
|
|
7986
8029
|
if (!node || typeof node !== "object" || depth > 40) return false;
|
|
@@ -7997,13 +8040,13 @@ function hasQueueCall(node, depth = 0) {
|
|
|
7997
8040
|
}
|
|
7998
8041
|
return false;
|
|
7999
8042
|
}
|
|
8000
|
-
var
|
|
8043
|
+
var rule106 = {
|
|
8001
8044
|
meta: {
|
|
8002
8045
|
type: "suggestion",
|
|
8003
8046
|
docs: {
|
|
8004
8047
|
description: "Audio sent before an OpenAI Realtime socket is open must be buffered, not dropped",
|
|
8005
8048
|
category: "reliability",
|
|
8006
|
-
docsUrl: "https://developers.openai.com/api/
|
|
8049
|
+
docsUrl: "https://developers.openai.com/api/reference/resources/realtime/client-events",
|
|
8007
8050
|
rationale: "Caller audio can arrive before the Realtime WebSocket finishes its connect + session.update round-trip. A readyState !== OPEN branch that only logs and returns silently drops every audio chunk that arrives in that window, meaning the first fragment of speech is lost to translation/processing on every call.",
|
|
8008
8051
|
recommended: true
|
|
8009
8052
|
},
|
|
@@ -8015,18 +8058,20 @@ var rule108 = {
|
|
|
8015
8058
|
create(context) {
|
|
8016
8059
|
return {
|
|
8017
8060
|
IfStatement(node) {
|
|
8018
|
-
|
|
8019
|
-
if (!
|
|
8020
|
-
|
|
8021
|
-
|
|
8061
|
+
const kind = readyStateOpenCheckKind(node.test);
|
|
8062
|
+
if (!kind) return;
|
|
8063
|
+
const notOpenBranch = kind === "is-open" ? node.alternate : node.consequent;
|
|
8064
|
+
if (!notOpenBranch) return;
|
|
8065
|
+
if (hasQueueCall(notOpenBranch)) return;
|
|
8066
|
+
context.report({ node: notOpenBranch, messageId: "audioDroppedNotBuffered" });
|
|
8022
8067
|
}
|
|
8023
8068
|
};
|
|
8024
8069
|
}
|
|
8025
8070
|
};
|
|
8026
|
-
var openaiRealtimeBufferAudioUntilSessionReadyRule =
|
|
8071
|
+
var openaiRealtimeBufferAudioUntilSessionReadyRule = rule106;
|
|
8027
8072
|
|
|
8028
8073
|
// src/providers/openai-realtime/rules/send-safety-identifier.ts
|
|
8029
|
-
var
|
|
8074
|
+
var rule107 = {
|
|
8030
8075
|
meta: {
|
|
8031
8076
|
type: "suggestion",
|
|
8032
8077
|
docs: {
|
|
@@ -8059,10 +8104,10 @@ var rule109 = {
|
|
|
8059
8104
|
};
|
|
8060
8105
|
}
|
|
8061
8106
|
};
|
|
8062
|
-
var openaiRealtimeSendSafetyIdentifierRule =
|
|
8107
|
+
var openaiRealtimeSendSafetyIdentifierRule = rule107;
|
|
8063
8108
|
|
|
8064
8109
|
// src/providers/openai-realtime/rules/transcription-model-choice.ts
|
|
8065
|
-
var
|
|
8110
|
+
var rule108 = {
|
|
8066
8111
|
meta: {
|
|
8067
8112
|
type: "suggestion",
|
|
8068
8113
|
docs: {
|
|
@@ -8073,7 +8118,7 @@ var rule110 = {
|
|
|
8073
8118
|
recommended: true
|
|
8074
8119
|
},
|
|
8075
8120
|
messages: {
|
|
8076
|
-
nonStreamingTranscriptionModel: "
|
|
8121
|
+
nonStreamingTranscriptionModel: "This session's input transcription is configured with 'whisper-1', which is not natively streaming and not optimized for realtime sessions."
|
|
8077
8122
|
},
|
|
8078
8123
|
schema: []
|
|
8079
8124
|
},
|
|
@@ -8086,7 +8131,12 @@ var rule110 = {
|
|
|
8086
8131
|
if (!isSessionUpdate) return;
|
|
8087
8132
|
const sessionProp = findProperty4(node, "session");
|
|
8088
8133
|
if (sessionProp?.value?.type !== "ObjectExpression") return;
|
|
8089
|
-
|
|
8134
|
+
let transcriptionProp = findProperty4(sessionProp.value, "input_audio_transcription");
|
|
8135
|
+
if (!transcriptionProp) {
|
|
8136
|
+
const audioProp = findProperty4(sessionProp.value, "audio");
|
|
8137
|
+
const inputProp = audioProp?.value?.type === "ObjectExpression" ? findProperty4(audioProp.value, "input") : null;
|
|
8138
|
+
transcriptionProp = inputProp?.value?.type === "ObjectExpression" ? findProperty4(inputProp.value, "transcription") : null;
|
|
8139
|
+
}
|
|
8090
8140
|
if (transcriptionProp?.value?.type !== "ObjectExpression") return;
|
|
8091
8141
|
const modelProp = findProperty4(transcriptionProp.value, "model");
|
|
8092
8142
|
const modelValue = modelProp?.value;
|
|
@@ -8097,7 +8147,7 @@ var rule110 = {
|
|
|
8097
8147
|
};
|
|
8098
8148
|
}
|
|
8099
8149
|
};
|
|
8100
|
-
var openaiRealtimeTranscriptionModelChoiceRule =
|
|
8150
|
+
var openaiRealtimeTranscriptionModelChoiceRule = rule108;
|
|
8101
8151
|
|
|
8102
8152
|
// src/plugin/index.ts
|
|
8103
8153
|
var plugin = {
|
|
@@ -8142,7 +8192,6 @@ var plugin = {
|
|
|
8142
8192
|
"firebase-rtdb-write-promise-not-handled": firebaseRtdbWritePromiseNotHandledRule,
|
|
8143
8193
|
"firebase-firestore-rules-expired": firebaseFirestoreRulesExpiredRule,
|
|
8144
8194
|
"firebase-id-token-cookie-flags": firebaseIdTokenCookieFlagsRule,
|
|
8145
|
-
"firebase-middleware-token-not-verified": firebaseMiddlewareTokenNotVerifiedRule,
|
|
8146
8195
|
"firebase-hardcoded-user-id": firebaseHardcodedUserIdRule,
|
|
8147
8196
|
"firebase-auth-user-not-found-disclosure": firebaseAuthUserNotFoundDisclosureRule,
|
|
8148
8197
|
"firebase-signup-password-confirm": firebaseSignupPasswordConfirmRule,
|
|
@@ -8181,7 +8230,6 @@ var plugin = {
|
|
|
8181
8230
|
"tiptap-appendTransaction-add-to-history": tiptapAppendTransactionAddToHistoryRule,
|
|
8182
8231
|
"tiptap-appendTransaction-full-scan": tiptapAppendTransactionFullScanRule,
|
|
8183
8232
|
"tiptap-atom-node-wrap-in": tiptapAtomNodeWrapInRule,
|
|
8184
|
-
"tiptap-twitter-url-regex": tiptapTwitterUrlRegexRule,
|
|
8185
8233
|
"tiptap-drop-handler-pos-precedence": tiptapDropHandlerPosPrecedenceRule,
|
|
8186
8234
|
"tiptap-prefer-table-kit": tiptapPreferTableKitRule,
|
|
8187
8235
|
"tiptap-tiptap-markdown-missing-node-spec": tiptapTiptapMarkdownMissingNodeSpecRule,
|