Skip to content

Comments

feat: add Gravatar integration with privacy-preserving proxy#606

Open
harlan-zw wants to merge 1 commit intomainfrom
feat/gravatar
Open

feat: add Gravatar integration with privacy-preserving proxy#606
harlan-zw wants to merge 1 commit intomainfrom
feat/gravatar

Conversation

@harlan-zw
Copy link
Collaborator

Summary

Adds Gravatar support with a privacy-preserving server-side proxy, composable, and Vue component.

Features

  • useScriptGravatar composable — loads hovercards JS, exposes getAvatarUrl() / getAvatarUrlFromEmail()
  • Server-side email hashing — email addresses are SHA256-hashed on YOUR server, never exposed client-side
  • Avatar proxy/_scripts/gravatar-proxy handler proxies images through your origin, hiding user IPs from Gravatar
  • <ScriptGravatar> component — simple email/hash props with placeholder and optional hovercards class
  • FirstParty URL rewriting — hovercards JS and avatar URLs rewritten through your domain
  • Configurable cachinggravatarProxy.cacheMaxAge (default 3600s)

Privacy Benefits

  • User IPs hidden from Gravatar servers
  • Email hashes stay server-side
  • Hovercards JS bundled through your domain
  • Referer validation prevents proxy abuse

Files

File Purpose
src/runtime/registry/gravatar.ts Composable
src/runtime/server/gravatar-proxy.ts Server handler
src/runtime/components/ScriptGravatar.vue Vue component
src/proxy-configs.ts Proxy rewrite rules
src/module.ts Module config + handler registration
src/registry.ts Registry entry
src/runtime/types.ts Type definitions
test/unit/gravatar-proxy.test.ts Unit tests (5 tests)
test/fixtures/basic/pages/tpc/gravatar.vue E2E fixture
docs/content/scripts/utility/gravatar.md Documentation

Usage

// nuxt.config.ts
export default defineNuxtConfig({
  scripts: {
    registry: { gravatar: true },
    gravatarProxy: { enabled: true, cacheMaxAge: 3600 }
  }
})
<script setup>
const { proxy } = useScriptGravatar()
const url = proxy.getAvatarUrlFromEmail('user@example.com', { size: 200 })
</script>

<- ## Or use the component -->
<template>
  <ScriptGravatar email="user@example.com" :size="80" />
</template>

Verification

  • ✅ 170/170 unit tests pass
  • ✅ Lint clean
  • ✅ Build succeeds

@vercel
Copy link
Contributor

vercel bot commented Feb 18, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
scripts-docs Ready Ready Preview, Comment Feb 18, 2026 9:13am
scripts-playground Ready Ready Preview, Comment Feb 18, 2026 9:13am

@pkg-pr-new
Copy link

pkg-pr-new bot commented Feb 18, 2026

Open in StackBlitz

npm i https://pkg.pr.new/nuxt/scripts/@nuxt/scripts@606

commit: 0c471e5

- Add useScriptGravatar composable with hovercards JS + avatar URL helpers
- Add gravatar-proxy server handler with server-side email SHA256 hashing
- Add ScriptGravatar.vue component with email/hash props
- Add gravatar proxy config for firstParty URL rewriting
- Add gravatarProxy module config option with configurable caching
- Add registry entry and ScriptRegistry type
- Add unit tests for proxy config
- Add E2E fixture page
- Add documentation
@coderabbitai
Copy link

coderabbitai bot commented Feb 18, 2026

📝 Walkthrough

Walkthrough

This pull request introduces a complete Gravatar integration feature for Nuxt Scripts, including a new Vue component, server-side proxy handler with hashing and caching, registry entry, and supporting types. It also adds planning documents for Google Maps enhancements (static maps proxy, color mode support, marker clusterer improvements, bug fixes), Rybbit page refresh handling, GTM configuration callback issues, and YouTube player fixes. Documentation is expanded with a Gravatar guide and a comprehensive GitHub sync summary, while planning files, tests, and test fixtures are added to support the new Gravatar feature and ongoing audit work.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~30 minutes

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 66.67% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The PR title 'feat: add Gravatar integration with privacy-preserving proxy' follows conventional commits, is concise, and clearly summarizes the main feature addition.
Description check ✅ Passed The PR description is comprehensive and mostly complete. While it doesn't explicitly check all template boxes, it provides detailed summaries of features, files, privacy benefits, usage examples, and verification details that satisfy the intent of required sections.

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

✨ Finishing Touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feat/gravatar

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 8

🧹 Nitpick comments (6)
src/runtime/types.ts (1)

189-191: Move gravatar? before the index signature for consistency.

Every other named property in ScriptRegistry is declared before [key: \${string}-npm`]: NpmInput. Placing gravatar?` after the index signature is valid TypeScript but breaks the convention established by all other ~25 entries.

♻️ Proposed move
   umamiAnalytics?: UmamiAnalyticsInput
+  gravatar?: GravatarInput
   [key: `${string}-npm`]: NpmInput
-  gravatar?: GravatarInput
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/runtime/types.ts` around lines 189 - 191, In the ScriptRegistry type
reorder, move the optional property gravatar? so it appears before the index
signature [key: `${string}-npm`]: NpmInput (i.e., place gravatar? alongside the
other named properties at the top of ScriptRegistry) to follow the established
convention and keep named properties listed before the computed key index
signature.
test/unit/gravatar-proxy.test.ts (1)

28-46: Consider asserting proxy target URLs under the custom prefix too.

The default-prefix test (lines 28–36) verifies both the route key and the proxy target value. The custom-prefix test (lines 38–46) only asserts that the property key exists, leaving the proxy target values untested for the custom-prefix code path.

♻️ Optional extension
   it('uses custom collectPrefix', () => {
     const config = getProxyConfig('gravatar', '/_custom/proxy')
     expect(config?.rewrite).toContainEqual({
       from: 'secure.gravatar.com',
       to: '/_custom/proxy/gravatar',
     })
     expect(config?.routes).toHaveProperty('/_custom/proxy/gravatar/**')
     expect(config?.routes).toHaveProperty('/_custom/proxy/gravatar-avatar/**')
+    expect(config?.routes?.['/_custom/proxy/gravatar/**']).toEqual({
+      proxy: 'https://secure.gravatar.com/**',
+    })
+    expect(config?.routes?.['/_custom/proxy/gravatar-avatar/**']).toEqual({
+      proxy: 'https://gravatar.com/avatar/**',
+    })
   })
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@test/unit/gravatar-proxy.test.ts` around lines 28 - 46, The custom-prefix
test for getProxyConfig only checks that route keys exist but doesn't assert
their proxy target URLs; update the "uses custom collectPrefix" test to also
assert that config.routes['/_custom/proxy/gravatar/**'] maps to the same proxy
target ('https://secure.gravatar.com/**') and that
config.routes['/_custom/proxy/gravatar-avatar/**'] maps to
'https://gravatar.com/avatar/**' so the custom-prefix code path is fully
verified.
.claude/plans/google-maps-fixes.md (1)

1-79: Planning document included in the PR — consider excluding from the merge.

This .claude/plans/ file is an internal development planning artifact. If it's not intended to be part of the repository long-term, consider adding it to .gitignore or removing it before merge.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.claude/plans/google-maps-fixes.md around lines 1 - 79, The planning doc
.claude/plans/google-maps-fixes.md is an internal artifact and should be removed
from the PR or excluded from the repository; either delete the file from the
commit or add the .claude/plans/ path to .gitignore and remove the file from the
index (git rm --cached) so it no longer gets committed. Locate the file named
"google-maps-fixes.md" under the .claude/plans directory in the diff and perform
the removal or ignore change before merging.
src/runtime/components/ScriptGravatar.vue (1)

36-45: Avatar URL is computed once on mount — prop changes won't update it.

imgSrc is set inside onMounted$script.then(...), which runs only once. If email or hash props change reactively (e.g., user switching profiles), the displayed avatar won't update.

Consider using a watch on the relevant props (or a watchEffect) to re-derive the URL when inputs change.

Suggested approach
-onMounted(() => {
-  $script.then((api) => {
-    if (props.email) {
-      imgSrc.value = api.getAvatarUrlFromEmail(props.email, queryOverrides.value)
-    }
-    else if (props.hash) {
-      imgSrc.value = api.getAvatarUrl(props.hash, queryOverrides.value)
-    }
-  })
-})
+const updateAvatar = async () => {
+  const api = await $script
+  if (props.email) {
+    imgSrc.value = api.getAvatarUrlFromEmail(props.email, queryOverrides.value)
+  }
+  else if (props.hash) {
+    imgSrc.value = api.getAvatarUrl(props.hash, queryOverrides.value)
+  }
+}
+
+onMounted(updateAvatar)
+watch(() => [props.email, props.hash, queryOverrides.value], updateAvatar)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/runtime/components/ScriptGravatar.vue` around lines 36 - 45, The avatar
URL is only computed inside onMounted via $script.then(...), so updates to
props.email or props.hash won't refresh imgSrc; replace that one-time
computation with a reactive watcher: use watch or watchEffect to observe
props.email, props.hash, and queryOverrides and inside the watcher call
$script.then(api => set imgSrc.value using api.getAvatarUrlFromEmail or
api.getAvatarUrl as before). Ensure the watcher also handles the case where both
props are absent and cleans up if needed.
src/runtime/registry/gravatar.ts (1)

59-62: The Gravatar hovercards script (gprofiles.js) loads unconditionally, even when hovercards are not used.

The scriptInput.src is always set to https://secure.gravatar.com/js/gprofiles.js, which is the hovercards JavaScript. If the user only wants avatar images (no hovercards), this is an unnecessary third-party script load.

Consider making the script load conditional on whether hovercards are actually needed, or documenting that the script is loaded regardless.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/runtime/registry/gravatar.ts` around lines 59 - 62, The Gravatar registry
currently always sets scriptInput.src to
'https://secure.gravatar.com/js/gprofiles.js' causing unconditional hovercards
JS loading; update the code that builds/returns scriptInput (the object
containing scriptInput.src in src/runtime/registry/gravatar.ts) so the src is
only assigned when hovercards are requested (e.g., check a passed-in option like
enableHovercards or a registry config flag) and otherwise omit scriptInput or
set it to null/undefined; ensure callers that consume scriptInput handle the
absence accordingly or document that the flag enables the external script.
src/runtime/server/gravatar-proxy.ts (1)

56-60: Consider using the canonical Gravatar domain.

Gravatar documentation recommends https://www.gravatar.com/avatar/ (or https://secure.gravatar.com/avatar/). Using bare gravatar.com may incur an extra redirect hop.

Minor fix
-  const gravatarUrl = withQuery(`https://gravatar.com/avatar/${hash}`, {
+  const gravatarUrl = withQuery(`https://www.gravatar.com/avatar/${hash}`, {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/runtime/server/gravatar-proxy.ts` around lines 56 - 60, The gravatar URL
construction uses a non-canonical host; update the string passed to withQuery in
the gravatarUrl assignment to use the canonical Gravatar domain (e.g.,
"https://www.gravatar.com/avatar/" or "https://secure.gravatar.com/avatar/") so
requests avoid an extra redirect; modify the gravatarUrl definition (the
withQuery call that builds the URL) to replace "https://gravatar.com/avatar/"
with the chosen canonical domain while keeping the existing query params (s, d,
r) unchanged.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In @.claude/.active-plan-700ed628-9515-46bf-b08f-9a19b2a0de92:
- Line 1: The commit accidentally includes ephemeral AI assistant artifacts
under the .claude/ directory (e.g., .claude/.active-plan-*,
.claude/.edit-count-*, .claude/session-context.md, .claude/plans/*.md); remove
those files from version control and prevent future commits by adding ".claude/"
to .gitignore, then run git rm --cached for the tracked .claude files to untrack
them (preserve local copies if needed) and commit the updated .gitignore; if any
planning notes under .claude/plans/*.md should be retained, move them into a
proper tracked location such as docs/ before removing from the repo.

In `@docs/content/scripts/utility/gravatar.md`:
- Around line 167-170: The fenced code block in gravatar.md currently uses
triple backticks with no language, causing markdownlint MD040; update that
fenced block (the snippet showing "/_scripts/gravatar-proxy?email=..." and
"/_scripts/gravatar-proxy?hash=...") to include a language identifier such as
"text" (i.e., change the opening "```" to "```text") so the block is explicitly
labeled and MD040 is resolved.
- Around line 48-56: The example declares the same const variable twice causing
a redeclaration error; update the snippet to use distinct variable names (e.g.,
urlFromHash and urlFromEmail) or convert one to let/var so both calls to
proxy.getAvatarUrl and proxy.getAvatarUrlFromEmail can coexist; change the
second declaration that uses getAvatarUrlFromEmail to a different identifier and
ensure references in the example match.

In `@src/runtime/components/ScriptGravatar.vue`:
- Around line 6-7: The JSDoc for the email prop in ScriptGravatar.vue is
inaccurate because getAvatarUrlFromEmail currently embeds the raw email into the
client-side <img src> URL (/_scripts/gravatar-proxy?email=…), exposing it in
HTML; either (A) update the JSDoc for the email prop to accurately state that
the email is sent to the server for hashing (e.g., "email is sent to server for
hashing, not exposed to Gravatar") and adjust comments around
getAvatarUrlFromEmail, or (B) implement client-side hashing: use
crypto.subtle.digest to compute the MD5 (or agreed hash) of the email in the
browser, change getAvatarUrlFromEmail to send hash instead of email (e.g.,
?hash=...), and update the server proxy (gravatar-proxy) to accept and use hash
rather than email; pick one approach and make the corresponding changes to
ScriptGravatar.vue, getAvatarUrlFromEmail, and the proxy handling.

In `@src/runtime/registry/gravatar.ts`:
- Around line 65-71: Update getAvatarUrl and getAvatarUrlFromEmail to respect a
runtime flag (e.g., gravatarProxyEnabled from module/runtime config) instead of
always returning "/_scripts/gravatar-proxy"; in both functions check
gravatarProxyEnabled: if true, return the existing proxy URL
(`/_scripts/gravatar-proxy?...` using buildQuery), otherwise for getAvatarUrl
return a direct Gravatar URL
("https://www.gravatar.com/avatar/{hash}?{buildQuery(overrides)}") as a safe
fallback, and for getAvatarUrlFromEmail emit a concise runtime warning
(console.warn or runtime logger) that the proxy is disabled and either require
the caller to provide a hash or enable the proxy (or implement MD5 conversion
later) before returning a best-effort value. Ensure you reference and use the
existing symbols getAvatarUrl, getAvatarUrlFromEmail, buildQuery and the
gravatar proxy path when making the change.

In `@src/runtime/server/gravatar-proxy.ts`:
- Around line 20-22: The Referer parsing (new URL(referer)) in the referer/host
check can throw on malformed client-controlled headers; wrap the construction in
a try/catch inside the same block that reads referer and host (the referer/host
check in gravatar-proxy handler) and handle parse failures by treating the
referer as not matching (or logging and continuing) instead of letting the
exception bubble up; update the code around the refererUrl variable (where new
URL(referer).host is used) to safely compute refererHost with try/catch and
fallbacks.
- Around line 62-71: The $fetch.raw call fetching gravatarUrl must explicitly
set responseType: 'arrayBuffer' so binary image data is returned as an
ArrayBuffer (matching the project's pattern in src/plugins/transform.ts) and can
be converted to a Node Buffer for the HTTP response; update the $fetch.raw
invocation (the call that assigns response from $fetch.raw(gravatarUrl, {...}))
to include responseType: 'arrayBuffer' alongside the existing headers, leaving
the existing error .catch intact and ensuring downstream code that uses the
response converts the ArrayBuffer to a Buffer before sending.

In `@src/runtime/types.ts`:
- Line 224: Adjust the indentation of the binary pipe operator in the union type
so it complies with the `@stylistic/indent-binary-ops` rule: locate the union line
containing "| Partial<InferIfSchema<T>> & (" in src/runtime/types.ts and indent
the leading "|" to use 4 spaces (so the pipe is aligned with the surrounding
type lines) rather than 2 spaces.

---

Nitpick comments:
In @.claude/plans/google-maps-fixes.md:
- Around line 1-79: The planning doc .claude/plans/google-maps-fixes.md is an
internal artifact and should be removed from the PR or excluded from the
repository; either delete the file from the commit or add the .claude/plans/
path to .gitignore and remove the file from the index (git rm --cached) so it no
longer gets committed. Locate the file named "google-maps-fixes.md" under the
.claude/plans directory in the diff and perform the removal or ignore change
before merging.

In `@src/runtime/components/ScriptGravatar.vue`:
- Around line 36-45: The avatar URL is only computed inside onMounted via
$script.then(...), so updates to props.email or props.hash won't refresh imgSrc;
replace that one-time computation with a reactive watcher: use watch or
watchEffect to observe props.email, props.hash, and queryOverrides and inside
the watcher call $script.then(api => set imgSrc.value using
api.getAvatarUrlFromEmail or api.getAvatarUrl as before). Ensure the watcher
also handles the case where both props are absent and cleans up if needed.

In `@src/runtime/registry/gravatar.ts`:
- Around line 59-62: The Gravatar registry currently always sets scriptInput.src
to 'https://secure.gravatar.com/js/gprofiles.js' causing unconditional
hovercards JS loading; update the code that builds/returns scriptInput (the
object containing scriptInput.src in src/runtime/registry/gravatar.ts) so the
src is only assigned when hovercards are requested (e.g., check a passed-in
option like enableHovercards or a registry config flag) and otherwise omit
scriptInput or set it to null/undefined; ensure callers that consume scriptInput
handle the absence accordingly or document that the flag enables the external
script.

In `@src/runtime/server/gravatar-proxy.ts`:
- Around line 56-60: The gravatar URL construction uses a non-canonical host;
update the string passed to withQuery in the gravatarUrl assignment to use the
canonical Gravatar domain (e.g., "https://www.gravatar.com/avatar/" or
"https://secure.gravatar.com/avatar/") so requests avoid an extra redirect;
modify the gravatarUrl definition (the withQuery call that builds the URL) to
replace "https://gravatar.com/avatar/" with the chosen canonical domain while
keeping the existing query params (s, d, r) unchanged.

In `@src/runtime/types.ts`:
- Around line 189-191: In the ScriptRegistry type reorder, move the optional
property gravatar? so it appears before the index signature [key:
`${string}-npm`]: NpmInput (i.e., place gravatar? alongside the other named
properties at the top of ScriptRegistry) to follow the established convention
and keep named properties listed before the computed key index signature.

In `@test/unit/gravatar-proxy.test.ts`:
- Around line 28-46: The custom-prefix test for getProxyConfig only checks that
route keys exist but doesn't assert their proxy target URLs; update the "uses
custom collectPrefix" test to also assert that
config.routes['/_custom/proxy/gravatar/**'] maps to the same proxy target
('https://secure.gravatar.com/**') and that
config.routes['/_custom/proxy/gravatar-avatar/**'] maps to
'https://gravatar.com/avatar/**' so the custom-prefix code path is fully
verified.

@@ -0,0 +1 @@
/home/harlan/pkg/nuxt-scripts/.claude/plans/461-rybbit-refresh.md
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Remove .claude/ directory from version control — add it to .gitignore.

The entire .claude/ directory is an internal Claude AI assistant working-state artifact (active plan pointers, edit counters, session context, planning notes). These files contain absolute developer machine paths (e.g. /home/harlan/pkg/nuxt-scripts/...) and internal agent session state that have no place in a public repository.

All files under .claude/ committed in this PR share this problem:

  • .claude/.active-plan-* — session-scoped plan pointers with absolute filesystem paths
  • .claude/.edit-count-* — internal edit counters
  • .claude/session-context.md — live git state, branch names, commit SHAs, recently modified file list
  • .claude/plans/*.md — planning notes (these have content value, but belong in a proper docs/ or project-tracked location, not ephemeral AI agent state files)
🛠️ Proposed fix — add `.claude/` to `.gitignore`
+# Claude AI assistant working files
+.claude/
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.claude/.active-plan-700ed628-9515-46bf-b08f-9a19b2a0de92 at line 1, The
commit accidentally includes ephemeral AI assistant artifacts under the .claude/
directory (e.g., .claude/.active-plan-*, .claude/.edit-count-*,
.claude/session-context.md, .claude/plans/*.md); remove those files from version
control and prevent future commits by adding ".claude/" to .gitignore, then run
git rm --cached for the tracked .claude files to untrack them (preserve local
copies if needed) and commit the updated .gitignore; if any planning notes under
.claude/plans/*.md should be retained, move them into a proper tracked location
such as docs/ before removing from the repo.

Comment on lines +48 to +56
```ts
const { proxy } = useScriptGravatar()

// Get avatar URL from a pre-computed SHA256 hash
const url = proxy.getAvatarUrl('sha256hash', { size: 200 })

// Get avatar URL from email (hashed server-side)
const url = proxy.getAvatarUrlFromEmail('user@example.com', { size: 200 })
```
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Duplicate const url declaration makes the example invalid JS/TS.

Both getAvatarUrl and getAvatarUrlFromEmail assign to the same const url inside the same code block. Readers who copy this snippet will hit Cannot redeclare block-scoped variable 'url'.

📝 Proposed fix
-// Get avatar URL from a pre-computed SHA256 hash
-const url = proxy.getAvatarUrl('sha256hash', { size: 200 })
-
-// Get avatar URL from email (hashed server-side)
-const url = proxy.getAvatarUrlFromEmail('user@example.com', { size: 200 })
+// Get avatar URL from a pre-computed SHA256 hash
+const avatarUrl = proxy.getAvatarUrl('sha256hash', { size: 200 })
+
+// Get avatar URL from email (hashed server-side)
+const emailAvatarUrl = proxy.getAvatarUrlFromEmail('user@example.com', { size: 200 })
📝 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
```ts
const { proxy } = useScriptGravatar()
// Get avatar URL from a pre-computed SHA256 hash
const url = proxy.getAvatarUrl('sha256hash', { size: 200 })
// Get avatar URL from email (hashed server-side)
const url = proxy.getAvatarUrlFromEmail('user@example.com', { size: 200 })
```
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@docs/content/scripts/utility/gravatar.md` around lines 48 - 56, The example
declares the same const variable twice causing a redeclaration error; update the
snippet to use distinct variable names (e.g., urlFromHash and urlFromEmail) or
convert one to let/var so both calls to proxy.getAvatarUrl and
proxy.getAvatarUrlFromEmail can coexist; change the second declaration that uses
getAvatarUrlFromEmail to a different identifier and ensure references in the
example match.

Comment on lines +167 to +170
```
/_scripts/gravatar-proxy?email=user@example.com&s=200&d=mp&r=g
/_scripts/gravatar-proxy?hash=abc123...&s=80
```
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Add a language identifier to the fenced code block to fix the markdownlint MD040 error.

📝 Proposed fix
-```
+```text
 /_scripts/gravatar-proxy?email=user@example.com&s=200&d=mp&r=g
 /_scripts/gravatar-proxy?hash=abc123...&s=80
</details>

<!-- suggestion_start -->

<details>
<summary>📝 Committable suggestion</summary>

> ‼️ **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.

```suggestion

🧰 Tools
🪛 markdownlint-cli2 (0.21.0)

[warning] 167-167: Fenced code blocks should have a language specified

(MD040, fenced-code-language)

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@docs/content/scripts/utility/gravatar.md` around lines 167 - 170, The fenced
code block in gravatar.md currently uses triple backticks with no language,
causing markdownlint MD040; update that fenced block (the snippet showing
"/_scripts/gravatar-proxy?email=..." and "/_scripts/gravatar-proxy?hash=...") to
include a language identifier such as "text" (i.e., change the opening "```" to
"```text") so the block is explicitly labeled and MD040 is resolved.

Comment on lines +6 to +7
/** Email address — hashed server-side, never exposed in client HTML */
email?: string
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Email is exposed in client HTML despite JSDoc claiming otherwise.

The email prop's JSDoc states "hashed server-side, never exposed in client HTML," but getAvatarUrlFromEmail generates a URL like /_scripts/gravatar-proxy?email=user@example.com&…, which becomes the <img src> attribute — fully visible in the DOM.

The server-side hashing prevents the email from reaching Gravatar, but it does appear in client-rendered HTML. Either:

  1. Update the JSDoc to accurately describe the privacy model ("email is sent to your server for hashing, not to Gravatar"), or
  2. Hash the email client-side before building the URL (using crypto.subtle.digest in the browser), which would truly keep the email out of HTML while still routing through the proxy.

Option 2 would also require adjusting the proxy to accept hash instead of email in that flow.

Also applies to: 36-44

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/runtime/components/ScriptGravatar.vue` around lines 6 - 7, The JSDoc for
the email prop in ScriptGravatar.vue is inaccurate because getAvatarUrlFromEmail
currently embeds the raw email into the client-side <img src> URL
(/_scripts/gravatar-proxy?email=…), exposing it in HTML; either (A) update the
JSDoc for the email prop to accurately state that the email is sent to the
server for hashing (e.g., "email is sent to server for hashing, not exposed to
Gravatar") and adjust comments around getAvatarUrlFromEmail, or (B) implement
client-side hashing: use crypto.subtle.digest to compute the MD5 (or agreed
hash) of the email in the browser, change getAvatarUrlFromEmail to send hash
instead of email (e.g., ?hash=...), and update the server proxy (gravatar-proxy)
to accept and use hash rather than email; pick one approach and make the
corresponding changes to ScriptGravatar.vue, getAvatarUrlFromEmail, and the
proxy handling.

Comment on lines +65 to +71
use: () => ({
getAvatarUrl: (hash: string, overrides?: { size?: number, default?: string, rating?: string }) => {
return `/_scripts/gravatar-proxy?hash=${encodeURIComponent(hash)}&${buildQuery(overrides)}`
},
getAvatarUrlFromEmail: (email: string, overrides?: { size?: number, default?: string, rating?: string }) => {
return `/_scripts/gravatar-proxy?email=${encodeURIComponent(email)}&${buildQuery(overrides)}`
},
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

# Check for gravatarProxy references and any guards
rg "gravatarProxy" --type ts --type vue -C 2

Repository: nuxt/scripts

Length of output: 83


🏁 Script executed:

# Read the gravatar.ts file to confirm implementation details
wc -l src/runtime/registry/gravatar.ts

Repository: nuxt/scripts

Length of output: 91


🏁 Script executed:

# Look for the actual file to understand structure
fd "gravatar" --type f

Repository: nuxt/scripts

Length of output: 284


🏁 Script executed:

# Retry the gravatarProxy search with correct file types
rg "gravatarProxy" --type ts -C 2

Repository: nuxt/scripts

Length of output: 1459


🏁 Script executed:

# Read the gravatar.ts file to verify the implementation
cat -n src/runtime/registry/gravatar.ts

Repository: nuxt/scripts

Length of output: 3355


🏁 Script executed:

# Check the gravatar-proxy.ts to see if there are any guards
cat -n src/runtime/server/gravatar-proxy.ts

Repository: nuxt/scripts

Length of output: 3036


🏁 Script executed:

# Check the module.ts to see full proxy configuration and registration
rg -A 5 "Add Gravatar proxy handler" src/module.ts

Repository: nuxt/scripts

Length of output: 289


🏁 Script executed:

# Check the ScriptGravatar component to see if it has any guards
cat -n src/runtime/components/ScriptGravatar.vue

Repository: nuxt/scripts

Length of output: 2007


🏁 Script executed:

# Check the documentation to see if this is documented
cat -n docs/content/scripts/utility/gravatar.md

Repository: nuxt/scripts

Length of output: 5678


Avatar URLs hard-code the proxy path with no fallback when disabled.

getAvatarUrl and getAvatarUrlFromEmail always return /_scripts/gravatar-proxy?… URLs. If the proxy isn't enabled in nuxt.config.ts, requests to this endpoint will return a 404 with message "Gravatar proxy is not enabled".

While the documentation mentions the proxy must be enabled, there is no runtime warning in the API itself or fallback to direct Gravatar URLs. If developers forget to enable the proxy, avatars will silently fail to load without clear feedback.

Consider either:

  1. Adding a runtime check/warning when the proxy isn't enabled, or
  2. Providing a fallback to direct Gravatar URLs (at least for the hash-based method).
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/runtime/registry/gravatar.ts` around lines 65 - 71, Update getAvatarUrl
and getAvatarUrlFromEmail to respect a runtime flag (e.g., gravatarProxyEnabled
from module/runtime config) instead of always returning
"/_scripts/gravatar-proxy"; in both functions check gravatarProxyEnabled: if
true, return the existing proxy URL (`/_scripts/gravatar-proxy?...` using
buildQuery), otherwise for getAvatarUrl return a direct Gravatar URL
("https://www.gravatar.com/avatar/{hash}?{buildQuery(overrides)}") as a safe
fallback, and for getAvatarUrlFromEmail emit a concise runtime warning
(console.warn or runtime logger) that the proxy is disabled and either require
the caller to provide a hash or enable the proxy (or implement MD5 conversion
later) before returning a best-effort value. Ensure you reference and use the
existing symbols getAvatarUrl, getAvatarUrlFromEmail, buildQuery and the
gravatar proxy path when making the change.

Comment on lines +20 to +22
if (referer && host) {
const refererUrl = new URL(referer).host
if (refererUrl !== host) {
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Malformed Referer header will crash the handler with an unhandled exception.

new URL(referer) throws on malformed values. Since Referer is a client-controlled header, this can cause 500 errors.

Wrap in try-catch
   if (referer && host) {
-    const refererUrl = new URL(referer).host
-    if (refererUrl !== host) {
-      throw createError({
-        statusCode: 403,
-        statusMessage: 'Invalid referer',
-      })
+    try {
+      const refererUrl = new URL(referer).host
+      if (refererUrl !== host) {
+        throw createError({
+          statusCode: 403,
+          statusMessage: 'Invalid referer',
+        })
+      }
+    }
+    catch {
+      throw createError({
+        statusCode: 403,
+        statusMessage: 'Invalid referer',
+      })
     }
   }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/runtime/server/gravatar-proxy.ts` around lines 20 - 22, The Referer
parsing (new URL(referer)) in the referer/host check can throw on malformed
client-controlled headers; wrap the construction in a try/catch inside the same
block that reads referer and host (the referer/host check in gravatar-proxy
handler) and handle parse failures by treating the referer as not matching (or
logging and continuing) instead of letting the exception bubble up; update the
code around the refererUrl variable (where new URL(referer).host is used) to
safely compute refererHost with try/catch and fallbacks.

Comment on lines +62 to +71
const response = await $fetch.raw(gravatarUrl, {
headers: {
'User-Agent': 'Nuxt Scripts Gravatar Proxy',
},
}).catch((error: any) => {
throw createError({
statusCode: error.statusCode || 500,
statusMessage: error.statusMessage || 'Failed to fetch Gravatar avatar',
})
})
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

cd /repo && git ls-files | grep -E "gravatar-proxy|src/runtime" | head -20

Repository: nuxt/scripts

Length of output: 111


🌐 Web query:

ofetch $fetch.raw responseType binary image handling default behavior

💡 Result:

  • Default behavior (no responseType set): Nuxt’s $fetch / $fetch.raw (ofetch) auto-detects how to read the body from the response Content-Type. For typical binary types like image/png, it will parse as blob; for image/svg+xml it parses as text; and if Content-Type is missing/empty it defaults to json. (github.com)
  • $fetch.raw + images: $fetch.raw(...) returns a response object where the parsed body is in response._data (so for image/png by default that will be a Blob in environments that support it). (github.com)
  • If you need “binary” explicitly: responseType: "binary" is not the ofetch option; use responseType: "blob" (common for images in browsers) or responseType: "arrayBuffer" (often best for “raw bytes” / Node Buffer conversion), or "stream" if you want streaming. (github.com)

Citations:


🏁 Script executed:

find /repo -name "gravatar-proxy.ts" -type f

Repository: nuxt/scripts

Length of output: 96


🏁 Script executed:

find . -name "gravatar-proxy.ts" -type f 2>/dev/null

Repository: nuxt/scripts

Length of output: 94


🏁 Script executed:

git ls-files | grep gravatar

Repository: nuxt/scripts

Length of output: 242


🏁 Script executed:

cat -n ./src/runtime/server/gravatar-proxy.ts

Repository: nuxt/scripts

Length of output: 3036


🏁 Script executed:

cat -n ./test/unit/gravatar-proxy.test.ts

Repository: nuxt/scripts

Length of output: 2024


🏁 Script executed:

cat ./package.json | grep -A 5 "engines"

Repository: nuxt/scripts

Length of output: 38


🏁 Script executed:

rg "responseType" --type ts -A 2 -B 2

Repository: nuxt/scripts

Length of output: 1996


🏁 Script executed:

cat -n ./src/plugins/transform.ts | head -50

Repository: nuxt/scripts

Length of output: 2220


🏁 Script executed:

cat -n ./src/plugins/transform.ts | grep -A 10 -B 10 "arrayBuffer"

Repository: nuxt/scripts

Length of output: 1268


🏁 Script executed:

rg "gravatar-proxy" --type ts -B 5 -A 5

Repository: nuxt/scripts

Length of output: 1624


🏁 Script executed:

rg "gravatar-avatar" test/ --type ts -B 3 -A 3

Repository: nuxt/scripts

Length of output: 1371


Add explicit responseType: 'arrayBuffer' for binary image data handling.

While ofetch auto-detects image/* responses as Blob, explicit responseType: 'arrayBuffer' is the preferred pattern in this codebase for binary responses (as seen in src/plugins/transform.ts:136) and ensures proper Buffer conversion for Node.js HTTP handler responses.

Suggested fix:
 const response = await $fetch.raw(gravatarUrl, {
+  responseType: 'arrayBuffer',
   headers: {
     'User-Agent': 'Nuxt Scripts Gravatar Proxy',
   },
 })
📝 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 response = await $fetch.raw(gravatarUrl, {
headers: {
'User-Agent': 'Nuxt Scripts Gravatar Proxy',
},
}).catch((error: any) => {
throw createError({
statusCode: error.statusCode || 500,
statusMessage: error.statusMessage || 'Failed to fetch Gravatar avatar',
})
})
const response = await $fetch.raw(gravatarUrl, {
responseType: 'arrayBuffer',
headers: {
'User-Agent': 'Nuxt Scripts Gravatar Proxy',
},
}).catch((error: any) => {
throw createError({
statusCode: error.statusCode || 500,
statusMessage: error.statusMessage || 'Failed to fetch Gravatar avatar',
})
})
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/runtime/server/gravatar-proxy.ts` around lines 62 - 71, The $fetch.raw
call fetching gravatarUrl must explicitly set responseType: 'arrayBuffer' so
binary image data is returned as an ArrayBuffer (matching the project's pattern
in src/plugins/transform.ts) and can be converted to a Node Buffer for the HTTP
response; update the $fetch.raw invocation (the call that assigns response from
$fetch.raw(gravatarUrl, {...})) to include responseType: 'arrayBuffer' alongside
the existing headers, leaving the existing error .catch intact and ensuring
downstream code that uses the response converts the ArrayBuffer to a Buffer
before sending.

scriptOptions?: Omit<NuxtUseScriptOptions, Bundelable extends true ? '' : 'bundle' | Usable extends true ? '' : 'use'>
})
| Partial<InferIfSchema<T>> & (
| Partial<InferIfSchema<T>> & (
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Fix ESLint @stylistic/indent-binary-ops error — pipeline will fail.

The | binary operator at line 224 is indented 2 spaces; the rule requires 4 spaces.

🔧 Proposed fix
-  | Partial<InferIfSchema<T>> & (
+    | Partial<InferIfSchema<T>> & (
📝 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
| Partial<InferIfSchema<T>> & (
| Partial<InferIfSchema<T>> & (
🧰 Tools
🪛 ESLint

[error] 224-224: Expected indentation of 4 spaces

(@stylistic/indent-binary-ops)

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/runtime/types.ts` at line 224, Adjust the indentation of the binary pipe
operator in the union type so it complies with the `@stylistic/indent-binary-ops`
rule: locate the union line containing "| Partial<InferIfSchema<T>> & (" in
src/runtime/types.ts and indent the leading "|" to use 4 spaces (so the pipe is
aligned with the surrounding type lines) rather than 2 spaces.

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