@api-doctor/cli 0.0.3 → 0.0.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/plugin.js CHANGED
@@ -2825,85 +2825,47 @@ var rule37 = {
2825
2825
  };
2826
2826
  var firebaseRtdbWritePromiseNotHandledRule = rule37;
2827
2827
 
2828
- // src/providers/lovable/utils.ts
2829
- var LLM_HOST_PATTERN = /api\.anthropic\.com|api\.openai\.com/;
2830
- function containsKnownLlmHost(node) {
2831
- if (node?.type === "Literal" && typeof node.value === "string") {
2832
- return LLM_HOST_PATTERN.test(node.value);
2833
- }
2834
- if (node?.type === "TemplateLiteral") {
2835
- return (node.quasis ?? []).some((q) => LLM_HOST_PATTERN.test(q.value?.raw ?? ""));
2836
- }
2837
- return false;
2838
- }
2839
-
2840
- // src/providers/lovable/rules/no-client-side-secret-fetch.ts
2828
+ // src/providers/firebase/rules/firestore-rules-expired.ts
2841
2829
  var rule38 = {
2842
2830
  meta: {
2843
2831
  type: "problem",
2844
2832
  docs: {
2845
- description: "LLM provider calls must not run client-side with a VITE_-exposed API key",
2833
+ description: "Firestore security rules contain a hard-coded expiry date that has already passed",
2846
2834
  category: "security",
2847
- cwe: "CWE-522",
2848
- owasp: "A02:2021 \u2013 Cryptographic Failures",
2849
- rationale: "Anything read from import.meta.env.VITE_* is inlined into the production JS bundle at build time and is visible to every site visitor via the Network tab or by reading the bundle \u2014 no git access required. Lovable documents Edge Functions specifically so the provider key stays server-side in Secrets and the browser calls your own Edge Function instead of the provider directly.",
2850
- docsUrl: "https://docs.lovable.dev/features/security",
2835
+ cwe: "CWE-284",
2836
+ owasp: "A01:2021 Broken Access Control",
2837
+ rationale: 'The Firebase "get started" rules template includes a timestamp.date() expiry. Once that date passes the condition permanently evaluates to false, denying every client read and write. Replace with proper auth-based rules scoped to the authenticated user.',
2838
+ docsUrl: "https://firebase.google.com/docs/firestore/security/insecure-rules",
2851
2839
  recommended: true
2852
2840
  },
2853
2841
  messages: {
2854
- clientSideSecretFetch: "This fetch() calls an LLM provider directly with a key sourced from import.meta.env.VITE_* \u2014 that key ships into the browser bundle and is visible to every visitor."
2855
- }
2842
+ firestoreRulesExpired: "Firestore security rules contain a hard-coded expiry date that has already passed. All client reads and writes are permanently denied. Replace with proper auth-based rules."
2843
+ },
2844
+ schema: []
2856
2845
  },
2857
2846
  create(context) {
2858
- const viteVars = /* @__PURE__ */ new Set();
2859
- function propName2(node) {
2860
- if (!node) return void 0;
2861
- if (node.type === "Identifier") return node.name;
2862
- if (node.type === "Literal" && typeof node.value === "string") return node.value;
2863
- return void 0;
2864
- }
2865
- function isImportMetaEnvViteAccess(node) {
2866
- if (node?.type !== "MemberExpression" || node.computed) return false;
2867
- const propertyName = node.property?.name;
2868
- if (typeof propertyName !== "string" || !propertyName.startsWith("VITE_")) return false;
2869
- const envMember = node.object;
2870
- if (envMember?.type !== "MemberExpression" || envMember.computed) return false;
2871
- if (envMember.property?.name !== "env") return false;
2872
- const metaProp = envMember.object;
2873
- return metaProp?.type === "MetaProperty" && metaProp.meta?.name === "import" && metaProp.property?.name === "meta";
2874
- }
2875
- function referencesViteSecret(node) {
2876
- if (!node) return false;
2877
- if (isImportMetaEnvViteAccess(node)) return true;
2878
- if (node.type === "Identifier" && viteVars.has(node.name)) return true;
2879
- if (node.type === "TemplateLiteral") {
2880
- return (node.expressions ?? []).some((e) => referencesViteSecret(e));
2847
+ const EXPIRED_YEAR = 2025;
2848
+ function checkStringForExpiredDate(value, reportNode) {
2849
+ const re = /timestamp\.date\(\s*(\d{4})\s*,/g;
2850
+ let match;
2851
+ while ((match = re.exec(value)) !== null) {
2852
+ const year = parseInt(match[1], 10);
2853
+ if (year <= EXPIRED_YEAR) {
2854
+ context.report({ node: reportNode, messageId: "firestoreRulesExpired" });
2855
+ return;
2856
+ }
2881
2857
  }
2882
- return false;
2883
2858
  }
2884
2859
  return {
2885
- VariableDeclarator(node) {
2886
- if (node.id?.type === "Identifier" && isImportMetaEnvViteAccess(node.init)) {
2887
- viteVars.add(node.id.name);
2888
- }
2860
+ Literal(node) {
2861
+ if (typeof node.value !== "string") return;
2862
+ checkStringForExpiredDate(node.value, node);
2889
2863
  },
2890
- CallExpression(node) {
2891
- const callee = node.callee;
2892
- if (callee?.type !== "Identifier" || callee.name !== "fetch") return;
2893
- const urlArg = node.arguments?.[0];
2894
- if (!containsKnownLlmHost(urlArg)) return;
2895
- const optsArg = node.arguments?.[1];
2896
- if (optsArg?.type !== "ObjectExpression") return;
2897
- const headersProp = (optsArg.properties ?? []).find(
2898
- (p) => p?.type === "Property" && propName2(p.key) === "headers"
2899
- );
2900
- if (!headersProp || headersProp.value?.type !== "ObjectExpression") return;
2901
- for (const hp of headersProp.value.properties ?? []) {
2902
- if (hp?.type !== "Property") continue;
2903
- const keyName = propName2(hp.key)?.toLowerCase();
2904
- if (keyName !== "x-api-key" && keyName !== "authorization") continue;
2905
- if (referencesViteSecret(hp.value)) {
2906
- context.report({ node, messageId: "clientSideSecretFetch" });
2864
+ TemplateLiteral(node) {
2865
+ for (const quasi of node.quasis ?? []) {
2866
+ const cooked = quasi?.value?.cooked ?? quasi?.value?.raw ?? "";
2867
+ if (typeof cooked === "string" && cooked.includes("timestamp.date(")) {
2868
+ checkStringForExpiredDate(cooked, node);
2907
2869
  return;
2908
2870
  }
2909
2871
  }
@@ -2911,1189 +2873,2320 @@ var rule38 = {
2911
2873
  };
2912
2874
  }
2913
2875
  };
2914
- var lovableNoClientSideSecretFetchRule = rule38;
2876
+ var firebaseFirestoreRulesExpiredRule = rule38;
2915
2877
 
2916
- // src/providers/lovable/rules/paid-flag-without-edge-function.ts
2917
- var PRICE_PATTERN = /\$\s?\d/;
2918
- var FLAG_PROPERTY_PATTERN = /^is_[a-z0-9_]+$/i;
2919
- var FLAG_SUFFIX_PATTERN = /_(unlocked|active|enabled)$/i;
2878
+ // src/providers/firebase/rules/id-token-cookie-flags.ts
2879
+ function findProp(obj, name) {
2880
+ if (obj?.type !== "ObjectExpression") return void 0;
2881
+ return (obj.properties ?? []).find(
2882
+ (p) => p?.type === "Property" && (p.key?.type === "Identifier" && p.key.name === name || p.key?.type === "Literal" && p.key.value === name)
2883
+ );
2884
+ }
2920
2885
  var rule39 = {
2921
2886
  meta: {
2922
2887
  type: "problem",
2923
2888
  docs: {
2924
- description: "A paid feature flag must not be set by a direct database update with no payment-provider call",
2889
+ description: "Firebase ID token stored in a cookie without httpOnly flag",
2925
2890
  category: "security",
2926
- cwe: "CWE-840",
2927
- owasp: "A04:2021 \u2013 Insecure Design",
2928
- 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.",
2929
- docsUrl: "https://docs.lovable.dev/features/cloud",
2891
+ cwe: "CWE-1004",
2892
+ owasp: "A02:2021 Cryptographic Failures",
2893
+ rationale: "Storing a Firebase ID token in a non-httpOnly cookie makes it readable by any JavaScript on the page. An XSS vulnerability can steal the token and impersonate the user. Use the Firebase Admin SDK createSessionCookie flow to issue a proper httpOnly session cookie instead.",
2894
+ docsUrl: "https://firebase.google.com/docs/auth/admin/manage-cookies",
2930
2895
  recommended: true
2931
2896
  },
2932
2897
  messages: {
2933
- paidFlagWithoutPayment: "This sets a paid-looking flag via a direct database update, but nothing in this function calls a payment provider or Edge Function first \u2014 the flag can be set without anyone paying."
2934
- }
2898
+ idTokenCookieMissingHttpOnly: "Firebase ID token stored in cookie without httpOnly: true. Any XSS vulnerability can steal the token. Use the Firebase Admin SDK createSessionCookie flow instead."
2899
+ },
2900
+ schema: []
2935
2901
  },
2936
2902
  create(context) {
2937
- const stack = [];
2938
- const states = /* @__PURE__ */ new Map();
2939
- let sawPriceLabel = false;
2940
- function ensureState(fn) {
2941
- let s = states.get(fn);
2942
- if (!s) {
2943
- s = { sawFlagUpdate: false, updateNode: null, sawPaymentCall: false };
2944
- states.set(fn, s);
2945
- }
2946
- return s;
2947
- }
2948
- function top() {
2949
- return stack[stack.length - 1];
2950
- }
2951
- function pushScope(node) {
2952
- stack.push(node);
2953
- ensureState(node);
2954
- }
2955
- function popScope() {
2956
- stack.pop();
2957
- }
2958
- function propName2(node) {
2959
- if (!node) return void 0;
2960
- if (node.type === "Identifier") return node.name;
2961
- if (node.type === "Literal" && typeof node.value === "string") return node.value;
2962
- return void 0;
2963
- }
2964
- function isBooleanFlagProperty(name) {
2965
- if (!name) return false;
2966
- return FLAG_PROPERTY_PATTERN.test(name) || FLAG_SUFFIX_PATTERN.test(name);
2967
- }
2968
- function isSupabaseUpdateCall(node) {
2969
- if (node?.type !== "CallExpression") return false;
2970
- const callee = node.callee;
2971
- return callee?.type === "MemberExpression" && callee.property?.name === "update";
2972
- }
2973
- function updateSetsBooleanFlag(node) {
2974
- const arg = node.arguments?.[0];
2975
- if (arg?.type !== "ObjectExpression") return false;
2976
- return (arg.properties ?? []).some(
2977
- (p) => p?.type === "Property" && isBooleanFlagProperty(propName2(p.key))
2978
- );
2979
- }
2980
- function memberChainNames2(node, names = []) {
2981
- if (node?.type === "MemberExpression") {
2982
- memberChainNames2(node.object, names);
2983
- const n = propName2(node.property);
2984
- if (n) names.push(n);
2985
- } else if (node?.type === "Identifier") {
2986
- names.push(node.name);
2987
- } else if (node?.type === "CallExpression") {
2988
- memberChainNames2(node.callee, names);
2989
- }
2990
- return names;
2903
+ function isTokenName(val) {
2904
+ return /token/i.test(val);
2991
2905
  }
2992
- function isPaymentRelatedCall(node) {
2993
- if (node?.type !== "CallExpression") return false;
2994
- const chain = memberChainNames2(node.callee).join(".").toLowerCase();
2995
- if (/stripe|checkout|payment/.test(chain)) return true;
2996
- if (chain.includes("functions") && chain.endsWith(".invoke")) return true;
2997
- if (node.callee?.type === "Identifier" && node.callee.name === "fetch") {
2998
- const urlArg = node.arguments?.[0];
2999
- if (urlArg?.type === "Literal" && typeof urlArg.value === "string") {
3000
- return urlArg.value.includes("/functions/");
3001
- }
3002
- if (urlArg?.type === "TemplateLiteral") {
3003
- return (urlArg.quasis ?? []).some((q) => (q.value?.raw ?? "").includes("/functions/"));
3004
- }
3005
- }
3006
- return false;
2906
+ function hasHttpOnlyTrue(optsNode) {
2907
+ if (optsNode?.type !== "ObjectExpression") return false;
2908
+ const prop = findProp(optsNode, "httpOnly");
2909
+ if (!prop) return false;
2910
+ return prop.value?.type === "Literal" && prop.value.value === true;
3007
2911
  }
3008
2912
  return {
3009
- Program(node) {
3010
- pushScope(node);
3011
- },
3012
- "Program:exit"() {
3013
- if (!sawPriceLabel) return;
3014
- for (const state of states.values()) {
3015
- if (state.sawFlagUpdate && !state.sawPaymentCall) {
3016
- context.report({ node: state.updateNode, messageId: "paidFlagWithoutPayment" });
3017
- }
3018
- }
3019
- },
3020
- FunctionDeclaration(node) {
3021
- pushScope(node);
3022
- },
3023
- "FunctionDeclaration:exit"() {
3024
- popScope();
3025
- },
3026
- FunctionExpression(node) {
3027
- pushScope(node);
3028
- },
3029
- "FunctionExpression:exit"() {
3030
- popScope();
3031
- },
3032
- ArrowFunctionExpression(node) {
3033
- pushScope(node);
3034
- },
3035
- "ArrowFunctionExpression:exit"() {
3036
- popScope();
3037
- },
3038
- Literal(node) {
3039
- if (typeof node.value === "string" && PRICE_PATTERN.test(node.value)) sawPriceLabel = true;
3040
- },
3041
- JSXText(node) {
3042
- if (typeof node.value === "string" && PRICE_PATTERN.test(node.value)) sawPriceLabel = true;
3043
- },
3044
- TemplateElement(node) {
3045
- if (PRICE_PATTERN.test(node.value?.raw ?? "")) sawPriceLabel = true;
3046
- },
3047
2913
  CallExpression(node) {
3048
- const fn = top();
3049
- if (!fn) return;
3050
- const state = ensureState(fn);
3051
- if (isSupabaseUpdateCall(node) && updateSetsBooleanFlag(node) && !state.sawFlagUpdate) {
3052
- state.sawFlagUpdate = true;
3053
- state.updateNode = node;
2914
+ const callee = node.callee;
2915
+ const isCookieSet = callee?.type === "Identifier" && callee.name === "setCookie";
2916
+ if (!isCookieSet) return;
2917
+ const args = node.arguments ?? [];
2918
+ const nameArg = args[0];
2919
+ if (nameArg?.type !== "Literal" || typeof nameArg.value !== "string") return;
2920
+ if (!isTokenName(nameArg.value)) return;
2921
+ const optsArg = args.length >= 3 ? args[2] : null;
2922
+ if (!optsArg || optsArg.type !== "ObjectExpression") {
2923
+ context.report({ node, messageId: "idTokenCookieMissingHttpOnly" });
2924
+ return;
3054
2925
  }
3055
- if (isPaymentRelatedCall(node)) {
3056
- state.sawPaymentCall = true;
2926
+ if (!hasHttpOnlyTrue(optsArg)) {
2927
+ context.report({ node, messageId: "idTokenCookieMissingHttpOnly" });
3057
2928
  }
3058
2929
  }
3059
2930
  };
3060
2931
  }
3061
2932
  };
3062
- var lovablePaidFlagWithoutEdgeFunctionRule = rule39;
2933
+ var firebaseIdTokenCookieFlagsRule = rule39;
3063
2934
 
3064
- // src/providers/lovable/rules/expiry-column-never-checked.ts
3065
- var EXPIRY_COLUMN_PATTERN = /_(until|expires?_at|expiry)$/i;
3066
- var DATE_COMPARISON_OPERATORS = /* @__PURE__ */ new Set([">", "<", ">=", "<="]);
3067
- var FILTER_METHODS = /* @__PURE__ */ new Set(["gt", "gte", "lt", "lte"]);
2935
+ // src/providers/firebase/rules/middleware-token-not-verified.ts
3068
2936
  var rule40 = {
3069
2937
  meta: {
3070
- type: "suggestion",
2938
+ type: "problem",
3071
2939
  docs: {
3072
- description: "An expiry column must be checked against the current time somewhere",
3073
- category: "correctness",
3074
- 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.",
3075
- docsUrl: "https://docs.lovable.dev/features/cloud",
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",
3076
2946
  recommended: true
3077
2947
  },
3078
2948
  messages: {
3079
- expiryNeverChecked: 'The "{{column}}" column is written here but is never compared against the current time anywhere in this file, so it never actually expires anything.'
3080
- }
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: []
3081
2952
  },
3082
2953
  create(context) {
3083
- const writtenColumns = /* @__PURE__ */ new Map();
3084
- const readColumns = /* @__PURE__ */ new Set();
3085
- function propName2(node) {
3086
- if (!node) return void 0;
3087
- if (node.type === "Identifier") return node.name;
3088
- if (node.type === "Literal" && typeof node.value === "string") return node.value;
3089
- return void 0;
3090
- }
3091
- function isSupabaseUpdateCall(node) {
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) {
3092
2960
  if (node?.type !== "CallExpression") return false;
3093
2961
  const callee = node.callee;
3094
- return callee?.type === "MemberExpression" && callee.property?.name === "update";
3095
- }
3096
- function containsDateNowOrNewDate(node, depth = 0) {
3097
- if (!node || typeof node !== "object" || depth > 6) return false;
3098
- if (node.type === "NewExpression" && node.callee?.type === "Identifier" && node.callee.name === "Date") {
3099
- return true;
3100
- }
3101
- if (node.type === "CallExpression" && node.callee?.type === "MemberExpression" && node.callee.object?.type === "Identifier" && node.callee.object.name === "Date" && node.callee.property?.name === "now") {
3102
- return true;
3103
- }
3104
- if (node.type === "MemberExpression") {
3105
- return containsDateNowOrNewDate(node.object, depth + 1) || containsDateNowOrNewDate(node.callee, depth + 1);
3106
- }
3107
- if (node.type === "CallExpression") {
3108
- return containsDateNowOrNewDate(node.callee, depth + 1);
3109
- }
3110
- return false;
3111
- }
3112
- function memberPropertyName(node) {
3113
- if (node?.type !== "MemberExpression") return void 0;
3114
- return propName2(node.property);
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);
3115
2967
  }
3116
2968
  return {
3117
2969
  CallExpression(node) {
3118
- if (isSupabaseUpdateCall(node)) {
3119
- const arg = node.arguments?.[0];
3120
- if (arg?.type === "ObjectExpression") {
3121
- for (const prop of arg.properties ?? []) {
3122
- if (prop?.type !== "Property") continue;
3123
- const keyName = propName2(prop.key);
3124
- if (keyName && EXPIRY_COLUMN_PATTERN.test(keyName) && !writtenColumns.has(keyName)) {
3125
- writtenColumns.set(keyName, node);
3126
- }
3127
- }
3128
- }
2970
+ if (isAuthCookieGet(node)) {
2971
+ readsCookieWithAuthName = true;
2972
+ cookieReadNodes.push(node);
3129
2973
  }
3130
2974
  const callee = node.callee;
3131
- if (callee?.type === "MemberExpression" && FILTER_METHODS.has(callee.property?.name)) {
3132
- const colArg = node.arguments?.[0];
3133
- const colName = colArg?.type === "Literal" ? propName2(colArg) : void 0;
3134
- if (colName && EXPIRY_COLUMN_PATTERN.test(colName)) {
3135
- readColumns.add(colName);
3136
- }
3137
- }
3138
- },
3139
- BinaryExpression(node) {
3140
- if (!DATE_COMPARISON_OPERATORS.has(node.operator)) return;
3141
- const leftCol = memberPropertyName(node.left);
3142
- const rightCol = memberPropertyName(node.right);
3143
- if (leftCol && EXPIRY_COLUMN_PATTERN.test(leftCol) && containsDateNowOrNewDate(node.right)) {
3144
- readColumns.add(leftCol);
2975
+ if (callee?.type === "Identifier" && VERIFY_METHOD_NAMES.has(callee.name)) {
2976
+ hasVerifyCall = true;
3145
2977
  }
3146
- if (rightCol && EXPIRY_COLUMN_PATTERN.test(rightCol) && containsDateNowOrNewDate(node.left)) {
3147
- readColumns.add(rightCol);
2978
+ if (callee?.type === "MemberExpression" && callee.property?.type === "Identifier" && VERIFY_METHOD_NAMES.has(callee.property.name)) {
2979
+ hasVerifyCall = true;
3148
2980
  }
3149
2981
  },
3150
2982
  "Program:exit"() {
3151
- for (const [column, node] of writtenColumns) {
3152
- if (!readColumns.has(column)) {
3153
- context.report({ node, messageId: "expiryNeverChecked", data: { column } });
2983
+ if (readsCookieWithAuthName && !hasVerifyCall) {
2984
+ for (const node of cookieReadNodes) {
2985
+ context.report({ node, messageId: "tokenNotVerified" });
3154
2986
  }
3155
2987
  }
3156
2988
  }
3157
2989
  };
3158
2990
  }
3159
2991
  };
3160
- var lovableExpiryColumnNeverCheckedRule = rule40;
2992
+ var firebaseMiddlewareTokenNotVerifiedRule = rule40;
3161
2993
 
3162
- // src/providers/lovable/rules/silent-catch-on-provider-call.ts
3163
- var LOGGING_CONSOLE_METHODS = /* @__PURE__ */ new Set(["error", "warn", "log", "info"]);
2994
+ // src/providers/firebase/rules/hardcoded-user-id.ts
3164
2995
  var rule41 = {
3165
2996
  meta: {
3166
- type: "suggestion",
2997
+ type: "problem",
3167
2998
  docs: {
3168
- description: "A catch block around an LLM provider call must log the failure",
3169
- category: "correctness",
3170
- 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.`,
3171
- docsUrl: "https://docs.lovable.dev/features/cloud",
2999
+ description: "Hardcoded string used as userId in Firestore operations",
3000
+ category: "security",
3001
+ cwe: "CWE-284",
3002
+ owasp: "A01:2021 Broken Access Control",
3003
+ rationale: "A hardcoded userId causes all users to share a single Firestore document. The last writer overwrites everyone else's data, and when security rules are tightened the writes are rejected. The userId must always come from the authenticated user object.",
3004
+ docsUrl: "https://firebase.google.com/docs/auth/web/manage-users",
3172
3005
  recommended: true
3173
3006
  },
3174
3007
  messages: {
3175
- silentCatch: 'This catch block swallows a failed LLM provider call with no logging \u2014 a real failure (expired key, rate limit, network error) is indistinguishable from "no key configured."'
3176
- }
3008
+ hardcodedUserId: "Hardcoded userId found. All users share this document. Read the userId from the authenticated user: const { user } = useAuth(); user?.uid"
3009
+ },
3010
+ schema: []
3177
3011
  },
3178
3012
  create(context) {
3179
- function findFetchToLlmHost(node, depth = 0) {
3180
- if (!node || typeof node !== "object" || depth > 12) return null;
3181
- if (Array.isArray(node)) {
3182
- for (const n of node) {
3183
- const found = findFetchToLlmHost(n, depth + 1);
3184
- if (found) return found;
3185
- }
3186
- return null;
3187
- }
3188
- if (node.type === "CallExpression" && node.callee?.type === "Identifier" && node.callee.name === "fetch") {
3189
- const urlArg = node.arguments?.[0];
3190
- if (containsKnownLlmHost(urlArg)) return node;
3191
- }
3192
- for (const key of Object.keys(node)) {
3193
- if (key === "parent" || key === "loc" || key === "range") continue;
3194
- const val = node[key];
3195
- if (val && typeof val === "object") {
3196
- const found = findFetchToLlmHost(val, depth + 1);
3197
- if (found) return found;
3198
- }
3199
- }
3200
- return null;
3201
- }
3202
- function containsLoggingCall(node, depth = 0) {
3203
- if (!node || typeof node !== "object" || depth > 12) return false;
3204
- if (Array.isArray(node)) return node.some((n) => containsLoggingCall(n, depth + 1));
3205
- if (node.type === "CallExpression") {
3206
- const callee = node.callee;
3207
- if (callee?.type === "MemberExpression") {
3208
- const objName = callee.object?.type === "Identifier" ? callee.object.name : void 0;
3209
- const propertyName = callee.property?.name;
3210
- if (objName === "console" && LOGGING_CONSOLE_METHODS.has(propertyName)) return true;
3211
- if (propertyName === "captureException") return true;
3212
- }
3213
- if (callee?.type === "Identifier" && callee.name === "reportError") return true;
3214
- }
3215
- for (const key of Object.keys(node)) {
3216
- if (key === "parent" || key === "loc" || key === "range") continue;
3217
- const val = node[key];
3218
- if (val && typeof val === "object") {
3219
- if (containsLoggingCall(val, depth + 1)) return true;
3220
- }
3221
- }
3222
- return false;
3223
- }
3013
+ const USER_ID_NAMES = /* @__PURE__ */ new Set(["userId", "uid", "userID", "user_id"]);
3224
3014
  return {
3225
- TryStatement(node) {
3226
- const fetchNode = findFetchToLlmHost(node.block);
3227
- if (!fetchNode) return;
3228
- const handler = node.handler;
3229
- if (!handler) return;
3230
- if (!containsLoggingCall(handler.body)) {
3231
- context.report({ node: handler, messageId: "silentCatch" });
3015
+ VariableDeclarator(node) {
3016
+ if (node.id?.type !== "Identifier") return;
3017
+ if (!USER_ID_NAMES.has(node.id.name)) return;
3018
+ const init = node.init;
3019
+ if (!init) return;
3020
+ if (init.type === "Literal" && typeof init.value === "string" && init.value.length > 0) {
3021
+ context.report({ node, messageId: "hardcodedUserId" });
3232
3022
  }
3233
3023
  }
3234
3024
  };
3235
3025
  }
3236
3026
  };
3237
- var lovableSilentCatchOnProviderCallRule = rule41;
3238
-
3239
- // src/providers/browserbase/utils.ts
3240
- function memberPropName3(node) {
3241
- if (node?.type !== "CallExpression") return void 0;
3242
- const callee = node.callee;
3243
- if (callee?.type !== "MemberExpression") return void 0;
3244
- const prop = callee.property;
3245
- if (!callee.computed && prop?.type === "Identifier") return prop.name;
3246
- if (callee.computed && prop?.type === "Literal" && typeof prop.value === "string") return prop.value;
3247
- return void 0;
3248
- }
3249
- function isSessionsCall(node, name) {
3250
- if (memberPropName3(node) !== name) return false;
3251
- const obj = node.callee.object;
3252
- return obj?.type === "MemberExpression" && !obj.computed && obj.property?.type === "Identifier" && obj.property.name === "sessions";
3253
- }
3254
- function isSessionsRecordingCall(node, name) {
3255
- if (memberPropName3(node) !== name) return false;
3256
- const recordingObj = node.callee.object;
3257
- if (recordingObj?.type !== "MemberExpression" || recordingObj.computed) return false;
3258
- if (recordingObj.property?.type !== "Identifier" || recordingObj.property.name !== "recording") return false;
3259
- const sessionsObj = recordingObj.object;
3260
- return sessionsObj?.type === "MemberExpression" && !sessionsObj.computed && sessionsObj.property?.type === "Identifier" && sessionsObj.property.name === "sessions";
3261
- }
3262
- function findProperty2(objectExpression, name) {
3263
- if (objectExpression?.type !== "ObjectExpression") return void 0;
3264
- return objectExpression.properties?.find(
3265
- (p) => p?.type === "Property" && (p.key?.type === "Identifier" && p.key.name === name || p.key?.type === "Literal" && p.key.value === name)
3266
- );
3267
- }
3268
- function startOffset3(n) {
3269
- if (typeof n?.range?.[0] === "number") return n.range[0];
3270
- if (typeof n?.start === "number") return n.start;
3271
- return (n?.loc?.start?.line ?? 0) * 1e6 + (n?.loc?.start?.column ?? 0);
3272
- }
3273
- function endOffset3(n) {
3274
- if (typeof n?.range?.[1] === "number") return n.range[1];
3275
- if (typeof n?.end === "number") return n.end;
3276
- return (n?.loc?.end?.line ?? n?.loc?.start?.line ?? 0) * 1e6 + (n?.loc?.end?.column ?? 0);
3277
- }
3278
- function contains3(outer, inner) {
3279
- const s = startOffset3(inner);
3280
- return s >= startOffset3(outer) && s <= endOffset3(outer);
3281
- }
3282
- function someDescendant2(node, predicate) {
3283
- let found = false;
3284
- function visit(n) {
3285
- if (found || !n || typeof n !== "object") return;
3286
- if (Array.isArray(n)) {
3287
- for (const item of n) visit(item);
3288
- return;
3289
- }
3290
- if (typeof n.type !== "string") return;
3291
- if (predicate(n)) {
3292
- found = true;
3293
- return;
3294
- }
3295
- for (const key of Object.keys(n)) {
3296
- if (key === "parent" || key === "loc" || key === "range") continue;
3297
- visit(n[key]);
3298
- }
3299
- }
3300
- visit(node);
3301
- return found;
3302
- }
3303
- function isResponseSendCall(node) {
3304
- const name = memberPropName3(node);
3305
- if (name !== "json" && name !== "send") return false;
3306
- const obj = node.callee.object;
3307
- if (obj?.type === "Identifier") {
3308
- return /^(res|response)$/i.test(obj.name) || obj.name === "Response" || obj.name === "NextResponse";
3309
- }
3310
- return false;
3311
- }
3312
- function isInsideTestFile3(filename) {
3313
- return /(^|[\\/])__tests__[\\/]|\.(test|spec)\.[cm]?[jt]sx?$/.test(filename);
3314
- }
3027
+ var firebaseHardcodedUserIdRule = rule41;
3315
3028
 
3316
- // src/providers/browserbase/rules/no-conditional-authz-on-anonymous-user.ts
3317
- function isUserLikeExpr(node) {
3318
- if (node?.type === "Identifier") return /user/i.test(node.name);
3319
- if (node?.type === "MemberExpression" && !node.computed && node.property?.type === "Identifier") {
3320
- return /user/i.test(node.property.name);
3321
- }
3322
- return false;
3323
- }
3324
- function isFalsyGuardTest(test) {
3325
- if (test?.type === "UnaryExpression" && test.operator === "!") return isUserLikeExpr(test.argument);
3326
- if (test?.type === "BinaryExpression" && (test.operator === "===" || test.operator === "==")) {
3327
- const sides = [test.left, test.right];
3328
- const isNullish = (n) => n?.type === "Literal" && n.value === null || n?.type === "Identifier" && n.name === "undefined";
3329
- const nullSide = sides.find(isNullish);
3330
- const otherSide = sides.find((s) => s !== nullSide);
3331
- return !!nullSide && isUserLikeExpr(otherSide);
3332
- }
3333
- return false;
3334
- }
3335
- function hasReturnOrThrow(stmt) {
3336
- if (!stmt) return false;
3337
- if (stmt.type === "ReturnStatement" || stmt.type === "ThrowStatement") return true;
3338
- if (stmt.type === "BlockStatement") {
3339
- return (stmt.body ?? []).some((s) => s.type === "ReturnStatement" || s.type === "ThrowStatement");
3340
- }
3341
- return false;
3342
- }
3343
- function isTruthyGateTest(test) {
3344
- if (isUserLikeExpr(test)) return true;
3345
- if (test?.type === "LogicalExpression" && test.operator === "&&") {
3346
- return isTruthyGateTest(test.left) || isTruthyGateTest(test.right);
3347
- }
3348
- return false;
3349
- }
3029
+ // src/providers/firebase/rules/auth-user-not-found-disclosure.ts
3350
3030
  var rule42 = {
3351
3031
  meta: {
3352
- type: "problem",
3032
+ type: "suggestion",
3353
3033
  docs: {
3354
- description: "Sensitive Browserbase resources must be guarded by unconditional authorization",
3034
+ description: "auth/user-not-found error code surfaced to the user enables email enumeration",
3355
3035
  category: "security",
3356
- cwe: "CWE-862",
3357
- owasp: "A01:2021 Broken Access Control",
3358
- rationale: "bb.sessions.debug() and bb.sessions.recording.retrieve() return a live CDP debugger URL or a full rrweb session replay \u2014 high-value resources. Gating the ownership check behind `if (user) { ...check... }` with no unconditional `if (!user) return` guard means an anonymous caller (no credentials at all) simply skips the whole block and falls through to the sensitive call. Authorization must reject the unauthenticated case explicitly, not rely on a conditional that happens to be true for legitimate callers.",
3359
- docsUrl: "https://docs.browserbase.com/reference/api/session-live-urls",
3036
+ cwe: "CWE-204",
3037
+ owasp: "A07:2021 Identification and Authentication Failures",
3038
+ rationale: "Displaying a distinct message for the auth/user-not-found error code reveals whether an email address is registered. Firebase email enumeration protection (default on since September 2023) stops returning this code \u2014 making this branch dead code while still being a security risk on older projects.",
3039
+ docsUrl: "https://firebase.google.com/docs/auth/web/password-auth#enumeration-protection",
3360
3040
  recommended: true
3361
3041
  },
3362
3042
  messages: {
3363
- conditionalAuthz: "This route returns a Browserbase live-view/recording resource gated only by `if (user) {...}` with no unconditional rejection for anonymous callers. Add an `if (!user) return ...` guard before this."
3043
+ userNotFoundDisclosure: "Checking for auth/user-not-found exposes whether an email is registered (user enumeration). Show the same generic message for all auth errors."
3364
3044
  },
3365
3045
  schema: []
3366
3046
  },
3367
3047
  create(context) {
3368
- const scopes = [];
3369
- const gates = [];
3370
- const sensitiveCalls = [];
3371
- function registerScope(node) {
3372
- scopes.push({ node, start: startOffset3(node), end: endOffset3(node) });
3048
+ function isErrorCode(n) {
3049
+ return n?.type === "MemberExpression" && !n.computed && n.property?.type === "Identifier" && n.property.name === "code";
3373
3050
  }
3374
- function innermostScope(pos) {
3375
- let best;
3376
- for (const scope of scopes) {
3377
- if (pos < scope.start || pos > scope.end) continue;
3378
- if (!best || scope.end - scope.start < best.end - best.start) best = scope;
3379
- }
3380
- return best;
3051
+ function isUserNotFoundLiteral(n) {
3052
+ return n?.type === "Literal" && n.value === "auth/user-not-found";
3381
3053
  }
3382
3054
  return {
3383
- FunctionDeclaration(node) {
3384
- registerScope(node);
3385
- },
3386
- FunctionExpression(node) {
3387
- registerScope(node);
3388
- },
3389
- ArrowFunctionExpression(node) {
3390
- registerScope(node);
3391
- },
3392
- IfStatement(node) {
3393
- if (isFalsyGuardTest(node.test) && hasReturnOrThrow(node.consequent)) {
3394
- const pos = startOffset3(node);
3395
- const scope = innermostScope(pos);
3396
- if (scope && (scope.guardPos === void 0 || pos < scope.guardPos)) scope.guardPos = pos;
3397
- return;
3398
- }
3399
- if (isTruthyGateTest(node.test) && !node.alternate) {
3400
- gates.push({ start: startOffset3(node), end: endOffset3(node) });
3055
+ BinaryExpression(node) {
3056
+ if (node.operator !== "===" && node.operator !== "==" && node.operator !== "!==" && node.operator !== "!=") return;
3057
+ if (isErrorCode(node.left) && isUserNotFoundLiteral(node.right) || isErrorCode(node.right) && isUserNotFoundLiteral(node.left)) {
3058
+ context.report({ node, messageId: "userNotFoundDisclosure" });
3401
3059
  }
3402
3060
  },
3403
- CallExpression(node) {
3404
- if (isSessionsCall(node, "debug") || isSessionsRecordingCall(node, "retrieve")) {
3405
- sensitiveCalls.push(node);
3406
- }
3407
- },
3408
- "Program:exit"() {
3409
- for (const call of sensitiveCalls) {
3410
- const gate = gates.find((g) => contains3(g, call));
3411
- if (!gate) continue;
3412
- const scope = innermostScope(startOffset3(call));
3413
- const hasGuardBeforeGate = scope?.guardPos !== void 0 && scope.guardPos < gate.start;
3414
- if (!hasGuardBeforeGate) {
3415
- context.report({ node: call, messageId: "conditionalAuthz" });
3416
- }
3061
+ SwitchCase(node) {
3062
+ if (isUserNotFoundLiteral(node.test)) {
3063
+ context.report({ node, messageId: "userNotFoundDisclosure" });
3417
3064
  }
3418
3065
  }
3419
3066
  };
3420
3067
  }
3421
3068
  };
3422
- var browserbaseNoConditionalAuthzOnAnonymousUserRule = rule42;
3069
+ var firebaseAuthUserNotFoundDisclosureRule = rule42;
3423
3070
 
3424
- // src/providers/browserbase/rules/no-connect-url-in-api-response.ts
3071
+ // src/providers/firebase/rules/signup-password-confirm.ts
3425
3072
  var rule43 = {
3426
3073
  meta: {
3427
- type: "problem",
3074
+ type: "suggestion",
3428
3075
  docs: {
3429
- description: "connectUrl must never be placed in an HTTP response body",
3430
- category: "security",
3431
- cwe: "CWE-200",
3432
- owasp: "A04:2021 Insecure Design",
3433
- rationale: "session.connectUrl is the WebSocket CDP URL returned by sessions.create() \u2014 it is self-authenticating; connecting to it grants full read/write control of the live browser (arbitrary JS execution, cookie/localStorage read, simulated input), equivalent to a bearer token. Putting it in a response body widens its exposure (network tab, browser extensions, error trackers, logs) for callers that almost never need it client-side \u2014 the intended sharable artifact is debuggerFullscreenUrl from sessions.debug().",
3434
- docsUrl: "https://docs.browserbase.com/reference/api/create-a-session",
3076
+ description: "createUserWithEmailAndPassword called without verifying password matches confirmPassword",
3077
+ category: "correctness",
3078
+ rationale: "If confirmPassword is collected but never compared to password before account creation, users who mistype their password will successfully create an account they cannot log in to. Always compare password === confirmPassword and bail early on mismatch.",
3079
+ docsUrl: "https://firebase.google.com/docs/auth/web/password-auth",
3435
3080
  recommended: true
3436
3081
  },
3437
3082
  messages: {
3438
- connectUrlInResponse: "connectUrl is included in an HTTP response body. Keep it server-side only \u2014 it is a bearer credential for the live browser, not a sharable URL."
3083
+ passwordConfirmNotChecked: "createUserWithEmailAndPassword is called without verifying password === confirmPassword. Users who mistype their password will create an account they cannot log in to."
3439
3084
  },
3440
3085
  schema: []
3441
3086
  },
3442
3087
  create(context) {
3088
+ let createUserLocalName;
3089
+ let hasConfirmPasswordIdentifier = false;
3090
+ let hasPasswordComparison = false;
3091
+ const createUserCalls = [];
3443
3092
  return {
3093
+ ImportDeclaration(node) {
3094
+ const imports = namedImportsFrom(node, "firebase/auth");
3095
+ if (imports.has("createUserWithEmailAndPassword")) {
3096
+ createUserLocalName = imports.get("createUserWithEmailAndPassword");
3097
+ }
3098
+ },
3099
+ Identifier(node) {
3100
+ if (node.name === "confirmPassword") {
3101
+ hasConfirmPasswordIdentifier = true;
3102
+ }
3103
+ },
3104
+ BinaryExpression(node) {
3105
+ if (node.operator !== "===" && node.operator !== "==" && node.operator !== "!==" && node.operator !== "!=") return;
3106
+ const mentionsConfirm = (n) => n?.type === "Identifier" && n.name === "confirmPassword";
3107
+ if (mentionsConfirm(node.left) || mentionsConfirm(node.right)) {
3108
+ hasPasswordComparison = true;
3109
+ }
3110
+ },
3444
3111
  CallExpression(node) {
3445
- if (!isResponseSendCall(node)) return;
3446
- const arg = node.arguments?.[0];
3447
- if (arg?.type !== "ObjectExpression") return;
3448
- if (findProperty2(arg, "connectUrl")) {
3449
- context.report({ node, messageId: "connectUrlInResponse" });
3112
+ if (!createUserLocalName) return;
3113
+ if (node.callee?.type === "Identifier" && node.callee.name === createUserLocalName) {
3114
+ createUserCalls.push(node);
3115
+ }
3116
+ },
3117
+ "Program:exit"() {
3118
+ if (!createUserLocalName) return;
3119
+ if (createUserCalls.length === 0) return;
3120
+ if (!hasConfirmPasswordIdentifier) return;
3121
+ if (hasPasswordComparison) return;
3122
+ for (const call of createUserCalls) {
3123
+ context.report({ node: call, messageId: "passwordConfirmNotChecked" });
3450
3124
  }
3451
3125
  }
3452
3126
  };
3453
3127
  }
3454
3128
  };
3455
- var browserbaseNoConnectUrlInApiResponseRule = rule43;
3129
+ var firebaseSignupPasswordConfirmRule = rule43;
3456
3130
 
3457
- // src/providers/browserbase/rules/session-id-requires-ownership-check.ts
3458
- function isOwnershipCheckCall(node) {
3459
- if (node?.type !== "CallExpression") return false;
3460
- const callee = node.callee;
3461
- const name = callee?.type === "Identifier" ? callee.name : callee?.type === "MemberExpression" && !callee.computed && callee.property?.type === "Identifier" ? callee.property.name : void 0;
3462
- if (!name) return false;
3463
- return /owner|belongsto|hasaccess|authorize|verifyaccess|checkaccess|findproject|getproject/i.test(name);
3464
- }
3465
- function isSessionIdLikeArg(node) {
3466
- if (node?.type === "Identifier") return /sessionid/i.test(node.name);
3467
- if (node?.type === "MemberExpression" && !node.computed && node.property?.type === "Identifier") {
3468
- return /sessionid/i.test(node.property.name);
3469
- }
3470
- return false;
3471
- }
3131
+ // src/providers/firebase/rules/use-array-union-remove.ts
3472
3132
  var rule44 = {
3473
3133
  meta: {
3474
3134
  type: "suggestion",
3475
3135
  docs: {
3476
- description: "Session id lookups must verify ownership before returning live-view data",
3477
- category: "security",
3478
- cwe: "CWE-862",
3479
- 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.",
3480
- docsUrl: "https://docs.browserbase.com/features/session-live-view",
3136
+ description: "Firestore array field updated with read-modify-write spread/filter instead of arrayUnion/arrayRemove",
3137
+ 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.",
3139
+ docsUrl: "https://firebase.google.com/docs/firestore/manage-data/add-data#update_elements_in_an_array",
3481
3140
  recommended: true
3482
3141
  },
3483
3142
  messages: {
3484
- missingOwnershipCheck: "sessionId is passed directly into a Browserbase live-view/recording call with no ownership check beforehand."
3143
+ useArrayUnionRemove: "Firestore array updated with read-modify-write spread/filter. Use arrayUnion() or arrayRemove() for atomic updates that do not require reading the document first."
3485
3144
  },
3486
3145
  schema: []
3487
3146
  },
3488
3147
  create(context) {
3489
- const scopes = [];
3490
- const sensitiveCalls = [];
3491
- function registerScope(node) {
3492
- scopes.push({ node, start: startOffset3(node), end: endOffset3(node), sawOwnershipCheck: false });
3493
- }
3494
- function innermostScope(pos) {
3495
- let best;
3496
- for (const scope of scopes) {
3497
- if (pos < scope.start || pos > scope.end) continue;
3498
- if (!best || scope.end - scope.start < best.end - best.start) best = scope;
3148
+ function hasSpreadOrFilterValue(obj) {
3149
+ if (obj?.type !== "ObjectExpression") return false;
3150
+ for (const prop of obj.properties ?? []) {
3151
+ if (prop?.type !== "Property") continue;
3152
+ const val = prop.value;
3153
+ if (val?.type === "ArrayExpression") {
3154
+ if ((val.elements ?? []).some((el) => el?.type === "SpreadElement")) {
3155
+ return true;
3156
+ }
3157
+ }
3158
+ if (val?.type === "CallExpression" && val.callee?.type === "MemberExpression" && val.callee.property?.type === "Identifier" && val.callee.property.name === "filter") {
3159
+ return true;
3160
+ }
3499
3161
  }
3500
- return best;
3162
+ return false;
3501
3163
  }
3502
3164
  return {
3503
- FunctionDeclaration(node) {
3504
- registerScope(node);
3505
- },
3506
- FunctionExpression(node) {
3507
- registerScope(node);
3508
- },
3509
- ArrowFunctionExpression(node) {
3510
- registerScope(node);
3511
- },
3512
3165
  CallExpression(node) {
3513
- const pos = startOffset3(node);
3514
- const scope = innermostScope(pos);
3515
- if (isOwnershipCheckCall(node)) {
3516
- for (const s of scopes) {
3517
- if (pos >= s.start && pos <= s.end) s.sawOwnershipCheck = true;
3518
- }
3519
- return;
3520
- }
3521
- if (isSessionsCall(node, "debug") || isSessionsRecordingCall(node, "retrieve")) {
3522
- const sessionIdArg = (node.arguments ?? []).some(isSessionIdLikeArg);
3523
- if (sessionIdArg) sensitiveCalls.push({ node, scope });
3524
- }
3525
- },
3526
- "Program:exit"() {
3527
- for (const { node, scope } of sensitiveCalls) {
3528
- if (!scope || !scope.sawOwnershipCheck) {
3529
- context.report({ node, messageId: "missingOwnershipCheck" });
3530
- }
3166
+ const callee = node.callee;
3167
+ if (callee?.type !== "Identifier" || callee.name !== "updateDoc") return;
3168
+ const args = node.arguments ?? [];
3169
+ if (args.length < 2) return;
3170
+ const payload = args[1];
3171
+ if (hasSpreadOrFilterValue(payload)) {
3172
+ context.report({ node, messageId: "useArrayUnionRemove" });
3531
3173
  }
3532
3174
  }
3533
3175
  };
3534
3176
  }
3535
3177
  };
3536
- var browserbaseSessionIdRequiresOwnershipCheckRule = rule44;
3178
+ var firebaseUseArrayUnionRemoveRule = rule44;
3537
3179
 
3538
- // src/providers/browserbase/rules/no-concurrent-shared-context.ts
3539
- function isPromiseAllCall2(node) {
3540
- return node?.type === "CallExpression" && node.callee?.type === "MemberExpression" && !node.callee.computed && node.callee.object?.type === "Identifier" && node.callee.object.name === "Promise" && node.callee.property?.type === "Identifier" && node.callee.property.name === "all";
3541
- }
3542
- function isMapCall2(node) {
3543
- return memberPropName3(node) === "map";
3544
- }
3545
- function callbackParamNames(callback) {
3546
- const names = /* @__PURE__ */ new Set();
3547
- const param = callback?.params?.[0];
3548
- if (param?.type === "Identifier") {
3549
- names.add(param.name);
3550
- } else if (param?.type === "ObjectPattern") {
3551
- for (const p of param.properties ?? []) {
3552
- if (p?.type === "Property" && p.value?.type === "Identifier") names.add(p.value.name);
3553
- else if (p?.type === "RestElement" && p.argument?.type === "Identifier") names.add(p.argument.name);
3554
- }
3555
- }
3556
- return names;
3557
- }
3558
- function contextIdValueIsShared(callback, contextIdNode) {
3559
- if (contextIdNode?.type !== "Identifier") return false;
3560
- const params = callbackParamNames(callback);
3561
- if (params.has(contextIdNode.name)) return false;
3562
- return true;
3563
- }
3564
- function findSharedContextCreateCall(callback) {
3565
- const body = callback?.body;
3566
- if (!body) return null;
3567
- let found = null;
3568
- function visit(n) {
3569
- if (found || !n || typeof n !== "object") return;
3570
- if (Array.isArray(n)) {
3571
- for (const item of n) visit(item);
3572
- return;
3573
- }
3574
- if (typeof n.type !== "string") return;
3575
- if (isSessionsCall(n, "create")) {
3576
- const optsArg = n.arguments?.[0];
3577
- const browserSettings = findProperty2(optsArg, "browserSettings")?.value;
3578
- const ctxProp = findProperty2(browserSettings, "context")?.value;
3579
- const idProp = findProperty2(ctxProp, "id");
3580
- if (idProp && contextIdValueIsShared(callback, idProp.value)) {
3581
- found = n;
3582
- return;
3583
- }
3584
- }
3585
- for (const key of Object.keys(n)) {
3586
- if (key === "parent" || key === "loc" || key === "range") continue;
3587
- visit(n[key]);
3588
- }
3589
- }
3590
- visit(body);
3591
- return found;
3592
- }
3180
+ // src/providers/firebase/rules/duplicate-initialize-app.ts
3593
3181
  var rule45 = {
3594
3182
  meta: {
3595
- type: "problem",
3183
+ type: "suggestion",
3596
3184
  docs: {
3597
- description: "A Browserbase Context must not be attached to concurrent sessions",
3185
+ description: "initializeApp called without checking getApps() first \u2014 risk of duplicate-app crash",
3598
3186
  category: "correctness",
3599
- 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.`,
3600
- docsUrl: "https://docs.browserbase.com/features/contexts",
3187
+ rationale: 'Calling initializeApp() when the default app already exists throws "Firebase App named [DEFAULT] already exists". Always guard with getApps().length to make initialization idempotent across hot reloads and server-side rendering.',
3188
+ docsUrl: "https://firebase.google.com/docs/projects/multiprojects",
3601
3189
  recommended: true
3602
3190
  },
3603
3191
  messages: {
3604
- concurrentSharedContext: "The same browserSettings.context.id is passed into every concurrent sessions.create() call in this Promise.all batch. Sites may force a log out when 2+ sessions share a Context at once."
3192
+ duplicateInitializeApp: 'initializeApp() called without checking getApps() first. Calling it twice throws "Firebase App already exists". Use: getApps().length ? getApp() : initializeApp(config)'
3605
3193
  },
3606
3194
  schema: []
3607
3195
  },
3608
3196
  create(context) {
3609
- const mapCallsByVarName = /* @__PURE__ */ new Map();
3610
- function checkMapCall(mapCallNode) {
3611
- if (!isMapCall2(mapCallNode)) return;
3612
- const callback = mapCallNode.arguments?.[mapCallNode.arguments.length - 1];
3613
- if (!callback) return;
3614
- const sharedCall = findSharedContextCreateCall(callback);
3615
- if (sharedCall) {
3616
- context.report({ node: sharedCall, messageId: "concurrentSharedContext" });
3617
- }
3618
- }
3197
+ let initializeAppLocalName;
3198
+ let hasGetAppsCall = false;
3199
+ const initializeAppCalls = [];
3200
+ const FIREBASE_APP_SOURCES = /* @__PURE__ */ new Set(["firebase/app", "firebase-admin/app", "firebase-admin"]);
3619
3201
  return {
3620
- VariableDeclarator(node) {
3621
- if (node.id?.type === "Identifier" && isMapCall2(node.init)) {
3622
- mapCallsByVarName.set(node.id.name, node.init);
3202
+ ImportDeclaration(node) {
3203
+ for (const src of FIREBASE_APP_SOURCES) {
3204
+ const imports = namedImportsFrom(node, src);
3205
+ if (imports.has("initializeApp")) {
3206
+ initializeAppLocalName = imports.get("initializeApp");
3207
+ }
3623
3208
  }
3624
3209
  },
3625
3210
  CallExpression(node) {
3626
- if (!isPromiseAllCall2(node)) return;
3627
- const arg = node.arguments?.[0];
3628
- if (isMapCall2(arg)) {
3629
- checkMapCall(arg);
3630
- return;
3211
+ const callee = node.callee;
3212
+ if (callee?.type !== "Identifier") return;
3213
+ if (initializeAppLocalName && callee.name === initializeAppLocalName) {
3214
+ initializeAppCalls.push(node);
3631
3215
  }
3632
- if (arg?.type === "Identifier") {
3633
- const mapCall = mapCallsByVarName.get(arg.name);
3634
- if (mapCall) checkMapCall(mapCall);
3216
+ if (callee.name === "getApps") {
3217
+ hasGetAppsCall = true;
3218
+ }
3219
+ },
3220
+ "Program:exit"() {
3221
+ if (hasGetAppsCall) return;
3222
+ for (const call of initializeAppCalls) {
3223
+ context.report({ node: call, messageId: "duplicateInitializeApp" });
3635
3224
  }
3636
3225
  }
3637
3226
  };
3638
3227
  }
3639
3228
  };
3640
- var browserbaseNoConcurrentSharedContextRule = rule45;
3229
+ var firebaseDuplicateInitializeAppRule = rule45;
3641
3230
 
3642
- // src/providers/browserbase/rules/mobile-device-requires-os-setting.ts
3643
- function isMobileLiteral(node) {
3644
- return node?.type === "Literal" && (node.value === "mobile" || node.value === "tablet");
3645
- }
3646
- function testComparesToMobile(test) {
3647
- if (test?.type !== "BinaryExpression") return false;
3648
- if (test.operator !== "===" && test.operator !== "==") return false;
3649
- return isMobileLiteral(test.left) || isMobileLiteral(test.right);
3650
- }
3651
- function isViewportReference(node) {
3652
- if (node?.type === "Identifier") return /viewport/i.test(node.name);
3653
- if (node?.type === "MemberExpression" && !node.computed && node.property?.type === "Identifier") {
3654
- return /viewport|width|height/i.test(node.property.name);
3655
- }
3656
- if (node?.type === "Property") {
3657
- return node.key?.type === "Identifier" && /viewport|width|height/i.test(node.key.name);
3658
- }
3659
- return false;
3660
- }
3661
- function setsViewport(node) {
3662
- return someDescendant2(node, isViewportReference);
3663
- }
3664
- function isOsMobileSetting(node) {
3665
- if (node?.type === "Property" && node.key?.type === "Identifier" && node.key.name === "os" && isMobileLiteral(node.value)) {
3666
- return true;
3667
- }
3668
- if (node?.type === "AssignmentExpression" && node.left?.type === "MemberExpression" && !node.left.computed && node.left.property?.type === "Identifier" && node.left.property.name === "os" && isMobileLiteral(node.right)) {
3669
- return true;
3670
- }
3671
- return false;
3672
- }
3231
+ // src/providers/firebase/rules/onSnapshot-async-throw.ts
3673
3232
  var rule46 = {
3674
3233
  meta: {
3675
- type: "problem",
3234
+ type: "suggestion",
3676
3235
  docs: {
3677
- description: "Mobile device combos must set browserSettings.os, not just resize the viewport",
3678
- category: "correctness",
3679
- 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.`,
3680
- docsUrl: "https://docs.browserbase.com/features/stealth-mode",
3236
+ description: "throw inside async onSnapshot callback creates an unhandled promise rejection",
3237
+ category: "reliability",
3238
+ rationale: "onSnapshot does not handle Promise rejections from its callback. A throw inside an async callback becomes an unhandled rejection, silently terminating the listener and leaving the UI in a broken state with no error feedback.",
3239
+ docsUrl: "https://firebase.google.com/docs/firestore/query-data/listen#handle_listen_errors",
3681
3240
  recommended: true
3682
3241
  },
3683
3242
  messages: {
3684
- missingOsSetting: 'A "mobile" branch resizes the viewport but never sets browserSettings.os to "mobile"/"tablet". Without it this is still a desktop browser in a small window.'
3243
+ asyncThrowInSnapshot: "throw inside an async onSnapshot callback creates an unhandled promise rejection. The listener silently stops. Use return with error logging or the onSnapshot error callback instead."
3685
3244
  },
3686
3245
  schema: []
3687
3246
  },
3688
3247
  create(context) {
3689
- const mobileViewportBranches = [];
3690
- let sawOsMobileSetting = false;
3248
+ function isAsyncFn(node) {
3249
+ return (node?.type === "ArrowFunctionExpression" || node?.type === "FunctionExpression") && node.async === true;
3250
+ }
3251
+ function hasDirectThrow(fnNode) {
3252
+ if (!fnNode?.body) return false;
3253
+ function visit(n, depth) {
3254
+ if (!n || typeof n !== "object") return false;
3255
+ if (Array.isArray(n)) return n.some((item) => visit(item, depth));
3256
+ if (typeof n.type !== "string") return false;
3257
+ if (depth > 0 && (n.type === "FunctionDeclaration" || n.type === "FunctionExpression" || n.type === "ArrowFunctionExpression")) {
3258
+ return false;
3259
+ }
3260
+ if (n.type === "ThrowStatement") return true;
3261
+ for (const key of Object.keys(n)) {
3262
+ if (key === "parent" || key === "loc" || key === "range" || key === "type") continue;
3263
+ if (visit(n[key], depth + 1)) return true;
3264
+ }
3265
+ return false;
3266
+ }
3267
+ return visit(fnNode.body, 0);
3268
+ }
3691
3269
  return {
3692
- IfStatement(node) {
3693
- if (testComparesToMobile(node.test) && setsViewport(node.consequent)) {
3694
- mobileViewportBranches.push(node);
3695
- }
3696
- },
3697
- Property(node) {
3698
- if (isOsMobileSetting(node)) sawOsMobileSetting = true;
3699
- },
3700
- AssignmentExpression(node) {
3701
- if (isOsMobileSetting(node)) sawOsMobileSetting = true;
3702
- },
3703
- "Program:exit"() {
3704
- if (sawOsMobileSetting) return;
3705
- for (const branch of mobileViewportBranches) {
3706
- context.report({ node: branch, messageId: "missingOsSetting" });
3270
+ CallExpression(node) {
3271
+ const callee = node.callee;
3272
+ if (callee?.type !== "Identifier" || callee.name !== "onSnapshot") return;
3273
+ for (const arg of node.arguments ?? []) {
3274
+ if (!isAsyncFn(arg)) continue;
3275
+ if (hasDirectThrow(arg)) {
3276
+ context.report({ node: arg, messageId: "asyncThrowInSnapshot" });
3277
+ }
3707
3278
  }
3708
3279
  }
3709
3280
  };
3710
3281
  }
3711
3282
  };
3712
- var browserbaseMobileDeviceRequiresOsSettingRule = rule46;
3283
+ var firebaseOnSnapshotAsyncThrowRule = rule46;
3713
3284
 
3714
- // src/providers/browserbase/rules/use-typed-exception-status-not-substring.ts
3715
- function isStringifiedErrorSource(node, errName) {
3716
- if (node?.type === "CallExpression" && node.callee?.type === "Identifier" && node.callee.name === "String") {
3717
- const arg = node.arguments?.[0];
3718
- return arg?.type === "Identifier" && arg.name === errName;
3719
- }
3720
- if (node?.type === "CallExpression" && node.callee?.type === "MemberExpression") {
3721
- const obj = node.callee.object;
3722
- const prop = node.callee.property;
3723
- if (obj?.type === "Identifier" && obj.name === errName && prop?.type === "Identifier" && prop.name === "toString") {
3724
- return true;
3725
- }
3726
- }
3727
- if (node?.type === "MemberExpression" && !node.computed) {
3728
- const obj = node.object;
3729
- const prop = node.property;
3730
- if (obj?.type === "Identifier" && obj.name === errName && prop?.type === "Identifier" && prop.name === "message") {
3731
- return true;
3732
- }
3733
- }
3734
- return false;
3735
- }
3736
- function unwrapToLowerCase(node) {
3737
- if (node?.type === "CallExpression" && node.callee?.type === "MemberExpression" && node.callee.property?.type === "Identifier" && node.callee.property.name === "toLowerCase") {
3738
- return node.callee.object;
3739
- }
3740
- return node;
3741
- }
3742
- function usesTypedStatus(body, errName) {
3743
- return someDescendant2(body, (n) => {
3744
- if (n.type === "MemberExpression" && !n.computed && n.object?.type === "Identifier" && n.object.name === errName) {
3745
- return n.property?.type === "Identifier" && n.property.name === "status";
3746
- }
3747
- if (n.type === "BinaryExpression" && n.operator === "instanceof" && n.left?.type === "Identifier" && n.left.name === errName) {
3748
- return true;
3749
- }
3750
- return false;
3751
- });
3752
- }
3753
- function usesSubstringMatch(body, errName, stringifiedVars) {
3754
- return someDescendant2(body, (n) => {
3755
- if (n.type !== "CallExpression") return false;
3756
- if (n.callee?.type !== "MemberExpression" || n.callee.computed) return false;
3757
- if (n.callee.property?.type !== "Identifier" || n.callee.property.name !== "includes") return false;
3758
- const source = unwrapToLowerCase(n.callee.object);
3759
- if (isStringifiedErrorSource(source, errName)) return true;
3760
- return source?.type === "Identifier" && stringifiedVars.has(source.name);
3761
- });
3762
- }
3763
- function collectStringifiedVars(body, errName) {
3764
- const names = /* @__PURE__ */ new Set();
3765
- someDescendant2(body, (n) => {
3766
- if (n.type === "VariableDeclarator" && n.id?.type === "Identifier" && n.init) {
3767
- const source = unwrapToLowerCase(n.init);
3768
- if (isStringifiedErrorSource(source, errName)) names.add(n.id.name);
3769
- }
3770
- return false;
3771
- });
3772
- return names;
3773
- }
3285
+ // src/providers/firebase/rules/onSnapshot-missing-error-callback.ts
3774
3286
  var rule47 = {
3775
3287
  meta: {
3776
3288
  type: "suggestion",
3777
3289
  docs: {
3778
- description: "Branch on err.status, not a substring match of the stringified error",
3779
- category: "correctness",
3780
- rationale: 'The SDK raises typed Browserbase.APIError (and subclasses like NotFoundError) carrying a real .status: number and .headers. Matching err.message or String(err) against substrings like "410" or "Session stopped" is fragile \u2014 that human-readable text is an unversioned implementation detail the provider can change at any time without notice. Check err.status (or `instanceof`) instead.',
3781
- docsUrl: "https://docs.browserbase.com/reference/api/session-live-urls",
3290
+ description: "onSnapshot called without an error callback \u2014 Firestore permission errors silently stop the listener",
3291
+ category: "reliability",
3292
+ rationale: "When Firestore denies a read, the listener silently stops emitting snapshots with no error state set or user feedback. Always provide a third argument error callback to onSnapshot so permission denials and connection drops are surfaced.",
3293
+ docsUrl: "https://firebase.google.com/docs/firestore/query-data/listen#handle_listen_errors",
3782
3294
  recommended: true
3783
3295
  },
3784
3296
  messages: {
3785
- substringStatusMatch: "This catch block matches a substring of the stringified error instead of checking err.status or `instanceof Browserbase.APIError`."
3297
+ missingErrorCallback: "onSnapshot called without an error callback. Firestore permission errors will silently stop the listener with no user feedback. Add an error callback: onSnapshot(ref, successFn, errorFn)."
3786
3298
  },
3787
3299
  schema: []
3788
3300
  },
3789
3301
  create(context) {
3302
+ function isFunctionNode(n) {
3303
+ return n?.type === "ArrowFunctionExpression" || n?.type === "FunctionExpression";
3304
+ }
3305
+ function isOptionsObject(n) {
3306
+ return n?.type === "ObjectExpression";
3307
+ }
3790
3308
  return {
3791
- CatchClause(node) {
3792
- const param = node.param;
3793
- const errName = param?.type === "Identifier" ? param.name : void 0;
3794
- if (!errName || !node.body) return;
3795
- if (usesTypedStatus(node.body, errName)) return;
3796
- const stringifiedVars = collectStringifiedVars(node.body, errName);
3797
- if (usesSubstringMatch(node.body, errName, stringifiedVars)) {
3798
- context.report({ node, messageId: "substringStatusMatch" });
3309
+ CallExpression(node) {
3310
+ const callee = node.callee;
3311
+ if (callee?.type !== "Identifier" || callee.name !== "onSnapshot") return;
3312
+ const args = node.arguments ?? [];
3313
+ if (args.length === 2 && isFunctionNode(args[1])) {
3314
+ context.report({ node, messageId: "missingErrorCallback" });
3315
+ return;
3316
+ }
3317
+ if (args.length === 3 && isOptionsObject(args[1]) && isFunctionNode(args[2])) {
3318
+ context.report({ node, messageId: "missingErrorCallback" });
3799
3319
  }
3800
3320
  }
3801
3321
  };
3802
3322
  }
3803
3323
  };
3804
- var browserbaseUseTypedExceptionStatusNotSubstringRule = rule47;
3324
+ var firebaseOnSnapshotMissingErrorCallbackRule = rule47;
3805
3325
 
3806
- // src/providers/browserbase/rules/release-session-on-connect-failure.ts
3807
- function isSessionsCreateAwait(node) {
3808
- return node?.type === "AwaitExpression" && isSessionsCall(node.argument, "create");
3809
- }
3810
- function isConnectCall(node) {
3811
- if (node?.type !== "CallExpression") return false;
3812
- const callee = node.callee;
3813
- const name = callee?.type === "Identifier" ? callee.name : memberPropName3(node);
3814
- return !!name && /connect/i.test(name);
3815
- }
3816
- function isReleaseCall(node) {
3817
- if (node?.type !== "CallExpression") return false;
3818
- if (isSessionsCall(node, "update")) {
3819
- const optsArg = node.arguments?.[1];
3820
- const statusProp = findProperty2(optsArg, "status");
3821
- if (statusProp?.value?.type === "Literal" && statusProp.value.value === "REQUEST_RELEASE") return true;
3822
- }
3823
- const callee = node.callee;
3824
- const name = callee?.type === "Identifier" ? callee.name : memberPropName3(node);
3825
- return !!name && /requeststop|releasesession|release_session/i.test(name);
3826
- }
3326
+ // src/providers/firebase/rules/firestore-document-size-guard.ts
3827
3327
  var rule48 = {
3828
3328
  meta: {
3829
- type: "problem",
3329
+ type: "suggestion",
3830
3330
  docs: {
3831
- description: "A failed connect after session creation must release the session",
3331
+ description: "Firestore write includes editor.getJSON() without a document size guard",
3832
3332
  category: "reliability",
3833
- rationale: `If the CDP connect call raises after sessions.create() already succeeded on Browserbase's side, the session stays RUNNING and only terminates once its timeout elapses (up to 6h) unless something explicitly calls sessions.update(id, { status: 'REQUEST_RELEASE' }). The SDK's own docs warn to do this before the timeout "to avoid additional charges." A connect call with no try/catch (or a catch that doesn't release) leaks the session on every connect failure.`,
3834
- docsUrl: "https://docs.browserbase.com/reference/api/update-a-session",
3333
+ rationale: "Firestore documents have a 1 MiB limit. When a rich-text editor stores base64 images inline, a single paste can push the document over the limit. The write silently fails with INVALID_ARGUMENT. Always check document size before calling updateDoc/setDoc when the payload includes editor JSON.",
3334
+ docsUrl: "https://firebase.google.com/docs/firestore/quotas#limits",
3835
3335
  recommended: true
3836
3336
  },
3837
3337
  messages: {
3838
- sessionNotReleasedOnFailure: 'A session was created but this connect call has no catch that releases it (sessions.update with status: "REQUEST_RELEASE") on failure. A failed connect will leak the session until its timeout.'
3338
+ missingDocumentSizeGuard: "Firestore write includes editor.getJSON() without a document size check. Base64 images can exceed the 1 MiB Firestore limit, silently failing the write. Add a Blob size guard before calling updateDoc/setDoc."
3839
3339
  },
3840
3340
  schema: []
3841
3341
  },
3842
3342
  create(context) {
3843
- let sawSessionCreate = false;
3844
- const connectCalls = [];
3845
- const tryStatements = [];
3343
+ const WRITE_FNS = /* @__PURE__ */ new Set(["updateDoc", "setDoc"]);
3344
+ const flagCandidates = [];
3345
+ let hasBlobCheck = false;
3346
+ function containsGetJson(args) {
3347
+ return args.some(
3348
+ (arg) => someDescendant(arg, (n) => {
3349
+ if (n?.type !== "CallExpression") return false;
3350
+ const callee = n.callee;
3351
+ return callee?.type === "MemberExpression" && callee.property?.type === "Identifier" && callee.property.name === "getJSON";
3352
+ })
3353
+ );
3354
+ }
3846
3355
  return {
3847
- AwaitExpression(node) {
3848
- if (isSessionsCreateAwait(node)) sawSessionCreate = true;
3849
- },
3850
- TryStatement(node) {
3851
- tryStatements.push(node);
3852
- },
3853
3356
  CallExpression(node) {
3854
- if (isConnectCall(node)) connectCalls.push(node);
3357
+ const callee = node.callee;
3358
+ if (callee?.type === "Identifier" && WRITE_FNS.has(callee.name)) {
3359
+ if (containsGetJson(node.arguments ?? [])) {
3360
+ flagCandidates.push(node);
3361
+ }
3362
+ }
3363
+ },
3364
+ NewExpression(node) {
3365
+ if (node.callee?.type === "Identifier" && node.callee.name === "Blob") {
3366
+ hasBlobCheck = true;
3367
+ }
3855
3368
  },
3856
3369
  "Program:exit"() {
3857
- if (!sawSessionCreate) return;
3858
- for (const call of connectCalls) {
3859
- const enclosingTry = tryStatements.find((t) => t.block && contains3(t.block, call));
3860
- const handled = !!enclosingTry?.handler && someDescendant2(enclosingTry.handler, isReleaseCall);
3861
- if (!handled) {
3862
- context.report({ node: call, messageId: "sessionNotReleasedOnFailure" });
3863
- }
3370
+ if (hasBlobCheck) return;
3371
+ for (const node of flagCandidates) {
3372
+ context.report({ node, messageId: "missingDocumentSizeGuard" });
3864
3373
  }
3865
3374
  }
3866
3375
  };
3867
3376
  }
3868
3377
  };
3869
- var browserbaseReleaseSessionOnConnectFailureRule = rule48;
3378
+ var firebaseFirestoreDocumentSizeGuardRule = rule48;
3870
3379
 
3871
- // src/providers/browserbase/rules/dont-stack-custom-retry-on-sdk-retry.ts
3380
+ // src/providers/firebase/rules/use-timestamp-now.ts
3872
3381
  var rule49 = {
3873
3382
  meta: {
3874
3383
  type: "suggestion",
3875
3384
  docs: {
3876
- description: "A custom retry loop around sessions.create must disable the SDK's own retry",
3877
- category: "reliability",
3878
- rationale: "The Browserbase client already retries internally (default maxRetries: 2, retrying 408/409/429/5xx with Retry-After-aware exponential backoff) before ever raising an exception back to the caller. A hand-rolled retry loop around sessions.create() that does not pass { maxRetries: 0 } stacks two independent, uncoordinated backoff schedules \u2014 a sustained 429 can trigger N outer attempts x (1 + 2 SDK-internal retries), multiplying worst-case latency well beyond what either layer alone would produce.",
3879
- docsUrl: "https://docs.browserbase.com/optimizations/concurrency/overview",
3385
+ description: "new Date() used for Firestore timestamp instead of Timestamp.now() or serverTimestamp()",
3386
+ category: "correctness",
3387
+ rationale: "While Firestore auto-converts Date objects on write, mixing new Date() with Timestamp.now() creates type inconsistencies. Firestore security rules that compare request.resource.data.createdAt against request.time expect a Timestamp on both sides; a bare Date object can cause silent rule evaluation failures.",
3388
+ docsUrl: "https://firebase.google.com/docs/reference/js/firestore_.timestamp",
3880
3389
  recommended: true
3881
3390
  },
3882
3391
  messages: {
3883
- stackedRetry: "sessions.create() is called inside a custom retry loop with no { maxRetries: 0 } override \u2014 the SDK retries this call internally too, stacking two backoff schedules."
3392
+ useTimestampNow: "Use Timestamp.now() or serverTimestamp() instead of new Date() for Firestore timestamp fields. new Date() creates type inconsistencies with security rules that compare against request.time."
3884
3393
  },
3885
3394
  schema: []
3886
3395
  },
3887
3396
  create(context) {
3888
- const loopRanges = [];
3397
+ let importsFromFirestore = false;
3889
3398
  return {
3890
- ForStatement(node) {
3891
- loopRanges.push(node);
3892
- },
3893
- WhileStatement(node) {
3894
- loopRanges.push(node);
3895
- },
3896
- DoWhileStatement(node) {
3897
- loopRanges.push(node);
3898
- },
3899
- CallExpression(node) {
3900
- if (!isSessionsCall(node, "create")) return;
3901
- const insideLoop = loopRanges.some((loop) => {
3902
- const start = loop.range?.[0] ?? loop.start;
3903
- const end = loop.range?.[1] ?? loop.end;
3904
- const pos = node.range?.[0] ?? node.start;
3905
- return pos >= start && pos <= end;
3906
- });
3907
- if (!insideLoop) return;
3908
- const optsArg = node.arguments?.[1];
3909
- const hasMaxRetries = optsArg?.type === "ObjectExpression" && !!findProperty2(optsArg, "maxRetries");
3910
- if (!hasMaxRetries) {
3911
- context.report({ node, messageId: "stackedRetry" });
3399
+ ImportDeclaration(node) {
3400
+ const src = node.source?.value;
3401
+ if (typeof src === "string" && (src.startsWith("firebase/") || src === "firebase-admin/firestore")) {
3402
+ importsFromFirestore = true;
3912
3403
  }
3404
+ },
3405
+ NewExpression(node) {
3406
+ if (!importsFromFirestore) return;
3407
+ if (node.callee?.type !== "Identifier" || node.callee.name !== "Date") return;
3408
+ if ((node.arguments ?? []).length !== 0) return;
3409
+ context.report({ node, messageId: "useTimestampNow" });
3913
3410
  }
3914
3411
  };
3915
3412
  }
3916
3413
  };
3917
- var browserbaseDontStackCustomRetryOnSdkRetryRule = rule49;
3414
+ var firebaseUseTimestampNowRule = rule49;
3918
3415
 
3919
- // src/providers/browserbase/rules/no-overbroad-error-substring-match.ts
3920
- var OVERBROAD_TERMS = /* @__PURE__ */ new Set(["session", "context", "browser", "browserbase"]);
3921
- function collectIncludesLiterals(test, literals) {
3922
- if (!test) return;
3923
- if (test.type === "LogicalExpression" && test.operator === "||") {
3924
- collectIncludesLiterals(test.left, literals);
3925
- collectIncludesLiterals(test.right, literals);
3926
- return;
3416
+ // src/providers/lovable/utils.ts
3417
+ var LLM_HOST_PATTERN = /api\.anthropic\.com|api\.openai\.com/;
3418
+ function containsKnownLlmHost(node) {
3419
+ if (node?.type === "Literal" && typeof node.value === "string") {
3420
+ return LLM_HOST_PATTERN.test(node.value);
3927
3421
  }
3928
- if (test.type === "CallExpression" && memberPropName3(test) === "includes") {
3929
- const arg = test.arguments?.[0];
3930
- if (arg?.type === "Literal" && typeof arg.value === "string") literals.push(arg.value.toLowerCase());
3422
+ if (node?.type === "TemplateLiteral") {
3423
+ return (node.quasis ?? []).some((q) => LLM_HOST_PATTERN.test(q.value?.raw ?? ""));
3931
3424
  }
3425
+ return false;
3932
3426
  }
3933
- function isCleanupCall(node) {
3934
- if (node?.type !== "CallExpression") return false;
3935
- const callee = node.callee;
3936
- const name = callee?.type === "Identifier" ? callee.name : memberPropName3(node);
3937
- return !!name && /release|remove|cleanup|stop|teardown/i.test(name);
3938
- }
3427
+
3428
+ // src/providers/lovable/rules/no-client-side-secret-fetch.ts
3939
3429
  var rule50 = {
3940
3430
  meta: {
3941
3431
  type: "problem",
3942
3432
  docs: {
3943
- description: "Error-message checks must not OR in a single generic resource-name substring",
3944
- category: "reliability",
3945
- rationale: 'A condition like `err.includes("not found") || err.includes("404") || err.includes("session")` looks like it is narrowing to a confirmed session-ended error, but the last clause matches a single generic word \u2014 the name of the resource being polled. Virtually any exception raised from a sessions.* call (a connection reset, an SSL error, a generic timeout that echoes the request URL) is likely to contain that substring too. A coincidental match then triggers the same destructive cleanup as a real 404/410, tearing down a healthy session on a transient blip.',
3946
- docsUrl: "https://docs.browserbase.com/reference/api/session-live-urls",
3433
+ description: "LLM provider calls must not run client-side with a VITE_-exposed API key",
3434
+ category: "security",
3435
+ cwe: "CWE-522",
3436
+ owasp: "A02:2021 \u2013 Cryptographic Failures",
3437
+ rationale: "Anything read from import.meta.env.VITE_* is inlined into the production JS bundle at build time and is visible to every site visitor via the Network tab or by reading the bundle \u2014 no git access required. Lovable documents Edge Functions specifically so the provider key stays server-side in Secrets and the browser calls your own Edge Function instead of the provider directly.",
3438
+ docsUrl: "https://docs.lovable.dev/features/security",
3947
3439
  recommended: true
3948
3440
  },
3949
3441
  messages: {
3950
- overbroadMatch: 'This OR-chain includes a generic substring check ("{{term}}") that matches almost any error from a sessions.* call, not just a confirmed session-ended response.'
3951
- },
3952
- schema: []
3442
+ clientSideSecretFetch: "This fetch() calls an LLM provider directly with a key sourced from import.meta.env.VITE_* \u2014 that key ships into the browser bundle and is visible to every visitor."
3443
+ }
3953
3444
  },
3954
3445
  create(context) {
3446
+ const viteVars = /* @__PURE__ */ new Set();
3447
+ function propName2(node) {
3448
+ if (!node) return void 0;
3449
+ if (node.type === "Identifier") return node.name;
3450
+ if (node.type === "Literal" && typeof node.value === "string") return node.value;
3451
+ return void 0;
3452
+ }
3453
+ function isImportMetaEnvViteAccess(node) {
3454
+ if (node?.type !== "MemberExpression" || node.computed) return false;
3455
+ const propertyName = node.property?.name;
3456
+ if (typeof propertyName !== "string" || !propertyName.startsWith("VITE_")) return false;
3457
+ const envMember = node.object;
3458
+ if (envMember?.type !== "MemberExpression" || envMember.computed) return false;
3459
+ if (envMember.property?.name !== "env") return false;
3460
+ const metaProp = envMember.object;
3461
+ return metaProp?.type === "MetaProperty" && metaProp.meta?.name === "import" && metaProp.property?.name === "meta";
3462
+ }
3463
+ function referencesViteSecret(node) {
3464
+ if (!node) return false;
3465
+ if (isImportMetaEnvViteAccess(node)) return true;
3466
+ if (node.type === "Identifier" && viteVars.has(node.name)) return true;
3467
+ if (node.type === "TemplateLiteral") {
3468
+ return (node.expressions ?? []).some((e) => referencesViteSecret(e));
3469
+ }
3470
+ return false;
3471
+ }
3955
3472
  return {
3956
- IfStatement(node) {
3957
- const literals = [];
3958
- collectIncludesLiterals(node.test, literals);
3959
- if (literals.length < 2) return;
3960
- const overbroad = literals.find((l) => OVERBROAD_TERMS.has(l));
3961
- if (!overbroad) return;
3962
- if (someDescendant2(node.consequent, isCleanupCall)) {
3963
- context.report({ node, messageId: "overbroadMatch", data: { term: overbroad } });
3473
+ VariableDeclarator(node) {
3474
+ if (node.id?.type === "Identifier" && isImportMetaEnvViteAccess(node.init)) {
3475
+ viteVars.add(node.id.name);
3964
3476
  }
3965
- }
3966
- };
3967
- }
3968
- };
3969
- var browserbaseNoOverbroadErrorSubstringMatchRule = rule50;
3970
-
3971
- // src/providers/browserbase/rules/use-sdk-not-raw-requests.ts
3972
- var BROWSERBASE_HOST = "api.browserbase.com";
3973
- function templateLiteralContainsHost(node) {
3974
- return (node.quasis ?? []).some((q) => {
3975
- const text = q?.value?.cooked ?? q?.value?.raw ?? "";
3976
- return typeof text === "string" && text.includes(BROWSERBASE_HOST);
3977
- });
3978
- }
3979
- function urlArgContainsHost(node) {
3980
- if (node?.type === "Literal" && typeof node.value === "string") return node.value.includes(BROWSERBASE_HOST);
3981
- if (node?.type === "TemplateLiteral") return templateLiteralContainsHost(node);
3982
- return false;
3983
- }
3984
- function isRawHttpCall(node) {
3985
- if (node?.type !== "CallExpression") return { isCall: false, urlArg: void 0 };
3986
- const callee = node.callee;
3987
- if (callee?.type === "Identifier" && callee.name === "fetch") {
3988
- return { isCall: true, urlArg: node.arguments?.[0] };
3989
- }
3990
- if (callee?.type === "MemberExpression" && !callee.computed && callee.property?.type === "Identifier") {
3991
- const objName = callee.object?.type === "Identifier" ? callee.object.name : void 0;
3992
- const methodName = callee.property.name;
3993
- if (objName && /^(axios|http|https)$/i.test(objName) && /^(get|post|put|patch|delete|request)$/i.test(methodName)) {
3994
- return { isCall: true, urlArg: node.arguments?.[0] };
3995
- }
3996
- }
3997
- if (callee?.type === "Identifier" && callee.name === "axios") {
3998
- const arg = node.arguments?.[0];
3999
- if (arg?.type === "ObjectExpression") {
4000
- const urlProp = arg.properties?.find(
4001
- (p) => p?.type === "Property" && p.key?.type === "Identifier" && p.key.name === "url"
4002
- );
4003
- return { isCall: true, urlArg: urlProp?.value };
4004
- }
4005
- return { isCall: true, urlArg: arg };
3477
+ },
3478
+ CallExpression(node) {
3479
+ const callee = node.callee;
3480
+ if (callee?.type !== "Identifier" || callee.name !== "fetch") return;
3481
+ const urlArg = node.arguments?.[0];
3482
+ if (!containsKnownLlmHost(urlArg)) return;
3483
+ const optsArg = node.arguments?.[1];
3484
+ if (optsArg?.type !== "ObjectExpression") return;
3485
+ const headersProp = (optsArg.properties ?? []).find(
3486
+ (p) => p?.type === "Property" && propName2(p.key) === "headers"
3487
+ );
3488
+ if (!headersProp || headersProp.value?.type !== "ObjectExpression") return;
3489
+ for (const hp of headersProp.value.properties ?? []) {
3490
+ if (hp?.type !== "Property") continue;
3491
+ const keyName = propName2(hp.key)?.toLowerCase();
3492
+ if (keyName !== "x-api-key" && keyName !== "authorization") continue;
3493
+ if (referencesViteSecret(hp.value)) {
3494
+ context.report({ node, messageId: "clientSideSecretFetch" });
3495
+ return;
3496
+ }
3497
+ }
3498
+ }
3499
+ };
4006
3500
  }
4007
- return { isCall: false, urlArg: void 0 };
4008
- }
3501
+ };
3502
+ var lovableNoClientSideSecretFetchRule = rule50;
3503
+
3504
+ // src/providers/lovable/rules/paid-flag-without-edge-function.ts
3505
+ var PRICE_PATTERN = /\$\s?\d/;
3506
+ var FLAG_PROPERTY_PATTERN = /^is_[a-z0-9_]+$/i;
3507
+ var FLAG_SUFFIX_PATTERN = /_(unlocked|active|enabled)$/i;
4009
3508
  var rule51 = {
4010
3509
  meta: {
4011
- type: "suggestion",
3510
+ type: "problem",
4012
3511
  docs: {
4013
- description: "Use the installed Browserbase SDK instead of hand-rolled HTTP requests",
4014
- category: "integration",
4015
- rationale: "A raw fetch/axios/http call to an api.browserbase.com endpoint duplicates a method the installed SDK already exposes (e.g. sessions.recording.retrieve()), forfeiting its typed exceptions, built-in retry/backoff, and consistent error handling \u2014 and risks drifting on auth header naming or response shape as the API evolves.",
4016
- docsUrl: "https://docs.browserbase.com/reference/sdk/nodejs",
3512
+ description: "A paid feature flag must not be set by a direct database update with no payment-provider call",
3513
+ category: "security",
3514
+ cwe: "CWE-840",
3515
+ owasp: "A04:2021 \u2013 Insecure Design",
3516
+ rationale: "Lovable documents payment processing as Edge Function territory specifically so purchase/premium-access flags are only ever set after a payment provider confirms payment server-side. A handler that writes a paid-looking flag straight from the client with no payment call in between either never actually charges anyone, or \u2014 even if a charge happens elsewhere \u2014 leaves the flag itself freely settable by any caller with write access to the row.",
3517
+ docsUrl: "https://docs.lovable.dev/features/cloud",
4017
3518
  recommended: true
4018
3519
  },
4019
3520
  messages: {
4020
- rawRequestToBrowserbase: "This makes a raw HTTP request to a Browserbase API endpoint instead of using the installed SDK."
4021
- },
4022
- schema: []
3521
+ paidFlagWithoutPayment: "This sets a paid-looking flag via a direct database update, but nothing in this function calls a payment provider or Edge Function first \u2014 the flag can be set without anyone paying."
3522
+ }
4023
3523
  },
4024
3524
  create(context) {
4025
- const urlVars = /* @__PURE__ */ new Map();
3525
+ const stack = [];
3526
+ const states = /* @__PURE__ */ new Map();
3527
+ let sawPriceLabel = false;
3528
+ function ensureState(fn) {
3529
+ let s = states.get(fn);
3530
+ if (!s) {
3531
+ s = { sawFlagUpdate: false, updateNode: null, sawPaymentCall: false };
3532
+ states.set(fn, s);
3533
+ }
3534
+ return s;
3535
+ }
3536
+ function top() {
3537
+ return stack[stack.length - 1];
3538
+ }
3539
+ function pushScope(node) {
3540
+ stack.push(node);
3541
+ ensureState(node);
3542
+ }
3543
+ function popScope() {
3544
+ stack.pop();
3545
+ }
3546
+ function propName2(node) {
3547
+ if (!node) return void 0;
3548
+ if (node.type === "Identifier") return node.name;
3549
+ if (node.type === "Literal" && typeof node.value === "string") return node.value;
3550
+ return void 0;
3551
+ }
3552
+ function isBooleanFlagProperty(name) {
3553
+ if (!name) return false;
3554
+ return FLAG_PROPERTY_PATTERN.test(name) || FLAG_SUFFIX_PATTERN.test(name);
3555
+ }
3556
+ function isSupabaseUpdateCall(node) {
3557
+ if (node?.type !== "CallExpression") return false;
3558
+ const callee = node.callee;
3559
+ return callee?.type === "MemberExpression" && callee.property?.name === "update";
3560
+ }
3561
+ function updateSetsBooleanFlag(node) {
3562
+ const arg = node.arguments?.[0];
3563
+ if (arg?.type !== "ObjectExpression") return false;
3564
+ return (arg.properties ?? []).some(
3565
+ (p) => p?.type === "Property" && isBooleanFlagProperty(propName2(p.key))
3566
+ );
3567
+ }
3568
+ function memberChainNames2(node, names = []) {
3569
+ if (node?.type === "MemberExpression") {
3570
+ memberChainNames2(node.object, names);
3571
+ const n = propName2(node.property);
3572
+ if (n) names.push(n);
3573
+ } else if (node?.type === "Identifier") {
3574
+ names.push(node.name);
3575
+ } else if (node?.type === "CallExpression") {
3576
+ memberChainNames2(node.callee, names);
3577
+ }
3578
+ return names;
3579
+ }
3580
+ function isPaymentRelatedCall(node) {
3581
+ if (node?.type !== "CallExpression") return false;
3582
+ const chain = memberChainNames2(node.callee).join(".").toLowerCase();
3583
+ if (/stripe|checkout|payment/.test(chain)) return true;
3584
+ if (chain.includes("functions") && chain.endsWith(".invoke")) return true;
3585
+ if (node.callee?.type === "Identifier" && node.callee.name === "fetch") {
3586
+ const urlArg = node.arguments?.[0];
3587
+ if (urlArg?.type === "Literal" && typeof urlArg.value === "string") {
3588
+ return urlArg.value.includes("/functions/");
3589
+ }
3590
+ if (urlArg?.type === "TemplateLiteral") {
3591
+ return (urlArg.quasis ?? []).some((q) => (q.value?.raw ?? "").includes("/functions/"));
3592
+ }
3593
+ }
3594
+ return false;
3595
+ }
4026
3596
  return {
4027
- VariableDeclarator(node) {
4028
- if (node.id?.type === "Identifier" && urlArgContainsHost(node.init)) {
4029
- urlVars.set(node.id.name, node.init);
3597
+ Program(node) {
3598
+ pushScope(node);
3599
+ },
3600
+ "Program:exit"() {
3601
+ if (!sawPriceLabel) return;
3602
+ for (const state of states.values()) {
3603
+ if (state.sawFlagUpdate && !state.sawPaymentCall) {
3604
+ context.report({ node: state.updateNode, messageId: "paidFlagWithoutPayment" });
3605
+ }
4030
3606
  }
4031
3607
  },
3608
+ FunctionDeclaration(node) {
3609
+ pushScope(node);
3610
+ },
3611
+ "FunctionDeclaration:exit"() {
3612
+ popScope();
3613
+ },
3614
+ FunctionExpression(node) {
3615
+ pushScope(node);
3616
+ },
3617
+ "FunctionExpression:exit"() {
3618
+ popScope();
3619
+ },
3620
+ ArrowFunctionExpression(node) {
3621
+ pushScope(node);
3622
+ },
3623
+ "ArrowFunctionExpression:exit"() {
3624
+ popScope();
3625
+ },
3626
+ Literal(node) {
3627
+ if (typeof node.value === "string" && PRICE_PATTERN.test(node.value)) sawPriceLabel = true;
3628
+ },
3629
+ JSXText(node) {
3630
+ if (typeof node.value === "string" && PRICE_PATTERN.test(node.value)) sawPriceLabel = true;
3631
+ },
3632
+ TemplateElement(node) {
3633
+ if (PRICE_PATTERN.test(node.value?.raw ?? "")) sawPriceLabel = true;
3634
+ },
4032
3635
  CallExpression(node) {
4033
- const { isCall, urlArg } = isRawHttpCall(node);
4034
- if (!isCall || !urlArg) return;
4035
- const resolved = urlArg.type === "Identifier" ? urlVars.get(urlArg.name) ?? urlArg : urlArg;
4036
- if (urlArgContainsHost(resolved)) {
4037
- context.report({ node, messageId: "rawRequestToBrowserbase" });
3636
+ const fn = top();
3637
+ if (!fn) return;
3638
+ const state = ensureState(fn);
3639
+ if (isSupabaseUpdateCall(node) && updateSetsBooleanFlag(node) && !state.sawFlagUpdate) {
3640
+ state.sawFlagUpdate = true;
3641
+ state.updateNode = node;
3642
+ }
3643
+ if (isPaymentRelatedCall(node)) {
3644
+ state.sawPaymentCall = true;
4038
3645
  }
4039
3646
  }
4040
3647
  };
4041
3648
  }
4042
3649
  };
4043
- var browserbaseUseSdkNotRawRequestsRule = rule51;
3650
+ var lovablePaidFlagWithoutEdgeFunctionRule = rule51;
4044
3651
 
4045
- // src/providers/browserbase/rules/centralize-request-release.ts
3652
+ // src/providers/lovable/rules/expiry-column-never-checked.ts
3653
+ var EXPIRY_COLUMN_PATTERN = /_(until|expires?_at|expiry)$/i;
3654
+ var DATE_COMPARISON_OPERATORS = /* @__PURE__ */ new Set([">", "<", ">=", "<="]);
3655
+ var FILTER_METHODS = /* @__PURE__ */ new Set(["gt", "gte", "lt", "lte"]);
4046
3656
  var rule52 = {
4047
3657
  meta: {
4048
3658
  type: "suggestion",
4049
3659
  docs: {
4050
- description: "Route REQUEST_RELEASE through a single shared abstraction",
4051
- category: "integration",
4052
- rationale: 'sessions.update(id, { status: "REQUEST_RELEASE" }) hand-rolled inline at each call site, with each one carrying slightly different error handling, means an API change (a new required field, a renamed status value) requires fixing every call site instead of one \u2014 and the duplicated copies already drift in error-handling quality. Centralize behind one designated provider method (e.g. requestStop()/releaseSession()) and call that everywhere instead.',
4053
- docsUrl: "https://docs.browserbase.com/reference/api/update-a-session",
3660
+ description: "An expiry column must be checked against the current time somewhere",
3661
+ category: "correctness",
3662
+ rationale: "Writing a *_until/*_expires_at column without ever comparing it to the current time anywhere in the codebase means the expiry is purely cosmetic \u2014 whatever it was meant to gate (a boost, a trial, a temporary unlock) never actually expires once granted, whether it was granted legitimately or through an unrelated bug.",
3663
+ docsUrl: "https://docs.lovable.dev/features/cloud",
4054
3664
  recommended: true
4055
3665
  },
4056
3666
  messages: {
4057
- inlineRequestRelease: 'sessions.update(..., { status: "REQUEST_RELEASE" }) is hand-rolled inline here. Route this through a single shared release/provider abstraction instead.'
4058
- },
4059
- schema: []
3667
+ expiryNeverChecked: 'The "{{column}}" column is written here but is never compared against the current time anywhere in this file, so it never actually expires anything.'
3668
+ }
4060
3669
  },
4061
3670
  create(context) {
4062
- const filename = String(context.filename ?? "");
4063
- if (isInsideTestFile3(filename) || /provider|browserbaseclient/i.test(filename)) {
4064
- return {};
3671
+ const writtenColumns = /* @__PURE__ */ new Map();
3672
+ const readColumns = /* @__PURE__ */ new Set();
3673
+ function propName2(node) {
3674
+ if (!node) return void 0;
3675
+ if (node.type === "Identifier") return node.name;
3676
+ if (node.type === "Literal" && typeof node.value === "string") return node.value;
3677
+ return void 0;
3678
+ }
3679
+ function isSupabaseUpdateCall(node) {
3680
+ if (node?.type !== "CallExpression") return false;
3681
+ const callee = node.callee;
3682
+ return callee?.type === "MemberExpression" && callee.property?.name === "update";
3683
+ }
3684
+ function containsDateNowOrNewDate(node, depth = 0) {
3685
+ if (!node || typeof node !== "object" || depth > 6) return false;
3686
+ if (node.type === "NewExpression" && node.callee?.type === "Identifier" && node.callee.name === "Date") {
3687
+ return true;
3688
+ }
3689
+ if (node.type === "CallExpression" && node.callee?.type === "MemberExpression" && node.callee.object?.type === "Identifier" && node.callee.object.name === "Date" && node.callee.property?.name === "now") {
3690
+ return true;
3691
+ }
3692
+ if (node.type === "MemberExpression") {
3693
+ return containsDateNowOrNewDate(node.object, depth + 1) || containsDateNowOrNewDate(node.callee, depth + 1);
3694
+ }
3695
+ if (node.type === "CallExpression") {
3696
+ return containsDateNowOrNewDate(node.callee, depth + 1);
3697
+ }
3698
+ return false;
3699
+ }
3700
+ function memberPropertyName(node) {
3701
+ if (node?.type !== "MemberExpression") return void 0;
3702
+ return propName2(node.property);
4065
3703
  }
4066
3704
  return {
4067
3705
  CallExpression(node) {
4068
- if (!isSessionsCall(node, "update")) return;
4069
- const optsArg = node.arguments?.[1];
4070
- const statusProp = findProperty2(optsArg, "status");
4071
- const val = statusProp?.value;
4072
- if (val?.type === "Literal" && val.value === "REQUEST_RELEASE") {
4073
- context.report({ node, messageId: "inlineRequestRelease" });
3706
+ if (isSupabaseUpdateCall(node)) {
3707
+ const arg = node.arguments?.[0];
3708
+ if (arg?.type === "ObjectExpression") {
3709
+ for (const prop of arg.properties ?? []) {
3710
+ if (prop?.type !== "Property") continue;
3711
+ const keyName = propName2(prop.key);
3712
+ if (keyName && EXPIRY_COLUMN_PATTERN.test(keyName) && !writtenColumns.has(keyName)) {
3713
+ writtenColumns.set(keyName, node);
3714
+ }
3715
+ }
3716
+ }
3717
+ }
3718
+ const callee = node.callee;
3719
+ if (callee?.type === "MemberExpression" && FILTER_METHODS.has(callee.property?.name)) {
3720
+ const colArg = node.arguments?.[0];
3721
+ const colName = colArg?.type === "Literal" ? propName2(colArg) : void 0;
3722
+ if (colName && EXPIRY_COLUMN_PATTERN.test(colName)) {
3723
+ readColumns.add(colName);
3724
+ }
3725
+ }
3726
+ },
3727
+ BinaryExpression(node) {
3728
+ if (!DATE_COMPARISON_OPERATORS.has(node.operator)) return;
3729
+ const leftCol = memberPropertyName(node.left);
3730
+ const rightCol = memberPropertyName(node.right);
3731
+ if (leftCol && EXPIRY_COLUMN_PATTERN.test(leftCol) && containsDateNowOrNewDate(node.right)) {
3732
+ readColumns.add(leftCol);
3733
+ }
3734
+ if (rightCol && EXPIRY_COLUMN_PATTERN.test(rightCol) && containsDateNowOrNewDate(node.left)) {
3735
+ readColumns.add(rightCol);
3736
+ }
3737
+ },
3738
+ "Program:exit"() {
3739
+ for (const [column, node] of writtenColumns) {
3740
+ if (!readColumns.has(column)) {
3741
+ context.report({ node, messageId: "expiryNeverChecked", data: { column } });
3742
+ }
3743
+ }
3744
+ }
3745
+ };
3746
+ }
3747
+ };
3748
+ var lovableExpiryColumnNeverCheckedRule = rule52;
3749
+
3750
+ // src/providers/lovable/rules/silent-catch-on-provider-call.ts
3751
+ var LOGGING_CONSOLE_METHODS = /* @__PURE__ */ new Set(["error", "warn", "log", "info"]);
3752
+ var rule53 = {
3753
+ meta: {
3754
+ type: "suggestion",
3755
+ docs: {
3756
+ description: "A catch block around an LLM provider call must log the failure",
3757
+ category: "correctness",
3758
+ rationale: `Returning null/undefined from a bare catch with no logging makes every failure mode \u2014 missing key, expired key, rate limit, malformed response, network error \u2014 look identical to "no key configured." If a deployment's provider key starts failing in production, this is invisible: there is nothing in the browser or error-tracking logs to distinguish a real outage from an intentionally-unconfigured feature.`,
3759
+ docsUrl: "https://docs.lovable.dev/features/cloud",
3760
+ recommended: true
3761
+ },
3762
+ messages: {
3763
+ silentCatch: 'This catch block swallows a failed LLM provider call with no logging \u2014 a real failure (expired key, rate limit, network error) is indistinguishable from "no key configured."'
3764
+ }
3765
+ },
3766
+ create(context) {
3767
+ function findFetchToLlmHost(node, depth = 0) {
3768
+ if (!node || typeof node !== "object" || depth > 12) return null;
3769
+ if (Array.isArray(node)) {
3770
+ for (const n of node) {
3771
+ const found = findFetchToLlmHost(n, depth + 1);
3772
+ if (found) return found;
3773
+ }
3774
+ return null;
3775
+ }
3776
+ if (node.type === "CallExpression" && node.callee?.type === "Identifier" && node.callee.name === "fetch") {
3777
+ const urlArg = node.arguments?.[0];
3778
+ if (containsKnownLlmHost(urlArg)) return node;
3779
+ }
3780
+ for (const key of Object.keys(node)) {
3781
+ if (key === "parent" || key === "loc" || key === "range") continue;
3782
+ const val = node[key];
3783
+ if (val && typeof val === "object") {
3784
+ const found = findFetchToLlmHost(val, depth + 1);
3785
+ if (found) return found;
3786
+ }
3787
+ }
3788
+ return null;
3789
+ }
3790
+ function containsLoggingCall(node, depth = 0) {
3791
+ if (!node || typeof node !== "object" || depth > 12) return false;
3792
+ if (Array.isArray(node)) return node.some((n) => containsLoggingCall(n, depth + 1));
3793
+ if (node.type === "CallExpression") {
3794
+ const callee = node.callee;
3795
+ if (callee?.type === "MemberExpression") {
3796
+ const objName = callee.object?.type === "Identifier" ? callee.object.name : void 0;
3797
+ const propertyName = callee.property?.name;
3798
+ if (objName === "console" && LOGGING_CONSOLE_METHODS.has(propertyName)) return true;
3799
+ if (propertyName === "captureException") return true;
3800
+ }
3801
+ if (callee?.type === "Identifier" && callee.name === "reportError") return true;
3802
+ }
3803
+ for (const key of Object.keys(node)) {
3804
+ if (key === "parent" || key === "loc" || key === "range") continue;
3805
+ const val = node[key];
3806
+ if (val && typeof val === "object") {
3807
+ if (containsLoggingCall(val, depth + 1)) return true;
3808
+ }
3809
+ }
3810
+ return false;
3811
+ }
3812
+ return {
3813
+ TryStatement(node) {
3814
+ const fetchNode = findFetchToLlmHost(node.block);
3815
+ if (!fetchNode) return;
3816
+ const handler = node.handler;
3817
+ if (!handler) return;
3818
+ if (!containsLoggingCall(handler.body)) {
3819
+ context.report({ node: handler, messageId: "silentCatch" });
3820
+ }
3821
+ }
3822
+ };
3823
+ }
3824
+ };
3825
+ var lovableSilentCatchOnProviderCallRule = rule53;
3826
+
3827
+ // src/providers/browserbase/utils.ts
3828
+ function memberPropName3(node) {
3829
+ if (node?.type !== "CallExpression") return void 0;
3830
+ const callee = node.callee;
3831
+ if (callee?.type !== "MemberExpression") return void 0;
3832
+ const prop = callee.property;
3833
+ if (!callee.computed && prop?.type === "Identifier") return prop.name;
3834
+ if (callee.computed && prop?.type === "Literal" && typeof prop.value === "string") return prop.value;
3835
+ return void 0;
3836
+ }
3837
+ function isSessionsCall(node, name) {
3838
+ if (memberPropName3(node) !== name) return false;
3839
+ const obj = node.callee.object;
3840
+ return obj?.type === "MemberExpression" && !obj.computed && obj.property?.type === "Identifier" && obj.property.name === "sessions";
3841
+ }
3842
+ function isSessionsRecordingCall(node, name) {
3843
+ if (memberPropName3(node) !== name) return false;
3844
+ const recordingObj = node.callee.object;
3845
+ if (recordingObj?.type !== "MemberExpression" || recordingObj.computed) return false;
3846
+ if (recordingObj.property?.type !== "Identifier" || recordingObj.property.name !== "recording") return false;
3847
+ const sessionsObj = recordingObj.object;
3848
+ return sessionsObj?.type === "MemberExpression" && !sessionsObj.computed && sessionsObj.property?.type === "Identifier" && sessionsObj.property.name === "sessions";
3849
+ }
3850
+ function findProperty2(objectExpression, name) {
3851
+ if (objectExpression?.type !== "ObjectExpression") return void 0;
3852
+ return objectExpression.properties?.find(
3853
+ (p) => p?.type === "Property" && (p.key?.type === "Identifier" && p.key.name === name || p.key?.type === "Literal" && p.key.value === name)
3854
+ );
3855
+ }
3856
+ function startOffset3(n) {
3857
+ if (typeof n?.range?.[0] === "number") return n.range[0];
3858
+ if (typeof n?.start === "number") return n.start;
3859
+ return (n?.loc?.start?.line ?? 0) * 1e6 + (n?.loc?.start?.column ?? 0);
3860
+ }
3861
+ function endOffset3(n) {
3862
+ if (typeof n?.range?.[1] === "number") return n.range[1];
3863
+ if (typeof n?.end === "number") return n.end;
3864
+ return (n?.loc?.end?.line ?? n?.loc?.start?.line ?? 0) * 1e6 + (n?.loc?.end?.column ?? 0);
3865
+ }
3866
+ function contains3(outer, inner) {
3867
+ const s = startOffset3(inner);
3868
+ return s >= startOffset3(outer) && s <= endOffset3(outer);
3869
+ }
3870
+ function someDescendant2(node, predicate) {
3871
+ let found = false;
3872
+ function visit(n) {
3873
+ if (found || !n || typeof n !== "object") return;
3874
+ if (Array.isArray(n)) {
3875
+ for (const item of n) visit(item);
3876
+ return;
3877
+ }
3878
+ if (typeof n.type !== "string") return;
3879
+ if (predicate(n)) {
3880
+ found = true;
3881
+ return;
3882
+ }
3883
+ for (const key of Object.keys(n)) {
3884
+ if (key === "parent" || key === "loc" || key === "range") continue;
3885
+ visit(n[key]);
3886
+ }
3887
+ }
3888
+ visit(node);
3889
+ return found;
3890
+ }
3891
+ function isResponseSendCall(node) {
3892
+ const name = memberPropName3(node);
3893
+ if (name !== "json" && name !== "send") return false;
3894
+ const obj = node.callee.object;
3895
+ if (obj?.type === "Identifier") {
3896
+ return /^(res|response)$/i.test(obj.name) || obj.name === "Response" || obj.name === "NextResponse";
3897
+ }
3898
+ return false;
3899
+ }
3900
+ function isInsideTestFile3(filename) {
3901
+ return /(^|[\\/])__tests__[\\/]|\.(test|spec)\.[cm]?[jt]sx?$/.test(filename);
3902
+ }
3903
+
3904
+ // src/providers/browserbase/rules/no-conditional-authz-on-anonymous-user.ts
3905
+ function isUserLikeExpr(node) {
3906
+ if (node?.type === "Identifier") return /user/i.test(node.name);
3907
+ if (node?.type === "MemberExpression" && !node.computed && node.property?.type === "Identifier") {
3908
+ return /user/i.test(node.property.name);
3909
+ }
3910
+ return false;
3911
+ }
3912
+ function isFalsyGuardTest(test) {
3913
+ if (test?.type === "UnaryExpression" && test.operator === "!") return isUserLikeExpr(test.argument);
3914
+ if (test?.type === "BinaryExpression" && (test.operator === "===" || test.operator === "==")) {
3915
+ const sides = [test.left, test.right];
3916
+ const isNullish = (n) => n?.type === "Literal" && n.value === null || n?.type === "Identifier" && n.name === "undefined";
3917
+ const nullSide = sides.find(isNullish);
3918
+ const otherSide = sides.find((s) => s !== nullSide);
3919
+ return !!nullSide && isUserLikeExpr(otherSide);
3920
+ }
3921
+ return false;
3922
+ }
3923
+ function hasReturnOrThrow(stmt) {
3924
+ if (!stmt) return false;
3925
+ if (stmt.type === "ReturnStatement" || stmt.type === "ThrowStatement") return true;
3926
+ if (stmt.type === "BlockStatement") {
3927
+ return (stmt.body ?? []).some((s) => s.type === "ReturnStatement" || s.type === "ThrowStatement");
3928
+ }
3929
+ return false;
3930
+ }
3931
+ function isTruthyGateTest(test) {
3932
+ if (isUserLikeExpr(test)) return true;
3933
+ if (test?.type === "LogicalExpression" && test.operator === "&&") {
3934
+ return isTruthyGateTest(test.left) || isTruthyGateTest(test.right);
3935
+ }
3936
+ return false;
3937
+ }
3938
+ var rule54 = {
3939
+ meta: {
3940
+ type: "problem",
3941
+ docs: {
3942
+ description: "Sensitive Browserbase resources must be guarded by unconditional authorization",
3943
+ category: "security",
3944
+ cwe: "CWE-862",
3945
+ owasp: "A01:2021 Broken Access Control",
3946
+ rationale: "bb.sessions.debug() and bb.sessions.recording.retrieve() return a live CDP debugger URL or a full rrweb session replay \u2014 high-value resources. Gating the ownership check behind `if (user) { ...check... }` with no unconditional `if (!user) return` guard means an anonymous caller (no credentials at all) simply skips the whole block and falls through to the sensitive call. Authorization must reject the unauthenticated case explicitly, not rely on a conditional that happens to be true for legitimate callers.",
3947
+ docsUrl: "https://docs.browserbase.com/reference/api/session-live-urls",
3948
+ recommended: true
3949
+ },
3950
+ messages: {
3951
+ conditionalAuthz: "This route returns a Browserbase live-view/recording resource gated only by `if (user) {...}` with no unconditional rejection for anonymous callers. Add an `if (!user) return ...` guard before this."
3952
+ },
3953
+ schema: []
3954
+ },
3955
+ create(context) {
3956
+ const scopes = [];
3957
+ const gates = [];
3958
+ const sensitiveCalls = [];
3959
+ function registerScope(node) {
3960
+ scopes.push({ node, start: startOffset3(node), end: endOffset3(node) });
3961
+ }
3962
+ function innermostScope(pos) {
3963
+ let best;
3964
+ for (const scope of scopes) {
3965
+ if (pos < scope.start || pos > scope.end) continue;
3966
+ if (!best || scope.end - scope.start < best.end - best.start) best = scope;
3967
+ }
3968
+ return best;
3969
+ }
3970
+ return {
3971
+ FunctionDeclaration(node) {
3972
+ registerScope(node);
3973
+ },
3974
+ FunctionExpression(node) {
3975
+ registerScope(node);
3976
+ },
3977
+ ArrowFunctionExpression(node) {
3978
+ registerScope(node);
3979
+ },
3980
+ IfStatement(node) {
3981
+ if (isFalsyGuardTest(node.test) && hasReturnOrThrow(node.consequent)) {
3982
+ const pos = startOffset3(node);
3983
+ const scope = innermostScope(pos);
3984
+ if (scope && (scope.guardPos === void 0 || pos < scope.guardPos)) scope.guardPos = pos;
3985
+ return;
3986
+ }
3987
+ if (isTruthyGateTest(node.test) && !node.alternate) {
3988
+ gates.push({ start: startOffset3(node), end: endOffset3(node) });
3989
+ }
3990
+ },
3991
+ CallExpression(node) {
3992
+ if (isSessionsCall(node, "debug") || isSessionsRecordingCall(node, "retrieve")) {
3993
+ sensitiveCalls.push(node);
3994
+ }
3995
+ },
3996
+ "Program:exit"() {
3997
+ for (const call of sensitiveCalls) {
3998
+ const gate = gates.find((g) => contains3(g, call));
3999
+ if (!gate) continue;
4000
+ const scope = innermostScope(startOffset3(call));
4001
+ const hasGuardBeforeGate = scope?.guardPos !== void 0 && scope.guardPos < gate.start;
4002
+ if (!hasGuardBeforeGate) {
4003
+ context.report({ node: call, messageId: "conditionalAuthz" });
4004
+ }
4005
+ }
4006
+ }
4007
+ };
4008
+ }
4009
+ };
4010
+ var browserbaseNoConditionalAuthzOnAnonymousUserRule = rule54;
4011
+
4012
+ // src/providers/browserbase/rules/no-connect-url-in-api-response.ts
4013
+ var rule55 = {
4014
+ meta: {
4015
+ type: "problem",
4016
+ docs: {
4017
+ description: "connectUrl must never be placed in an HTTP response body",
4018
+ category: "security",
4019
+ cwe: "CWE-200",
4020
+ owasp: "A04:2021 Insecure Design",
4021
+ rationale: "session.connectUrl is the WebSocket CDP URL returned by sessions.create() \u2014 it is self-authenticating; connecting to it grants full read/write control of the live browser (arbitrary JS execution, cookie/localStorage read, simulated input), equivalent to a bearer token. Putting it in a response body widens its exposure (network tab, browser extensions, error trackers, logs) for callers that almost never need it client-side \u2014 the intended sharable artifact is debuggerFullscreenUrl from sessions.debug().",
4022
+ docsUrl: "https://docs.browserbase.com/reference/api/create-a-session",
4023
+ recommended: true
4024
+ },
4025
+ messages: {
4026
+ connectUrlInResponse: "connectUrl is included in an HTTP response body. Keep it server-side only \u2014 it is a bearer credential for the live browser, not a sharable URL."
4027
+ },
4028
+ schema: []
4029
+ },
4030
+ create(context) {
4031
+ return {
4032
+ CallExpression(node) {
4033
+ if (!isResponseSendCall(node)) return;
4034
+ const arg = node.arguments?.[0];
4035
+ if (arg?.type !== "ObjectExpression") return;
4036
+ if (findProperty2(arg, "connectUrl")) {
4037
+ context.report({ node, messageId: "connectUrlInResponse" });
4038
+ }
4039
+ }
4040
+ };
4041
+ }
4042
+ };
4043
+ var browserbaseNoConnectUrlInApiResponseRule = rule55;
4044
+
4045
+ // src/providers/browserbase/rules/session-id-requires-ownership-check.ts
4046
+ function isOwnershipCheckCall(node) {
4047
+ if (node?.type !== "CallExpression") return false;
4048
+ const callee = node.callee;
4049
+ const name = callee?.type === "Identifier" ? callee.name : callee?.type === "MemberExpression" && !callee.computed && callee.property?.type === "Identifier" ? callee.property.name : void 0;
4050
+ if (!name) return false;
4051
+ return /owner|belongsto|hasaccess|authorize|verifyaccess|checkaccess|findproject|getproject/i.test(name);
4052
+ }
4053
+ function isSessionIdLikeArg(node) {
4054
+ if (node?.type === "Identifier") return /sessionid/i.test(node.name);
4055
+ if (node?.type === "MemberExpression" && !node.computed && node.property?.type === "Identifier") {
4056
+ return /sessionid/i.test(node.property.name);
4057
+ }
4058
+ return false;
4059
+ }
4060
+ var rule56 = {
4061
+ meta: {
4062
+ type: "suggestion",
4063
+ docs: {
4064
+ description: "Session id lookups must verify ownership before returning live-view data",
4065
+ category: "security",
4066
+ cwe: "CWE-862",
4067
+ rationale: "A session id alone is not an authorization token \u2014 it is an opaque identifier that can leak via links, logs, screenshots, or support tickets. A handler that resolves a sessionId straight into sessions.debug()/sessions.recording.retrieve() with no ownership/org-membership check lets any authenticated caller view or replay a session that belongs to someone else.",
4068
+ docsUrl: "https://docs.browserbase.com/features/session-live-view",
4069
+ recommended: true
4070
+ },
4071
+ messages: {
4072
+ missingOwnershipCheck: "sessionId is passed directly into a Browserbase live-view/recording call with no ownership check beforehand."
4073
+ },
4074
+ schema: []
4075
+ },
4076
+ create(context) {
4077
+ const scopes = [];
4078
+ const sensitiveCalls = [];
4079
+ function registerScope(node) {
4080
+ scopes.push({ node, start: startOffset3(node), end: endOffset3(node), sawOwnershipCheck: false });
4081
+ }
4082
+ function innermostScope(pos) {
4083
+ let best;
4084
+ for (const scope of scopes) {
4085
+ if (pos < scope.start || pos > scope.end) continue;
4086
+ if (!best || scope.end - scope.start < best.end - best.start) best = scope;
4087
+ }
4088
+ return best;
4089
+ }
4090
+ return {
4091
+ FunctionDeclaration(node) {
4092
+ registerScope(node);
4093
+ },
4094
+ FunctionExpression(node) {
4095
+ registerScope(node);
4096
+ },
4097
+ ArrowFunctionExpression(node) {
4098
+ registerScope(node);
4099
+ },
4100
+ CallExpression(node) {
4101
+ const pos = startOffset3(node);
4102
+ const scope = innermostScope(pos);
4103
+ if (isOwnershipCheckCall(node)) {
4104
+ for (const s of scopes) {
4105
+ if (pos >= s.start && pos <= s.end) s.sawOwnershipCheck = true;
4106
+ }
4107
+ return;
4108
+ }
4109
+ if (isSessionsCall(node, "debug") || isSessionsRecordingCall(node, "retrieve")) {
4110
+ const sessionIdArg = (node.arguments ?? []).some(isSessionIdLikeArg);
4111
+ if (sessionIdArg) sensitiveCalls.push({ node, scope });
4112
+ }
4113
+ },
4114
+ "Program:exit"() {
4115
+ for (const { node, scope } of sensitiveCalls) {
4116
+ if (!scope || !scope.sawOwnershipCheck) {
4117
+ context.report({ node, messageId: "missingOwnershipCheck" });
4118
+ }
4119
+ }
4120
+ }
4121
+ };
4122
+ }
4123
+ };
4124
+ var browserbaseSessionIdRequiresOwnershipCheckRule = rule56;
4125
+
4126
+ // src/providers/browserbase/rules/no-concurrent-shared-context.ts
4127
+ function isPromiseAllCall2(node) {
4128
+ return node?.type === "CallExpression" && node.callee?.type === "MemberExpression" && !node.callee.computed && node.callee.object?.type === "Identifier" && node.callee.object.name === "Promise" && node.callee.property?.type === "Identifier" && node.callee.property.name === "all";
4129
+ }
4130
+ function isMapCall2(node) {
4131
+ return memberPropName3(node) === "map";
4132
+ }
4133
+ function callbackParamNames(callback) {
4134
+ const names = /* @__PURE__ */ new Set();
4135
+ const param = callback?.params?.[0];
4136
+ if (param?.type === "Identifier") {
4137
+ names.add(param.name);
4138
+ } else if (param?.type === "ObjectPattern") {
4139
+ for (const p of param.properties ?? []) {
4140
+ if (p?.type === "Property" && p.value?.type === "Identifier") names.add(p.value.name);
4141
+ else if (p?.type === "RestElement" && p.argument?.type === "Identifier") names.add(p.argument.name);
4142
+ }
4143
+ }
4144
+ return names;
4145
+ }
4146
+ function contextIdValueIsShared(callback, contextIdNode) {
4147
+ if (contextIdNode?.type !== "Identifier") return false;
4148
+ const params = callbackParamNames(callback);
4149
+ if (params.has(contextIdNode.name)) return false;
4150
+ return true;
4151
+ }
4152
+ function findSharedContextCreateCall(callback) {
4153
+ const body = callback?.body;
4154
+ if (!body) return null;
4155
+ let found = null;
4156
+ function visit(n) {
4157
+ if (found || !n || typeof n !== "object") return;
4158
+ if (Array.isArray(n)) {
4159
+ for (const item of n) visit(item);
4160
+ return;
4161
+ }
4162
+ if (typeof n.type !== "string") return;
4163
+ if (isSessionsCall(n, "create")) {
4164
+ const optsArg = n.arguments?.[0];
4165
+ const browserSettings = findProperty2(optsArg, "browserSettings")?.value;
4166
+ const ctxProp = findProperty2(browserSettings, "context")?.value;
4167
+ const idProp = findProperty2(ctxProp, "id");
4168
+ if (idProp && contextIdValueIsShared(callback, idProp.value)) {
4169
+ found = n;
4170
+ return;
4171
+ }
4172
+ }
4173
+ for (const key of Object.keys(n)) {
4174
+ if (key === "parent" || key === "loc" || key === "range") continue;
4175
+ visit(n[key]);
4176
+ }
4177
+ }
4178
+ visit(body);
4179
+ return found;
4180
+ }
4181
+ var rule57 = {
4182
+ meta: {
4183
+ type: "problem",
4184
+ docs: {
4185
+ description: "A Browserbase Context must not be attached to concurrent sessions",
4186
+ category: "correctness",
4187
+ 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",
4189
+ recommended: true
4190
+ },
4191
+ messages: {
4192
+ concurrentSharedContext: "The same browserSettings.context.id is passed into every concurrent sessions.create() call in this Promise.all batch. Sites may force a log out when 2+ sessions share a Context at once."
4193
+ },
4194
+ schema: []
4195
+ },
4196
+ create(context) {
4197
+ const mapCallsByVarName = /* @__PURE__ */ new Map();
4198
+ function checkMapCall(mapCallNode) {
4199
+ if (!isMapCall2(mapCallNode)) return;
4200
+ const callback = mapCallNode.arguments?.[mapCallNode.arguments.length - 1];
4201
+ if (!callback) return;
4202
+ const sharedCall = findSharedContextCreateCall(callback);
4203
+ if (sharedCall) {
4204
+ context.report({ node: sharedCall, messageId: "concurrentSharedContext" });
4205
+ }
4206
+ }
4207
+ return {
4208
+ VariableDeclarator(node) {
4209
+ if (node.id?.type === "Identifier" && isMapCall2(node.init)) {
4210
+ mapCallsByVarName.set(node.id.name, node.init);
4211
+ }
4212
+ },
4213
+ CallExpression(node) {
4214
+ if (!isPromiseAllCall2(node)) return;
4215
+ const arg = node.arguments?.[0];
4216
+ if (isMapCall2(arg)) {
4217
+ checkMapCall(arg);
4218
+ return;
4219
+ }
4220
+ if (arg?.type === "Identifier") {
4221
+ const mapCall = mapCallsByVarName.get(arg.name);
4222
+ if (mapCall) checkMapCall(mapCall);
4223
+ }
4224
+ }
4225
+ };
4226
+ }
4227
+ };
4228
+ var browserbaseNoConcurrentSharedContextRule = rule57;
4229
+
4230
+ // src/providers/browserbase/rules/mobile-device-requires-os-setting.ts
4231
+ function isMobileLiteral(node) {
4232
+ return node?.type === "Literal" && (node.value === "mobile" || node.value === "tablet");
4233
+ }
4234
+ function testComparesToMobile(test) {
4235
+ if (test?.type !== "BinaryExpression") return false;
4236
+ if (test.operator !== "===" && test.operator !== "==") return false;
4237
+ return isMobileLiteral(test.left) || isMobileLiteral(test.right);
4238
+ }
4239
+ function isViewportReference(node) {
4240
+ if (node?.type === "Identifier") return /viewport/i.test(node.name);
4241
+ if (node?.type === "MemberExpression" && !node.computed && node.property?.type === "Identifier") {
4242
+ return /viewport|width|height/i.test(node.property.name);
4243
+ }
4244
+ if (node?.type === "Property") {
4245
+ return node.key?.type === "Identifier" && /viewport|width|height/i.test(node.key.name);
4246
+ }
4247
+ return false;
4248
+ }
4249
+ function setsViewport(node) {
4250
+ return someDescendant2(node, isViewportReference);
4251
+ }
4252
+ function isOsMobileSetting(node) {
4253
+ if (node?.type === "Property" && node.key?.type === "Identifier" && node.key.name === "os" && isMobileLiteral(node.value)) {
4254
+ return true;
4255
+ }
4256
+ if (node?.type === "AssignmentExpression" && node.left?.type === "MemberExpression" && !node.left.computed && node.left.property?.type === "Identifier" && node.left.property.name === "os" && isMobileLiteral(node.right)) {
4257
+ return true;
4258
+ }
4259
+ return false;
4260
+ }
4261
+ var rule58 = {
4262
+ meta: {
4263
+ type: "problem",
4264
+ docs: {
4265
+ description: "Mobile device combos must set browserSettings.os, not just resize the viewport",
4266
+ category: "correctness",
4267
+ rationale: `The Node SDK's device emulation lever is browserSettings.os: 'mobile' | 'tablet' \u2014 there is no fingerprint API in this SDK. A "mobile" session that only resizes the Playwright viewport is still a desktop Chrome browser with a desktop user-agent string in a small window. Sites that branch behavior on UA/touch capability (the majority of responsive sites) never actually exercise their mobile code path, silently undermining "test on mobile" results.`,
4268
+ docsUrl: "https://docs.browserbase.com/features/stealth-mode",
4269
+ recommended: true
4270
+ },
4271
+ messages: {
4272
+ missingOsSetting: 'A "mobile" branch resizes the viewport but never sets browserSettings.os to "mobile"/"tablet". Without it this is still a desktop browser in a small window.'
4273
+ },
4274
+ schema: []
4275
+ },
4276
+ create(context) {
4277
+ const mobileViewportBranches = [];
4278
+ let sawOsMobileSetting = false;
4279
+ return {
4280
+ IfStatement(node) {
4281
+ if (testComparesToMobile(node.test) && setsViewport(node.consequent)) {
4282
+ mobileViewportBranches.push(node);
4283
+ }
4284
+ },
4285
+ Property(node) {
4286
+ if (isOsMobileSetting(node)) sawOsMobileSetting = true;
4287
+ },
4288
+ AssignmentExpression(node) {
4289
+ if (isOsMobileSetting(node)) sawOsMobileSetting = true;
4290
+ },
4291
+ "Program:exit"() {
4292
+ if (sawOsMobileSetting) return;
4293
+ for (const branch of mobileViewportBranches) {
4294
+ context.report({ node: branch, messageId: "missingOsSetting" });
4295
+ }
4296
+ }
4297
+ };
4298
+ }
4299
+ };
4300
+ var browserbaseMobileDeviceRequiresOsSettingRule = rule58;
4301
+
4302
+ // src/providers/browserbase/rules/use-typed-exception-status-not-substring.ts
4303
+ function isStringifiedErrorSource(node, errName) {
4304
+ if (node?.type === "CallExpression" && node.callee?.type === "Identifier" && node.callee.name === "String") {
4305
+ const arg = node.arguments?.[0];
4306
+ return arg?.type === "Identifier" && arg.name === errName;
4307
+ }
4308
+ if (node?.type === "CallExpression" && node.callee?.type === "MemberExpression") {
4309
+ const obj = node.callee.object;
4310
+ const prop = node.callee.property;
4311
+ if (obj?.type === "Identifier" && obj.name === errName && prop?.type === "Identifier" && prop.name === "toString") {
4312
+ return true;
4313
+ }
4314
+ }
4315
+ if (node?.type === "MemberExpression" && !node.computed) {
4316
+ const obj = node.object;
4317
+ const prop = node.property;
4318
+ if (obj?.type === "Identifier" && obj.name === errName && prop?.type === "Identifier" && prop.name === "message") {
4319
+ return true;
4320
+ }
4321
+ }
4322
+ return false;
4323
+ }
4324
+ function unwrapToLowerCase(node) {
4325
+ if (node?.type === "CallExpression" && node.callee?.type === "MemberExpression" && node.callee.property?.type === "Identifier" && node.callee.property.name === "toLowerCase") {
4326
+ return node.callee.object;
4327
+ }
4328
+ return node;
4329
+ }
4330
+ function usesTypedStatus(body, errName) {
4331
+ return someDescendant2(body, (n) => {
4332
+ if (n.type === "MemberExpression" && !n.computed && n.object?.type === "Identifier" && n.object.name === errName) {
4333
+ return n.property?.type === "Identifier" && n.property.name === "status";
4334
+ }
4335
+ if (n.type === "BinaryExpression" && n.operator === "instanceof" && n.left?.type === "Identifier" && n.left.name === errName) {
4336
+ return true;
4337
+ }
4338
+ return false;
4339
+ });
4340
+ }
4341
+ function usesSubstringMatch(body, errName, stringifiedVars) {
4342
+ return someDescendant2(body, (n) => {
4343
+ if (n.type !== "CallExpression") return false;
4344
+ if (n.callee?.type !== "MemberExpression" || n.callee.computed) return false;
4345
+ if (n.callee.property?.type !== "Identifier" || n.callee.property.name !== "includes") return false;
4346
+ const source = unwrapToLowerCase(n.callee.object);
4347
+ if (isStringifiedErrorSource(source, errName)) return true;
4348
+ return source?.type === "Identifier" && stringifiedVars.has(source.name);
4349
+ });
4350
+ }
4351
+ function collectStringifiedVars(body, errName) {
4352
+ const names = /* @__PURE__ */ new Set();
4353
+ someDescendant2(body, (n) => {
4354
+ if (n.type === "VariableDeclarator" && n.id?.type === "Identifier" && n.init) {
4355
+ const source = unwrapToLowerCase(n.init);
4356
+ if (isStringifiedErrorSource(source, errName)) names.add(n.id.name);
4357
+ }
4358
+ return false;
4359
+ });
4360
+ return names;
4361
+ }
4362
+ var rule59 = {
4363
+ meta: {
4364
+ type: "suggestion",
4365
+ docs: {
4366
+ description: "Branch on err.status, not a substring match of the stringified error",
4367
+ category: "correctness",
4368
+ rationale: 'The SDK raises typed Browserbase.APIError (and subclasses like NotFoundError) carrying a real .status: number and .headers. Matching err.message or String(err) against substrings like "410" or "Session stopped" is fragile \u2014 that human-readable text is an unversioned implementation detail the provider can change at any time without notice. Check err.status (or `instanceof`) instead.',
4369
+ docsUrl: "https://docs.browserbase.com/reference/api/session-live-urls",
4370
+ recommended: true
4371
+ },
4372
+ messages: {
4373
+ substringStatusMatch: "This catch block matches a substring of the stringified error instead of checking err.status or `instanceof Browserbase.APIError`."
4374
+ },
4375
+ schema: []
4376
+ },
4377
+ create(context) {
4378
+ return {
4379
+ CatchClause(node) {
4380
+ const param = node.param;
4381
+ const errName = param?.type === "Identifier" ? param.name : void 0;
4382
+ if (!errName || !node.body) return;
4383
+ if (usesTypedStatus(node.body, errName)) return;
4384
+ const stringifiedVars = collectStringifiedVars(node.body, errName);
4385
+ if (usesSubstringMatch(node.body, errName, stringifiedVars)) {
4386
+ context.report({ node, messageId: "substringStatusMatch" });
4387
+ }
4388
+ }
4389
+ };
4390
+ }
4391
+ };
4392
+ var browserbaseUseTypedExceptionStatusNotSubstringRule = rule59;
4393
+
4394
+ // src/providers/browserbase/rules/release-session-on-connect-failure.ts
4395
+ function isSessionsCreateAwait(node) {
4396
+ return node?.type === "AwaitExpression" && isSessionsCall(node.argument, "create");
4397
+ }
4398
+ function isConnectCall(node) {
4399
+ if (node?.type !== "CallExpression") return false;
4400
+ const callee = node.callee;
4401
+ const name = callee?.type === "Identifier" ? callee.name : memberPropName3(node);
4402
+ return !!name && /connect/i.test(name);
4403
+ }
4404
+ function isReleaseCall(node) {
4405
+ if (node?.type !== "CallExpression") return false;
4406
+ if (isSessionsCall(node, "update")) {
4407
+ const optsArg = node.arguments?.[1];
4408
+ const statusProp = findProperty2(optsArg, "status");
4409
+ if (statusProp?.value?.type === "Literal" && statusProp.value.value === "REQUEST_RELEASE") return true;
4410
+ }
4411
+ const callee = node.callee;
4412
+ const name = callee?.type === "Identifier" ? callee.name : memberPropName3(node);
4413
+ return !!name && /requeststop|releasesession|release_session/i.test(name);
4414
+ }
4415
+ var rule60 = {
4416
+ meta: {
4417
+ type: "problem",
4418
+ docs: {
4419
+ description: "A failed connect after session creation must release the session",
4420
+ category: "reliability",
4421
+ rationale: `If the CDP connect call raises after sessions.create() already succeeded on Browserbase's side, the session stays RUNNING and only terminates once its timeout elapses (up to 6h) unless something explicitly calls sessions.update(id, { status: 'REQUEST_RELEASE' }). The SDK's own docs warn to do this before the timeout "to avoid additional charges." A connect call with no try/catch (or a catch that doesn't release) leaks the session on every connect failure.`,
4422
+ docsUrl: "https://docs.browserbase.com/reference/api/update-a-session",
4423
+ recommended: true
4424
+ },
4425
+ messages: {
4426
+ sessionNotReleasedOnFailure: 'A session was created but this connect call has no catch that releases it (sessions.update with status: "REQUEST_RELEASE") on failure. A failed connect will leak the session until its timeout.'
4427
+ },
4428
+ schema: []
4429
+ },
4430
+ create(context) {
4431
+ let sawSessionCreate = false;
4432
+ const connectCalls = [];
4433
+ const tryStatements = [];
4434
+ return {
4435
+ AwaitExpression(node) {
4436
+ if (isSessionsCreateAwait(node)) sawSessionCreate = true;
4437
+ },
4438
+ TryStatement(node) {
4439
+ tryStatements.push(node);
4440
+ },
4441
+ CallExpression(node) {
4442
+ if (isConnectCall(node)) connectCalls.push(node);
4443
+ },
4444
+ "Program:exit"() {
4445
+ if (!sawSessionCreate) return;
4446
+ for (const call of connectCalls) {
4447
+ const enclosingTry = tryStatements.find((t) => t.block && contains3(t.block, call));
4448
+ const handled = !!enclosingTry?.handler && someDescendant2(enclosingTry.handler, isReleaseCall);
4449
+ if (!handled) {
4450
+ context.report({ node: call, messageId: "sessionNotReleasedOnFailure" });
4451
+ }
4452
+ }
4453
+ }
4454
+ };
4455
+ }
4456
+ };
4457
+ var browserbaseReleaseSessionOnConnectFailureRule = rule60;
4458
+
4459
+ // src/providers/browserbase/rules/dont-stack-custom-retry-on-sdk-retry.ts
4460
+ var rule61 = {
4461
+ meta: {
4462
+ type: "suggestion",
4463
+ docs: {
4464
+ description: "A custom retry loop around sessions.create must disable the SDK's own retry",
4465
+ category: "reliability",
4466
+ rationale: "The Browserbase client already retries internally (default maxRetries: 2, retrying 408/409/429/5xx with Retry-After-aware exponential backoff) before ever raising an exception back to the caller. A hand-rolled retry loop around sessions.create() that does not pass { maxRetries: 0 } stacks two independent, uncoordinated backoff schedules \u2014 a sustained 429 can trigger N outer attempts x (1 + 2 SDK-internal retries), multiplying worst-case latency well beyond what either layer alone would produce.",
4467
+ docsUrl: "https://docs.browserbase.com/optimizations/concurrency/overview",
4468
+ recommended: true
4469
+ },
4470
+ messages: {
4471
+ stackedRetry: "sessions.create() is called inside a custom retry loop with no { maxRetries: 0 } override \u2014 the SDK retries this call internally too, stacking two backoff schedules."
4472
+ },
4473
+ schema: []
4474
+ },
4475
+ create(context) {
4476
+ const loopRanges = [];
4477
+ return {
4478
+ ForStatement(node) {
4479
+ loopRanges.push(node);
4480
+ },
4481
+ WhileStatement(node) {
4482
+ loopRanges.push(node);
4483
+ },
4484
+ DoWhileStatement(node) {
4485
+ loopRanges.push(node);
4486
+ },
4487
+ CallExpression(node) {
4488
+ if (!isSessionsCall(node, "create")) return;
4489
+ const insideLoop = loopRanges.some((loop) => {
4490
+ const start = loop.range?.[0] ?? loop.start;
4491
+ const end = loop.range?.[1] ?? loop.end;
4492
+ const pos = node.range?.[0] ?? node.start;
4493
+ return pos >= start && pos <= end;
4494
+ });
4495
+ if (!insideLoop) return;
4496
+ const optsArg = node.arguments?.[1];
4497
+ const hasMaxRetries = optsArg?.type === "ObjectExpression" && !!findProperty2(optsArg, "maxRetries");
4498
+ if (!hasMaxRetries) {
4499
+ context.report({ node, messageId: "stackedRetry" });
4500
+ }
4501
+ }
4502
+ };
4503
+ }
4504
+ };
4505
+ var browserbaseDontStackCustomRetryOnSdkRetryRule = rule61;
4506
+
4507
+ // src/providers/browserbase/rules/no-overbroad-error-substring-match.ts
4508
+ var OVERBROAD_TERMS = /* @__PURE__ */ new Set(["session", "context", "browser", "browserbase"]);
4509
+ function collectIncludesLiterals(test, literals) {
4510
+ if (!test) return;
4511
+ if (test.type === "LogicalExpression" && test.operator === "||") {
4512
+ collectIncludesLiterals(test.left, literals);
4513
+ collectIncludesLiterals(test.right, literals);
4514
+ return;
4515
+ }
4516
+ if (test.type === "CallExpression" && memberPropName3(test) === "includes") {
4517
+ const arg = test.arguments?.[0];
4518
+ if (arg?.type === "Literal" && typeof arg.value === "string") literals.push(arg.value.toLowerCase());
4519
+ }
4520
+ }
4521
+ function isCleanupCall(node) {
4522
+ if (node?.type !== "CallExpression") return false;
4523
+ const callee = node.callee;
4524
+ const name = callee?.type === "Identifier" ? callee.name : memberPropName3(node);
4525
+ return !!name && /release|remove|cleanup|stop|teardown/i.test(name);
4526
+ }
4527
+ var rule62 = {
4528
+ meta: {
4529
+ type: "problem",
4530
+ docs: {
4531
+ description: "Error-message checks must not OR in a single generic resource-name substring",
4532
+ category: "reliability",
4533
+ rationale: 'A condition like `err.includes("not found") || err.includes("404") || err.includes("session")` looks like it is narrowing to a confirmed session-ended error, but the last clause matches a single generic word \u2014 the name of the resource being polled. Virtually any exception raised from a sessions.* call (a connection reset, an SSL error, a generic timeout that echoes the request URL) is likely to contain that substring too. A coincidental match then triggers the same destructive cleanup as a real 404/410, tearing down a healthy session on a transient blip.',
4534
+ docsUrl: "https://docs.browserbase.com/reference/api/session-live-urls",
4535
+ recommended: true
4536
+ },
4537
+ messages: {
4538
+ overbroadMatch: 'This OR-chain includes a generic substring check ("{{term}}") that matches almost any error from a sessions.* call, not just a confirmed session-ended response.'
4539
+ },
4540
+ schema: []
4541
+ },
4542
+ create(context) {
4543
+ return {
4544
+ IfStatement(node) {
4545
+ const literals = [];
4546
+ collectIncludesLiterals(node.test, literals);
4547
+ if (literals.length < 2) return;
4548
+ const overbroad = literals.find((l) => OVERBROAD_TERMS.has(l));
4549
+ if (!overbroad) return;
4550
+ if (someDescendant2(node.consequent, isCleanupCall)) {
4551
+ context.report({ node, messageId: "overbroadMatch", data: { term: overbroad } });
4552
+ }
4553
+ }
4554
+ };
4555
+ }
4556
+ };
4557
+ var browserbaseNoOverbroadErrorSubstringMatchRule = rule62;
4558
+
4559
+ // src/providers/browserbase/rules/use-sdk-not-raw-requests.ts
4560
+ var BROWSERBASE_HOST = "api.browserbase.com";
4561
+ function templateLiteralContainsHost(node) {
4562
+ return (node.quasis ?? []).some((q) => {
4563
+ const text = q?.value?.cooked ?? q?.value?.raw ?? "";
4564
+ return typeof text === "string" && text.includes(BROWSERBASE_HOST);
4565
+ });
4566
+ }
4567
+ function urlArgContainsHost(node) {
4568
+ if (node?.type === "Literal" && typeof node.value === "string") return node.value.includes(BROWSERBASE_HOST);
4569
+ if (node?.type === "TemplateLiteral") return templateLiteralContainsHost(node);
4570
+ return false;
4571
+ }
4572
+ function isRawHttpCall(node) {
4573
+ if (node?.type !== "CallExpression") return { isCall: false, urlArg: void 0 };
4574
+ const callee = node.callee;
4575
+ if (callee?.type === "Identifier" && callee.name === "fetch") {
4576
+ return { isCall: true, urlArg: node.arguments?.[0] };
4577
+ }
4578
+ if (callee?.type === "MemberExpression" && !callee.computed && callee.property?.type === "Identifier") {
4579
+ const objName = callee.object?.type === "Identifier" ? callee.object.name : void 0;
4580
+ const methodName = callee.property.name;
4581
+ if (objName && /^(axios|http|https)$/i.test(objName) && /^(get|post|put|patch|delete|request)$/i.test(methodName)) {
4582
+ return { isCall: true, urlArg: node.arguments?.[0] };
4583
+ }
4584
+ }
4585
+ if (callee?.type === "Identifier" && callee.name === "axios") {
4586
+ const arg = node.arguments?.[0];
4587
+ if (arg?.type === "ObjectExpression") {
4588
+ const urlProp = arg.properties?.find(
4589
+ (p) => p?.type === "Property" && p.key?.type === "Identifier" && p.key.name === "url"
4590
+ );
4591
+ return { isCall: true, urlArg: urlProp?.value };
4592
+ }
4593
+ return { isCall: true, urlArg: arg };
4594
+ }
4595
+ return { isCall: false, urlArg: void 0 };
4596
+ }
4597
+ var rule63 = {
4598
+ meta: {
4599
+ type: "suggestion",
4600
+ docs: {
4601
+ description: "Use the installed Browserbase SDK instead of hand-rolled HTTP requests",
4602
+ category: "integration",
4603
+ rationale: "A raw fetch/axios/http call to an api.browserbase.com endpoint duplicates a method the installed SDK already exposes (e.g. sessions.recording.retrieve()), forfeiting its typed exceptions, built-in retry/backoff, and consistent error handling \u2014 and risks drifting on auth header naming or response shape as the API evolves.",
4604
+ docsUrl: "https://docs.browserbase.com/reference/sdk/nodejs",
4605
+ recommended: true
4606
+ },
4607
+ messages: {
4608
+ rawRequestToBrowserbase: "This makes a raw HTTP request to a Browserbase API endpoint instead of using the installed SDK."
4609
+ },
4610
+ schema: []
4611
+ },
4612
+ create(context) {
4613
+ const urlVars = /* @__PURE__ */ new Map();
4614
+ return {
4615
+ VariableDeclarator(node) {
4616
+ if (node.id?.type === "Identifier" && urlArgContainsHost(node.init)) {
4617
+ urlVars.set(node.id.name, node.init);
4618
+ }
4619
+ },
4620
+ CallExpression(node) {
4621
+ const { isCall, urlArg } = isRawHttpCall(node);
4622
+ if (!isCall || !urlArg) return;
4623
+ const resolved = urlArg.type === "Identifier" ? urlVars.get(urlArg.name) ?? urlArg : urlArg;
4624
+ if (urlArgContainsHost(resolved)) {
4625
+ context.report({ node, messageId: "rawRequestToBrowserbase" });
4626
+ }
4627
+ }
4628
+ };
4629
+ }
4630
+ };
4631
+ var browserbaseUseSdkNotRawRequestsRule = rule63;
4632
+
4633
+ // src/providers/browserbase/rules/centralize-request-release.ts
4634
+ var rule64 = {
4635
+ meta: {
4636
+ type: "suggestion",
4637
+ docs: {
4638
+ description: "Route REQUEST_RELEASE through a single shared abstraction",
4639
+ category: "integration",
4640
+ rationale: 'sessions.update(id, { status: "REQUEST_RELEASE" }) hand-rolled inline at each call site, with each one carrying slightly different error handling, means an API change (a new required field, a renamed status value) requires fixing every call site instead of one \u2014 and the duplicated copies already drift in error-handling quality. Centralize behind one designated provider method (e.g. requestStop()/releaseSession()) and call that everywhere instead.',
4641
+ docsUrl: "https://docs.browserbase.com/reference/api/update-a-session",
4642
+ recommended: true
4643
+ },
4644
+ messages: {
4645
+ inlineRequestRelease: 'sessions.update(..., { status: "REQUEST_RELEASE" }) is hand-rolled inline here. Route this through a single shared release/provider abstraction instead.'
4646
+ },
4647
+ schema: []
4648
+ },
4649
+ create(context) {
4650
+ const filename = String(context.filename ?? "");
4651
+ if (isInsideTestFile3(filename) || /provider|browserbaseclient/i.test(filename)) {
4652
+ return {};
4653
+ }
4654
+ return {
4655
+ CallExpression(node) {
4656
+ if (!isSessionsCall(node, "update")) return;
4657
+ const optsArg = node.arguments?.[1];
4658
+ const statusProp = findProperty2(optsArg, "status");
4659
+ const val = statusProp?.value;
4660
+ if (val?.type === "Literal" && val.value === "REQUEST_RELEASE") {
4661
+ context.report({ node, messageId: "inlineRequestRelease" });
4662
+ }
4663
+ }
4664
+ };
4665
+ }
4666
+ };
4667
+ var browserbaseCentralizeRequestReleaseRule = rule64;
4668
+
4669
+ // src/providers/openai-cua/rules/no-domain-allowlist.ts
4670
+ var ACTION_METHOD_NAMES = /* @__PURE__ */ new Set(["click", "type", "press", "move", "goto", "fill", "dblclick", "dragAndDrop"]);
4671
+ var rule65 = {
4672
+ meta: {
4673
+ type: "problem",
4674
+ docs: {
4675
+ description: "Computer-use action execution must check the page origin against an allowlist",
4676
+ category: "security",
4677
+ cwe: "CWE-284",
4678
+ owasp: "A01:2021 \u2013 Broken Access Control",
4679
+ rationale: `OpenAI's Computer Use guide instructs integrators to "keep an allow list of domains and actions your agent should use, and block everything else." Without an origin check before executing actions, a click that follows an off-domain redirect (phishing link, ad, malicious iframe) is executed exactly like any in-domain click \u2014 and a field-fill action has no origin awareness either, so credentials could be typed into a page the agent was never meant to reach.`,
4680
+ docsUrl: "https://developers.openai.com/api/docs/guides/tools-computer-use",
4681
+ recommended: true
4682
+ },
4683
+ messages: {
4684
+ noOriginCheck: "This function executes computer-use actions on the current page with no origin/domain allowlist check anywhere in it."
4685
+ }
4686
+ },
4687
+ create(context) {
4688
+ const stack = [];
4689
+ const states = /* @__PURE__ */ new Map();
4690
+ function ensureState(fn) {
4691
+ let s = states.get(fn);
4692
+ if (!s) {
4693
+ s = { sawAction: false, actionNode: null, sawOriginCheck: false };
4694
+ states.set(fn, s);
4695
+ }
4696
+ return s;
4697
+ }
4698
+ function top() {
4699
+ return stack[stack.length - 1];
4700
+ }
4701
+ function pushScope(node) {
4702
+ stack.push(node);
4703
+ ensureState(node);
4704
+ }
4705
+ function popScope() {
4706
+ stack.pop();
4707
+ }
4708
+ function propName2(node) {
4709
+ if (!node) return void 0;
4710
+ if (node.type === "Identifier") return node.name;
4711
+ if (node.type === "Literal" && typeof node.value === "string") return node.value;
4712
+ return void 0;
4713
+ }
4714
+ function memberChainNames2(node, names = []) {
4715
+ if (node?.type === "MemberExpression") {
4716
+ memberChainNames2(node.object, names);
4717
+ const n = propName2(node.property);
4718
+ if (n) names.push(n);
4719
+ } else if (node?.type === "Identifier") {
4720
+ names.push(node.name);
4721
+ }
4722
+ return names;
4723
+ }
4724
+ function isPageActionCall(node) {
4725
+ if (node?.type !== "CallExpression") return false;
4726
+ if (node.callee?.type !== "MemberExpression") return false;
4727
+ const chain = memberChainNames2(node.callee);
4728
+ if (chain.length === 0) return false;
4729
+ const last = chain[chain.length - 1];
4730
+ if (!ACTION_METHOD_NAMES.has(last)) return false;
4731
+ return chain.some((n) => /^page$/i.test(n) || /page$/i.test(n));
4732
+ }
4733
+ function markOriginCheckSeen() {
4734
+ const fn = top();
4735
+ if (fn) ensureState(fn).sawOriginCheck = true;
4736
+ }
4737
+ return {
4738
+ Program(node) {
4739
+ pushScope(node);
4740
+ },
4741
+ "Program:exit"() {
4742
+ for (const state of states.values()) {
4743
+ if (state.sawAction && !state.sawOriginCheck) {
4744
+ context.report({ node: state.actionNode, messageId: "noOriginCheck" });
4745
+ }
4746
+ }
4747
+ },
4748
+ FunctionDeclaration(node) {
4749
+ pushScope(node);
4750
+ },
4751
+ "FunctionDeclaration:exit"() {
4752
+ popScope();
4753
+ },
4754
+ FunctionExpression(node) {
4755
+ pushScope(node);
4756
+ },
4757
+ "FunctionExpression:exit"() {
4758
+ popScope();
4759
+ },
4760
+ ArrowFunctionExpression(node) {
4761
+ pushScope(node);
4762
+ },
4763
+ "ArrowFunctionExpression:exit"() {
4764
+ popScope();
4765
+ },
4766
+ CallExpression(node) {
4767
+ if (isPageActionCall(node)) {
4768
+ const fn = top();
4769
+ if (fn) {
4770
+ const state = ensureState(fn);
4771
+ if (!state.sawAction) {
4772
+ state.sawAction = true;
4773
+ state.actionNode = node;
4774
+ }
4775
+ }
4776
+ }
4777
+ },
4778
+ NewExpression(node) {
4779
+ if (node.callee?.type === "Identifier" && node.callee.name === "URL") {
4780
+ markOriginCheckSeen();
4781
+ }
4782
+ },
4783
+ MemberExpression(node) {
4784
+ const name = propName2(node.property);
4785
+ if (name === "hostname" || name === "origin") {
4786
+ markOriginCheckSeen();
4787
+ }
4788
+ },
4789
+ Identifier(node) {
4790
+ if (/allow.?list/i.test(node.name) || /allowed.?domain/i.test(node.name)) {
4791
+ markOriginCheckSeen();
4792
+ }
4793
+ }
4794
+ };
4795
+ }
4796
+ };
4797
+ var openaiCuaNoDomainAllowlistRule = rule65;
4798
+
4799
+ // src/providers/openai-cua/rules/scroll-delta-default-zero.ts
4800
+ var VERTICAL_DELTA_NAME_PATTERN = /^(dy|delta_?y|scroll_?y)$/i;
4801
+ var rule66 = {
4802
+ meta: {
4803
+ type: "problem",
4804
+ docs: {
4805
+ description: "A missing vertical scroll delta must default to 0, not an arbitrary non-zero value",
4806
+ category: "correctness",
4807
+ rationale: "The reference scroll handler in OpenAI's Computer Use guide defaults a missing scroll delta to 0 on both axes. Defaulting the vertical delta to any other value injects an unintended scroll the model never asked for whenever it omits that field, desyncing the model's mental model of the page from the page's actual state on the next turn.",
4808
+ docsUrl: "https://developers.openai.com/api/docs/guides/tools-computer-use",
4809
+ recommended: true
4810
+ },
4811
+ messages: {
4812
+ nonZeroDefault: "A missing vertical scroll delta defaults to {{value}} here instead of 0, injecting an unintended scroll the model never requested."
4813
+ }
4814
+ },
4815
+ create(context) {
4816
+ function propName2(node) {
4817
+ if (!node) return void 0;
4818
+ if (node.type === "Identifier") return node.name;
4819
+ if (node.type === "Literal" && typeof node.value === "string") return node.value;
4820
+ return void 0;
4821
+ }
4822
+ function targetName(node) {
4823
+ if (node?.type === "Identifier") return node.name;
4824
+ if (node?.type === "MemberExpression") return propName2(node.property);
4825
+ return void 0;
4826
+ }
4827
+ function isNonZeroNumberLiteral(node) {
4828
+ return node?.type === "Literal" && typeof node.value === "number" && node.value !== 0;
4829
+ }
4830
+ function reportIfBadDefault(targetNode, valueNode, reportNode) {
4831
+ const name = targetName(targetNode);
4832
+ if (!name || !VERTICAL_DELTA_NAME_PATTERN.test(name)) return;
4833
+ if (!isNonZeroNumberLiteral(valueNode)) return;
4834
+ context.report({ node: reportNode, messageId: "nonZeroDefault", data: { value: String(valueNode.value) } });
4835
+ }
4836
+ function findDirectAssignment(stmt) {
4837
+ let inner = stmt;
4838
+ if (inner?.type === "BlockStatement") {
4839
+ const exprStmts = (inner.body ?? []).filter((s) => s.type === "ExpressionStatement");
4840
+ if (exprStmts.length !== 1) return void 0;
4841
+ inner = exprStmts[0];
4842
+ }
4843
+ if (inner?.type !== "ExpressionStatement") return void 0;
4844
+ const expr = inner.expression;
4845
+ if (expr?.type !== "AssignmentExpression" || expr.operator !== "=") return void 0;
4846
+ return { target: expr.left, value: expr.right };
4847
+ }
4848
+ function undefinedCheckTargetName(test) {
4849
+ if (test?.type !== "BinaryExpression") return void 0;
4850
+ if (test.operator !== "===" && test.operator !== "==") return void 0;
4851
+ const sides = [test.left, test.right];
4852
+ const undefinedSide = sides.find(
4853
+ (s) => s?.type === "Identifier" && s.name === "undefined" || s?.type === "Literal" && s.value === null || s?.type === "UnaryExpression" && s.operator === "typeof"
4854
+ );
4855
+ const otherSide = sides.find((s) => s !== undefinedSide);
4856
+ if (!undefinedSide || !otherSide) return void 0;
4857
+ if (undefinedSide.type === "UnaryExpression") {
4858
+ return targetName(undefinedSide.argument);
4859
+ }
4860
+ return targetName(otherSide);
4861
+ }
4862
+ return {
4863
+ LogicalExpression(node) {
4864
+ if (node.operator !== "??") return;
4865
+ reportIfBadDefault(node.left, node.right, node);
4866
+ },
4867
+ IfStatement(node) {
4868
+ const name = undefinedCheckTargetName(node.test);
4869
+ if (!name) return;
4870
+ const assignment = findDirectAssignment(node.consequent);
4871
+ if (!assignment) return;
4872
+ reportIfBadDefault(assignment.target, assignment.value, node);
4873
+ }
4874
+ };
4875
+ }
4876
+ };
4877
+ var openaiCuaScrollDeltaDefaultZeroRule = rule66;
4878
+
4879
+ // src/providers/openai-cua/rules/structured-step-metadata-not-text-json.ts
4880
+ var BRACE_SEARCH_METHODS = /* @__PURE__ */ new Set(["indexOf", "lastIndexOf"]);
4881
+ var SLICE_METHODS = /* @__PURE__ */ new Set(["slice", "substring", "substr"]);
4882
+ var rule67 = {
4883
+ meta: {
4884
+ type: "suggestion",
4885
+ docs: {
4886
+ description: "Step metadata must come from structured tool output, not brace-hunting in free text",
4887
+ category: "correctness",
4888
+ rationale: "Manually locating a JSON object inside a free-text model message (via indexOf/lastIndexOf plus a slice) is fragile by construction: it breaks if the model adds trailing commentary, wraps the JSON in a markdown fence, or reorders fields. The Responses API supports function tools and structured output specifically so required metadata is schema-validated by the API instead of regex/brace-scraped out of arbitrary text.",
4889
+ docsUrl: "https://developers.openai.com/api/docs/guides/tools-computer-use",
4890
+ recommended: true
4891
+ },
4892
+ messages: {
4893
+ textJsonExtraction: "This function locates a JSON object in free text via indexOf/lastIndexOf and parses a slice of it \u2014 use a function tool or structured output instead of brace-hunting in the model's text message."
4894
+ }
4895
+ },
4896
+ create(context) {
4897
+ const stack = [];
4898
+ const states = /* @__PURE__ */ new Map();
4899
+ function ensureState(fn) {
4900
+ let s = states.get(fn);
4901
+ if (!s) {
4902
+ s = { sawBraceSearch: false, sawJsonParseOnSlice: false, reportNode: null };
4903
+ states.set(fn, s);
4904
+ }
4905
+ return s;
4906
+ }
4907
+ function top() {
4908
+ return stack[stack.length - 1];
4909
+ }
4910
+ function pushScope(node) {
4911
+ stack.push(node);
4912
+ ensureState(node);
4913
+ }
4914
+ function popScope() {
4915
+ stack.pop();
4916
+ }
4917
+ function isBraceSearchCall(node) {
4918
+ if (node?.type !== "CallExpression") return false;
4919
+ const callee = node.callee;
4920
+ if (callee?.type !== "MemberExpression") return false;
4921
+ if (!BRACE_SEARCH_METHODS.has(callee.property?.name)) return false;
4922
+ const arg = node.arguments?.[0];
4923
+ return arg?.type === "Literal" && typeof arg.value === "string" && arg.value.includes("{");
4924
+ }
4925
+ function isJsonParseOnSliceCall(node) {
4926
+ if (node?.type !== "CallExpression") return false;
4927
+ const callee = node.callee;
4928
+ if (callee?.type !== "MemberExpression") return false;
4929
+ if (callee.object?.type !== "Identifier" || callee.object.name !== "JSON") return false;
4930
+ if (callee.property?.name !== "parse") return false;
4931
+ const arg = node.arguments?.[0];
4932
+ if (arg?.type !== "CallExpression") return false;
4933
+ const argCallee = arg.callee;
4934
+ return argCallee?.type === "MemberExpression" && SLICE_METHODS.has(argCallee.property?.name);
4935
+ }
4936
+ return {
4937
+ Program(node) {
4938
+ pushScope(node);
4939
+ },
4940
+ "Program:exit"() {
4941
+ for (const state of states.values()) {
4942
+ if (state.sawBraceSearch && state.sawJsonParseOnSlice) {
4943
+ context.report({ node: state.reportNode, messageId: "textJsonExtraction" });
4944
+ }
4945
+ }
4946
+ },
4947
+ FunctionDeclaration(node) {
4948
+ pushScope(node);
4949
+ },
4950
+ "FunctionDeclaration:exit"() {
4951
+ popScope();
4952
+ },
4953
+ FunctionExpression(node) {
4954
+ pushScope(node);
4955
+ },
4956
+ "FunctionExpression:exit"() {
4957
+ popScope();
4958
+ },
4959
+ ArrowFunctionExpression(node) {
4960
+ pushScope(node);
4961
+ },
4962
+ "ArrowFunctionExpression:exit"() {
4963
+ popScope();
4964
+ },
4965
+ CallExpression(node) {
4966
+ const fn = top();
4967
+ if (!fn) return;
4968
+ const state = ensureState(fn);
4969
+ if (isBraceSearchCall(node)) {
4970
+ state.sawBraceSearch = true;
4971
+ if (!state.reportNode) state.reportNode = node;
4972
+ }
4973
+ if (isJsonParseOnSliceCall(node)) {
4974
+ state.sawJsonParseOnSlice = true;
4975
+ if (!state.reportNode) state.reportNode = node;
4976
+ }
4977
+ }
4978
+ };
4979
+ }
4980
+ };
4981
+ var openaiCuaStructuredStepMetadataNotTextJsonRule = rule67;
4982
+
4983
+ // src/providers/openai-cua/rules/no-blind-safety-check-ack.ts
4984
+ var rule68 = {
4985
+ meta: {
4986
+ type: "suggestion",
4987
+ docs: {
4988
+ description: "Acknowledging pending safety checks must evaluate each check, not blanket-pass them",
4989
+ category: "correctness",
4990
+ rationale: "`acknowledged_safety_checks` exists specifically so a developer can selectively confirm checks they have actually reviewed \u2014 each entry carries an id/code/message. A filter that only checks the row's shape (e.g. that it is an object) and never inspects `.code` or `.message` acknowledges every check present, defeating the purpose of the field entirely.",
4991
+ docsUrl: "https://developers.openai.com/api/docs/guides/tools-computer-use",
4992
+ recommended: true
4993
+ },
4994
+ messages: {
4995
+ blindAck: "This filter never inspects .code or .message on each safety check \u2014 it acknowledges every check present instead of evaluating it against a policy."
4996
+ }
4997
+ },
4998
+ create(context) {
4999
+ function propName2(node) {
5000
+ if (!node) return void 0;
5001
+ if (node.type === "Identifier") return node.name;
5002
+ if (node.type === "Literal" && typeof node.value === "string") return node.value;
5003
+ return void 0;
5004
+ }
5005
+ function sourceLooksLikeSafetyChecks(node) {
5006
+ if (!node) return false;
5007
+ const name = propName2(node) ?? (node.type === "MemberExpression" ? propName2(node.property) : void 0);
5008
+ if (name && /safety.?check/i.test(name)) return true;
5009
+ if (node.type === "LogicalExpression") {
5010
+ return sourceLooksLikeSafetyChecks(node.left) || sourceLooksLikeSafetyChecks(node.right);
5011
+ }
5012
+ if (node.type === "MemberExpression") {
5013
+ return sourceLooksLikeSafetyChecks(node.object) || sourceLooksLikeSafetyChecks(node.property);
5014
+ }
5015
+ return false;
5016
+ }
5017
+ function referencesCodeOrMessage(node, depth = 0) {
5018
+ if (!node || typeof node !== "object" || depth > 10) return false;
5019
+ if (Array.isArray(node)) return node.some((n) => referencesCodeOrMessage(n, depth + 1));
5020
+ if (node.type === "MemberExpression") {
5021
+ const name = propName2(node.property);
5022
+ if (name === "code" || name === "message") return true;
5023
+ }
5024
+ for (const key of Object.keys(node)) {
5025
+ if (key === "parent" || key === "loc" || key === "range") continue;
5026
+ const val = node[key];
5027
+ if (val && typeof val === "object") {
5028
+ if (referencesCodeOrMessage(val, depth + 1)) return true;
5029
+ }
5030
+ }
5031
+ return false;
5032
+ }
5033
+ return {
5034
+ CallExpression(node) {
5035
+ const callee = node.callee;
5036
+ if (callee?.type !== "MemberExpression" || callee.property?.name !== "filter") return;
5037
+ if (!sourceLooksLikeSafetyChecks(callee.object)) return;
5038
+ const fn = node.arguments?.[0];
5039
+ if (fn?.type !== "ArrowFunctionExpression" && fn?.type !== "FunctionExpression") return;
5040
+ if (!referencesCodeOrMessage(fn.body)) {
5041
+ context.report({ node, messageId: "blindAck" });
5042
+ }
5043
+ }
5044
+ };
5045
+ }
5046
+ };
5047
+ var openaiCuaNoBlindSafetyCheckAckRule = rule68;
5048
+
5049
+ // src/providers/openai-cua/utils.ts
5050
+ function propName(node) {
5051
+ if (!node) return void 0;
5052
+ if (node.type === "Identifier") return node.name;
5053
+ if (node.type === "Literal" && typeof node.value === "string") return node.value;
5054
+ return void 0;
5055
+ }
5056
+ function memberChainNames(node, names = []) {
5057
+ if (node?.type === "MemberExpression") {
5058
+ memberChainNames(node.object, names);
5059
+ const n = propName(node.property);
5060
+ if (n) names.push(n);
5061
+ } else if (node?.type === "Identifier") {
5062
+ names.push(node.name);
5063
+ }
5064
+ return names;
5065
+ }
5066
+ function isResponsesCreateCall(node) {
5067
+ if (node?.type !== "CallExpression" || node.callee?.type !== "MemberExpression") return false;
5068
+ const chain = memberChainNames(node.callee);
5069
+ return chain.length >= 2 && chain[chain.length - 1] === "create" && chain[chain.length - 2] === "responses";
5070
+ }
5071
+ function findResponsesCreateCall(node, depth = 0) {
5072
+ if (!node || typeof node !== "object" || depth > 12) return null;
5073
+ if (Array.isArray(node)) {
5074
+ for (const n of node) {
5075
+ const found = findResponsesCreateCall(n, depth + 1);
5076
+ if (found) return found;
5077
+ }
5078
+ return null;
5079
+ }
5080
+ if (isResponsesCreateCall(node)) return node;
5081
+ for (const key of Object.keys(node)) {
5082
+ if (key === "parent" || key === "loc" || key === "range") continue;
5083
+ const val = node[key];
5084
+ if (val && typeof val === "object") {
5085
+ const found = findResponsesCreateCall(val, depth + 1);
5086
+ if (found) return found;
5087
+ }
5088
+ }
5089
+ return null;
5090
+ }
5091
+
5092
+ // src/providers/openai-cua/rules/retry-transient-turn-errors.ts
5093
+ var rule69 = {
5094
+ meta: {
5095
+ type: "problem",
5096
+ docs: {
5097
+ description: "A responses.create() call must be retried at the turn level on transient errors",
5098
+ category: "reliability",
5099
+ rationale: "The SDK's own internal retry budget (DEFAULT_MAX_RETRIES) is finite. Treating any exception that survives it as fatal for the whole run \u2014 instead of retrying that one turn with backoff \u2014 means a single transient RateLimitError/APIConnectionError/InternalServerError can discard a long-running, multi-turn, expensive agent loop that may have already executed dozens of prior turns.",
5100
+ docsUrl: "https://developers.openai.com/api/docs/guides/tools-computer-use",
5101
+ recommended: true
5102
+ },
5103
+ messages: {
5104
+ noTurnRetry: "This responses.create() call has no turn-level retry \u2014 neither a surrounding loop nor a retry call in the catch block \u2014 so any error beyond the SDK's own retry budget ends the entire run."
5105
+ }
5106
+ },
5107
+ create(context) {
5108
+ let loopDepth = 0;
5109
+ function propName2(node) {
5110
+ if (!node) return void 0;
5111
+ if (node.type === "Identifier") return node.name;
5112
+ if (node.type === "Literal" && typeof node.value === "string") return node.value;
5113
+ return void 0;
5114
+ }
5115
+ function catchHasRetryCall(node, depth = 0) {
5116
+ if (!node || typeof node !== "object" || depth > 12) return false;
5117
+ if (Array.isArray(node)) return node.some((n) => catchHasRetryCall(n, depth + 1));
5118
+ if (node.type === "CallExpression") {
5119
+ const callee = node.callee;
5120
+ const name = callee?.type === "Identifier" ? callee.name : callee?.type === "MemberExpression" ? propName2(callee.property) : void 0;
5121
+ if (name && /retry/i.test(name)) return true;
5122
+ }
5123
+ for (const key of Object.keys(node)) {
5124
+ if (key === "parent" || key === "loc" || key === "range") continue;
5125
+ const val = node[key];
5126
+ if (val && typeof val === "object") {
5127
+ if (catchHasRetryCall(val, depth + 1)) return true;
4074
5128
  }
4075
5129
  }
5130
+ return false;
5131
+ }
5132
+ return {
5133
+ ForStatement() {
5134
+ loopDepth += 1;
5135
+ },
5136
+ "ForStatement:exit"() {
5137
+ loopDepth -= 1;
5138
+ },
5139
+ WhileStatement() {
5140
+ loopDepth += 1;
5141
+ },
5142
+ "WhileStatement:exit"() {
5143
+ loopDepth -= 1;
5144
+ },
5145
+ DoWhileStatement() {
5146
+ loopDepth += 1;
5147
+ },
5148
+ "DoWhileStatement:exit"() {
5149
+ loopDepth -= 1;
5150
+ },
5151
+ ForOfStatement() {
5152
+ loopDepth += 1;
5153
+ },
5154
+ "ForOfStatement:exit"() {
5155
+ loopDepth -= 1;
5156
+ },
5157
+ ForInStatement() {
5158
+ loopDepth += 1;
5159
+ },
5160
+ "ForInStatement:exit"() {
5161
+ loopDepth -= 1;
5162
+ },
5163
+ TryStatement(node) {
5164
+ const createCall = findResponsesCreateCall(node.block);
5165
+ if (!createCall) return;
5166
+ if (loopDepth > 0) return;
5167
+ const handler = node.handler;
5168
+ if (!handler) return;
5169
+ if (catchHasRetryCall(handler.body)) return;
5170
+ context.report({ node: handler, messageId: "noTurnRetry" });
5171
+ }
4076
5172
  };
4077
5173
  }
4078
5174
  };
4079
- var browserbaseCentralizeRequestReleaseRule = rule52;
5175
+ var openaiCuaRetryTransientTurnErrorsRule = rule69;
4080
5176
 
4081
- // src/providers/openai-cua/rules/no-domain-allowlist.ts
4082
- var ACTION_METHOD_NAMES = /* @__PURE__ */ new Set(["click", "type", "press", "move", "goto", "fill", "dblclick", "dragAndDrop"]);
4083
- var rule53 = {
5177
+ // src/providers/openai-cua/rules/check-response-status-incomplete.ts
5178
+ var rule70 = {
4084
5179
  meta: {
4085
5180
  type: "problem",
4086
5181
  docs: {
4087
- description: "Computer-use action execution must check the page origin against an allowlist",
4088
- category: "security",
4089
- cwe: "CWE-284",
4090
- owasp: "A01:2021 \u2013 Broken Access Control",
4091
- rationale: `OpenAI's Computer Use guide instructs integrators to "keep an allow list of domains and actions your agent should use, and block everything else." Without an origin check before executing actions, a click that follows an off-domain redirect (phishing link, ad, malicious iframe) is executed exactly like any in-domain click \u2014 and a field-fill action has no origin awareness either, so credentials could be typed into a page the agent was never meant to reach.`,
5182
+ description: 'A completion check must rule out response.status === "incomplete" before reporting success',
5183
+ category: "reliability",
5184
+ rationale: 'The Responses API can return status "incomplete" with zero visible message output when generation is cut off by the token budget \u2014 the same shape ("no computer_call, no message") a purely structural completion check treats as "the model voluntarily finished." Without checking response.status, a token-budget truncation can be silently reported as a successful task completion.',
4092
5185
  docsUrl: "https://developers.openai.com/api/docs/guides/tools-computer-use",
4093
5186
  recommended: true
4094
5187
  },
4095
5188
  messages: {
4096
- noOriginCheck: "This function executes computer-use actions on the current page with no origin/domain allowlist check anywhere in it."
5189
+ missingIncompleteCheck: 'This treats the absence of tool calls as a successful completion, but never checks response.status === "incomplete" \u2014 a token-budget truncation can be misreported as success.'
4097
5190
  }
4098
5191
  },
4099
5192
  create(context) {
@@ -4102,7 +5195,7 @@ var rule53 = {
4102
5195
  function ensureState(fn) {
4103
5196
  let s = states.get(fn);
4104
5197
  if (!s) {
4105
- s = { sawAction: false, actionNode: null, sawOriginCheck: false };
5198
+ s = { sawCreateCall: false, sawSuccessReturn: false, successNode: null, sawStatusCheck: false };
4106
5199
  states.set(fn, s);
4107
5200
  }
4108
5201
  return s;
@@ -4123,28 +5216,13 @@ var rule53 = {
4123
5216
  if (node.type === "Literal" && typeof node.value === "string") return node.value;
4124
5217
  return void 0;
4125
5218
  }
4126
- function memberChainNames2(node, names = []) {
4127
- if (node?.type === "MemberExpression") {
4128
- memberChainNames2(node.object, names);
4129
- const n = propName2(node.property);
4130
- if (n) names.push(n);
4131
- } else if (node?.type === "Identifier") {
4132
- names.push(node.name);
4133
- }
4134
- return names;
4135
- }
4136
- function isPageActionCall(node) {
4137
- if (node?.type !== "CallExpression") return false;
4138
- if (node.callee?.type !== "MemberExpression") return false;
4139
- const chain = memberChainNames2(node.callee);
4140
- if (chain.length === 0) return false;
4141
- const last = chain[chain.length - 1];
4142
- if (!ACTION_METHOD_NAMES.has(last)) return false;
4143
- return chain.some((n) => /^page$/i.test(n) || /page$/i.test(n));
4144
- }
4145
- function markOriginCheckSeen() {
4146
- const fn = top();
4147
- if (fn) ensureState(fn).sawOriginCheck = true;
5219
+ function isSuccessObjectLiteral(node) {
5220
+ if (node?.type !== "ObjectExpression") return false;
5221
+ return (node.properties ?? []).some((p) => {
5222
+ if (p?.type !== "Property") return false;
5223
+ const keyName = propName2(p.key);
5224
+ return (keyName === "success" || keyName === "completed") && p.value?.type === "Literal" && p.value.value === true;
5225
+ });
4148
5226
  }
4149
5227
  return {
4150
5228
  Program(node) {
@@ -4152,8 +5230,8 @@ var rule53 = {
4152
5230
  },
4153
5231
  "Program:exit"() {
4154
5232
  for (const state of states.values()) {
4155
- if (state.sawAction && !state.sawOriginCheck) {
4156
- context.report({ node: state.actionNode, messageId: "noOriginCheck" });
5233
+ if (state.sawCreateCall && state.sawSuccessReturn && !state.sawStatusCheck) {
5234
+ context.report({ node: state.successNode, messageId: "missingIncompleteCheck" });
4157
5235
  }
4158
5236
  }
4159
5237
  },
@@ -4176,52 +5254,51 @@ var rule53 = {
4176
5254
  popScope();
4177
5255
  },
4178
5256
  CallExpression(node) {
4179
- if (isPageActionCall(node)) {
5257
+ if (findResponsesCreateCall(node)) {
5258
+ const fn = top();
5259
+ if (fn) ensureState(fn).sawCreateCall = true;
5260
+ }
5261
+ },
5262
+ ObjectExpression(node) {
5263
+ if (isSuccessObjectLiteral(node)) {
4180
5264
  const fn = top();
4181
5265
  if (fn) {
4182
5266
  const state = ensureState(fn);
4183
- if (!state.sawAction) {
4184
- state.sawAction = true;
4185
- state.actionNode = node;
5267
+ if (!state.sawSuccessReturn) {
5268
+ state.sawSuccessReturn = true;
5269
+ state.successNode = node;
4186
5270
  }
4187
5271
  }
4188
5272
  }
4189
5273
  },
4190
- NewExpression(node) {
4191
- if (node.callee?.type === "Identifier" && node.callee.name === "URL") {
4192
- markOriginCheckSeen();
4193
- }
4194
- },
4195
- MemberExpression(node) {
4196
- const name = propName2(node.property);
4197
- if (name === "hostname" || name === "origin") {
4198
- markOriginCheckSeen();
4199
- }
4200
- },
4201
- Identifier(node) {
4202
- if (/allow.?list/i.test(node.name) || /allowed.?domain/i.test(node.name)) {
4203
- markOriginCheckSeen();
5274
+ BinaryExpression(node) {
5275
+ if (node.operator !== "===" && node.operator !== "==") return;
5276
+ const sides = [node.left, node.right];
5277
+ const statusSide = sides.find((s) => s?.type === "MemberExpression" && propName2(s.property) === "status");
5278
+ const valueSide = sides.find((s) => s !== statusSide);
5279
+ if (statusSide && valueSide?.type === "Literal" && valueSide.value === "incomplete") {
5280
+ const fn = top();
5281
+ if (fn) ensureState(fn).sawStatusCheck = true;
4204
5282
  }
4205
5283
  }
4206
5284
  };
4207
5285
  }
4208
5286
  };
4209
- var openaiCuaNoDomainAllowlistRule = rule53;
5287
+ var openaiCuaCheckResponseStatusIncompleteRule = rule70;
4210
5288
 
4211
- // src/providers/openai-cua/rules/scroll-delta-default-zero.ts
4212
- var VERTICAL_DELTA_NAME_PATTERN = /^(dy|delta_?y|scroll_?y)$/i;
4213
- var rule54 = {
5289
+ // src/providers/openai-cua/rules/set-safety-identifier.ts
5290
+ var rule71 = {
4214
5291
  meta: {
4215
5292
  type: "problem",
4216
5293
  docs: {
4217
- description: "A missing vertical scroll delta must default to 0, not an arbitrary non-zero value",
4218
- category: "correctness",
4219
- rationale: "The reference scroll handler in OpenAI's Computer Use guide defaults a missing scroll delta to 0 on both axes. Defaulting the vertical delta to any other value injects an unintended scroll the model never asked for whenever it omits that field, desyncing the model's mental model of the page from the page's actual state on the next turn.",
4220
- docsUrl: "https://developers.openai.com/api/docs/guides/tools-computer-use",
5294
+ description: "responses.create() must set safety_identifier for per-end-user policy attribution",
5295
+ category: "integration",
5296
+ rationale: "OpenAI documents that a stable per-end-user safety_identifier lets policy violations be attributed and acted on per end user. Without one on a multi-tenant integration routing many customers through a single shared API key, a single customer triggering a high-confidence policy-violation heuristic can result in access being temporarily revoked for the entire organization, not just the offending identifier.",
5297
+ docsUrl: "https://help.openai.com/en/articles/5428082-how-to-incorporate-a-safety-identifier",
4221
5298
  recommended: true
4222
5299
  },
4223
5300
  messages: {
4224
- nonZeroDefault: "A missing vertical scroll delta defaults to {{value}} here instead of 0, injecting an unintended scroll the model never requested."
5301
+ missingSafetyIdentifier: "This responses.create() call sets no safety_identifier (or user) \u2014 a policy violation here could be attributed to the entire shared API key instead of one end user."
4225
5302
  }
4226
5303
  },
4227
5304
  create(context) {
@@ -4231,513 +5308,722 @@ var rule54 = {
4231
5308
  if (node.type === "Literal" && typeof node.value === "string") return node.value;
4232
5309
  return void 0;
4233
5310
  }
4234
- function targetName(node) {
4235
- if (node?.type === "Identifier") return node.name;
4236
- if (node?.type === "MemberExpression") return propName2(node.property);
4237
- return void 0;
4238
- }
4239
- function isNonZeroNumberLiteral(node) {
4240
- return node?.type === "Literal" && typeof node.value === "number" && node.value !== 0;
4241
- }
4242
- function reportIfBadDefault(targetNode, valueNode, reportNode) {
4243
- const name = targetName(targetNode);
4244
- if (!name || !VERTICAL_DELTA_NAME_PATTERN.test(name)) return;
4245
- if (!isNonZeroNumberLiteral(valueNode)) return;
4246
- context.report({ node: reportNode, messageId: "nonZeroDefault", data: { value: String(valueNode.value) } });
4247
- }
4248
- function findDirectAssignment(stmt) {
4249
- let inner = stmt;
4250
- if (inner?.type === "BlockStatement") {
4251
- const exprStmts = (inner.body ?? []).filter((s) => s.type === "ExpressionStatement");
4252
- if (exprStmts.length !== 1) return void 0;
4253
- inner = exprStmts[0];
5311
+ return {
5312
+ CallExpression(node) {
5313
+ if (!isResponsesCreateCall(node)) return;
5314
+ const optionsArg = node.arguments?.[0];
5315
+ if (optionsArg?.type !== "ObjectExpression") return;
5316
+ const hasIdentifier = (optionsArg.properties ?? []).some((p) => {
5317
+ if (p?.type !== "Property") return false;
5318
+ const keyName = propName2(p.key);
5319
+ return keyName === "safety_identifier" || keyName === "user";
5320
+ });
5321
+ if (!hasIdentifier) {
5322
+ context.report({ node, messageId: "missingSafetyIdentifier" });
5323
+ }
4254
5324
  }
4255
- if (inner?.type !== "ExpressionStatement") return void 0;
4256
- const expr = inner.expression;
4257
- if (expr?.type !== "AssignmentExpression" || expr.operator !== "=") return void 0;
4258
- return { target: expr.left, value: expr.right };
4259
- }
4260
- function undefinedCheckTargetName(test) {
4261
- if (test?.type !== "BinaryExpression") return void 0;
4262
- if (test.operator !== "===" && test.operator !== "==") return void 0;
4263
- const sides = [test.left, test.right];
4264
- const undefinedSide = sides.find(
4265
- (s) => s?.type === "Identifier" && s.name === "undefined" || s?.type === "Literal" && s.value === null || s?.type === "UnaryExpression" && s.operator === "typeof"
4266
- );
4267
- const otherSide = sides.find((s) => s !== undefinedSide);
4268
- if (!undefinedSide || !otherSide) return void 0;
4269
- if (undefinedSide.type === "UnaryExpression") {
4270
- return targetName(undefinedSide.argument);
5325
+ };
5326
+ }
5327
+ };
5328
+ var openaiCuaSetSafetyIdentifierRule = rule71;
5329
+
5330
+ // src/providers/tiptap/rules/upload-validate-fn-void.ts
5331
+ var rule72 = {
5332
+ meta: {
5333
+ type: "problem",
5334
+ docs: {
5335
+ description: "validateFn must return boolean so callers can act on the result",
5336
+ category: "security",
5337
+ cwe: "CWE-20",
5338
+ owasp: "API3:2023 Broken Object Property Level Authorization",
5339
+ rationale: "When validateFn is typed as (file: File) => void, the return value is always discarded. A consumer that returns false to reject an oversized or wrong-type file will have that rejection silently ignored \u2014 the upload proceeds regardless. Changing the return type to boolean and guarding with if (validateFn && !validateFn(file)) return; ensures validation actually blocks uploads.",
5340
+ docsUrl: "https://tiptap.dev/docs/editor/extensions/custom-extensions/node-views",
5341
+ recommended: true
5342
+ },
5343
+ messages: {
5344
+ validateFnVoid: "validateFn return value is discarded. Change the return type to boolean and guard the call: if (validateFn && !validateFn(file)) return;"
5345
+ },
5346
+ schema: []
5347
+ },
5348
+ create(context) {
5349
+ let importsTiptap = false;
5350
+ function isVoidReturnType(typeAnnotation) {
5351
+ if (!typeAnnotation) return false;
5352
+ const t = typeAnnotation.typeAnnotation ?? typeAnnotation;
5353
+ if (t?.type === "TSFunctionType") {
5354
+ const ret = t.returnType?.typeAnnotation ?? t.returnType;
5355
+ return ret?.type === "TSVoidKeyword";
4271
5356
  }
4272
- return targetName(otherSide);
5357
+ return false;
5358
+ }
5359
+ function hasProperty(members, name) {
5360
+ return members?.some(
5361
+ (m) => m?.key?.type === "Identifier" && m.key.name === name || m?.key?.type === "Literal" && m.key.value === name
5362
+ ) ?? false;
4273
5363
  }
4274
5364
  return {
4275
- LogicalExpression(node) {
4276
- if (node.operator !== "??") return;
4277
- reportIfBadDefault(node.left, node.right, node);
5365
+ ImportDeclaration(node) {
5366
+ const src = node?.source?.value ?? "";
5367
+ if (src.startsWith("@tiptap/")) importsTiptap = true;
5368
+ },
5369
+ // Pattern (a): interface / type with validateFn: (...) => void + onUpload
5370
+ TSInterfaceDeclaration(node) {
5371
+ const members = node?.body?.body ?? [];
5372
+ if (!hasProperty(members, "onUpload")) return;
5373
+ for (const member of members) {
5374
+ const name = member?.key?.type === "Identifier" ? member.key.name : member?.key?.type === "Literal" ? member.key.value : null;
5375
+ if (name !== "validateFn") continue;
5376
+ const typeAnno = member.typeAnnotation;
5377
+ if (isVoidReturnType(typeAnno)) {
5378
+ context.report({ node: member, messageId: "validateFnVoid" });
5379
+ }
5380
+ }
4278
5381
  },
4279
- IfStatement(node) {
4280
- const name = undefinedCheckTargetName(node.test);
4281
- if (!name) return;
4282
- const assignment = findDirectAssignment(node.consequent);
4283
- if (!assignment) return;
4284
- reportIfBadDefault(assignment.target, assignment.value, node);
5382
+ // Pattern (b): validateFn?.() or validateFn() used as a bare statement
5383
+ ExpressionStatement(node) {
5384
+ const expr = node.expression;
5385
+ const call = expr?.type === "CallExpression" ? expr : expr?.type === "ChainExpression" && expr.expression?.type === "CallExpression" ? expr.expression : null;
5386
+ if (!call) return;
5387
+ const callee = call.callee;
5388
+ const calleeName = callee?.type === "Identifier" ? callee.name : callee?.type === "MemberExpression" && callee.property?.type === "Identifier" ? callee.property.name : null;
5389
+ if (calleeName !== "validateFn") return;
5390
+ context.report({ node, messageId: "validateFnVoid" });
4285
5391
  }
4286
5392
  };
4287
5393
  }
4288
5394
  };
4289
- var openaiCuaScrollDeltaDefaultZeroRule = rule54;
5395
+ var tiptapUploadValidateFnVoidRule = rule72;
4290
5396
 
4291
- // src/providers/openai-cua/rules/structured-step-metadata-not-text-json.ts
4292
- var BRACE_SEARCH_METHODS = /* @__PURE__ */ new Set(["indexOf", "lastIndexOf"]);
4293
- var SLICE_METHODS = /* @__PURE__ */ new Set(["slice", "substring", "substr"]);
4294
- var rule55 = {
5397
+ // src/providers/tiptap/rules/script-src-hardcoded-api-key.ts
5398
+ var rule73 = {
5399
+ meta: {
5400
+ type: "problem",
5401
+ docs: {
5402
+ description: "script.src must not contain a hardcoded API key",
5403
+ category: "security",
5404
+ cwe: "CWE-798",
5405
+ owasp: "API8:2023 Security Misconfiguration",
5406
+ rationale: "Hardcoding an API key directly in a script URL string commits the credential to version control and ships it in the client bundle. The key is trivially visible to anyone who views the page source. Read the key from an environment variable at runtime so it can be rotated without a code change and kept out of the repository.",
5407
+ docsUrl: "https://tiptap.dev/docs/editor/extensions/custom-extensions/node-views",
5408
+ recommended: true
5409
+ },
5410
+ messages: {
5411
+ hardcodedApiKey: "script.src contains a hardcoded API key. Read the key from an environment variable instead."
5412
+ },
5413
+ schema: []
5414
+ },
5415
+ create(context) {
5416
+ function isSrcAssignment(node) {
5417
+ return node?.type === "AssignmentExpression" && node.left?.type === "MemberExpression" && node.left.property?.type === "Identifier" && node.left.property.name === "src";
5418
+ }
5419
+ function isEnvAccess(node) {
5420
+ if (node?.type !== "MemberExpression") return false;
5421
+ const obj = node.object;
5422
+ return obj?.type === "MemberExpression" && obj.object?.type === "Identifier" && obj.object.name === "process" && obj.property?.type === "Identifier" && obj.property.name === "env";
5423
+ }
5424
+ return {
5425
+ AssignmentExpression(node) {
5426
+ if (!isSrcAssignment(node)) return;
5427
+ const right = node.right;
5428
+ if (right?.type === "Literal" && typeof right.value === "string") {
5429
+ if (right.value.includes("apiKey=")) {
5430
+ context.report({ node, messageId: "hardcodedApiKey" });
5431
+ }
5432
+ return;
5433
+ }
5434
+ if (right?.type === "TemplateLiteral") {
5435
+ const quasis = right.quasis ?? [];
5436
+ const expressions = right.expressions ?? [];
5437
+ for (let i = 0; i < quasis.length; i++) {
5438
+ const cooked = quasis[i]?.value?.cooked ?? "";
5439
+ if (cooked.includes("apiKey=")) {
5440
+ const nextExpr = expressions[i];
5441
+ if (!nextExpr) {
5442
+ context.report({ node, messageId: "hardcodedApiKey" });
5443
+ return;
5444
+ }
5445
+ if (!isEnvAccess(nextExpr)) {
5446
+ context.report({ node, messageId: "hardcodedApiKey" });
5447
+ return;
5448
+ }
5449
+ }
5450
+ }
5451
+ }
5452
+ }
5453
+ };
5454
+ }
5455
+ };
5456
+ var tiptapScriptSrcHardcodedApiKeyRule = rule73;
5457
+
5458
+ // src/providers/tiptap/rules/dynamic-script-no-sri.ts
5459
+ var rule74 = {
4295
5460
  meta: {
4296
- type: "suggestion",
5461
+ type: "problem",
4297
5462
  docs: {
4298
- description: "Step metadata must come from structured tool output, not brace-hunting in free text",
4299
- category: "correctness",
4300
- rationale: "Manually locating a JSON object inside a free-text model message (via indexOf/lastIndexOf plus a slice) is fragile by construction: it breaks if the model adds trailing commentary, wraps the JSON in a markdown fence, or reorders fields. The Responses API supports function tools and structured output specifically so required metadata is schema-validated by the API instead of regex/brace-scraped out of arbitrary text.",
4301
- docsUrl: "https://developers.openai.com/api/docs/guides/tools-computer-use",
5463
+ description: "Dynamically injected script elements must include an SRI integrity attribute",
5464
+ category: "security",
5465
+ cwe: "CWE-829",
5466
+ owasp: "API8:2023 Security Misconfiguration",
5467
+ 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",
4302
5469
  recommended: true
4303
5470
  },
4304
5471
  messages: {
4305
- textJsonExtraction: "This function locates a JSON object in free text via indexOf/lastIndexOf and parses a slice of it \u2014 use a function tool or structured output instead of brace-hunting in the model's text message."
4306
- }
5472
+ missingIntegrity: 'Dynamically created script element is appended without an integrity (SRI) attribute. Add script.setAttribute("integrity", "sha384-...") before appending.'
5473
+ },
5474
+ schema: []
4307
5475
  },
4308
5476
  create(context) {
4309
- const stack = [];
4310
- const states = /* @__PURE__ */ new Map();
4311
- function ensureState(fn) {
4312
- let s = states.get(fn);
4313
- if (!s) {
4314
- s = { sawBraceSearch: false, sawJsonParseOnSlice: false, reportNode: null };
4315
- states.set(fn, s);
4316
- }
4317
- return s;
4318
- }
4319
- function top() {
4320
- return stack[stack.length - 1];
4321
- }
4322
- function pushScope(node) {
4323
- stack.push(node);
4324
- ensureState(node);
4325
- }
4326
- function popScope() {
4327
- stack.pop();
4328
- }
4329
- function isBraceSearchCall(node) {
4330
- if (node?.type !== "CallExpression") return false;
5477
+ const scriptVars = /* @__PURE__ */ new Map();
5478
+ function isCreateScriptCall(node) {
5479
+ if (node?.type !== "CallExpression") return null;
4331
5480
  const callee = node.callee;
4332
- if (callee?.type !== "MemberExpression") return false;
4333
- if (!BRACE_SEARCH_METHODS.has(callee.property?.name)) return false;
5481
+ if (callee?.type !== "MemberExpression") return null;
5482
+ if (callee.property?.name !== "createElement") return null;
4334
5483
  const arg = node.arguments?.[0];
4335
- return arg?.type === "Literal" && typeof arg.value === "string" && arg.value.includes("{");
5484
+ if (arg?.type !== "Literal" || arg.value !== "script") return null;
5485
+ return "script";
4336
5486
  }
4337
- function isJsonParseOnSliceCall(node) {
5487
+ function getVarNameFromDeclarator(node) {
5488
+ if (node?.type === "VariableDeclarator" && node.id?.type === "Identifier") {
5489
+ return node.id.name;
5490
+ }
5491
+ return null;
5492
+ }
5493
+ function isIntegritySet(node, varName) {
5494
+ if (node?.type === "CallExpression") {
5495
+ const callee = node.callee;
5496
+ if (callee?.type === "MemberExpression" && callee.object?.type === "Identifier" && callee.object.name === varName && (callee.property?.name === "setAttribute" || callee.property?.value === "setAttribute")) {
5497
+ const firstArg = node.arguments?.[0];
5498
+ if (firstArg?.type === "Literal" && firstArg.value === "integrity") return true;
5499
+ }
5500
+ }
5501
+ if (node?.type === "AssignmentExpression" && node.left?.type === "MemberExpression" && node.left.object?.type === "Identifier" && node.left.object.name === varName && node.left.property?.name === "integrity") {
5502
+ return true;
5503
+ }
5504
+ return false;
5505
+ }
5506
+ function isAppendCall(node, varName) {
4338
5507
  if (node?.type !== "CallExpression") return false;
4339
5508
  const callee = node.callee;
4340
5509
  if (callee?.type !== "MemberExpression") return false;
4341
- if (callee.object?.type !== "Identifier" || callee.object.name !== "JSON") return false;
4342
- if (callee.property?.name !== "parse") return false;
4343
- const arg = node.arguments?.[0];
4344
- if (arg?.type !== "CallExpression") return false;
4345
- const argCallee = arg.callee;
4346
- return argCallee?.type === "MemberExpression" && SLICE_METHODS.has(argCallee.property?.name);
5510
+ const methodName = callee.property?.name;
5511
+ if (!["appendChild", "append", "insertBefore", "prepend"].includes(methodName)) return false;
5512
+ const firstArg = node.arguments?.[0];
5513
+ return firstArg?.type === "Identifier" && firstArg.name === varName;
4347
5514
  }
4348
5515
  return {
4349
- Program(node) {
4350
- pushScope(node);
4351
- },
4352
- "Program:exit"() {
4353
- for (const state of states.values()) {
4354
- if (state.sawBraceSearch && state.sawJsonParseOnSlice) {
4355
- context.report({ node: state.reportNode, messageId: "textJsonExtraction" });
5516
+ VariableDeclarator(node) {
5517
+ if (node.init && isCreateScriptCall(node.init)) {
5518
+ const name = getVarNameFromDeclarator(node);
5519
+ if (name) {
5520
+ scriptVars.set(name, { createNode: node, appendNode: null, hasIntegrity: false });
4356
5521
  }
4357
5522
  }
4358
5523
  },
4359
- FunctionDeclaration(node) {
4360
- pushScope(node);
4361
- },
4362
- "FunctionDeclaration:exit"() {
4363
- popScope();
4364
- },
4365
- FunctionExpression(node) {
4366
- pushScope(node);
4367
- },
4368
- "FunctionExpression:exit"() {
4369
- popScope();
4370
- },
4371
- ArrowFunctionExpression(node) {
4372
- pushScope(node);
4373
- },
4374
- "ArrowFunctionExpression:exit"() {
4375
- popScope();
5524
+ ExpressionStatement(node) {
5525
+ const expr = node.expression;
5526
+ if (!expr) return;
5527
+ for (const [varName, info] of scriptVars) {
5528
+ if (isIntegritySet(expr, varName)) {
5529
+ info.hasIntegrity = true;
5530
+ }
5531
+ if (isAppendCall(expr, varName) && !info.appendNode) {
5532
+ info.appendNode = node;
5533
+ }
5534
+ }
4376
5535
  },
4377
- CallExpression(node) {
4378
- const fn = top();
4379
- if (!fn) return;
4380
- const state = ensureState(fn);
4381
- if (isBraceSearchCall(node)) {
4382
- state.sawBraceSearch = true;
4383
- if (!state.reportNode) state.reportNode = node;
5536
+ // Also catch assignments: e.g. document.body.innerHTML won't appear here,
5537
+ // but handle AssignmentExpression for integrity
5538
+ AssignmentExpression(node) {
5539
+ for (const [varName, info] of scriptVars) {
5540
+ if (isIntegritySet(node, varName)) {
5541
+ info.hasIntegrity = true;
5542
+ }
4384
5543
  }
4385
- if (isJsonParseOnSliceCall(node)) {
4386
- state.sawJsonParseOnSlice = true;
4387
- if (!state.reportNode) state.reportNode = node;
5544
+ },
5545
+ "Program:exit"() {
5546
+ for (const [, info] of scriptVars) {
5547
+ if (info.appendNode && !info.hasIntegrity) {
5548
+ context.report({ node: info.appendNode, messageId: "missingIntegrity" });
5549
+ }
4388
5550
  }
4389
5551
  }
4390
5552
  };
4391
5553
  }
4392
5554
  };
4393
- var openaiCuaStructuredStepMetadataNotTextJsonRule = rule55;
5555
+ var tiptapDynamicScriptNoSriRule = rule74;
4394
5556
 
4395
- // src/providers/openai-cua/rules/no-blind-safety-check-ack.ts
4396
- var rule56 = {
5557
+ // src/providers/tiptap/utils.ts
5558
+ function findProperty3(objectExpression, name) {
5559
+ if (objectExpression?.type !== "ObjectExpression") return void 0;
5560
+ return objectExpression.properties?.find(
5561
+ (p) => p?.type === "Property" && (p.key?.type === "Identifier" && p.key.name === name || p.key?.type === "Literal" && p.key.value === name)
5562
+ );
5563
+ }
5564
+ function walk(node, visit) {
5565
+ if (!node || typeof node !== "object") return;
5566
+ const result = visit(node);
5567
+ if (result === false) return;
5568
+ for (const key of Object.keys(node)) {
5569
+ if (key === "parent") continue;
5570
+ const child = node[key];
5571
+ if (Array.isArray(child)) {
5572
+ for (const item of child) walk(item, visit);
5573
+ } else if (child && typeof child === "object" && child.type) {
5574
+ walk(child, visit);
5575
+ }
5576
+ }
5577
+ }
5578
+
5579
+ // src/providers/tiptap/rules/addAttributes-missing-renderHTML.ts
5580
+ var rule75 = {
4397
5581
  meta: {
4398
- type: "suggestion",
5582
+ type: "problem",
4399
5583
  docs: {
4400
- description: "Acknowledging pending safety checks must evaluate each check, not blanket-pass them",
5584
+ description: "TipTap addAttributes descriptors with parseHTML must also define renderHTML",
4401
5585
  category: "correctness",
4402
- rationale: "`acknowledged_safety_checks` exists specifically so a developer can selectively confirm checks they have actually reviewed \u2014 each entry carries an id/code/message. A filter that only checks the row's shape (e.g. that it is an object) and never inspects `.code` or `.message` acknowledges every check present, defeating the purpose of the field entirely.",
4403
- docsUrl: "https://developers.openai.com/api/docs/guides/tools-computer-use",
5586
+ rationale: 'When an attribute defines parseHTML: (el) => el.getAttribute("data-label") but no renderHTML, TipTap emits the attribute as a plain HTML attribute (label="...") instead of data-label="...". On re-parse, getAttribute("data-label") returns null and the value falls back to the default, silently discarding any customization across HTML export/import cycles.',
5587
+ docsUrl: "https://tiptap.dev/docs/editor/extensions/custom-extensions/create-new/node#attributes",
4404
5588
  recommended: true
4405
5589
  },
4406
5590
  messages: {
4407
- blindAck: "This filter never inspects .code or .message on each safety check \u2014 it acknowledges every check present instead of evaluating it against a policy."
4408
- }
5591
+ missingRenderHTML: 'Attribute defines parseHTML but no renderHTML. TipTap will serialize this attribute with the default name, breaking the HTML round-trip. Add renderHTML: (attrs) => ({ "data-<name>": attrs.<name> }).'
5592
+ },
5593
+ schema: []
4409
5594
  },
4410
5595
  create(context) {
4411
- function propName2(node) {
4412
- if (!node) return void 0;
4413
- if (node.type === "Identifier") return node.name;
4414
- if (node.type === "Literal" && typeof node.value === "string") return node.value;
4415
- return void 0;
4416
- }
4417
- function sourceLooksLikeSafetyChecks(node) {
4418
- if (!node) return false;
4419
- const name = propName2(node) ?? (node.type === "MemberExpression" ? propName2(node.property) : void 0);
4420
- if (name && /safety.?check/i.test(name)) return true;
4421
- if (node.type === "LogicalExpression") {
4422
- return sourceLooksLikeSafetyChecks(node.left) || sourceLooksLikeSafetyChecks(node.right);
4423
- }
4424
- if (node.type === "MemberExpression") {
4425
- return sourceLooksLikeSafetyChecks(node.object) || sourceLooksLikeSafetyChecks(node.property);
5596
+ function checkAddAttributesReturn(returnObj) {
5597
+ if (returnObj?.type !== "ObjectExpression") return;
5598
+ for (const prop of returnObj.properties ?? []) {
5599
+ if (prop?.type !== "Property") continue;
5600
+ const val = prop.value;
5601
+ if (val?.type !== "ObjectExpression") continue;
5602
+ if (findProperty3(val, "parseHTML") && !findProperty3(val, "renderHTML")) {
5603
+ context.report({ node: val, messageId: "missingRenderHTML" });
5604
+ }
4426
5605
  }
4427
- return false;
4428
5606
  }
4429
- function referencesCodeOrMessage(node, depth = 0) {
4430
- if (!node || typeof node !== "object" || depth > 10) return false;
4431
- if (Array.isArray(node)) return node.some((n) => referencesCodeOrMessage(n, depth + 1));
4432
- if (node.type === "MemberExpression") {
4433
- const name = propName2(node.property);
4434
- if (name === "code" || name === "message") return true;
5607
+ function checkAddAttributesFunction(fn) {
5608
+ walk(fn, (node) => {
5609
+ if (node?.type === "ReturnStatement" && node.argument) {
5610
+ checkAddAttributesReturn(node.argument);
5611
+ }
5612
+ });
5613
+ }
5614
+ return {
5615
+ // Handles: addAttributes() { return { ... } } as method shorthand
5616
+ // and addAttributes: function() { } / addAttributes: () => { }
5617
+ Property(node) {
5618
+ const keyName = node.key?.type === "Identifier" ? node.key.name : node.key?.type === "Literal" ? node.key.value : null;
5619
+ if (keyName !== "addAttributes") return;
5620
+ const val = node.value;
5621
+ if (!val) return;
5622
+ if (val.type === "FunctionExpression" || val.type === "ArrowFunctionExpression") {
5623
+ if (val.body?.type === "ObjectExpression") {
5624
+ checkAddAttributesReturn(val.body);
5625
+ } else {
5626
+ checkAddAttributesFunction(val.body);
5627
+ }
5628
+ }
4435
5629
  }
4436
- for (const key of Object.keys(node)) {
4437
- if (key === "parent" || key === "loc" || key === "range") continue;
4438
- const val = node[key];
4439
- if (val && typeof val === "object") {
4440
- if (referencesCodeOrMessage(val, depth + 1)) return true;
5630
+ };
5631
+ }
5632
+ };
5633
+ var tiptapAddAttributesMissingRenderHTMLRule = rule75;
5634
+
5635
+ // src/providers/tiptap/rules/appendTransaction-add-to-history.ts
5636
+ var rule76 = {
5637
+ meta: {
5638
+ type: "suggestion",
5639
+ docs: {
5640
+ description: 'appendTransaction mutations must include tr.setMeta("addToHistory", false)',
5641
+ category: "correctness",
5642
+ rationale: 'ProseMirror records every transaction dispatched by appendTransaction into the undo history unless it is annotated with setMeta("addToHistory", false). When an auto-correction (e.g. inserting a space into an empty node) is recorded, pressing Ctrl-Z undoes the correction and re-enters the broken state, which the plugin immediately re-corrects \u2014 the undo and redo keys fight each other.',
5643
+ docsUrl: "https://prosemirror.net/docs/ref/#state.Transaction.setMeta",
5644
+ recommended: true
5645
+ },
5646
+ messages: {
5647
+ missingAddToHistory: 'appendTransaction mutates the transaction without setMeta("addToHistory", false). This pollutes the undo stack. Add tr.setMeta("addToHistory", false) to the transaction.'
5648
+ },
5649
+ schema: []
5650
+ },
5651
+ create(context) {
5652
+ function hasAddToHistoryMeta(fnBody) {
5653
+ let found = false;
5654
+ walk(fnBody, (node) => {
5655
+ if (found) return false;
5656
+ if (node?.type !== "CallExpression") return;
5657
+ const callee = node.callee;
5658
+ if (callee?.type !== "MemberExpression") return;
5659
+ if (callee.property?.name !== "setMeta") return;
5660
+ const firstArg = node.arguments?.[0];
5661
+ if (firstArg?.type === "Literal" && firstArg.value === "addToHistory") {
5662
+ found = true;
5663
+ }
5664
+ });
5665
+ return found;
5666
+ }
5667
+ function hasMutation(fnBody) {
5668
+ const mutatingMethods = /* @__PURE__ */ new Set([
5669
+ "insert",
5670
+ "insertText",
5671
+ "replace",
5672
+ "replaceWith",
5673
+ "replaceRange",
5674
+ "replaceRangeWith",
5675
+ "delete",
5676
+ "deleteRange",
5677
+ "addMark",
5678
+ "removeMark",
5679
+ "setNodeMarkup",
5680
+ "setNodeAttribute",
5681
+ "setDocAttribute"
5682
+ ]);
5683
+ let found = false;
5684
+ walk(fnBody, (node) => {
5685
+ if (found) return false;
5686
+ if (node?.type !== "CallExpression") return;
5687
+ const callee = node.callee;
5688
+ if (callee?.type !== "MemberExpression") return;
5689
+ const methodName = callee.property?.name;
5690
+ if (methodName && mutatingMethods.has(methodName)) {
5691
+ found = true;
4441
5692
  }
5693
+ });
5694
+ return found;
5695
+ }
5696
+ function checkAppendTransactionFn(fn, reportNode) {
5697
+ const body = fn?.body;
5698
+ if (!body) return;
5699
+ if (hasMutation(body) && !hasAddToHistoryMeta(body)) {
5700
+ context.report({ node: reportNode, messageId: "missingAddToHistory" });
4442
5701
  }
4443
- return false;
4444
5702
  }
4445
5703
  return {
4446
- CallExpression(node) {
4447
- const callee = node.callee;
4448
- if (callee?.type !== "MemberExpression" || callee.property?.name !== "filter") return;
4449
- if (!sourceLooksLikeSafetyChecks(callee.object)) return;
4450
- const fn = node.arguments?.[0];
4451
- if (fn?.type !== "ArrowFunctionExpression" && fn?.type !== "FunctionExpression") return;
4452
- if (!referencesCodeOrMessage(fn.body)) {
4453
- context.report({ node, messageId: "blindAck" });
5704
+ Property(node) {
5705
+ const keyName = node.key?.type === "Identifier" ? node.key.name : node.key?.type === "Literal" ? node.key.value : null;
5706
+ if (keyName !== "appendTransaction") return;
5707
+ const val = node.value;
5708
+ if (val?.type === "FunctionExpression" || val?.type === "ArrowFunctionExpression") {
5709
+ checkAppendTransactionFn(val, node);
4454
5710
  }
4455
5711
  }
4456
5712
  };
4457
5713
  }
4458
5714
  };
4459
- var openaiCuaNoBlindSafetyCheckAckRule = rule56;
5715
+ var tiptapAppendTransactionAddToHistoryRule = rule76;
4460
5716
 
4461
- // src/providers/openai-cua/utils.ts
4462
- function propName(node) {
4463
- if (!node) return void 0;
4464
- if (node.type === "Identifier") return node.name;
4465
- if (node.type === "Literal" && typeof node.value === "string") return node.value;
4466
- return void 0;
4467
- }
4468
- function memberChainNames(node, names = []) {
4469
- if (node?.type === "MemberExpression") {
4470
- memberChainNames(node.object, names);
4471
- const n = propName(node.property);
4472
- if (n) names.push(n);
4473
- } else if (node?.type === "Identifier") {
4474
- names.push(node.name);
4475
- }
4476
- return names;
4477
- }
4478
- function isResponsesCreateCall(node) {
4479
- if (node?.type !== "CallExpression" || node.callee?.type !== "MemberExpression") return false;
4480
- const chain = memberChainNames(node.callee);
4481
- return chain.length >= 2 && chain[chain.length - 1] === "create" && chain[chain.length - 2] === "responses";
4482
- }
4483
- function findResponsesCreateCall(node, depth = 0) {
4484
- if (!node || typeof node !== "object" || depth > 12) return null;
4485
- if (Array.isArray(node)) {
4486
- for (const n of node) {
4487
- const found = findResponsesCreateCall(n, depth + 1);
4488
- if (found) return found;
5717
+ // src/providers/tiptap/rules/appendTransaction-full-scan.ts
5718
+ var rule77 = {
5719
+ meta: {
5720
+ type: "suggestion",
5721
+ docs: {
5722
+ description: "appendTransaction must guard doc.descendants() with a docChanged check",
5723
+ category: "reliability",
5724
+ rationale: "doc.descendants() visits every node in the document on every transaction \u2014 every keystroke, selection change, and formatting toggle. For documents with many nodes this is O(n) per transaction and causes visible input latency. ProseMirror provides the docChanged flag precisely to avoid this work when the document structure has not changed.",
5725
+ docsUrl: "https://prosemirror.net/docs/ref/#state.PluginSpec.appendTransaction",
5726
+ recommended: true
5727
+ },
5728
+ messages: {
5729
+ fullScanOnEveryTransaction: "appendTransaction calls doc.descendants() on every transaction. Check docChanged first: const docChanged = transactions.some(tr => tr.docChanged); if (!docChanged) return;"
5730
+ },
5731
+ schema: []
5732
+ },
5733
+ create(context) {
5734
+ function hasDocChangedGuard(fnBody) {
5735
+ let found = false;
5736
+ walk(fnBody, (node) => {
5737
+ if (found) return false;
5738
+ if (node?.type === "MemberExpression" && node.property?.name === "docChanged") {
5739
+ found = true;
5740
+ return false;
5741
+ }
5742
+ if (node?.type === "Identifier" && node.name === "docChanged") {
5743
+ found = true;
5744
+ return false;
5745
+ }
5746
+ });
5747
+ return found;
4489
5748
  }
4490
- return null;
4491
- }
4492
- if (isResponsesCreateCall(node)) return node;
4493
- for (const key of Object.keys(node)) {
4494
- if (key === "parent" || key === "loc" || key === "range") continue;
4495
- const val = node[key];
4496
- if (val && typeof val === "object") {
4497
- const found = findResponsesCreateCall(val, depth + 1);
4498
- if (found) return found;
5749
+ function hasDescendantsCall(fnBody) {
5750
+ let found = false;
5751
+ walk(fnBody, (node) => {
5752
+ if (found) return false;
5753
+ if (node?.type === "CallExpression" && node.callee?.type === "MemberExpression" && node.callee.property?.name === "descendants") {
5754
+ found = true;
5755
+ }
5756
+ });
5757
+ return found;
5758
+ }
5759
+ function checkAppendTransactionFn(fn, reportNode) {
5760
+ const body = fn?.body;
5761
+ if (!body) return;
5762
+ if (hasDescendantsCall(body) && !hasDocChangedGuard(body)) {
5763
+ context.report({ node: reportNode, messageId: "fullScanOnEveryTransaction" });
5764
+ }
4499
5765
  }
5766
+ return {
5767
+ Property(node) {
5768
+ const keyName = node.key?.type === "Identifier" ? node.key.name : node.key?.type === "Literal" ? node.key.value : null;
5769
+ if (keyName !== "appendTransaction") return;
5770
+ const val = node.value;
5771
+ if (val?.type === "FunctionExpression" || val?.type === "ArrowFunctionExpression") {
5772
+ checkAppendTransactionFn(val, node);
5773
+ }
5774
+ }
5775
+ };
4500
5776
  }
4501
- return null;
4502
- }
5777
+ };
5778
+ var tiptapAppendTransactionFullScanRule = rule77;
4503
5779
 
4504
- // src/providers/openai-cua/rules/retry-transient-turn-errors.ts
4505
- var rule57 = {
5780
+ // src/providers/tiptap/rules/atom-node-wrap-in.ts
5781
+ var rule78 = {
4506
5782
  meta: {
4507
5783
  type: "problem",
4508
5784
  docs: {
4509
- description: "A responses.create() call must be retried at the turn level on transient errors",
4510
- category: "reliability",
4511
- rationale: "The SDK's own internal retry budget (DEFAULT_MAX_RETRIES) is finite. Treating any exception that survives it as fatal for the whole run \u2014 instead of retrying that one turn with backoff \u2014 means a single transient RateLimitError/APIConnectionError/InternalServerError can discard a long-running, multi-turn, expensive agent loop that may have already executed dozens of prior turns.",
4512
- docsUrl: "https://developers.openai.com/api/docs/guides/tools-computer-use",
5785
+ description: "wrapIn() must not be called with an atom node type",
5786
+ category: "correctness",
5787
+ rationale: 'Atom nodes have no content model \u2014 ProseMirror cannot wrap other nodes inside them. Calling wrapIn("atomNode") always returns false silently, making the command appear broken when triggered on selected text. Use replaceSelectionWith(node.create()) or insertContent() instead.',
5788
+ docsUrl: "https://tiptap.dev/docs/editor/extensions/custom-extensions/create-new/node",
4513
5789
  recommended: true
4514
5790
  },
4515
5791
  messages: {
4516
- noTurnRetry: "This responses.create() call has no turn-level retry \u2014 neither a surrounding loop nor a retry call in the catch block \u2014 so any error beyond the SDK's own retry budget ends the entire run."
4517
- }
5792
+ wrapInAtomNode: "wrapIn() is called with an atom node type. Atom nodes cannot contain content, so wrapIn() always silently returns false. Replace the selection instead of wrapping it."
5793
+ },
5794
+ schema: []
4518
5795
  },
4519
5796
  create(context) {
4520
- let loopDepth = 0;
4521
- function propName2(node) {
4522
- if (!node) return void 0;
4523
- if (node.type === "Identifier") return node.name;
4524
- if (node.type === "Literal" && typeof node.value === "string") return node.value;
4525
- return void 0;
5797
+ const atomNodeNames = /* @__PURE__ */ new Set();
5798
+ const wrapInCalls = [];
5799
+ function extractNodeName(configObj) {
5800
+ if (configObj?.type !== "ObjectExpression") return null;
5801
+ const nameProp = configObj.properties?.find(
5802
+ (p) => p?.type === "Property" && (p.key?.type === "Identifier" && p.key.name === "name" || p.key?.type === "Literal" && p.key.value === "name")
5803
+ );
5804
+ const nameVal = nameProp?.value;
5805
+ if (nameVal?.type === "Literal" && typeof nameVal.value === "string") {
5806
+ return nameVal.value;
5807
+ }
5808
+ return null;
4526
5809
  }
4527
- function catchHasRetryCall(node, depth = 0) {
4528
- if (!node || typeof node !== "object" || depth > 12) return false;
4529
- if (Array.isArray(node)) return node.some((n) => catchHasRetryCall(n, depth + 1));
4530
- if (node.type === "CallExpression") {
5810
+ function hasAtomTrue(configObj) {
5811
+ if (configObj?.type !== "ObjectExpression") return false;
5812
+ return configObj.properties?.some(
5813
+ (p) => p?.type === "Property" && (p.key?.type === "Identifier" && p.key.name === "atom" || p.key?.type === "Literal" && p.key.value === "atom") && p.value?.type === "Literal" && p.value.value === true
5814
+ ) ?? false;
5815
+ }
5816
+ return {
5817
+ CallExpression(node) {
5818
+ if (node.callee?.type === "MemberExpression" && node.callee.property?.name === "create") {
5819
+ const configArg = node.arguments?.[0];
5820
+ if (configArg?.type === "ObjectExpression" && hasAtomTrue(configArg)) {
5821
+ const name = extractNodeName(configArg);
5822
+ if (name) atomNodeNames.add(name);
5823
+ }
5824
+ }
4531
5825
  const callee = node.callee;
4532
- const name = callee?.type === "Identifier" ? callee.name : callee?.type === "MemberExpression" ? propName2(callee.property) : void 0;
4533
- if (name && /retry/i.test(name)) return true;
4534
- }
4535
- for (const key of Object.keys(node)) {
4536
- if (key === "parent" || key === "loc" || key === "range") continue;
4537
- const val = node[key];
4538
- if (val && typeof val === "object") {
4539
- if (catchHasRetryCall(val, depth + 1)) return true;
5826
+ const isWrapIn = callee?.type === "Identifier" && callee.name === "wrapIn" || callee?.type === "MemberExpression" && callee.property?.name === "wrapIn";
5827
+ if (isWrapIn) {
5828
+ const firstArg = node.arguments?.[0];
5829
+ if (firstArg?.type === "Literal" && typeof firstArg.value === "string") {
5830
+ wrapInCalls.push({ node, name: firstArg.value });
5831
+ }
5832
+ }
5833
+ },
5834
+ "Program:exit"() {
5835
+ for (const { node, name } of wrapInCalls) {
5836
+ if (atomNodeNames.has(name)) {
5837
+ context.report({ node, messageId: "wrapInAtomNode" });
5838
+ }
4540
5839
  }
4541
5840
  }
4542
- return false;
5841
+ };
5842
+ }
5843
+ };
5844
+ var tiptapAtomNodeWrapInRule = rule78;
5845
+
5846
+ // src/providers/tiptap/rules/twitter-url-regex.ts
5847
+ var rule79 = {
5848
+ meta: {
5849
+ type: "suggestion",
5850
+ docs: {
5851
+ description: "Twitter/X URL regex must match both x.com and twitter.com",
5852
+ category: "integration",
5853
+ rationale: "Twitter rebranded to X but twitter.com URLs still resolve and embed correctly. A regex that only matches x.com silently rejects all legacy twitter.com links pasted by users. Update the pattern to (x\\.com|twitter\\.com) so both domains are handled.",
5854
+ docsUrl: "https://tiptap.dev/docs/editor/extensions/nodes",
5855
+ recommended: true
5856
+ },
5857
+ messages: {
5858
+ twitterRegexMissingLegacyDomain: "This regex matches x.com but not twitter.com. Legacy twitter.com URLs are still valid and should be supported. Add (x\\.com|twitter\\.com) to the pattern."
5859
+ },
5860
+ schema: []
5861
+ },
5862
+ create(context) {
5863
+ function checkPattern(pattern, node) {
5864
+ if (!pattern.includes("x\\.com") && !pattern.includes("x.com")) return;
5865
+ if (pattern.includes("twitter\\.com") || pattern.includes("twitter.com")) return;
5866
+ context.report({ node, messageId: "twitterRegexMissingLegacyDomain" });
4543
5867
  }
4544
5868
  return {
4545
- ForStatement() {
4546
- loopDepth += 1;
4547
- },
4548
- "ForStatement:exit"() {
4549
- loopDepth -= 1;
4550
- },
4551
- WhileStatement() {
4552
- loopDepth += 1;
4553
- },
4554
- "WhileStatement:exit"() {
4555
- loopDepth -= 1;
4556
- },
4557
- DoWhileStatement() {
4558
- loopDepth += 1;
4559
- },
4560
- "DoWhileStatement:exit"() {
4561
- loopDepth -= 1;
4562
- },
4563
- ForOfStatement() {
4564
- loopDepth += 1;
4565
- },
4566
- "ForOfStatement:exit"() {
4567
- loopDepth -= 1;
4568
- },
4569
- ForInStatement() {
4570
- loopDepth += 1;
4571
- },
4572
- "ForInStatement:exit"() {
4573
- loopDepth -= 1;
5869
+ Literal(node) {
5870
+ if (node.regex?.pattern) {
5871
+ checkPattern(node.regex.pattern, node);
5872
+ }
4574
5873
  },
4575
- TryStatement(node) {
4576
- const createCall = findResponsesCreateCall(node.block);
4577
- if (!createCall) return;
4578
- if (loopDepth > 0) return;
4579
- const handler = node.handler;
4580
- if (!handler) return;
4581
- if (catchHasRetryCall(handler.body)) return;
4582
- context.report({ node: handler, messageId: "noTurnRetry" });
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
+ }
4583
5881
  }
4584
5882
  };
4585
5883
  }
4586
5884
  };
4587
- var openaiCuaRetryTransientTurnErrorsRule = rule57;
5885
+ var tiptapTwitterUrlRegexRule = rule79;
4588
5886
 
4589
- // src/providers/openai-cua/rules/check-response-status-incomplete.ts
4590
- var rule58 = {
5887
+ // src/providers/tiptap/rules/drop-handler-pos-precedence.ts
5888
+ var rule80 = {
4591
5889
  meta: {
4592
5890
  type: "problem",
4593
5891
  docs: {
4594
- description: 'A completion check must rule out response.status === "incomplete" before reporting success',
4595
- category: "reliability",
4596
- rationale: 'The Responses API can return status "incomplete" with zero visible message output when generation is cut off by the token budget \u2014 the same shape ("no computer_call, no message") a purely structural completion check treats as "the model voluntarily finished." Without checking response.status, a token-budget truncation can be silently reported as a successful task completion.',
4597
- docsUrl: "https://developers.openai.com/api/docs/guides/tools-computer-use",
5892
+ description: "x ?? 0 - 1 is parsed as x ?? (0 - 1) = x ?? -1 due to operator precedence",
5893
+ category: "correctness",
5894
+ rationale: "The nullish coalescing operator (??) has lower precedence than arithmetic operators. Writing `pos ?? 0 - 1` evaluates as `pos ?? -1`, not `(pos ?? 0) - 1`. When pos is null or undefined, the fallback is -1 instead of the intended -1 offset from 0. In ProseMirror drop handlers this produces a decoration at position -1, which corrupts the placeholder position map and can insert content at the document start.",
5895
+ docsUrl: "https://prosemirror.net/docs/ref/#view.EditorView.posAtCoords",
4598
5896
  recommended: true
4599
5897
  },
4600
5898
  messages: {
4601
- missingIncompleteCheck: 'This treats the absence of tool calls as a successful completion, but never checks response.status === "incomplete" \u2014 a token-budget truncation can be misreported as success.'
4602
- }
5899
+ posNullCoalescePrecedence: 'Operator precedence bug: "x ?? 0 - 1" is parsed as "x ?? (0 - 1)" = "x ?? -1". Wrap the intended expression: "(x ?? 0) - 1".'
5900
+ },
5901
+ schema: []
4603
5902
  },
4604
5903
  create(context) {
4605
- const stack = [];
4606
- const states = /* @__PURE__ */ new Map();
4607
- function ensureState(fn) {
4608
- let s = states.get(fn);
4609
- if (!s) {
4610
- s = { sawCreateCall: false, sawSuccessReturn: false, successNode: null, sawStatusCheck: false };
4611
- states.set(fn, s);
4612
- }
4613
- return s;
4614
- }
4615
- function top() {
4616
- return stack[stack.length - 1];
4617
- }
4618
- function pushScope(node) {
4619
- stack.push(node);
4620
- ensureState(node);
4621
- }
4622
- function popScope() {
4623
- stack.pop();
4624
- }
4625
- function propName2(node) {
4626
- if (!node) return void 0;
4627
- if (node.type === "Identifier") return node.name;
4628
- if (node.type === "Literal" && typeof node.value === "string") return node.value;
4629
- return void 0;
4630
- }
4631
- function isSuccessObjectLiteral(node) {
4632
- if (node?.type !== "ObjectExpression") return false;
4633
- return (node.properties ?? []).some((p) => {
4634
- if (p?.type !== "Property") return false;
4635
- const keyName = propName2(p.key);
4636
- return (keyName === "success" || keyName === "completed") && p.value?.type === "Literal" && p.value.value === true;
4637
- });
4638
- }
4639
5904
  return {
4640
- Program(node) {
4641
- pushScope(node);
4642
- },
4643
- "Program:exit"() {
4644
- for (const state of states.values()) {
4645
- if (state.sawCreateCall && state.sawSuccessReturn && !state.sawStatusCheck) {
4646
- context.report({ node: state.successNode, messageId: "missingIncompleteCheck" });
4647
- }
5905
+ LogicalExpression(node) {
5906
+ if (node.operator !== "??") return;
5907
+ const right = node.right;
5908
+ if (right?.type === "BinaryExpression" && right.operator === "-" && right.left?.type === "Literal" && right.left.value === 0 && right.right?.type === "Literal" && right.right.value === 1) {
5909
+ context.report({ node, messageId: "posNullCoalescePrecedence" });
4648
5910
  }
4649
- },
4650
- FunctionDeclaration(node) {
4651
- pushScope(node);
4652
- },
4653
- "FunctionDeclaration:exit"() {
4654
- popScope();
4655
- },
4656
- FunctionExpression(node) {
4657
- pushScope(node);
4658
- },
4659
- "FunctionExpression:exit"() {
4660
- popScope();
4661
- },
4662
- ArrowFunctionExpression(node) {
4663
- pushScope(node);
4664
- },
4665
- "ArrowFunctionExpression:exit"() {
4666
- popScope();
4667
- },
4668
- CallExpression(node) {
4669
- if (findResponsesCreateCall(node)) {
4670
- const fn = top();
4671
- if (fn) ensureState(fn).sawCreateCall = true;
5911
+ }
5912
+ };
5913
+ }
5914
+ };
5915
+ var tiptapDropHandlerPosPrecedenceRule = rule80;
5916
+
5917
+ // src/providers/tiptap/rules/prefer-table-kit.ts
5918
+ var INDIVIDUAL_TABLE_PACKAGES = /* @__PURE__ */ new Set([
5919
+ "@tiptap/extension-table-row",
5920
+ "@tiptap/extension-table-cell",
5921
+ "@tiptap/extension-table-header"
5922
+ ]);
5923
+ var rule81 = {
5924
+ meta: {
5925
+ type: "suggestion",
5926
+ docs: {
5927
+ description: "Use TableKit from @tiptap/extension-table instead of individual table packages",
5928
+ category: "integration",
5929
+ rationale: "Importing table extensions individually from their separate packages bypasses the coordinated wiring that TableKit provides: shared configuration, the mergeOrSplit command, and consistent HTMLAttributes across all table elements. Individual imports also leave TableCell and TableHeader unconfigured by default.",
5930
+ docsUrl: "https://tiptap.dev/docs/editor/extensions/functionality/table-kit",
5931
+ recommended: true
5932
+ },
5933
+ messages: {
5934
+ preferTableKit: "Individual TipTap table extension imported. Use TableKit from @tiptap/extension-table for coordinated configuration and the mergeOrSplit command."
5935
+ },
5936
+ schema: []
5937
+ },
5938
+ create(context) {
5939
+ const individualImports = [];
5940
+ return {
5941
+ ImportDeclaration(node) {
5942
+ const src = node?.source?.value ?? "";
5943
+ if (INDIVIDUAL_TABLE_PACKAGES.has(src)) {
5944
+ individualImports.push(node);
4672
5945
  }
4673
5946
  },
4674
- ObjectExpression(node) {
4675
- if (isSuccessObjectLiteral(node)) {
4676
- const fn = top();
4677
- if (fn) {
4678
- const state = ensureState(fn);
4679
- if (!state.sawSuccessReturn) {
4680
- state.sawSuccessReturn = true;
4681
- state.successNode = node;
4682
- }
5947
+ "Program:exit"() {
5948
+ if (individualImports.length >= 2) {
5949
+ for (const node of individualImports) {
5950
+ context.report({ node, messageId: "preferTableKit" });
4683
5951
  }
4684
5952
  }
4685
- },
4686
- BinaryExpression(node) {
4687
- if (node.operator !== "===" && node.operator !== "==") return;
4688
- const sides = [node.left, node.right];
4689
- const statusSide = sides.find((s) => s?.type === "MemberExpression" && propName2(s.property) === "status");
4690
- const valueSide = sides.find((s) => s !== statusSide);
4691
- if (statusSide && valueSide?.type === "Literal" && valueSide.value === "incomplete") {
4692
- const fn = top();
4693
- if (fn) ensureState(fn).sawStatusCheck = true;
4694
- }
4695
5953
  }
4696
5954
  };
4697
5955
  }
4698
5956
  };
4699
- var openaiCuaCheckResponseStatusIncompleteRule = rule58;
5957
+ var tiptapPreferTableKitRule = rule81;
4700
5958
 
4701
- // src/providers/openai-cua/rules/set-safety-identifier.ts
4702
- var rule59 = {
5959
+ // src/providers/tiptap/rules/tiptap-markdown-missing-node-spec.ts
5960
+ var rule82 = {
4703
5961
  meta: {
4704
- type: "problem",
5962
+ type: "suggestion",
4705
5963
  docs: {
4706
- description: "responses.create() must set safety_identifier for per-end-user policy attribution",
5964
+ description: "TipTap nodes used with tiptap-markdown must define a markdown serialization spec",
4707
5965
  category: "integration",
4708
- 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.",
4709
- docsUrl: "https://help.openai.com/en/articles/5428082-how-to-incorporate-a-safety-identifier",
5966
+ rationale: "When tiptap-markdown serializes a document, nodes without a markdown spec are silently dropped or emitted as empty strings. Custom node types must register a serialize/parse pair via MarkdownNodeSpec (in addStorage or addOptions) so content survives markdown export/import cycles.",
5967
+ docsUrl: "https://github.com/ueberdosis/tiptap-markdown",
4710
5968
  recommended: true
4711
5969
  },
4712
5970
  messages: {
4713
- missingSafetyIdentifier: "This responses.create() call sets no safety_identifier (or user) \u2014 a policy violation here could be attributed to the entire shared API key instead of one end user."
4714
- }
5971
+ missingMarkdownNodeSpec: "This TipTap node is used alongside tiptap-markdown but defines no markdown serialization. Nodes without a markdown spec are silently dropped on export. Add a MarkdownNodeSpec to addStorage or addOptions."
5972
+ },
5973
+ schema: []
4715
5974
  },
4716
5975
  create(context) {
4717
- function propName2(node) {
4718
- if (!node) return void 0;
4719
- if (node.type === "Identifier") return node.name;
4720
- if (node.type === "Literal" && typeof node.value === "string") return node.value;
4721
- return void 0;
5976
+ let importsTiptapCore = false;
5977
+ let importsTiptapMarkdown = false;
5978
+ const nodeCreateCalls = [];
5979
+ function hasMarkdownSpec(configObj) {
5980
+ if (configObj?.type !== "ObjectExpression") return false;
5981
+ let found = false;
5982
+ walk(configObj, (node) => {
5983
+ if (found) return false;
5984
+ if (node?.type === "Property") {
5985
+ const keyName = node.key?.type === "Identifier" ? node.key.name : node.key?.type === "Literal" ? String(node.key.value) : null;
5986
+ if (keyName === "markdown") {
5987
+ found = true;
5988
+ return false;
5989
+ }
5990
+ }
5991
+ if (node?.type === "Identifier" && node.name === "MarkdownNodeSpec") {
5992
+ found = true;
5993
+ return false;
5994
+ }
5995
+ });
5996
+ return found;
4722
5997
  }
4723
5998
  return {
5999
+ ImportDeclaration(node) {
6000
+ const src = node?.source?.value ?? "";
6001
+ if (src.startsWith("@tiptap/core")) importsTiptapCore = true;
6002
+ if (src === "tiptap-markdown" || src === "@tiptap/extension-markdown") {
6003
+ importsTiptapMarkdown = true;
6004
+ }
6005
+ },
4724
6006
  CallExpression(node) {
4725
- if (!isResponsesCreateCall(node)) return;
4726
- const optionsArg = node.arguments?.[0];
4727
- if (optionsArg?.type !== "ObjectExpression") return;
4728
- const hasIdentifier = (optionsArg.properties ?? []).some((p) => {
4729
- if (p?.type !== "Property") return false;
4730
- const keyName = propName2(p.key);
4731
- return keyName === "safety_identifier" || keyName === "user";
4732
- });
4733
- if (!hasIdentifier) {
4734
- context.report({ node, messageId: "missingSafetyIdentifier" });
6007
+ if (node.callee?.type === "MemberExpression" && node.callee.property?.name === "create") {
6008
+ const configArg = node.arguments?.[0];
6009
+ if (configArg?.type === "ObjectExpression") {
6010
+ nodeCreateCalls.push({ callNode: node, configObj: configArg });
6011
+ }
6012
+ }
6013
+ },
6014
+ // Also check for MarkdownNodeSpec at program level
6015
+ "Program:exit"() {
6016
+ if (!importsTiptapCore || !importsTiptapMarkdown) return;
6017
+ for (const { callNode, configObj } of nodeCreateCalls) {
6018
+ if (!hasMarkdownSpec(configObj)) {
6019
+ context.report({ node: callNode, messageId: "missingMarkdownNodeSpec" });
6020
+ }
4735
6021
  }
4736
6022
  }
4737
6023
  };
4738
6024
  }
4739
6025
  };
4740
- var openaiCuaSetSafetyIdentifierRule = rule59;
6026
+ var tiptapTiptapMarkdownMissingNodeSpecRule = rule82;
4741
6027
 
4742
6028
  // src/plugin/index.ts
4743
6029
  var plugin = {
@@ -4780,6 +6066,18 @@ var plugin = {
4780
6066
  "firebase-rtdb-listener-error-not-handled": firebaseRtdbListenerErrorNotHandledRule,
4781
6067
  "firebase-effect-deps-whole-user-object": firebaseEffectDepsWholeUserObjectRule,
4782
6068
  "firebase-rtdb-write-promise-not-handled": firebaseRtdbWritePromiseNotHandledRule,
6069
+ "firebase-firestore-rules-expired": firebaseFirestoreRulesExpiredRule,
6070
+ "firebase-id-token-cookie-flags": firebaseIdTokenCookieFlagsRule,
6071
+ "firebase-middleware-token-not-verified": firebaseMiddlewareTokenNotVerifiedRule,
6072
+ "firebase-hardcoded-user-id": firebaseHardcodedUserIdRule,
6073
+ "firebase-auth-user-not-found-disclosure": firebaseAuthUserNotFoundDisclosureRule,
6074
+ "firebase-signup-password-confirm": firebaseSignupPasswordConfirmRule,
6075
+ "firebase-use-array-union-remove": firebaseUseArrayUnionRemoveRule,
6076
+ "firebase-duplicate-initialize-app": firebaseDuplicateInitializeAppRule,
6077
+ "firebase-onSnapshot-async-throw": firebaseOnSnapshotAsyncThrowRule,
6078
+ "firebase-onSnapshot-missing-error-callback": firebaseOnSnapshotMissingErrorCallbackRule,
6079
+ "firebase-firestore-document-size-guard": firebaseFirestoreDocumentSizeGuardRule,
6080
+ "firebase-use-timestamp-now": firebaseUseTimestampNowRule,
4783
6081
  "lovable-no-client-side-secret-fetch": lovableNoClientSideSecretFetchRule,
4784
6082
  "lovable-paid-flag-without-edge-function": lovablePaidFlagWithoutEdgeFunctionRule,
4785
6083
  "lovable-expiry-column-never-checked": lovableExpiryColumnNeverCheckedRule,
@@ -4801,7 +6099,18 @@ var plugin = {
4801
6099
  "openai-cua-no-blind-safety-check-ack": openaiCuaNoBlindSafetyCheckAckRule,
4802
6100
  "openai-cua-retry-transient-turn-errors": openaiCuaRetryTransientTurnErrorsRule,
4803
6101
  "openai-cua-check-response-status-incomplete": openaiCuaCheckResponseStatusIncompleteRule,
4804
- "openai-cua-set-safety-identifier": openaiCuaSetSafetyIdentifierRule
6102
+ "openai-cua-set-safety-identifier": openaiCuaSetSafetyIdentifierRule,
6103
+ "tiptap-upload-validate-fn-void": tiptapUploadValidateFnVoidRule,
6104
+ "tiptap-script-src-hardcoded-api-key": tiptapScriptSrcHardcodedApiKeyRule,
6105
+ "tiptap-dynamic-script-no-sri": tiptapDynamicScriptNoSriRule,
6106
+ "tiptap-addAttributes-missing-renderHTML": tiptapAddAttributesMissingRenderHTMLRule,
6107
+ "tiptap-appendTransaction-add-to-history": tiptapAppendTransactionAddToHistoryRule,
6108
+ "tiptap-appendTransaction-full-scan": tiptapAppendTransactionFullScanRule,
6109
+ "tiptap-atom-node-wrap-in": tiptapAtomNodeWrapInRule,
6110
+ "tiptap-twitter-url-regex": tiptapTwitterUrlRegexRule,
6111
+ "tiptap-drop-handler-pos-precedence": tiptapDropHandlerPosPrecedenceRule,
6112
+ "tiptap-prefer-table-kit": tiptapPreferTableKitRule,
6113
+ "tiptap-tiptap-markdown-missing-node-spec": tiptapTiptapMarkdownMissingNodeSpecRule
4805
6114
  }
4806
6115
  };
4807
6116
  var plugin_default = plugin;