feat: avatars for humans #273

Merged
talksik merged 11 commits from worktree-refactored-strolling-treasure into main 2026-06-11 22:30:21 +00:00
talksik commented 2026-06-11 22:04:17 +00:00 (Migrated from github.com)

Closes #70

Summary by CodeRabbit

  • New Features
    • Profile picture management in Settings: upload, replace, or remove your avatar (includes camera capture and re-cropping).
    • Avatars everywhere: user avatars now appear across the app (network, particles, reactions, members, top bars).
    • Faster, secure avatar loading: avatar previews use signed download URLs with caching/refresh for smoother display.
    • Edit dialog UI and preview: clickable avatar opens editor with preview, validation, and size limits.
Closes #70 <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **New Features** * Profile picture management in Settings: upload, replace, or remove your avatar (includes camera capture and re-cropping). * Avatars everywhere: user avatars now appear across the app (network, particles, reactions, members, top bars). * Faster, secure avatar loading: avatar previews use signed download URLs with caching/refresh for smoother display. * Edit dialog UI and preview: clickable avatar opens editor with preview, validation, and size limits. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
coderabbitai[bot] commented 2026-06-11 22:04:31 +00:00 (Migrated from github.com)

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 73480fa0-39ce-4438-aeef-71baef5ae49d

📥 Commits

Reviewing files that changed from the base of the PR and between 6f8349c62f and 6ce7cc8651.

📒 Files selected for processing (3)
  • go/internal/handler/handler.go
  • go/migrations/000017_human_avatar.down.sql
  • js/desktop/src/features/particles/stream-card.tsx
💤 Files with no reviewable changes (1)
  • js/desktop/src/features/particles/stream-card.tsx
🚧 Files skipped from review as they are similar to previous changes (2)
  • go/internal/handler/handler.go
  • go/migrations/000017_human_avatar.down.sql

📝 Walkthrough

Walkthrough

Implements user-controlled profile avatars: DB migration and model field, repository/service update/delete support, HTTP handlers and routes, desktop API client methods, avatar utilities/hooks/components, settings edit dialog with upload/camera, and avatar rendering across particle and network UIs.

Changes

Avatar Management Feature

Layer / File(s) Summary
Database schema and model
go/migrations/000017_human_avatar.up.sql, go/migrations/000017_human_avatar.down.sql, go/internal/human/models.go
Adds nullable avatar_object_id column and maps it into the Go Human model as AvatarObjectID.
Repository and service layer
go/internal/human/repository.go, go/internal/human/service.go, go/internal/human/service_test.go
Repository selects/scans avatar_object_id, adds updateAvatarObjectID; service exposes UpdateAvatar/DeleteAvatar, validates parameters and maps repo errors; tests cover avatar lifecycle.
HTTP handlers and routing
go/internal/handler/handler.go, go/cmd/orion/main.go
Adds UpdateAvatar and DeleteAvatar handlers (5MB request limit, depot uploads, best-effort depot cleanup), maps avatar field into DTO, renames DownloadParticleMediaGetObjectDownloadUrl, and wires settings/particles routes.
API client refactor and avatar methods
js/desktop/src/api/client.ts, js/desktop/src/api/types.ts
Introduces centralized send helper, JSON fetch delegation, and client methods updateAvatar, deleteAvatar, getAvatarDownloadUrl; HumanSchema adds optional avatar_object_id.
Avatar data resolution and utilities
js/desktop/src/hooks/use-avatar-url.ts, js/desktop/src/lib/avatar-image.ts, js/desktop/src/lib/humans.ts, js/desktop/src/hooks/use-presence-positions.ts, js/desktop/src/stores/auth-store.ts
Adds useAvatarUrl (React Query) to fetch signed URLs, toAvatarBlob to center-crop and encode images, surfaces avatarObjectId in human display/presence shapes, and adds refreshUser to auth store.
HumanAvatar component
js/desktop/src/components/human-avatar.tsx
New component resolves signed avatar URLs via useAvatarUrl and renders AvatarImage or AvatarFallback (initials) as fallback.
Avatar edit dialog
js/desktop/src/features/settings/avatar-edit-dialog.tsx
Adds AvatarEditDialog with upload + camera capture UI, file validation (MIME and 30MB source cap), image conversion via toAvatarBlob, API calls for save/remove, preview lifecycle, and CameraCapture subcomponent.
Settings page avatar UI
js/desktop/src/features/settings-page.tsx
Makes profile avatar clickable HumanAvatar with camera overlay and opens AvatarEditDialog for editing.
Particle and network views avatar integration
js/desktop/src/features/particles/*, js/desktop/src/features/network-settings.tsx
Replaces direct initials avatars with HumanAvatar across particle list, cards, reaction bar, presence indicators, members overlay, top bar, and network settings; memoized avatar resolution now includes avatarObjectId.

Sequence Diagram(s)

sequenceDiagram
  participant Desktop as Desktop Client
  participant Api as ApiServer (handler)
  participant Depot as DepotService
  participant DB as Database

  Desktop->>Api: PUT /humans/me/avatar (blob)
  Api->>Depot: Upload avatar blob -> returns objectId
  Depot-->>Api: objectId
  Api->>Api: humanSvc.UpdateAvatar(userId, objectId)
  Api->>DB: repository.updateAvatarObjectID(userId, objectId)
  DB-->>Api: OK
  Api-->>Desktop: 204 No Content

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Poem

🐰 I hopped to town with camera bright,

A tiny blob compressed just right,
Snap or drop — a portrait new,
Replaced the initials with a view,
Now profiles glow with me and you.

🚥 Pre-merge checks | 4 | 1

Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 37.50% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Passed checks (4 passed)
Check name Status Explanation
Description Check Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check Passed The title 'feat: avatars for humans' clearly summarizes the main objective of adding avatar functionality for users.
Linked Issues check Passed The pull request implements the core requirements from issue #70: user-controlled avatar creation via photo snapping or uploading, persistent user-controlled avatars to ensure recognition and consent, and avoids arbitrary video frame usage.
Out of Scope Changes check Passed All changes are within scope of the linked issue. Backend adds avatar storage/retrieval infrastructure, frontend implements UI for avatar upload/capture, and media handling ensures avatars are square-cropped and properly formatted.

✏️ 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 worktree-refactored-strolling-treasure

Warning

There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure.

🔧 golangci-lint (2.12.2)

level=error msg="[linters_context] typechecking error: pattern ./...: directory prefix . does not contain main module or its selected dependencies"


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

<!-- This is an auto-generated comment: summarize by coderabbit.ai --> <!-- review_stack_entry_start --> [![Review Change Stack](https://storage.googleapis.com/coderabbit_public_assets/review-stack-in-coderabbit-ui.svg)](https://app.coderabbit.ai/change-stack/flowy-live/llink/pull/273?utm_source=github_walkthrough&utm_medium=github&utm_campaign=change_stack) <!-- review_stack_entry_end --> No actionable comments were generated in the recent review. 🎉 <details> <summary>ℹ️ Recent review info</summary> <details> <summary>⚙️ Run configuration</summary> **Configuration used**: defaults **Review profile**: CHILL **Plan**: Pro Plus **Run ID**: `73480fa0-39ce-4438-aeef-71baef5ae49d` </details> <details> <summary>📥 Commits</summary> Reviewing files that changed from the base of the PR and between 6f8349c62f5c33eea90050e4cf45d8740322f316 and 6ce7cc865174662c305e04498f5d412b1a13223c. </details> <details> <summary>📒 Files selected for processing (3)</summary> * `go/internal/handler/handler.go` * `go/migrations/000017_human_avatar.down.sql` * `js/desktop/src/features/particles/stream-card.tsx` </details> <details> <summary>💤 Files with no reviewable changes (1)</summary> * js/desktop/src/features/particles/stream-card.tsx </details> <details> <summary>🚧 Files skipped from review as they are similar to previous changes (2)</summary> * go/internal/handler/handler.go * go/migrations/000017_human_avatar.down.sql </details> </details> --- <!-- walkthrough_start --> <details> <summary>📝 Walkthrough</summary> ## Walkthrough Implements user-controlled profile avatars: DB migration and model field, repository/service update/delete support, HTTP handlers and routes, desktop API client methods, avatar utilities/hooks/components, settings edit dialog with upload/camera, and avatar rendering across particle and network UIs. ## Changes **Avatar Management Feature** |Layer / File(s)|Summary| |---|---| |**Database schema and model** <br> `go/migrations/000017_human_avatar.up.sql`, `go/migrations/000017_human_avatar.down.sql`, `go/internal/human/models.go`|Adds nullable `avatar_object_id` column and maps it into the Go `Human` model as `AvatarObjectID`.| |**Repository and service layer** <br> `go/internal/human/repository.go`, `go/internal/human/service.go`, `go/internal/human/service_test.go`|Repository selects/scans `avatar_object_id`, adds `updateAvatarObjectID`; service exposes `UpdateAvatar`/`DeleteAvatar`, validates parameters and maps repo errors; tests cover avatar lifecycle.| |**HTTP handlers and routing** <br> `go/internal/handler/handler.go`, `go/cmd/orion/main.go`|Adds `UpdateAvatar` and `DeleteAvatar` handlers (5MB request limit, depot uploads, best-effort depot cleanup), maps avatar field into DTO, renames `DownloadParticleMedia`→`GetObjectDownloadUrl`, and wires settings/particles routes.| |**API client refactor and avatar methods** <br> `js/desktop/src/api/client.ts`, `js/desktop/src/api/types.ts`|Introduces centralized `send` helper, JSON fetch delegation, and client methods `updateAvatar`, `deleteAvatar`, `getAvatarDownloadUrl`; `HumanSchema` adds optional `avatar_object_id`.| |**Avatar data resolution and utilities** <br> `js/desktop/src/hooks/use-avatar-url.ts`, `js/desktop/src/lib/avatar-image.ts`, `js/desktop/src/lib/humans.ts`, `js/desktop/src/hooks/use-presence-positions.ts`, `js/desktop/src/stores/auth-store.ts`|Adds `useAvatarUrl` (React Query) to fetch signed URLs, `toAvatarBlob` to center-crop and encode images, surfaces `avatarObjectId` in human display/presence shapes, and adds `refreshUser` to auth store.| |**HumanAvatar component** <br> `js/desktop/src/components/human-avatar.tsx`|New component resolves signed avatar URLs via `useAvatarUrl` and renders `AvatarImage` or `AvatarFallback` (initials) as fallback.| |**Avatar edit dialog** <br> `js/desktop/src/features/settings/avatar-edit-dialog.tsx`|Adds AvatarEditDialog with upload + camera capture UI, file validation (MIME and 30MB source cap), image conversion via `toAvatarBlob`, API calls for save/remove, preview lifecycle, and CameraCapture subcomponent.| |**Settings page avatar UI** <br> `js/desktop/src/features/settings-page.tsx`|Makes profile avatar clickable `HumanAvatar` with camera overlay and opens AvatarEditDialog for editing.| |**Particle and network views avatar integration** <br> `js/desktop/src/features/particles/*`, `js/desktop/src/features/network-settings.tsx`|Replaces direct initials avatars with `HumanAvatar` across particle list, cards, reaction bar, presence indicators, members overlay, top bar, and network settings; memoized avatar resolution now includes `avatarObjectId`.| ## Sequence Diagram(s) ```mermaid sequenceDiagram participant Desktop as Desktop Client participant Api as ApiServer (handler) participant Depot as DepotService participant DB as Database Desktop->>Api: PUT /humans/me/avatar (blob) Api->>Depot: Upload avatar blob -> returns objectId Depot-->>Api: objectId Api->>Api: humanSvc.UpdateAvatar(userId, objectId) Api->>DB: repository.updateAvatarObjectID(userId, objectId) DB-->>Api: OK Api-->>Desktop: 204 No Content ``` ## Estimated code review effort 🎯 4 (Complex) | ⏱️ ~45 minutes ## Poem > 🐰 I hopped to town with camera bright, > > A tiny blob compressed just right, > > Snap or drop — a portrait new, > > Replaced the initials with a view, > > Now profiles glow with me and you. </details> <!-- walkthrough_end --> <!-- pre_merge_checks_walkthrough_start --> <details> <summary>🚥 Pre-merge checks | ✅ 4 | ❌ 1</summary> ### ❌ Failed checks (1 warning) | Check name | Status | Explanation | Resolution | | :----------------: | :--------- | :------------------------------------------------------------------------------------ | :--------------------------------------------------------------------------------- | | Docstring Coverage | ⚠️ Warning | Docstring coverage is 37.50% which is insufficient. The required threshold is 80.00%. | Write docstrings for the functions missing them to satisfy the coverage threshold. | <details> <summary>✅ Passed checks (4 passed)</summary> | Check name | Status | Explanation | | :------------------------: | :------- | :----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | Description Check | ✅ Passed | Check skipped - CodeRabbit’s high-level summary is enabled. | | Title check | ✅ Passed | The title 'feat: avatars for humans' clearly summarizes the main objective of adding avatar functionality for users. | | Linked Issues check | ✅ Passed | The pull request implements the core requirements from issue `#70`: user-controlled avatar creation via photo snapping or uploading, persistent user-controlled avatars to ensure recognition and consent, and avoids arbitrary video frame usage. | | Out of Scope Changes check | ✅ Passed | All changes are within scope of the linked issue. Backend adds avatar storage/retrieval infrastructure, frontend implements UI for avatar upload/capture, and media handling ensures avatars are square-cropped and properly formatted. | </details> <sub>✏️ Tip: You can configure your own custom pre-merge checks in the settings.</sub> </details> <!-- pre_merge_checks_walkthrough_end --> <!-- finishing_touch_checkbox_start --> <details> <summary>✨ Finishing Touches</summary> <details> <summary>📝 Generate docstrings</summary> - [ ] <!-- {"checkboxId": "7962f53c-55bc-4827-bfbf-6a18da830691"} --> Create stacked PR - [ ] <!-- {"checkboxId": "3e1879ae-f29b-4d0d-8e06-d12b7ba33d98"} --> Commit on current branch </details> <details> <summary>🧪 Generate unit tests (beta)</summary> - [ ] <!-- {"checkboxId": "f47ac10b-58cc-4372-a567-0e02b2c3d479", "radioGroupId": "utg-output-choice-group-unknown_comment_id"} --> Create PR with unit tests - [ ] <!-- {"checkboxId": "6ba7b810-9dad-11d1-80b4-00c04fd430c8", "radioGroupId": "utg-output-choice-group-unknown_comment_id"} --> Commit unit tests in branch `worktree-refactored-strolling-treasure` </details> </details> <!-- finishing_touch_checkbox_end --> <!-- This is an auto-generated comment: all tool run failures by coderabbit.ai --> > [!WARNING] > There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure. > > <details> > <summary>🔧 golangci-lint (2.12.2)</summary> > > level=error msg="[linters_context] typechecking error: pattern ./...: directory prefix . does not contain main module or its selected dependencies" > > > > > </details> <!-- end of auto-generated comment: all tool run failures by coderabbit.ai --> <!-- tips_start --> --- <sub>Comment `@coderabbitai help` to get the list of available commands and usage tips.</sub> <!-- tips_end --> <!-- internal state start --> <!-- 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= --> <!-- internal state end -->
coderabbitai[bot] (Migrated from github.com) reviewed 2026-06-11 22:11:54 +00:00
coderabbitai[bot] (Migrated from github.com) left a comment

Actionable comments posted: 5

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
go/internal/handler/handler.go (1)

787-800: ⚠️ Potential issue | 🔴 Critical | 🏗️ Heavy lift

Missing authorization check allows arbitrary object download URL issuance.

Line 789 only verifies authentication, then Line 799 returns a signed URL for any provided object ID. This is an IDOR risk and can expose private media/avatars across users or networks.

🤖 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 `@go/internal/handler/handler.go` around lines 787 - 800, GetObjectDownloadUrl
currently only checks authentication via middleware.EmailFromContext but then
issues a signed URL from h.depotSvc.GetDownloadURL for any object id; to fix,
inside Handler.GetObjectDownloadUrl fetch the particle/media metadata and verify
the caller has access (ownership or visibility) before calling
h.depotSvc.GetDownloadURL—use the authenticated identity returned by
middleware.EmailFromContext to authorize (or call your particle service /
Firebase as noted in the TODO) and return http.StatusForbidden when the caller
is not permitted; ensure this authorization check happens prior to requesting
the signed URL.
🧹 Nitpick comments (1)
js/desktop/src/features/particles/particle-list-view.tsx (1)

153-193: Quick win

Extract shared avatar-derivation logic to avoid drift with stream-card.tsx.

This memoized DM/creator/fallback avatar resolution is duplicated with near-identical logic in js/desktop/src/features/particles/stream-card.tsx (Line 44-84). A shared helper (e.g., resolveStreamAvatarDisplay) would keep behavior consistent and reduce future regressions.

As per coding guidelines, "Write simpler, maintainable code in the Electron app instead of clever behavior."

🤖 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 `@js/desktop/src/features/particles/particle-list-view.tsx` around lines 153 -
193, The avatar derivation logic inside the avatar useMemo should be extracted
into a shared helper (e.g., export function resolveStreamAvatarDisplay) and both
particle-list-view.tsx and stream-card.tsx should call that helper instead of
duplicating logic; implement resolveStreamAvatarDisplay to accept the same
inputs used in the memo (particle, userId, network, latestChild, isDM), return
the same shape ({ initials, avatarObjectId }), export it from a common utilities
module, replace the useMemo body in particle-list-view (the avatar variable) and
the duplicated block in stream-card.tsx to call the helper (keeping memoization
callers as needed), and update imports accordingly so behavior and return shape
remain identical.

Source: Coding guidelines

🤖 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 `@go/internal/handler/handler.go`:
- Around line 345-365: The handler currently clears avatar_object_id before
capturing the prior object and thus leaks the old object; also on upload the
code uploads to h.depotSvc but if the subsequent DB update
(h.humanSvc.*Update/SetAvatar) fails the newly uploaded blob is orphaned. Fix by
first calling h.humanSvc.GetByID(r.Context(), humanId) to capture
oldAvatarObjectID, then call h.humanSvc.DeleteAvatar(...) (or the DB change);
after the DB operation succeeds, call h.depotSvc.Delete(r.Context(),
oldAvatarObjectID) to remove the previous blob; for uploads, perform
h.depotSvc.Upload(...) then attempt the DB update (e.g.,
h.humanSvc.SetAvatar/UpdateAvatar); if the DB update fails, call
h.depotSvc.Delete(r.Context(), newObjectID) to roll back the uploaded blob so it
isn’t orphaned. Ensure you reference and use h.humanSvc.GetByID,
h.humanSvc.DeleteAvatar (or Update/SetAvatar), h.depotSvc.Upload, and
h.depotSvc.Delete in this new order and add error logging around the rollback
delete.
- Around line 381-391: The handler wraps the request body with
http.MaxBytesReader but treats any upload-size error from
h.depotSvc.CreateFromReader as a 500; detect the MaxBytesReader overflow and
return http.StatusRequestEntityTooLarge instead. Update the error handling after
calling h.depotSvc.CreateFromReader: if errors.Is(err, http.ErrBodyTooLarge) (or
if that sentinel isn't available, match the "request body too large" error
string) respond with http.Error(w, "request entity too large",
http.StatusRequestEntityTooLarge); otherwise keep the existing flog.Error and
500 behavior. Ensure you import the errors package if needed and reference the
r.Body wrapping and h.depotSvc.CreateFromReader call in your change.

In `@go/migrations/000017_human_avatar.down.sql`:
- Line 4: The rollback migration SQL in 000017_human_avatar.down.sql contains a
trailing comma after the DROP COLUMN list ("DROP COLUMN IF EXISTS
avatar_object_id,") which makes the statement invalid; remove the trailing comma
so the DROP COLUMN clause is a properly terminated SQL statement (ensure the
DROP COLUMN list ends without a comma and the statement ends with the
appropriate semicolon or end-of-statement token).

In `@js/desktop/src/api/client.ts`:
- Around line 160-163: The getAvatarDownloadUrl method builds the path using
objectId directly, which can break for reserved characters; update
getAvatarDownloadUrl to encode the id before interpolation (e.g., use
encodeURIComponent on the objectId when constructing `/humans/avatar/${...}`) so
the call to this.fetch('GET', `/humans/avatar/${objectId}`) always sends a valid
path; ensure any callers that pass objectId still provide the raw id and let
this method perform encoding.

In `@js/desktop/src/lib/avatar-image.ts`:
- Around line 11-12: Validate and normalize the incoming size parameter at the
start of the avatar generation function (the function with signature "{ size =
512, mirror = false }: { size?: number; mirror?: boolean } = {}, ):
Promise<Blob>"). Ensure size is a positive integer before using it to create the
canvas: check Number.isInteger(size) && size > 0 (or coerce via Math.floor and
then verify >0), and if invalid either throw a clear error or fall back to a
safe default (e.g., 512); then use that validated/normalized value for the
canvas creation to prevent runtime errors or broken avatars.

---

Outside diff comments:
In `@go/internal/handler/handler.go`:
- Around line 787-800: GetObjectDownloadUrl currently only checks authentication
via middleware.EmailFromContext but then issues a signed URL from
h.depotSvc.GetDownloadURL for any object id; to fix, inside
Handler.GetObjectDownloadUrl fetch the particle/media metadata and verify the
caller has access (ownership or visibility) before calling
h.depotSvc.GetDownloadURL—use the authenticated identity returned by
middleware.EmailFromContext to authorize (or call your particle service /
Firebase as noted in the TODO) and return http.StatusForbidden when the caller
is not permitted; ensure this authorization check happens prior to requesting
the signed URL.

---

Nitpick comments:
In `@js/desktop/src/features/particles/particle-list-view.tsx`:
- Around line 153-193: The avatar derivation logic inside the avatar useMemo
should be extracted into a shared helper (e.g., export function
resolveStreamAvatarDisplay) and both particle-list-view.tsx and stream-card.tsx
should call that helper instead of duplicating logic; implement
resolveStreamAvatarDisplay to accept the same inputs used in the memo (particle,
userId, network, latestChild, isDM), return the same shape ({ initials,
avatarObjectId }), export it from a common utilities module, replace the useMemo
body in particle-list-view (the avatar variable) and the duplicated block in
stream-card.tsx to call the helper (keeping memoization callers as needed), and
update imports accordingly so behavior and return shape remain identical.
🪄 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: f1b2fa73-7efe-4b67-abc5-45c5f2ee1d57

📥 Commits

Reviewing files that changed from the base of the PR and between ca3bbf204e and fa9d54aab5.

📒 Files selected for processing (25)
  • go/cmd/orion/main.go
  • go/internal/handler/handler.go
  • go/internal/human/models.go
  • go/internal/human/repository.go
  • go/internal/human/service.go
  • go/internal/human/service_test.go
  • go/migrations/000017_human_avatar.down.sql
  • go/migrations/000017_human_avatar.up.sql
  • js/desktop/src/api/client.ts
  • js/desktop/src/api/types.ts
  • js/desktop/src/components/human-avatar.tsx
  • js/desktop/src/features/network-settings.tsx
  • js/desktop/src/features/particles/particle-list-view.tsx
  • js/desktop/src/features/particles/playback-page-indicator.tsx
  • js/desktop/src/features/particles/reaction-bar.tsx
  • js/desktop/src/features/particles/stream-card.tsx
  • js/desktop/src/features/particles/stream-members-overlay.tsx
  • js/desktop/src/features/particles/stream-top-bar.tsx
  • js/desktop/src/features/settings-page.tsx
  • js/desktop/src/features/settings/avatar-edit-dialog.tsx
  • js/desktop/src/hooks/use-avatar-url.ts
  • js/desktop/src/hooks/use-presence-positions.ts
  • js/desktop/src/lib/avatar-image.ts
  • js/desktop/src/lib/humans.ts
  • js/desktop/src/stores/auth-store.ts
**Actionable comments posted: 5** > [!CAUTION] > Some comments are outside the diff and can’t be posted inline due to platform limitations. > > > > <details> > <summary>⚠️ Outside diff range comments (1)</summary><blockquote> > > <details> > <summary>go/internal/handler/handler.go (1)</summary><blockquote> > > `787-800`: _⚠️ Potential issue_ | _🔴 Critical_ | _🏗️ Heavy lift_ > > **Missing authorization check allows arbitrary object download URL issuance.** > > Line 789 only verifies authentication, then Line 799 returns a signed URL for any provided object ID. This is an IDOR risk and can expose private media/avatars across users or networks. > > <details> > <summary>🤖 Prompt for AI Agents</summary> > > ``` > Verify each finding against current code. Fix only still-valid issues, skip the > rest with a brief reason, keep changes minimal, and validate. > > In `@go/internal/handler/handler.go` around lines 787 - 800, GetObjectDownloadUrl > currently only checks authentication via middleware.EmailFromContext but then > issues a signed URL from h.depotSvc.GetDownloadURL for any object id; to fix, > inside Handler.GetObjectDownloadUrl fetch the particle/media metadata and verify > the caller has access (ownership or visibility) before calling > h.depotSvc.GetDownloadURL—use the authenticated identity returned by > middleware.EmailFromContext to authorize (or call your particle service / > Firebase as noted in the TODO) and return http.StatusForbidden when the caller > is not permitted; ensure this authorization check happens prior to requesting > the signed URL. > ``` > > </details> > > <!-- cr-comment:v1:f6198c13127b7e94f0db6355 --> > > </blockquote></details> > > </blockquote></details> <details> <summary>🧹 Nitpick comments (1)</summary><blockquote> <details> <summary>js/desktop/src/features/particles/particle-list-view.tsx (1)</summary><blockquote> `153-193`: _⚡ Quick win_ **Extract shared avatar-derivation logic to avoid drift with `stream-card.tsx`.** This memoized DM/creator/fallback avatar resolution is duplicated with near-identical logic in `js/desktop/src/features/particles/stream-card.tsx` (Line 44-84). A shared helper (e.g., `resolveStreamAvatarDisplay`) would keep behavior consistent and reduce future regressions. As per coding guidelines, "Write simpler, maintainable code in the Electron app instead of clever behavior." <details> <summary>🤖 Prompt for AI Agents</summary> ``` Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@js/desktop/src/features/particles/particle-list-view.tsx` around lines 153 - 193, The avatar derivation logic inside the avatar useMemo should be extracted into a shared helper (e.g., export function resolveStreamAvatarDisplay) and both particle-list-view.tsx and stream-card.tsx should call that helper instead of duplicating logic; implement resolveStreamAvatarDisplay to accept the same inputs used in the memo (particle, userId, network, latestChild, isDM), return the same shape ({ initials, avatarObjectId }), export it from a common utilities module, replace the useMemo body in particle-list-view (the avatar variable) and the duplicated block in stream-card.tsx to call the helper (keeping memoization callers as needed), and update imports accordingly so behavior and return shape remain identical. ``` </details> <!-- cr-comment:v1:62302258cffac352c66c3d8d --> _Source: Coding guidelines_ </blockquote></details> </blockquote></details> <details> <summary>🤖 Prompt for all review comments with AI agents</summary> ``` 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 `@go/internal/handler/handler.go`: - Around line 345-365: The handler currently clears avatar_object_id before capturing the prior object and thus leaks the old object; also on upload the code uploads to h.depotSvc but if the subsequent DB update (h.humanSvc.*Update/SetAvatar) fails the newly uploaded blob is orphaned. Fix by first calling h.humanSvc.GetByID(r.Context(), humanId) to capture oldAvatarObjectID, then call h.humanSvc.DeleteAvatar(...) (or the DB change); after the DB operation succeeds, call h.depotSvc.Delete(r.Context(), oldAvatarObjectID) to remove the previous blob; for uploads, perform h.depotSvc.Upload(...) then attempt the DB update (e.g., h.humanSvc.SetAvatar/UpdateAvatar); if the DB update fails, call h.depotSvc.Delete(r.Context(), newObjectID) to roll back the uploaded blob so it isn’t orphaned. Ensure you reference and use h.humanSvc.GetByID, h.humanSvc.DeleteAvatar (or Update/SetAvatar), h.depotSvc.Upload, and h.depotSvc.Delete in this new order and add error logging around the rollback delete. - Around line 381-391: The handler wraps the request body with http.MaxBytesReader but treats any upload-size error from h.depotSvc.CreateFromReader as a 500; detect the MaxBytesReader overflow and return http.StatusRequestEntityTooLarge instead. Update the error handling after calling h.depotSvc.CreateFromReader: if errors.Is(err, http.ErrBodyTooLarge) (or if that sentinel isn't available, match the "request body too large" error string) respond with http.Error(w, "request entity too large", http.StatusRequestEntityTooLarge); otherwise keep the existing flog.Error and 500 behavior. Ensure you import the errors package if needed and reference the r.Body wrapping and h.depotSvc.CreateFromReader call in your change. In `@go/migrations/000017_human_avatar.down.sql`: - Line 4: The rollback migration SQL in 000017_human_avatar.down.sql contains a trailing comma after the DROP COLUMN list ("DROP COLUMN IF EXISTS avatar_object_id,") which makes the statement invalid; remove the trailing comma so the DROP COLUMN clause is a properly terminated SQL statement (ensure the DROP COLUMN list ends without a comma and the statement ends with the appropriate semicolon or end-of-statement token). In `@js/desktop/src/api/client.ts`: - Around line 160-163: The getAvatarDownloadUrl method builds the path using objectId directly, which can break for reserved characters; update getAvatarDownloadUrl to encode the id before interpolation (e.g., use encodeURIComponent on the objectId when constructing `/humans/avatar/${...}`) so the call to this.fetch('GET', `/humans/avatar/${objectId}`) always sends a valid path; ensure any callers that pass objectId still provide the raw id and let this method perform encoding. In `@js/desktop/src/lib/avatar-image.ts`: - Around line 11-12: Validate and normalize the incoming size parameter at the start of the avatar generation function (the function with signature "{ size = 512, mirror = false }: { size?: number; mirror?: boolean } = {}, ): Promise<Blob>"). Ensure size is a positive integer before using it to create the canvas: check Number.isInteger(size) && size > 0 (or coerce via Math.floor and then verify >0), and if invalid either throw a clear error or fall back to a safe default (e.g., 512); then use that validated/normalized value for the canvas creation to prevent runtime errors or broken avatars. --- Outside diff comments: In `@go/internal/handler/handler.go`: - Around line 787-800: GetObjectDownloadUrl currently only checks authentication via middleware.EmailFromContext but then issues a signed URL from h.depotSvc.GetDownloadURL for any object id; to fix, inside Handler.GetObjectDownloadUrl fetch the particle/media metadata and verify the caller has access (ownership or visibility) before calling h.depotSvc.GetDownloadURL—use the authenticated identity returned by middleware.EmailFromContext to authorize (or call your particle service / Firebase as noted in the TODO) and return http.StatusForbidden when the caller is not permitted; ensure this authorization check happens prior to requesting the signed URL. --- Nitpick comments: In `@js/desktop/src/features/particles/particle-list-view.tsx`: - Around line 153-193: The avatar derivation logic inside the avatar useMemo should be extracted into a shared helper (e.g., export function resolveStreamAvatarDisplay) and both particle-list-view.tsx and stream-card.tsx should call that helper instead of duplicating logic; implement resolveStreamAvatarDisplay to accept the same inputs used in the memo (particle, userId, network, latestChild, isDM), return the same shape ({ initials, avatarObjectId }), export it from a common utilities module, replace the useMemo body in particle-list-view (the avatar variable) and the duplicated block in stream-card.tsx to call the helper (keeping memoization callers as needed), and update imports accordingly so behavior and return shape remain identical. ``` </details> <details> <summary>🪄 Autofix (Beta)</summary> Fix all unresolved CodeRabbit comments on this PR: - [ ] <!-- {"checkboxId": "4b0d0e0a-96d7-4f10-b296-3a18ea78f0b9"} --> Push a commit to this branch (recommended) - [ ] <!-- {"checkboxId": "ff5b1114-7d8c-49e6-8ac1-43f82af23a33"} --> Create a new PR with the fixes </details> --- <details> <summary>ℹ️ Review info</summary> <details> <summary>⚙️ Run configuration</summary> **Configuration used**: defaults **Review profile**: CHILL **Plan**: Pro Plus **Run ID**: `f1b2fa73-7efe-4b67-abc5-45c5f2ee1d57` </details> <details> <summary>📥 Commits</summary> Reviewing files that changed from the base of the PR and between ca3bbf204eb997627d682e97a2dfd3e03235bdfb and fa9d54aab5ba3fc28c8c8930154394e653515576. </details> <details> <summary>📒 Files selected for processing (25)</summary> * `go/cmd/orion/main.go` * `go/internal/handler/handler.go` * `go/internal/human/models.go` * `go/internal/human/repository.go` * `go/internal/human/service.go` * `go/internal/human/service_test.go` * `go/migrations/000017_human_avatar.down.sql` * `go/migrations/000017_human_avatar.up.sql` * `js/desktop/src/api/client.ts` * `js/desktop/src/api/types.ts` * `js/desktop/src/components/human-avatar.tsx` * `js/desktop/src/features/network-settings.tsx` * `js/desktop/src/features/particles/particle-list-view.tsx` * `js/desktop/src/features/particles/playback-page-indicator.tsx` * `js/desktop/src/features/particles/reaction-bar.tsx` * `js/desktop/src/features/particles/stream-card.tsx` * `js/desktop/src/features/particles/stream-members-overlay.tsx` * `js/desktop/src/features/particles/stream-top-bar.tsx` * `js/desktop/src/features/settings-page.tsx` * `js/desktop/src/features/settings/avatar-edit-dialog.tsx` * `js/desktop/src/hooks/use-avatar-url.ts` * `js/desktop/src/hooks/use-presence-positions.ts` * `js/desktop/src/lib/avatar-image.ts` * `js/desktop/src/lib/humans.ts` * `js/desktop/src/stores/auth-store.ts` </details> </details> <!-- This is an auto-generated comment by CodeRabbit for review status -->
@@ -334,6 +335,83 @@ func (h *Handler) UpdateSettings(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusNoContent)
coderabbitai[bot] (Migrated from github.com) commented 2026-06-11 22:11:52 +00:00

⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

Avatar blob lifecycle is inconsistent and leaks objects.

Line 345 clears avatar_object_id before reading prior state, so the old avatar object ID is lost and never deleted.
Also, if Line 383 upload succeeds but Line 394 DB update fails, the newly uploaded object is orphaned.

Suggested lifecycle shape
+// DeleteAvatar:
+// 1) read current human/avatar_object_id
+// 2) clear avatar_object_id in DB
+// 3) best-effort delete old object using captured ID

+// UpdateAvatar:
+// 1) read current avatar_object_id (oldID)
+// 2) upload new object (newID)
+// 3) update DB to newID
+// 4) if DB update fails, best-effort delete newID
+// 5) after successful DB update, best-effort delete oldID

Also applies to: 383-399

🤖 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 `@go/internal/handler/handler.go` around lines 345 - 365, The handler currently
clears avatar_object_id before capturing the prior object and thus leaks the old
object; also on upload the code uploads to h.depotSvc but if the subsequent DB
update (h.humanSvc.*Update/SetAvatar) fails the newly uploaded blob is orphaned.
Fix by first calling h.humanSvc.GetByID(r.Context(), humanId) to capture
oldAvatarObjectID, then call h.humanSvc.DeleteAvatar(...) (or the DB change);
after the DB operation succeeds, call h.depotSvc.Delete(r.Context(),
oldAvatarObjectID) to remove the previous blob; for uploads, perform
h.depotSvc.Upload(...) then attempt the DB update (e.g.,
h.humanSvc.SetAvatar/UpdateAvatar); if the DB update fails, call
h.depotSvc.Delete(r.Context(), newObjectID) to roll back the uploaded blob so it
isn’t orphaned. Ensure you reference and use h.humanSvc.GetByID,
h.humanSvc.DeleteAvatar (or Update/SetAvatar), h.depotSvc.Upload, and
h.depotSvc.Delete in this new order and add error logging around the rollback
delete.
_⚠️ Potential issue_ | _🟠 Major_ | _🏗️ Heavy lift_ **Avatar blob lifecycle is inconsistent and leaks objects.** Line 345 clears `avatar_object_id` before reading prior state, so the old avatar object ID is lost and never deleted. Also, if Line 383 upload succeeds but Line 394 DB update fails, the newly uploaded object is orphaned. <details> <summary>Suggested lifecycle shape</summary> ```diff +// DeleteAvatar: +// 1) read current human/avatar_object_id +// 2) clear avatar_object_id in DB +// 3) best-effort delete old object using captured ID +// UpdateAvatar: +// 1) read current avatar_object_id (oldID) +// 2) upload new object (newID) +// 3) update DB to newID +// 4) if DB update fails, best-effort delete newID +// 5) after successful DB update, best-effort delete oldID ``` </details> Also applies to: 383-399 <details> <summary>🤖 Prompt for AI Agents</summary> ``` Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@go/internal/handler/handler.go` around lines 345 - 365, The handler currently clears avatar_object_id before capturing the prior object and thus leaks the old object; also on upload the code uploads to h.depotSvc but if the subsequent DB update (h.humanSvc.*Update/SetAvatar) fails the newly uploaded blob is orphaned. Fix by first calling h.humanSvc.GetByID(r.Context(), humanId) to capture oldAvatarObjectID, then call h.humanSvc.DeleteAvatar(...) (or the DB change); after the DB operation succeeds, call h.depotSvc.Delete(r.Context(), oldAvatarObjectID) to remove the previous blob; for uploads, perform h.depotSvc.Upload(...) then attempt the DB update (e.g., h.humanSvc.SetAvatar/UpdateAvatar); if the DB update fails, call h.depotSvc.Delete(r.Context(), newObjectID) to roll back the uploaded blob so it isn’t orphaned. Ensure you reference and use h.humanSvc.GetByID, h.humanSvc.DeleteAvatar (or Update/SetAvatar), h.depotSvc.Upload, and h.depotSvc.Delete in this new order and add error logging around the rollback delete. ``` </details> <!-- fingerprinting:phantom:poseidon:hawk --> <!-- cr-comment:v1:843b5388d0579f91608f888a --> <!-- This is an auto-generated comment by CodeRabbit -->
@@ -337,0 +400,4 @@
flog.Error("failed to update human avatar", "error", err, "humanId", humanId)
http.Error(w, "internal server error", http.StatusInternalServerError)
// best effort
coderabbitai[bot] (Migrated from github.com) commented 2026-06-11 22:11:53 +00:00

⚠️ Potential issue | 🟡 Minor | Quick win

Return 413 for oversized avatar uploads instead of 500.

When http.MaxBytesReader limit is exceeded, the handler currently falls into a generic 500. This should map to http.StatusRequestEntityTooLarge.

Suggested fix
  object, err := h.depotSvc.CreateFromReader(r.Context(), depot.CreateFromReaderInput{
      Prefix:      "avatars",
      Name:        fmt.Sprintf("%s-avatar", humanId),
      ContentType: r.Header.Get("Content-Type"),
  }, r.Body)
  if err != nil {
+     var maxErr *http.MaxBytesError
+     if errors.As(err, &maxErr) {
+         http.Error(w, "avatar file too large", http.StatusRequestEntityTooLarge)
+         return
+     }
      flog.Error("failed to upload avatar with depo", "error", err, "humanId", humanId)
      http.Error(w, "internal server error", http.StatusInternalServerError)
      return
  }
📝 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.

	r.Body = http.MaxBytesReader(w, r.Body, maxBodySize)

	object, err := h.depotSvc.CreateFromReader(r.Context(), depot.CreateFromReaderInput{
		Prefix:      "avatars",
		Name:        fmt.Sprintf("%s-avatar", humanId),
		ContentType: r.Header.Get("Content-Type"),
	}, r.Body)
	if err != nil {
		var maxErr *http.MaxBytesError
		if errors.As(err, &maxErr) {
			http.Error(w, "avatar file too large", http.StatusRequestEntityTooLarge)
			return
		}
		flog.Error("failed to upload avatar with depo", "error", err, "humanId", humanId)
		http.Error(w, "internal server error", http.StatusInternalServerError)
		return
	}
🤖 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 `@go/internal/handler/handler.go` around lines 381 - 391, The handler wraps the
request body with http.MaxBytesReader but treats any upload-size error from
h.depotSvc.CreateFromReader as a 500; detect the MaxBytesReader overflow and
return http.StatusRequestEntityTooLarge instead. Update the error handling after
calling h.depotSvc.CreateFromReader: if errors.Is(err, http.ErrBodyTooLarge) (or
if that sentinel isn't available, match the "request body too large" error
string) respond with http.Error(w, "request entity too large",
http.StatusRequestEntityTooLarge); otherwise keep the existing flog.Error and
500 behavior. Ensure you import the errors package if needed and reference the
r.Body wrapping and h.depotSvc.CreateFromReader call in your change.

Addressed in commits ffac812 to 6bf2c99

_⚠️ Potential issue_ | _🟡 Minor_ | _⚡ Quick win_ **Return 413 for oversized avatar uploads instead of 500.** When `http.MaxBytesReader` limit is exceeded, the handler currently falls into a generic 500. This should map to `http.StatusRequestEntityTooLarge`. <details> <summary>Suggested fix</summary> ```diff object, err := h.depotSvc.CreateFromReader(r.Context(), depot.CreateFromReaderInput{ Prefix: "avatars", Name: fmt.Sprintf("%s-avatar", humanId), ContentType: r.Header.Get("Content-Type"), }, r.Body) if err != nil { + var maxErr *http.MaxBytesError + if errors.As(err, &maxErr) { + http.Error(w, "avatar file too large", http.StatusRequestEntityTooLarge) + return + } flog.Error("failed to upload avatar with depo", "error", err, "humanId", humanId) http.Error(w, "internal server error", http.StatusInternalServerError) return } ``` </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 r.Body = http.MaxBytesReader(w, r.Body, maxBodySize) object, err := h.depotSvc.CreateFromReader(r.Context(), depot.CreateFromReaderInput{ Prefix: "avatars", Name: fmt.Sprintf("%s-avatar", humanId), ContentType: r.Header.Get("Content-Type"), }, r.Body) if err != nil { var maxErr *http.MaxBytesError if errors.As(err, &maxErr) { http.Error(w, "avatar file too large", http.StatusRequestEntityTooLarge) return } flog.Error("failed to upload avatar with depo", "error", err, "humanId", humanId) http.Error(w, "internal server error", http.StatusInternalServerError) return } ``` </details> <!-- suggestion_end --> <details> <summary>🤖 Prompt for AI Agents</summary> ``` Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@go/internal/handler/handler.go` around lines 381 - 391, The handler wraps the request body with http.MaxBytesReader but treats any upload-size error from h.depotSvc.CreateFromReader as a 500; detect the MaxBytesReader overflow and return http.StatusRequestEntityTooLarge instead. Update the error handling after calling h.depotSvc.CreateFromReader: if errors.Is(err, http.ErrBodyTooLarge) (or if that sentinel isn't available, match the "request body too large" error string) respond with http.Error(w, "request entity too large", http.StatusRequestEntityTooLarge); otherwise keep the existing flog.Error and 500 behavior. Ensure you import the errors package if needed and reference the r.Body wrapping and h.depotSvc.CreateFromReader call in your change. ``` </details> <!-- fingerprinting:phantom:poseidon:hawk --> <!-- cr-comment:v1:6fffad183715aaf0f26ca668 --> <!-- This is an auto-generated comment by CodeRabbit --> ✅ Addressed in commits ffac812 to 6bf2c99
@@ -0,0 +1,6 @@
BEGIN;
coderabbitai[bot] (Migrated from github.com) commented 2026-06-11 22:11:53 +00:00

⚠️ Potential issue | 🔴 Critical | Quick win

Fix invalid SQL in rollback migration.

Line 4 has a trailing comma, which makes the down migration fail to execute.

Suggested fix
 ALTER TABLE humans
-  DROP COLUMN IF EXISTS avatar_object_id,
+  DROP COLUMN IF EXISTS avatar_object_id;
📝 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.

ALTER TABLE humans
  DROP COLUMN IF EXISTS avatar_object_id;
🤖 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 `@go/migrations/000017_human_avatar.down.sql` at line 4, The rollback migration
SQL in 000017_human_avatar.down.sql contains a trailing comma after the DROP
COLUMN list ("DROP COLUMN IF EXISTS avatar_object_id,") which makes the
statement invalid; remove the trailing comma so the DROP COLUMN clause is a
properly terminated SQL statement (ensure the DROP COLUMN list ends without a
comma and the statement ends with the appropriate semicolon or end-of-statement
token).

Addressed in commits a45beb9 to a25fb83

_⚠️ Potential issue_ | _🔴 Critical_ | _⚡ Quick win_ **Fix invalid SQL in rollback migration.** Line 4 has a trailing comma, which makes the down migration fail to execute. <details> <summary>Suggested fix</summary> ```diff ALTER TABLE humans - DROP COLUMN IF EXISTS avatar_object_id, + DROP COLUMN IF EXISTS avatar_object_id; ``` </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 ALTER TABLE humans DROP COLUMN IF EXISTS avatar_object_id; ``` </details> <!-- suggestion_end --> <details> <summary>🤖 Prompt for AI Agents</summary> ``` Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@go/migrations/000017_human_avatar.down.sql` at line 4, The rollback migration SQL in 000017_human_avatar.down.sql contains a trailing comma after the DROP COLUMN list ("DROP COLUMN IF EXISTS avatar_object_id,") which makes the statement invalid; remove the trailing comma so the DROP COLUMN clause is a properly terminated SQL statement (ensure the DROP COLUMN list ends without a comma and the statement ends with the appropriate semicolon or end-of-statement token). ``` </details> <!-- fingerprinting:phantom:poseidon:hawk --> <!-- cr-comment:v1:6d0227aabf7772fa5ddadda5 --> <!-- This is an auto-generated comment by CodeRabbit --> ✅ Addressed in commits a45beb9 to a25fb83
coderabbitai[bot] (Migrated from github.com) commented 2026-06-11 22:11:53 +00:00

⚠️ Potential issue | 🟡 Minor | Quick win

Encode objectId before building the avatar URL path.

Line 161 interpolates objectId directly. Reserved characters can produce a malformed route and wrong request target.

Suggested fix
   async getAvatarDownloadUrl(objectId: string): Promise<string> {
-    const response = await this.fetch('GET', `/humans/avatar/${objectId}`);
+    const encodedObjectId = encodeURIComponent(objectId);
+    const response = await this.fetch('GET', `/humans/avatar/${encodedObjectId}`);
     const data = await response.json();
     return data.url;
   }
📝 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.

  async getAvatarDownloadUrl(objectId: string): Promise<string> {
    const encodedObjectId = encodeURIComponent(objectId);
    const response = await this.fetch('GET', `/humans/avatar/${encodedObjectId}`);
    const data = await response.json();
    return data.url;
  }
🤖 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 `@js/desktop/src/api/client.ts` around lines 160 - 163, The
getAvatarDownloadUrl method builds the path using objectId directly, which can
break for reserved characters; update getAvatarDownloadUrl to encode the id
before interpolation (e.g., use encodeURIComponent on the objectId when
constructing `/humans/avatar/${...}`) so the call to this.fetch('GET',
`/humans/avatar/${objectId}`) always sends a valid path; ensure any callers that
pass objectId still provide the raw id and let this method perform encoding.
_⚠️ Potential issue_ | _🟡 Minor_ | _⚡ Quick win_ **Encode `objectId` before building the avatar URL path.** Line 161 interpolates `objectId` directly. Reserved characters can produce a malformed route and wrong request target. <details> <summary>Suggested fix</summary> ```diff async getAvatarDownloadUrl(objectId: string): Promise<string> { - const response = await this.fetch('GET', `/humans/avatar/${objectId}`); + const encodedObjectId = encodeURIComponent(objectId); + const response = await this.fetch('GET', `/humans/avatar/${encodedObjectId}`); const data = await response.json(); return data.url; } ``` </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 async getAvatarDownloadUrl(objectId: string): Promise<string> { const encodedObjectId = encodeURIComponent(objectId); const response = await this.fetch('GET', `/humans/avatar/${encodedObjectId}`); const data = await response.json(); return data.url; } ``` </details> <!-- suggestion_end --> <details> <summary>🤖 Prompt for AI Agents</summary> ``` Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@js/desktop/src/api/client.ts` around lines 160 - 163, The getAvatarDownloadUrl method builds the path using objectId directly, which can break for reserved characters; update getAvatarDownloadUrl to encode the id before interpolation (e.g., use encodeURIComponent on the objectId when constructing `/humans/avatar/${...}`) so the call to this.fetch('GET', `/humans/avatar/${objectId}`) always sends a valid path; ensure any callers that pass objectId still provide the raw id and let this method perform encoding. ``` </details> <!-- fingerprinting:phantom:poseidon:hawk --> <!-- cr-comment:v1:7687b9b14bbed6dbbd798b45 --> <!-- This is an auto-generated comment by CodeRabbit -->
@@ -0,0 +9,4 @@
export async function toAvatarBlob(
source: ImageBitmap,
{ size = 512, mirror = false }: { size?: number; mirror?: boolean } = {},
): Promise<Blob> {
coderabbitai[bot] (Migrated from github.com) commented 2026-06-11 22:11:53 +00:00

⚠️ Potential issue | 🟡 Minor | Quick win

Validate size as a positive integer before creating the canvas.

Line 22 depends on size; invalid values can yield broken avatar output or throw at runtime.

Suggested fix
 export async function toAvatarBlob(
   source: ImageBitmap,
   { size = 512, mirror = false }: { size?: number; mirror?: boolean } = {},
 ): Promise<Blob> {
+  if (!Number.isInteger(size) || size <= 0) {
+    throw new Error('Avatar size must be a positive integer');
+  }
   const side = Math.min(source.width, source.height);
   if (side === 0) {
📝 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.

  { size = 512, mirror = false }: { size?: number; mirror?: boolean } = {},
): Promise<Blob> {
  if (!Number.isInteger(size) || size <= 0) {
    throw new Error('Avatar size must be a positive integer');
  }
🤖 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 `@js/desktop/src/lib/avatar-image.ts` around lines 11 - 12, Validate and
normalize the incoming size parameter at the start of the avatar generation
function (the function with signature "{ size = 512, mirror = false }: { size?:
number; mirror?: boolean } = {}, ): Promise<Blob>"). Ensure size is a positive
integer before using it to create the canvas: check Number.isInteger(size) &&
size > 0 (or coerce via Math.floor and then verify >0), and if invalid either
throw a clear error or fall back to a safe default (e.g., 512); then use that
validated/normalized value for the canvas creation to prevent runtime errors or
broken avatars.

Addressed in commits ffac812 to 6bf2c99

_⚠️ Potential issue_ | _🟡 Minor_ | _⚡ Quick win_ **Validate `size` as a positive integer before creating the canvas.** Line 22 depends on `size`; invalid values can yield broken avatar output or throw at runtime. <details> <summary>Suggested fix</summary> ```diff export async function toAvatarBlob( source: ImageBitmap, { size = 512, mirror = false }: { size?: number; mirror?: boolean } = {}, ): Promise<Blob> { + if (!Number.isInteger(size) || size <= 0) { + throw new Error('Avatar size must be a positive integer'); + } const side = Math.min(source.width, source.height); if (side === 0) { ``` </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 { size = 512, mirror = false }: { size?: number; mirror?: boolean } = {}, ): Promise<Blob> { if (!Number.isInteger(size) || size <= 0) { throw new Error('Avatar size must be a positive integer'); } ``` </details> <!-- suggestion_end --> <details> <summary>🤖 Prompt for AI Agents</summary> ``` Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@js/desktop/src/lib/avatar-image.ts` around lines 11 - 12, Validate and normalize the incoming size parameter at the start of the avatar generation function (the function with signature "{ size = 512, mirror = false }: { size?: number; mirror?: boolean } = {}, ): Promise<Blob>"). Ensure size is a positive integer before using it to create the canvas: check Number.isInteger(size) && size > 0 (or coerce via Math.floor and then verify >0), and if invalid either throw a clear error or fall back to a safe default (e.g., 512); then use that validated/normalized value for the canvas creation to prevent runtime errors or broken avatars. ``` </details> <!-- fingerprinting:phantom:poseidon:hawk --> <!-- cr-comment:v1:3412b6e39458ff9b8c5d8ef9 --> <!-- This is an auto-generated comment by CodeRabbit --> ✅ Addressed in commits ffac812 to 6bf2c99
coderabbitai[bot] (Migrated from github.com) reviewed 2026-06-11 22:21:17 +00:00
coderabbitai[bot] (Migrated from github.com) left a comment

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
go/internal/handler/handler.go (1)

793-805: ⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

Enforce object-level authorization before minting signed URLs.

This handler only checks that the caller is authenticated, then signs whatever object ID is in the path. With this layer now serving avatar/object downloads, any authenticated user who learns another object's ID can mint a valid URL without a membership or ownership check.

🤖 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 `@go/internal/handler/handler.go` around lines 793 - 805, In
GetObjectDownloadUrl, do an object-level authorization check before calling
h.depotSvc.GetDownloadURL: extract the caller identity using
middleware.EmailFromContext(r.Context()), fetch the object's metadata (e.g., via
an existing particle/object lookup like a particle service or depot metadata
method), verify the object's visibility or that the caller is an owner/member
(compare the fetched metadata's owner/members/visibility fields to the
email/identity), and if the check fails return http.Error(...,
http.StatusForbidden); only call h.depotSvc.GetDownloadURL(r.Context(),
objectID) and write the URL on success. Ensure you use the existing symbols
Handler.GetObjectDownloadUrl, middleware.EmailFromContext, and
h.depotSvc.GetDownloadURL to locate the code to change.
🤖 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 `@go/internal/handler/handler.go`:
- Around line 388-398: The UpdateAvatar function contains an extra closing brace
'}' after the error handling block (the one closing the function early) which
causes a parse error; remove that unmatched '}' so the function body continues
normally (locate the error handling block in UpdateAvatar around the errors.As
check and the flog.Error/http.Error calls and delete the extraneous closing
brace), then run go build to confirm parsing succeeds.

---

Outside diff comments:
In `@go/internal/handler/handler.go`:
- Around line 793-805: In GetObjectDownloadUrl, do an object-level authorization
check before calling h.depotSvc.GetDownloadURL: extract the caller identity
using middleware.EmailFromContext(r.Context()), fetch the object's metadata
(e.g., via an existing particle/object lookup like a particle service or depot
metadata method), verify the object's visibility or that the caller is an
owner/member (compare the fetched metadata's owner/members/visibility fields to
the email/identity), and if the check fails return http.Error(...,
http.StatusForbidden); only call h.depotSvc.GetDownloadURL(r.Context(),
objectID) and write the URL on success. Ensure you use the existing symbols
Handler.GetObjectDownloadUrl, middleware.EmailFromContext, and
h.depotSvc.GetDownloadURL to locate the code to change.
🪄 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: 5eee9ab3-504d-4409-8a31-6fdeca6719b6

📥 Commits

Reviewing files that changed from the base of the PR and between fa9d54aab5 and 6bf2c9983a.

📒 Files selected for processing (2)
  • go/internal/handler/handler.go
  • js/desktop/src/lib/avatar-image.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • js/desktop/src/lib/avatar-image.ts
**Actionable comments posted: 1** > [!CAUTION] > Some comments are outside the diff and can’t be posted inline due to platform limitations. > > > > <details> > <summary>⚠️ Outside diff range comments (1)</summary><blockquote> > > <details> > <summary>go/internal/handler/handler.go (1)</summary><blockquote> > > `793-805`: _⚠️ Potential issue_ | _🟠 Major_ | _🏗️ Heavy lift_ > > **Enforce object-level authorization before minting signed URLs.** > > This handler only checks that the caller is authenticated, then signs whatever object ID is in the path. With this layer now serving avatar/object downloads, any authenticated user who learns another object's ID can mint a valid URL without a membership or ownership check. > > <details> > <summary>🤖 Prompt for AI Agents</summary> > > ``` > Verify each finding against current code. Fix only still-valid issues, skip the > rest with a brief reason, keep changes minimal, and validate. > > In `@go/internal/handler/handler.go` around lines 793 - 805, In > GetObjectDownloadUrl, do an object-level authorization check before calling > h.depotSvc.GetDownloadURL: extract the caller identity using > middleware.EmailFromContext(r.Context()), fetch the object's metadata (e.g., via > an existing particle/object lookup like a particle service or depot metadata > method), verify the object's visibility or that the caller is an owner/member > (compare the fetched metadata's owner/members/visibility fields to the > email/identity), and if the check fails return http.Error(..., > http.StatusForbidden); only call h.depotSvc.GetDownloadURL(r.Context(), > objectID) and write the URL on success. Ensure you use the existing symbols > Handler.GetObjectDownloadUrl, middleware.EmailFromContext, and > h.depotSvc.GetDownloadURL to locate the code to change. > ``` > > </details> > > <!-- cr-comment:v1:bd0b7c6ddefafc5436932dca --> > > </blockquote></details> > > </blockquote></details> <details> <summary>🤖 Prompt for all review comments with AI agents</summary> ``` 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 `@go/internal/handler/handler.go`: - Around line 388-398: The UpdateAvatar function contains an extra closing brace '}' after the error handling block (the one closing the function early) which causes a parse error; remove that unmatched '}' so the function body continues normally (locate the error handling block in UpdateAvatar around the errors.As check and the flog.Error/http.Error calls and delete the extraneous closing brace), then run go build to confirm parsing succeeds. --- Outside diff comments: In `@go/internal/handler/handler.go`: - Around line 793-805: In GetObjectDownloadUrl, do an object-level authorization check before calling h.depotSvc.GetDownloadURL: extract the caller identity using middleware.EmailFromContext(r.Context()), fetch the object's metadata (e.g., via an existing particle/object lookup like a particle service or depot metadata method), verify the object's visibility or that the caller is an owner/member (compare the fetched metadata's owner/members/visibility fields to the email/identity), and if the check fails return http.Error(..., http.StatusForbidden); only call h.depotSvc.GetDownloadURL(r.Context(), objectID) and write the URL on success. Ensure you use the existing symbols Handler.GetObjectDownloadUrl, middleware.EmailFromContext, and h.depotSvc.GetDownloadURL to locate the code to change. ``` </details> <details> <summary>🪄 Autofix (Beta)</summary> Fix all unresolved CodeRabbit comments on this PR: - [ ] <!-- {"checkboxId": "4b0d0e0a-96d7-4f10-b296-3a18ea78f0b9"} --> Push a commit to this branch (recommended) - [ ] <!-- {"checkboxId": "ff5b1114-7d8c-49e6-8ac1-43f82af23a33"} --> Create a new PR with the fixes </details> --- <details> <summary>ℹ️ Review info</summary> <details> <summary>⚙️ Run configuration</summary> **Configuration used**: defaults **Review profile**: CHILL **Plan**: Pro Plus **Run ID**: `5eee9ab3-504d-4409-8a31-6fdeca6719b6` </details> <details> <summary>📥 Commits</summary> Reviewing files that changed from the base of the PR and between fa9d54aab5ba3fc28c8c8930154394e653515576 and 6bf2c9983a0f6d4a00f24c531ca201850f7a6537. </details> <details> <summary>📒 Files selected for processing (2)</summary> * `go/internal/handler/handler.go` * `js/desktop/src/lib/avatar-image.ts` </details> <details> <summary>🚧 Files skipped from review as they are similar to previous changes (1)</summary> * js/desktop/src/lib/avatar-image.ts </details> </details> <!-- This is an auto-generated comment by CodeRabbit for review status -->
@@ -334,6 +335,83 @@ func (h *Handler) UpdateSettings(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusNoContent)
coderabbitai[bot] (Migrated from github.com) commented 2026-06-11 22:21:17 +00:00

⚠️ Potential issue | 🔴 Critical | Quick win

Remove the unmatched } in UpdateAvatar.

Line 398 closes the function early, so Line 400 starts with a bare assignment and the file no longer parses. That matches the CI failure exactly.

Suggested fix
  if err != nil {
  	var maxErr *http.MaxBytesError
  	if errors.As(err, &maxErr) {
  		http.Error(w, "avatar file too large", http.StatusRequestEntityTooLarge)
  		return
  	}
  	flog.Error("failed to upload avatar with depo", "error", err, "humanId", humanId)
  	http.Error(w, "internal server error", http.StatusInternalServerError)
  	return
  }
-	}
 
 	err = h.humanSvc.UpdateAvatar(r.Context(), humanId, object.ID)
📝 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.

	if err != nil {
		var maxErr *http.MaxBytesError
		if errors.As(err, &maxErr) {
			http.Error(w, "avatar file too large", http.StatusRequestEntityTooLarge)
			return
		}
		flog.Error("failed to upload avatar with depo", "error", err, "humanId", humanId)
		http.Error(w, "internal server error", http.StatusInternalServerError)
		return
	}

	err = h.humanSvc.UpdateAvatar(r.Context(), humanId, object.ID)
🤖 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 `@go/internal/handler/handler.go` around lines 388 - 398, The UpdateAvatar
function contains an extra closing brace '}' after the error handling block (the
one closing the function early) which causes a parse error; remove that
unmatched '}' so the function body continues normally (locate the error handling
block in UpdateAvatar around the errors.As check and the flog.Error/http.Error
calls and delete the extraneous closing brace), then run go build to confirm
parsing succeeds.

Source: Pipeline failures

_⚠️ Potential issue_ | _🔴 Critical_ | _⚡ Quick win_ **Remove the unmatched `}` in `UpdateAvatar`.** Line 398 closes the function early, so Line 400 starts with a bare assignment and the file no longer parses. That matches the CI failure exactly. <details> <summary>Suggested fix</summary> ```diff if err != nil { var maxErr *http.MaxBytesError if errors.As(err, &maxErr) { http.Error(w, "avatar file too large", http.StatusRequestEntityTooLarge) return } flog.Error("failed to upload avatar with depo", "error", err, "humanId", humanId) http.Error(w, "internal server error", http.StatusInternalServerError) return } - } err = h.humanSvc.UpdateAvatar(r.Context(), humanId, object.ID) ``` </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 if err != nil { var maxErr *http.MaxBytesError if errors.As(err, &maxErr) { http.Error(w, "avatar file too large", http.StatusRequestEntityTooLarge) return } flog.Error("failed to upload avatar with depo", "error", err, "humanId", humanId) http.Error(w, "internal server error", http.StatusInternalServerError) return } err = h.humanSvc.UpdateAvatar(r.Context(), humanId, object.ID) ``` </details> <!-- suggestion_end --> <details> <summary>🤖 Prompt for AI Agents</summary> ``` Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@go/internal/handler/handler.go` around lines 388 - 398, The UpdateAvatar function contains an extra closing brace '}' after the error handling block (the one closing the function early) which causes a parse error; remove that unmatched '}' so the function body continues normally (locate the error handling block in UpdateAvatar around the errors.As check and the flog.Error/http.Error calls and delete the extraneous closing brace), then run go build to confirm parsing succeeds. ``` </details> <!-- fingerprinting:phantom:medusa:grasshopper --> <!-- cr-comment:v1:30a7d713243d8df97f8f25a3 --> _Source: Pipeline failures_ <!-- This is an auto-generated comment by CodeRabbit -->
Sign in to join this conversation.