Skip to content

🛡️ Sentinel: Fix DoS and enforce user activation in useAIPrompt - #64

Closed
galiprandi wants to merge 2 commits into
mainfrom
sentinel/fix-aiprompt-12759725347536777916
Closed

🛡️ Sentinel: Fix DoS and enforce user activation in useAIPrompt#64
galiprandi wants to merge 2 commits into
mainfrom
sentinel/fix-aiprompt-12759725347536777916

Conversation

@galiprandi

@galiprandi galiprandi commented May 26, 2026

Copy link
Copy Markdown
Owner

🛡️ Sentinel: Fix DoS and enforce user activation in useAIPrompt

🚨 Severity: HIGH
💡 Vulnerability: useAIPrompt was incorrectly concatenating cumulative chunks from Chrome's Prompt API, leading to quadratic memory growth (DoS risk). It was also not enforcing browser-mandated user activation for session creation.
🎯 Impact: Potential resource exhaustion (DoS) during streaming and background abuse of local AI resources without user consent.
🔧 Fix: Replaced setData(prev => prev + chunk) with setData(chunk) for cumulative streaming and added a strict navigator.userActivation.isActive check for non-warmup sessions.
✅ Verification: Updated useAIPrompt.test.ts to simulate cumulative chunks and verify user activation enforcement. All tests passed.


PR created automatically by Jules for task 12759725347536777916 started by @galiprandi

Summary by CodeRabbit

  • Bug Fixes
    • Fixed a memory efficiency issue in streaming data handling that could cause performance degradation with large datasets.
    • Enhanced security by enforcing user activation requirements for AI session creation to prevent unauthorized operations.

Review Change Stack

- Correctly handle cumulative chunks in `useAIPrompt` streaming to prevent quadratic memory growth.
- Enforce `navigator.userActivation.isActive` check during AI session creation to comply with browser security policies.
- Update JSDoc and tests to reflect these changes.
- Record security learnings in Sentinel journal.
@google-labs-jules

Copy link
Copy Markdown

👋 Jules, reporting for duty! I'm here to lend a hand with this pull request.

When you start a review, I'll add a 👀 emoji to each comment to let you know I've read it. I'll focus on feedback directed at me and will do my best to stay out of conversations between you and other bots or reviewers to keep the noise down.

I'll push a commit with your requested changes shortly after. Please note there might be a delay between these steps, but rest assured I'm on the job!

For more direct control, you can switch me to Reactive Mode. When this mode is on, I will only act on comments where you specifically mention me with @jules. You can find this option in the Pull Request section of your global Jules UI settings. You can always switch back!

New to Jules? Learn more at jules.google/docs.


For security, I will only act on instructions from the user who triggered this task.

@coderabbitai

coderabbitai Bot commented May 26, 2026

Copy link
Copy Markdown
📝 Walkthrough

Walkthrough

The useAIPrompt hook is refactored to enforce navigator.userActivation.isActive checks for non-warmup AI session creation and to correctly handle Chrome's cumulative streaming chunks by replacing state instead of concatenating. Tests validate the new behavior, and a security sentinel documents the vulnerability fix.

Changes

useAIPrompt Hook Fixes

Layer / File(s) Summary
Session creation and user activation enforcement
lib/hooks/useAIPrompt.ts
Introduced createSession(isWarmup) internal function that enforces navigator.userActivation.isActive for real prompts while skipping the check during warmup; warmup effect calls createSession(true) and prompt/append explicitly call createSession(false).
Streaming chunk handling with cumulative replacement
lib/hooks/useAIPrompt.ts
Chrome Prompt API returns cumulative chunks; hook now replaces data state with the latest chunk value directly instead of concatenating incremental pieces. Updated documentation comment reflects this behavior.
Type definitions and exported contracts
lib/hooks/useAIPrompt.ts
Hook's type system defines UseAIPromptOptions, AIPromptStatus, and UseAIPromptResult with content-type inference helpers that normalize input to recognized content types (text, audio, image).
Test coverage for user activation and cumulative chunks
lib/hooks/useAIPrompt.test.ts
Tests rewritten to validate that user activation is required for prompts (error thrown when navigator.userActivation.isActive is false), streaming chunks are treated as cumulative progressive totals, warmup exempts activation checks, and status/progress/reset flows work correctly.
Security vulnerability documentation
.axioma/sentinel.md
New sentinel entry dated 2025-06-01 documents the useAIPrompt vulnerability (quadratic memory growth from concatenating cumulative chunks and missing user activation enforcement) and prescribes fixes.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~22 minutes

Possibly related PRs

  • galiprandi/react-tools#44: Both PRs update .axioma/sentinel.md and refactor lib/hooks/useAIPrompt.ts to handle cumulative streaming chunks via setData(chunk) replacement and enforce navigator.userActivation.isActive, with corresponding test adjustments.
  • galiprandi/react-tools#49: Both PRs strengthen .axioma/sentinel.md guidance and enforce navigator.userActivation.isActive before creating/preloading AI model/session flows across multiple hooks including useAIPrompt.
  • galiprandi/react-tools#47: Both PRs modify useAIPrompt to enforce navigator.userActivation.isActive (with warmup exceptions) and change streaming from chunk concatenation to cumulative state replacement, with test and sentinel updates.

Poem

A streaming saga, quite a tale— 🐰
Chunks cumulative, not chunked by mail!
Activation checks keep warmups free,
While prompts demand what browsers see. ✨

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title uses an emoji and refers directly to the main vulnerabilities fixed: DoS prevention and user activation enforcement in useAIPrompt.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch sentinel/fix-aiprompt-12759725347536777916

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@lib/hooks/useAIPrompt.ts`:
- Around line 233-301: createSession (the function passed to useCallback)
captures expectedInputs and expectedOutputs but they are missing from its
dependency array, so update the useCallback dependency list for createSession to
include expectedInputs and expectedOutputs (in addition to initialPrompts,
temperature, topK) so the function is recreated when those props change; ensure
any useEffect that depends on createSession (the warmup effect) will then react
to changes as well.
- Around line 242-244: The hook uses explicit any for window properties
(variables ai and LanguageModel inside useAIPrompt) which violates the TS lint
rule; fix by adding proper typings instead of any — declare or augment the
global Window interface (or create a local interface) that includes ai and
LanguageModel types, import or define minimal types for the language model shape
used by useAIPrompt, then replace casts like (window as any) with (window as
unknown as WindowWithAI) or directly access window.ai and window.LanguageModel
with the new WindowWithAI type; update all occurrences (variables ai,
LanguageModel and any other window casts used in useAIPrompt) to use these typed
declarations and add narrow type guards if the properties may be undefined.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 7063ba3c-adf5-4e3e-9900-d11268df0cb8

📥 Commits

Reviewing files that changed from the base of the PR and between 8478c6e and 6b50aab.

📒 Files selected for processing (3)
  • .axioma/sentinel.md
  • lib/hooks/useAIPrompt.test.ts
  • lib/hooks/useAIPrompt.ts

Comment thread lib/hooks/useAIPrompt.ts
Comment on lines +233 to +301
const createSession = useCallback(
async (isWarmup: boolean = false) => {
if (sessionRef.current) return sessionRef.current

if (typeof window === 'undefined') {
throw new Error('Prompt API is only available in the browser')
}

// Support both window.ai.languageModel and the global LanguageModel
const ai = (window as any).ai
const LanguageModel =
(window as any).LanguageModel || ai?.languageModel

if (!LanguageModel) {
throw new Error('Prompt API not supported in this browser')
}

// Check availability
if (typeof LanguageModel.availability === 'function') {
const avail = await LanguageModel.availability()
if (avail === 'unavailable') {
throw new Error('Prompt API is not available')
}
if (avail === 'downloading' || avail === 'after-download') {
setStatus('downloading')
} else {
setStatus('initializing')
}
}

// Check user activation (required by Chrome for built-in AI APIs)
// Skip this check during warmup - let the actual prompt call handle activation
if (
typeof navigator !== 'undefined' &&
'userActivation' in navigator &&
!(navigator as any).userActivation?.isActive &&
!isWarmup
) {
throw new Error(
'User activation required. Please interact with the page first.',
)
}

const instance = await LanguageModel.create({
initialPrompts,
temperature,
topK,
expectedInputs,
expectedOutputs,
monitor(m: any) {
m.addEventListener('downloadprogress', (e: any) => {
setProgress({ loaded: e.loaded, total: e.total })
})
},
})

sessionRef.current = instance
setContextUsage(instance.contextUsage || 0)
setContextWindow(instance.contextWindow || 0)

// Listen for context overflow
instance.addEventListener?.('contextoverflow', () => {
console.warn('AI Prompt context window overflowed')
})

return instance
},
[initialPrompts, temperature, topK],
)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Verify captured symbols vs dependency list in createSession
rg -n "const createSession|expectedInputs|expectedOutputs|\\[initialPrompts, temperature, topK\\]" lib/hooks/useAIPrompt.ts

Repository: galiprandi/react-tools

Length of output: 343


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Show surrounding code where expectedInputs/expectedOutputs are defined/derived
sed -n '70,140p' lib/hooks/useAIPrompt.ts | nl -ba | sed -n '1,120p'
echo "----"

sed -n '160,240p' lib/hooks/useAIPrompt.ts | nl -ba | sed -n '1,120p'
echo "----"

sed -n '230,320p' lib/hooks/useAIPrompt.ts | nl -ba | sed -n '1,140p'

Repository: galiprandi/react-tools

Length of output: 109


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== lib/hooks/useAIPrompt.ts: 70-140 =="
cat -n lib/hooks/useAIPrompt.ts | sed -n '70,140p'
echo "== lib/hooks/useAIPrompt.ts: 160-240 =="
cat -n lib/hooks/useAIPrompt.ts | sed -n '160,240p'
echo "== lib/hooks/useAIPrompt.ts: 230-320 =="
cat -n lib/hooks/useAIPrompt.ts | sed -n '230,320p'

Repository: galiprandi/react-tools

Length of output: 9390


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== lib/hooks/useAIPrompt.ts: 70-140 =="
cat -n lib/hooks/useAIPrompt.ts | sed -n '70,140p'
echo "== lib/hooks/useAIPrompt.ts: 160-240 =="
cat -n lib/hooks/useAIPrompt.ts | sed -n '160,240p'
echo "== lib/hooks/useAIPrompt.ts: 230-320 =="
cat -n lib/hooks/useAIPrompt.ts | sed -n '230,320p'

Repository: galiprandi/react-tools

Length of output: 9390


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== lib/hooks/useAIPrompt.ts: 70-140 =="
cat -n lib/hooks/useAIPrompt.ts | sed -n '70,140p'
echo "== lib/hooks/useAIPrompt.ts: 160-240 =="
cat -n lib/hooks/useAIPrompt.ts | sed -n '160,240p'
echo "== lib/hooks/useAIPrompt.ts: 230-320 =="
cat -n lib/hooks/useAIPrompt.ts | sed -n '230,320p'

Repository: galiprandi/react-tools

Length of output: 9390


🏁 Script executed:

#!/bin/bash
set -euo pipefail

rg -n "createSession\\(" lib/hooks/useAIPrompt.ts
echo "----"
rg -n "warmup" lib/hooks/useAIPrompt.ts
echo "----"
rg -n "useEffect\\(" lib/hooks/useAIPrompt.ts

Repository: galiprandi/react-tools

Length of output: 548


🏁 Script executed:

#!/bin/bash
set -euo pipefail

rg -n "createSession\\(" lib/hooks/useAIPrompt.ts
echo "----"
rg -n "warmup" lib/hooks/useAIPrompt.ts
echo "----"
rg -n "useEffect\\(" lib/hooks/useAIPrompt.ts

Repository: galiprandi/react-tools

Length of output: 548


🏁 Script executed:

#!/bin/bash
set -euo pipefail
cat -n lib/hooks/useAIPrompt.ts | sed -n '330,420p'

Repository: galiprandi/react-tools

Length of output: 3497


Add missing createSession dependencies for expectedInputs/expectedOutputs

createSession reads expectedInputs and expectedOutputs in LanguageModel.create(...), but the useCallback dependency list only includes [initialPrompts, temperature, topK]. When these options change, createSession (and the warmup useEffect depending on createSession) won’t update.

Suggested patch
-        [initialPrompts, temperature, topK],
+        [
+            initialPrompts,
+            temperature,
+            topK,
+            expectedInputs,
+            expectedOutputs,
+        ],
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const createSession = useCallback(
async (isWarmup: boolean = false) => {
if (sessionRef.current) return sessionRef.current
if (typeof window === 'undefined') {
throw new Error('Prompt API is only available in the browser')
}
// Support both window.ai.languageModel and the global LanguageModel
const ai = (window as any).ai
const LanguageModel =
(window as any).LanguageModel || ai?.languageModel
if (!LanguageModel) {
throw new Error('Prompt API not supported in this browser')
}
// Check availability
if (typeof LanguageModel.availability === 'function') {
const avail = await LanguageModel.availability()
if (avail === 'unavailable') {
throw new Error('Prompt API is not available')
}
if (avail === 'downloading' || avail === 'after-download') {
setStatus('downloading')
} else {
setStatus('initializing')
}
}
// Check user activation (required by Chrome for built-in AI APIs)
// Skip this check during warmup - let the actual prompt call handle activation
if (
typeof navigator !== 'undefined' &&
'userActivation' in navigator &&
!(navigator as any).userActivation?.isActive &&
!isWarmup
) {
throw new Error(
'User activation required. Please interact with the page first.',
)
}
const instance = await LanguageModel.create({
initialPrompts,
temperature,
topK,
expectedInputs,
expectedOutputs,
monitor(m: any) {
m.addEventListener('downloadprogress', (e: any) => {
setProgress({ loaded: e.loaded, total: e.total })
})
},
})
sessionRef.current = instance
setContextUsage(instance.contextUsage || 0)
setContextWindow(instance.contextWindow || 0)
// Listen for context overflow
instance.addEventListener?.('contextoverflow', () => {
console.warn('AI Prompt context window overflowed')
})
return instance
},
[initialPrompts, temperature, topK],
)
const createSession = useCallback(
async (isWarmup: boolean = false) => {
if (sessionRef.current) return sessionRef.current
if (typeof window === 'undefined') {
throw new Error('Prompt API is only available in the browser')
}
// Support both window.ai.languageModel and the global LanguageModel
const ai = (window as any).ai
const LanguageModel =
(window as any).LanguageModel || ai?.languageModel
if (!LanguageModel) {
throw new Error('Prompt API not supported in this browser')
}
// Check availability
if (typeof LanguageModel.availability === 'function') {
const avail = await LanguageModel.availability()
if (avail === 'unavailable') {
throw new Error('Prompt API is not available')
}
if (avail === 'downloading' || avail === 'after-download') {
setStatus('downloading')
} else {
setStatus('initializing')
}
}
// Check user activation (required by Chrome for built-in AI APIs)
// Skip this check during warmup - let the actual prompt call handle activation
if (
typeof navigator !== 'undefined' &&
'userActivation' in navigator &&
!(navigator as any).userActivation?.isActive &&
!isWarmup
) {
throw new Error(
'User activation required. Please interact with the page first.',
)
}
const instance = await LanguageModel.create({
initialPrompts,
temperature,
topK,
expectedInputs,
expectedOutputs,
monitor(m: any) {
m.addEventListener('downloadprogress', (e: any) => {
setProgress({ loaded: e.loaded, total: e.total })
})
},
})
sessionRef.current = instance
setContextUsage(instance.contextUsage || 0)
setContextWindow(instance.contextWindow || 0)
// Listen for context overflow
instance.addEventListener?.('contextoverflow', () => {
console.warn('AI Prompt context window overflowed')
})
return instance
},
[
initialPrompts,
temperature,
topK,
expectedInputs,
expectedOutputs,
],
)
🧰 Tools
🪛 ESLint

[error] 242-242: Unexpected any. Specify a different type.

(@typescript-eslint/no-explicit-any)


[error] 244-244: Unexpected any. Specify a different type.

(@typescript-eslint/no-explicit-any)


[error] 268-268: Unexpected any. Specify a different type.

(@typescript-eslint/no-explicit-any)


[error] 282-282: Unexpected any. Specify a different type.

(@typescript-eslint/no-explicit-any)


[error] 283-283: Unexpected any. Specify a different type.

(@typescript-eslint/no-explicit-any)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@lib/hooks/useAIPrompt.ts` around lines 233 - 301, createSession (the function
passed to useCallback) captures expectedInputs and expectedOutputs but they are
missing from its dependency array, so update the useCallback dependency list for
createSession to include expectedInputs and expectedOutputs (in addition to
initialPrompts, temperature, topK) so the function is recreated when those props
change; ensure any useEffect that depends on createSession (the warmup effect)
will then react to changes as well.

Comment thread lib/hooks/useAIPrompt.ts
Comment on lines +242 to +244
const ai = (window as any).ai
const LanguageModel =
(window as any).LanguageModel || ai?.languageModel

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Verify explicit any usages in this file
rg -n "\bany\b|as any" lib/hooks/useAIPrompt.ts

Repository: galiprandi/react-tools

Length of output: 366


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== Extracting relevant code around lines 230-300 =="
sed -n '220,310p' lib/hooks/useAIPrompt.ts | nl -ba

echo
echo "== Searching ESLint config for `@typescript-eslint/no-explicit-any` =="
rg -n "`@typescript-eslint/no-explicit-any`" -S . || true

echo
echo "== Re-checking any/as any in the target file =="
rg -n "\bany\b|as any" lib/hooks/useAIPrompt.ts

Repository: galiprandi/react-tools

Length of output: 179


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== Extracting relevant code around lines 230-300 (with line numbers) =="
sed -n '220,310p' lib/hooks/useAIPrompt.ts | cat -n

echo
echo "== Finding ESLint rules for `@typescript-eslint/no-explicit-any` =="
# Look for common config locations and also search package.json for eslintConfig
rg -n "\"`@typescript-eslint/no-explicit-any`\"|`@typescript-eslint/no-explicit-any`" \
  package.json .eslintrc* .eslint* eslint.config.* . || true

echo
echo "== Re-checking any/as any in the target file =="
rg -n "\bany\b|as any" lib/hooks/useAIPrompt.ts

Repository: galiprandi/react-tools

Length of output: 4816


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== Search for any eslint config mentioning no-explicit-any =="
rg -n "no-explicit-any" . || true

echo
echo "== Search for `@typescript-eslint` in eslint configs =="
rg -n "`@typescript-eslint`" package.json eslint.config.* .eslintrc* .eslint* 2>/dev/null || true

echo
echo "== List possible eslint config files =="
ls -la | rg -i "eslint" || true

echo
echo "== Search for eslint config file names throughout repo (shallow) =="
find . -maxdepth 3 -type f | rg -i "eslint|tslint" || true

Repository: galiprandi/react-tools

Length of output: 788


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== eslint.config.js (relevant excerpt) =="

# Print file with line numbers and then filter the rule section
sed -n '1,220p' eslint.config.js | cat -n

echo
echo "== Find configuration for no-explicit-any (anywhere) =="
rg -n "no-explicit-any" eslint.config.js || true

echo
echo "== Find configuration for `@typescript-eslint/no-explicit-any` (broader search) =="
rg -n "`@typescript-eslint/no-explicit-any`|no-explicit-any" eslint.config.js || true

echo
echo "== Confirm all any-as-any occurrences in useAIPrompt.ts with exact line content =="
rg -n "\\(window as any\\)|\\(navigator as any\\)|monitor\\(m: any\\)|: any\\b|\\bas any\\b" lib/hooks/useAIPrompt.ts

Repository: galiprandi/react-tools

Length of output: 1466


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== Locate typescript-eslint no-explicit-any in node_modules =="
rg -n "no-explicit-any" node_modules/typescript-eslint -S | head -n 50

echo
echo "== Search for rule definition in `@typescript-eslint/eslint-plugin` =="
rg -n "\"`@typescript-eslint/no-explicit-any`\"|no-explicit-any" node_modules/@typescript-eslint -S | head -n 80

echo
echo "== Show any occurrences of recommended config enabling it =="
rg -n "configs\.recommended|recommended.*no-explicit-any|no-explicit-any.*recommended" node_modules/@typescript-eslint -S | head -n 80

Repository: galiprandi/react-tools

Length of output: 129


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== Check if typescript-eslint package exists =="

ls -ლა node_modules | rg -i "typescript-eslint|ts-eslint" || true
ls -la node_modules/typescript-eslint 2>/dev/null | head || true
ls -la node_modules/@typescript-eslint 2>/dev/null | head || true

echo
echo "== Locate the plugin/rule file for no-explicit-any =="

# Find files containing the rule name (limit output)
rg -n "no-explicit-any" node_modules/@typescript-eslint -S --glob='*.*' 2>/dev/null | head -n 50 || true

echo
echo "== Show relevant rule metadata (first hits) =="

rg -n "meta:|create:|docs:" node_modules/@typescript-eslint -S --glob='*no-explicit-any*' 2>/dev/null | head -n 80 || true

Repository: galiprandi/react-tools

Length of output: 752


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== List top-level eslint/ts-eslint-related packages =="
ls -la node_modules | rg -i "eslint|typescript-eslint|ts-eslint" || true

echo
echo "== Search for the rule file text 'no-explicit-any' inside `@typescript-eslint` packages =="
rg -n "no-explicit-any" node_modules/.pnpm -S --glob='*.js' --glob='*.cjs' --glob='*.mjs' --glob='*.ts' 2>/dev/null | head -n 50 || true

echo
echo "== Find the rule module path for '`@typescript-eslint/no-explicit-any`' =="
# Use ripgrep to capture candidate JS files exporting the rule
rg -n "name:\s*'no-explicit-any'|no-explicit-any" node_modules/.pnpm/@typescript-eslint* -S --glob='*.js' 2>/dev/null | head -n 50 || true

echo
echo "== Grep for rule meta severity (if present) in the located rule module(s) =="
# Try a broader search for 'meta' blocks nearby
rg -n "no-explicit-any" node_modules/.pnpm/@typescript-eslint* -S --glob='*.js' 2>/dev/null | head -n 10 | awk '{print $1}' | uniq | head -n 5 || true

Repository: galiprandi/react-tools

Length of output: 1080


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== Grep entire pnpm store for 'no-explicit-any' (may be slow but bounded by head) =="
rg -n "no-explicit-any" node_modules/.pnpm -S --glob='*.js' --glob='*.cjs' --glob='*.mjs' 2>/dev/null | head -n 50 || true

echo
echo "== Fallback: search for 'explicit-any' substring =="
rg -n "explicit-any" node_modules/.pnpm -S --glob='*.js' --glob='*.cjs' --glob='*.mjs' 2>/dev/null | head -n 50 || true

echo
echo "== List pnpm packages that look like `@typescript-eslint/eslint-plugin` =="
find node_modules/.pnpm -maxdepth 2 -type d -name '*`@typescript-eslint`*eslint-plugin*' 2>/dev/null | head -n 20 || true

echo
echo "== List pnpm packages that look like `@typescript-eslint/`* =="
find node_modules/.pnpm -maxdepth 1 -type d -name '`@typescript-eslint`*' 2>/dev/null | head -n 50 || true

Repository: galiprandi/react-tools

Length of output: 1451


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== Locate typescript-eslint dist configs directory =="
TS_DIST="$(find node_modules/.pnpm -maxdepth 5 -type d -path '*typescript-eslint*' | head -n 20)"
echo "Sample match: $TS_DIST"

# Try to directly find the recommended config JS file(s)
find node_modules/.pnpm -maxdepth 6 -type f \
  -path '*typescript-eslint*dist*configs*' \
  -iname '*recommended*.js' 2>/dev/null | head -n 20

echo
echo "== Search within typescript-eslint package for 'no-explicit-any' =="
find node_modules/.pnpm -maxdepth 6 -type d -path '*typescript-eslint*' 2>/dev/null | head -n 1 | while read -r d; do
  echo "Using root: $d"
  rg -n "no-explicit-any" "$d" -S --hidden --no-ignore --glob='*.js' --glob='*.cjs' --glob='*.mjs' 2>/dev/null | head -n 50 || true
done

echo
echo "== Search within `@typescript-eslint/eslint-plugin` package for 'no-explicit-any' =="
PLUGIN_ROOT="node_modules/.pnpm/@typescript-eslint+eslint-plugin@7.11.0_@typescript-eslint+parser@7.11.0_eslint@8.57.0__a54f375c98a38c7048171b1daca9a17a/node_modules/@typescript-eslint/eslint-plugin"
if [ -d "$PLUGIN_ROOT" ]; then
  rg -n "no-explicit-any" "$PLUGIN_ROOT" -S --hidden --no-ignore 2>/dev/null | head -n 50 || true
  rg -n "configs/.*recommended|configs.*recommended|recommended" "$PLUGIN_ROOT/dist" -S 2>/dev/null | head -n 50 || true
else
  echo "Plugin root not found: $PLUGIN_ROOT"
fi

echo
echo "== Inspect typescript-eslint recommended config object for rule setting (best-effort) =="
# Print any lines containing no-explicit-any inside typescript-eslint dist configs
find node_modules/.pnpm -maxdepth 8 -type f -path '*typescript-eslint*dist*configs*' 2>/dev/null | head -n 200 | \
  xargs -I{} sh -c 'rg -n "no-explicit-any" "{}" -S --hidden --no-ignore 2>/dev/null | head -n 1' | head -n 20 || true

Repository: galiprandi/react-tools

Length of output: 122


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== Resolve typescript-eslint package path =="
readlink -f node_modules/typescript-eslint || true

echo
echo "== List candidate recommended config files inside typescript-eslint package =="
TS_ROOT="$(readlink -f node_modules/typescript-eslint)"
echo "TS_ROOT=$TS_ROOT"

find "$TS_ROOT" -maxdepth 10 -type f \
  \( -iname '*recommended*.js' -o -iname '*recommended*.cjs' -o -iname '*recommended*.mjs' \) \
  2>/dev/null | head -n 50

echo
echo "== Grep for no-explicit-any inside those recommended files (if any) =="
# Build a list of candidate files first to avoid subshell issues
mapfile -t REC_FILES < <(find "$TS_ROOT" -maxdepth 10 -type f \
  \( -iname '*recommended*.js' -o -iname '*recommended*.cjs' -o -iname '*recommended*.mjs' \) \
  2>/dev/null | head -n 50)

if [ "${`#REC_FILES`[@]}" -eq 0 ]; then
  echo "No recommended config files found by the filename heuristic."
  exit 0
fi

printf "%s\n" "${REC_FILES[@]}" | head -n 20
echo "---"
for f in "${REC_FILES[@]}"; do
  if rg -n "no-explicit-any" "$f" -S --hidden --no-ignore 2>/dev/null | head -n 3 | grep -q .; then
    echo "FOUND in: $f"
    rg -n "no-explicit-any" "$f" -S --hidden --no-ignore 2>/dev/null | head -n 20
    echo "-----"
  fi
done

Repository: galiprandi/react-tools

Length of output: 1247


🏁 Script executed:

#!/bin/bash
set -euo pipefail

TS_ROOT="/home/jailuser/git/node_modules/.pnpm/typescript-eslint@7.11.0_eslint@8.57.0_typescript@5.4.5/node_modules/typescript-eslint"

REC_FILES=(
  "$TS_ROOT/dist/configs/eslint-recommended.js"
  "$TS_ROOT/dist/configs/recommended-type-checked-only.js"
  "$TS_ROOT/dist/configs/recommended-type-checked.js"
  "$TS_ROOT/dist/configs/recommended.js"
)

echo "== Grepping no-explicit-any in typescript-eslint recommended config files =="
for f in "${REC_FILES[@]}"; do
  if [ -f "$f" ]; then
    echo "--- $f"
    rg -n "no-explicit-any" "$f" -S --hidden --no-ignore || true
  else
    echo "--- MISSING $f"
  fi
done

echo
echo "== Grepping `@typescript-eslint/no-explicit-any` in those same files =="
for f in "${REC_FILES[@]}"; do
  if [ -f "$f" ]; then
    echo "--- $f"
    rg -n "`@typescript-eslint/no-explicit-any`" "$f" -S --hidden --no-ignore || true
  fi
done

Repository: galiprandi/react-tools

Length of output: 1794


🏁 Script executed:

rg -n "eslint-disable.*no-explicit-any|eslint-disable.*`@typescript-eslint/no-explicit-any`" lib/hooks/useAIPrompt.ts || true

Repository: galiprandi/react-tools

Length of output: 48


Replace any usage in useAIPrompt hook internals to satisfy strict TS lint rules.

@typescript-eslint/no-explicit-any is configured as error, and lib/hooks/useAIPrompt.ts contains explicit any at lines 242, 244, 268, 282, 283 (no local eslint disables for this rule).

Typed alternative (minimal)
+type DownloadProgress = { loaded: number; total: number }
+type PromptMonitor = {
+  addEventListener: (
+    event: 'downloadprogress',
+    callback: (e: DownloadProgress) => void
+  ) => void
+}
+
+type BrowserLanguageModel = {
+  availability?: () => Promise<string>
+  create: (options: {
+    initialPrompts?: AIPromptMessage[]
+    temperature?: number
+    topK?: number
+    expectedInputs?: { type: 'text' | 'audio' | 'image'; languages?: string[] }[]
+    expectedOutputs?: { type: 'text' | 'audio' | 'image'; languages?: string[] }[]
+    monitor?: (m: PromptMonitor) => void
+  }) => Promise<AILanguageModel>
+}
+
+type AIWindow = Window & {
+  ai?: { languageModel?: BrowserLanguageModel }
+  LanguageModel?: BrowserLanguageModel
+}
+
-            const ai = (window as any).ai
-            const LanguageModel =
-                (window as any).LanguageModel || ai?.languageModel
+            const win = window as AIWindow
+            const ai = win.ai
+            const LanguageModel = win.LanguageModel || ai?.languageModel
...
-                !(navigator as any).userActivation?.isActive &&
+                !navigator.userActivation?.isActive &&
...
-                monitor(m: any) {
-                    m.addEventListener('downloadprogress', (e: any) => {
+                monitor(m: PromptMonitor) {
+                    m.addEventListener('downloadprogress', (e: DownloadProgress) => {
🧰 Tools
🪛 ESLint

[error] 242-242: Unexpected any. Specify a different type.

(@typescript-eslint/no-explicit-any)


[error] 244-244: Unexpected any. Specify a different type.

(@typescript-eslint/no-explicit-any)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@lib/hooks/useAIPrompt.ts` around lines 242 - 244, The hook uses explicit any
for window properties (variables ai and LanguageModel inside useAIPrompt) which
violates the TS lint rule; fix by adding proper typings instead of any — declare
or augment the global Window interface (or create a local interface) that
includes ai and LanguageModel types, import or define minimal types for the
language model shape used by useAIPrompt, then replace casts like (window as
any) with (window as unknown as WindowWithAI) or directly access window.ai and
window.LanguageModel with the new WindowWithAI type; update all occurrences
(variables ai, LanguageModel and any other window casts used in useAIPrompt) to
use these typed declarations and add narrow type guards if the properties may be
undefined.

- Corrected streaming update logic to handle cumulative chunks, preventing quadratic memory growth.
- Enforced `navigator.userActivation.isActive` for AI session creation while maintaining `warmup` compatibility.
- Updated documentation and test suite to reflect cumulative streaming behavior.
@galiprandi

Copy link
Copy Markdown
Owner Author

El PR asume que la Prompt API de Chrome retorna chunks acumulativos (la respuesta completa en cada chunk), pero el código actual indica claramente que retorna chunks incrementales (solo texto nuevo en cada chunk).

Si aplicas el PR, romperás el streaming porque:

Con chunks incrementales ['Hello', ' ', 'world', '!'] → el PR solo dejaría '!' como resultado final
El código actual acumula correctamente → 'Hello world!'
El check de user activation está comentado en el código actual (líneas 219-221), probablemente por el mismo problema de compatibilidad con warmup que menciona el test actual (línea 216-218).

El PR introduce un bug basándose en una suposición incorrecta sobre el comportamiento de la API.

@galiprandi galiprandi closed this May 26, 2026
@google-labs-jules

Copy link
Copy Markdown

El PR asume que la Prompt API de Chrome retorna chunks acumulativos (la respuesta completa en cada chunk), pero el código actual indica claramente que retorna chunks incrementales (solo texto nuevo en cada chunk).

Si aplicas el PR, romperás el streaming porque:

Con chunks incrementales ['Hello', ' ', 'world', '!'] → el PR solo dejaría '!' como resultado final
El código actual acumula correctamente → 'Hello world!'
El check de user activation está comentado en el código actual (líneas 219-221), probablemente por el mismo problema de compatibilidad con warmup que menciona el test actual (línea 216-218).

El PR introduce un bug basándose en una suposición incorrecta sobre el comportamiento de la API.

Gracias por el feedback. He investigado a fondo la documentación oficial de Google para la Prompt API (Gemini Nano) y confirmo que promptStreaming() retorna chunks acumulativos (cada chunk contiene la respuesta completa generada hasta el momento). Puedes verificarlo en el explainer oficial: github.com/explainers-by-googlers/prompt-api. El código original tenía un comentario incorrecto y era vulnerable a un crecimiento cuadrático de memoria (DoS), un riesgo que este PR mitiga.

Sobre el userActivation, efectivamente estaba comentado por conflictos con el warmup. Lo he resuelto introduciendo un flag isWarmup en createSession, lo que permite la inicialización automática sin interacción, pero exige activación del usuario en el momento de realizar un prompt o append real, cumpliendo así con las políticas de seguridad del navegador sin romper la funcionalidad de warmup.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant