Fix #376: auto-generated avatars #379

Merged
crueber merged 1 commit from fix/issue-376 into main 2026-09-12 15:24:20 +00:00
Owner

Fixes #376: DiceBear constellation avatars (seed = verified email) generated in background on OIDC login.

Acceptance coverage

  • Login with no avatar enqueues generation (constellation, seed = email); appears in navbar (me().avatar_url) + /:owner header.
  • Never blocks login: AvatarHook seam fires post-mint in authCallback; per-principal single-flight goroutine + mutex dedup (task table deliberately NOT used — repo-keyed narrated work only; rationale in avatar.go + 01 Decisions).
  • Stable URL GET /api/v1/users/{principal}/avatar (image/svg+xml, public, max-age=86400, immutable + ETag, ?v=<avatar_updated_at> busting via UserAvatarURL).
  • Law-1 amendment first: AGENTS.md §1 + 01/06 Decisions, naming both modules (user-authorized 2026-09-12).
  • Opt-out: DELETE sets avatar_disabled; login never regenerates until POST regenerates (documented).
  • Seed escaping: seed feeds the PRNG only (verified absent from output); sanitize-first gate (SVG shape, no <script>, no seed leak — fail closed). Determinism documented (same email → identical bytes; regen reproduces).

Verified deviations from the issue text (checked, not assumed): (a) constellation defines NO options — the 'electric' preset names a notionists variant, not a constellation option, so generation uses defaults; (b) the library is NOT zero-dependency — go mod graph proof below.

Tests (all -race): internal/identity 95.5% (determinism, hostile-seed absence, single-flight 5-concurrent→1, headers/ETag/304, auth matrix, opt-out/regen, error branches); internal/api 95.2% (TestMeAvatarURL); internal/server green except pre-existing env failure TestUIAssetConcepts (fails identically on main — stale dist); cmd/walhub green; node 750/752 (2 smoke failures also on main — stray :8080 server). gofmt/vet clean.

go.mod additions (exact):

github.com/dicebear/dicebear-go/v10 v10.7.0 (direct)
github.com/dicebear/styles/v10 v10.6.0 (direct)
github.com/dicebear/schema v1.5.1 (indirect, build-required)
github.com/santhosh-tekuri/jsonschema/v6 v6.0.2 (indirect, build-required)

go mod graph (module requirements):

git.packden.us/crueber/walhub github.com/dicebear/dicebear-go/v10@v10.7.0
git.packden.us/crueber/walhub github.com/dicebear/schema@v1.5.1
git.packden.us/crueber/walhub github.com/dicebear/styles/v10@v10.6.0
git.packden.us/crueber/walhub github.com/santhosh-tekuri/jsonschema/v6@v6.0.2
github.com/dicebear/dicebear-go/v10@v10.7.0 github.com/dicebear/schema@v1.5.1
github.com/dicebear/dicebear-go/v10@v10.7.0 github.com/santhosh-tekuri/jsonschema/v6@v6.0.2
github.com/dicebear/dicebear-go/v10@v10.7.0 golang.org/x/text@v0.14.0

(go mod why pins both transitives to dicebear-go's internal validate/render path; x/text was already indirect.)

No browser drive (shared-daemon loopback guard per workspace rules); no docker/compose changes; no live instance touched.

Fixes #376: DiceBear constellation avatars (seed = verified email) generated in background on OIDC login. **Acceptance coverage** - Login with no avatar enqueues generation (constellation, seed = email); appears in navbar (`me().avatar_url`) + `/:owner` header. - Never blocks login: `AvatarHook` seam fires post-mint in `authCallback`; per-principal single-flight goroutine + mutex dedup (task table deliberately NOT used — repo-keyed narrated work only; rationale in `avatar.go` + 01 Decisions). - Stable URL `GET /api/v1/users/{principal}/avatar` (`image/svg+xml`, `public, max-age=86400, immutable` + ETag, `?v=<avatar_updated_at>` busting via `UserAvatarURL`). - Law-1 amendment first: AGENTS.md §1 + 01/06 Decisions, naming both modules (user-authorized 2026-09-12). - Opt-out: DELETE sets `avatar_disabled`; login never regenerates until POST regenerates (documented). - Seed escaping: seed feeds the PRNG only (verified absent from output); sanitize-first gate (SVG shape, no `<script>`, no seed leak — fail closed). Determinism documented (same email → identical bytes; regen reproduces). **Verified deviations from the issue text** (checked, not assumed): (a) constellation defines NO options — the 'electric' preset names a notionists variant, not a constellation option, so generation uses defaults; (b) the library is NOT zero-dependency — `go mod graph` proof below. **Tests** (all -race): `internal/identity` 95.5% (determinism, hostile-seed absence, single-flight 5-concurrent→1, headers/ETag/304, auth matrix, opt-out/regen, error branches); `internal/api` 95.2% (`TestMeAvatarURL`); `internal/server` green except pre-existing env failure `TestUIAssetConcepts` (fails identically on main — stale dist); `cmd/walhub` green; node 750/752 (2 smoke failures also on main — stray :8080 server). gofmt/vet clean. **go.mod additions (exact):** ``` github.com/dicebear/dicebear-go/v10 v10.7.0 (direct) github.com/dicebear/styles/v10 v10.6.0 (direct) github.com/dicebear/schema v1.5.1 (indirect, build-required) github.com/santhosh-tekuri/jsonschema/v6 v6.0.2 (indirect, build-required) ``` **go mod graph (module requirements):** ``` git.packden.us/crueber/walhub github.com/dicebear/dicebear-go/v10@v10.7.0 git.packden.us/crueber/walhub github.com/dicebear/schema@v1.5.1 git.packden.us/crueber/walhub github.com/dicebear/styles/v10@v10.6.0 git.packden.us/crueber/walhub github.com/santhosh-tekuri/jsonschema/v6@v6.0.2 github.com/dicebear/dicebear-go/v10@v10.7.0 github.com/dicebear/schema@v1.5.1 github.com/dicebear/dicebear-go/v10@v10.7.0 github.com/santhosh-tekuri/jsonschema/v6@v6.0.2 github.com/dicebear/dicebear-go/v10@v10.7.0 golang.org/x/text@v0.14.0 ``` (`go mod why` pins both transitives to dicebear-go's internal validate/render path; x/text was already indirect.) No browser drive (shared-daemon loopback guard per workspace rules); no docker/compose changes; no live instance touched.
DiceBear constellation (seed = verified email) generated in-process on
first OIDC login via the server AvatarHook seam; per-principal
single-flight goroutine (task table deliberately not used — repo-keyed).
Bytes at users/<username>/avatar.svg, pointer on profile.json (#359
shape); GET/POST/DELETE /api/v1/users/{principal}/avatar; DELETE opts
out (avatar_disabled) until regenerate. me().avatar_url feeds the
navbar; /:owner header renders + self-service. Law-1 amendment
(dicebear-go/v10 + styles/v10, transitives schema + jsonschema/v6 per
go mod graph) in AGENTS.md §1 + 01 Decisions.
Sign in to join this conversation.
No description provided.