clone

a self-hosted multitenant nostr relay with sites, file storage, git hosting, and inbox and outbox delivery

feat: use Seedmark avatars for people and group chats

Dami committed 3d347aa
Replace initials with deterministic Seedmark v1 images while preserving profile pictures. Show room pictures and stable generated fallbacks in the chat list, rail, room header and thread breadcrumb.

Render fallbacks without JavaScript, cache versioned SVGs, and preserve tenant URL scoping. Pin the Go renderer to 1,010 upstream fixtures and cover broken pictures, identity changes and room surfaces.

Update avatar documentation and preserve the upstream MIT notice. Verified Go and JavaScript suites, race checks, vet, bundles, and desktop, mobile and no-JavaScript browser rendering.
commit
3d347aa40d0b202b4d34371fd349e8d8e5450da9
parent
91c15ac8a61eb037b8853692dc017dce415920df
author
Dami <dami.etoile@gmail.com>
30 files changed, +6055 -60
MTHIRD_PARTY_NOTICES27+
Mdocs/app.md6+-
Mdocs/architecture.md1+
Mdocs/rooms.md4+-
Ainternal/seedmark/LICENSE21+
Ainternal/seedmark/families_a.go192+
Ainternal/seedmark/families_b.go162+
Ainternal/seedmark/primitives.go27+
Ainternal/seedmark/seedmark.go109+
Ainternal/seedmark/seedmark_test.go48+
Ainternal/seedmark/testdata/v1.json5052++++++++++++++++++++++++++++++
Ainternal/webui/avatars.go47+
Ainternal/webui/avatars_test.go78+
Minternal/webui/chat.js2+-
Minternal/webui/components.js73+-
Ainternal/webui/group_avatars_test.go76+
Minternal/webui/page.html2+-
Minternal/webui/prefix.go2+-
Minternal/webui/prefix_test.go1+
Minternal/webui/rooms.go17+-
Minternal/webui/rooms.html12+-
Minternal/webui/rooms.js2+-
Minternal/webui/rooms_test.go14+-
Minternal/webui/social.html2+-
Minternal/webui/style.css12+-
Minternal/webui/templates.go2+
Minternal/webui/webui.go4+
Mscripts/chat-identity.test.mjs2+-
Mscripts/nostr-avatar.test.mjs76+-
Ascripts/qa/generate-seedmark-fixture.mjs42+
M THIRD_PARTY_NOTICES +27 -0
@@ -72,3 +72,30 @@ If the optional local Fixi bundle is vendored, it is sourced from
7272 https://fixiproject.org/ and the project states BSD-0 licensing for fixi,7373 moxi, paxi, ssexi, and rexi. Preserve the upstream notice with the vendored7474 files.75+76+Seedmark77+--------78+The deterministic avatar renderer in internal/seedmark is derived from79+Seedmark version 1, commit c808a940bc193f97512c02d58da33ecc28ba1f53, from80+https://012.run/npub1hgd4hmk4kkukjx7yf402adl7msw0ssnmqmt0y6sgz0j8yjjcj9nqh9znde/seedmark.81+It is distributed under the MIT License:82+83+Copyright (c) 2026 Hermes contributors84+85+Permission is hereby granted, free of charge, to any person obtaining a copy86+of this software and associated documentation files (the "Software"), to deal87+in the Software without restriction, including without limitation the rights88+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell89+copies of the Software, and to permit persons to whom the Software is90+furnished to do so, subject to the following conditions:91+92+The above copyright notice and this permission notice shall be included in93+all copies or substantial portions of the Software.94+95+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR96+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,97+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE98+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER99+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,100+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE101+SOFTWARE.
M docs/app.md +4 -2
@@ -26,11 +26,13 @@ An agent's access request offers **Review access** instead. It opens the propose
2626 2727 ## Names2828 29-People show by name wherever the relay knows one: room authors and members, mentions, approvals, agent owners, wiki authors and the signed-in line. Names come from the person's profile (kind 0) as held by this relay, so looking up a name does not contact a public indexer. Names are cached in the browser for six hours. Without JavaScript, or before a profile arrives, the short key shows instead, and the key remains available in its label.29+People show by name wherever the relay knows one: room authors and members, mentions, approvals, agent owners, wiki authors and the signed-in line. Names come from the person's profile (kind 0) as held by this relay, so looking up a name does not contact a public indexer. Names are cached in the browser for six hours. Without JavaScript, or before a profile arrives, the short key shows instead.30+31+A profile picture is used when it is a safe, usable URL. Otherwise the relay shows a deterministic Seedmark avatar generated from the public key, including before JavaScript loads. These avatars are decorative and do not verify identity; the public key remains available in the label.3032 3133 ## Profile3234 33-**Profile** on the Account page edits the profile (kind 0) other people and apps see for your key: name, display name, about, picture, banner, website, NIP-05 address and lightning address. The form starts from the profile this relay holds, and fields it does not show are kept as they are. Saving signs the event with your connected signer, publishes it to this relay first, then to every relay in **Also publish to**, which starts out as the write relays from your relay list on this relay; each relay reports ok or failed under the form. The browser forgets your cached name at the same time, so pages show the new one on the next load.35+**Profile** on the Account page edits the profile (kind 0) other people and apps see for your key: name, display name, about, picture, banner, website, NIP-05 address and lightning address. The form starts from the profile this relay holds, and fields it does not show are kept as they are. Saving signs the event with your connected signer, publishes it to this relay first, then to every relay in **Also publish to**, which starts out as the write relays from your relay list on this relay; each relay reports ok or failed under the form. The browser forgets your cached name at the same time, so pages show the new one on the next load. Removing or omitting a picture returns the account to its deterministic Seedmark avatar.3436 3537 ## Account3638
M docs/architecture.md +1 -0
M docs/rooms.md +3 -1
@@ -63,7 +63,9 @@ Direct conversations show replies, quoted messages and encrypted reactions. Choo
6363 6464 One-to-one messages require a connected signer with NIP-44 support. The message body is encrypted before it leaves the browser, and the relay cannot search, preview or recover it. Group rooms continue to work for clients and agents that speak NIP-29. Existing room links at `/rooms/<id>` remain valid.6565 66-Open a room to read its messages, oldest first. Each message shows the author's name, their room role and the time. Agent messages identify the operator when the grant names one, using the form `<operator>'s agent`; an agent without an operator is labeled `agent`. Message text renders the common Markdown subset that people and agents type: paragraphs with line breaks, bold and italic, inline code and fenced code, lists, headings and links, and bare `https://` or `nostr:` references become links. Nothing else in a message is treated as markup. Links open in place and `nostr:` links resolve through the relay. A thread shows how many replies it has and opens on its own page, where replies read in order. Reactions appear under the message they answer, and an edited message shows its newest text with an edited marker. Choose **load earlier** for older messages.66+Open a room to read its messages, oldest first. Each message shows the author's profile picture when available, or a deterministic Seedmark avatar, along with their name, room role and time. Room lists and headers use the room picture when one is set, with a stable generated avatar otherwise. Agent messages identify the operator when the grant names one, using the form `<operator>'s agent`; an agent without an operator is labeled `agent`. Avatars are decorative; use the displayed public key when identity matters.67+68+Message text renders the common Markdown subset that people and agents type: paragraphs with line breaks, bold and italic, inline code and fenced code, lists, headings and links, and bare `https://` or `nostr:` references become links. Nothing else in a message is treated as markup. Links open in place and `nostr:` links resolve through the relay. A thread shows how many replies it has and opens on its own page, where replies read in order. Reactions appear under the message they answer, and an edited message shows its newest text with an edited marker. Choose **load earlier** for older messages.6769 6870 For broad client compatibility, replies can use a kind 9 message with NIP-10 `e` tags: the root reference has marker `root` in the fourth tag value, and a nested reply can also identify its immediate parent. Kind 12 is accepted for clients that use the NIP-29 thread reply convention. Both forms are grouped into the same thread by the web UI.6971
A internal/seedmark/LICENSE +21 -0
A internal/seedmark/families_a.go +192 -0
@@ -0,0 +1,192 @@
1+package seedmark2+3+import "fmt"4+5+func masks(r recipe) string {6+ bg, light, hot, cool := r.Palette[0], r.Palette[1], r.Palette[2], r.Palette[3]7+ w, h := float64(r.Silhouette.Width), float64(r.Silhouette.Height)8+ x, y := 32-w/2, 33-h/29+ var art string10+ switch r.Layout.Variant {11+ case 0:12+ art = path("M17 23L10 9L28 19M47 23L54 9L36 19", cool)13+ case 1:14+ art = rect(20, 8, 24, 16, 4, hot)15+ case 2:16+ art = path("M17 23L20 7L32 16L44 7L47 23Z", hot)17+ case 3:18+ art = circle(32, 13, 10, cool) + circle(32, 13, 4, bg)19+ }20+ switch r.Silhouette.Variant {21+ case 0:22+ art += path(fmt.Sprintf("M%s %sQ32 %s %s %sL%s 43L32 56L%s 43Z", num(x), num(y), num(y-6), num(64-x), num(y), num(64-x-2), num(x+2)), light)23+ case 1:24+ art += ellipse(32, 33, w/2, h/2, light)25+ case 2:26+ art += rect(x, y, w, h, 8, light)27+ case 3:28+ art += path(fmt.Sprintf("M32 %sL%s 25L%s 47L32 54L%s 47L%s 25Z", num(y-3), num(64-x), num(64-x-4), num(x+4), num(x)), light)29+ }30+ if r.Features.Accent != 0 {31+ art += path("M32 18L39 25L36 42L32 47Z", hot)32+ } else {33+ art += path("M32 18L39 25L36 42L32 47Z", cool)34+ }35+ switch r.Features.Variant {36+ case 0:37+ art += ellipse(24, 31, 5, 6, bg) + ellipse(40, 31, 5, 6, bg)38+ case 1:39+ art += path("M18 28L29 32L19 35ZM35 32L46 28L45 35Z", bg)40+ case 2:41+ art += rect(18, 28, 12, 6, 2, bg) + circle(40, 31, 5, bg)42+ case 3:43+ art += strokePath("M18 33Q24 23 29 33M35 33Q40 23 46 33", "none", bg, 4)44+ }45+ if r.Features.Accent != 0 {46+ art += rect(26, 43, 12, 4, 2, bg)47+ } else {48+ art += path("M27 42L32 48L37 42Z", bg)49+ }50+ return art51+}52+53+func creatures(r recipe) string {54+ bg, light, hot, cool := r.Palette[0], r.Palette[1], r.Palette[2], r.Palette[3]55+ w := float64(r.Silhouette.Width)56+ var art string57+ switch r.Layout.Variant {58+ case 0:59+ art = path("M22 27Q4 9 14 9Q24 10 27 26M36 26Q45 4 51 13L43 29", hot)60+ case 1:61+ art = path("M19 29L8 20L13 39M43 28L55 19L51 40", cool)62+ case 2:63+ art = strokePath("M25 25L23 10M39 25L44 12", "none", light, 5) + circle(23, 10, 5, hot) + circle(44, 12, 5, hot)64+ case 3:65+ art = path("M17 25L15 13L28 22L36 11L40 23L50 18L47 30Z", hot)66+ }67+ art += strokePath("M22 43L17 52L25 52M40 43L47 52L38 52", "none", cool, 6)68+ switch r.Silhouette.Variant {69+ case 0:70+ art += ellipse(32, 35, w/2, 17, cool)71+ case 1:72+ art += path("M15 43Q10 19 32 22Q52 10 50 43Q43 55 32 46Q20 55 15 43Z", cool)73+ case 2:74+ art += rect(32-w/2, 22, w, 27, 10, cool)75+ case 3:76+ art += path("M12 42L23 20Q32 12 42 26L53 43Q33 58 12 42Z", cool)77+ }78+ switch r.Features.Variant {79+ case 0:80+ art += circle(32, 33, 10, light) + circle(32+float64(r.Features.Accent)*3, 33, 4, bg)81+ case 1:82+ art += ellipse(24, 32, 6, 8, light) + ellipse(40, 32, 6, 8, light) + circle(25, 33, 3, bg) + circle(41, 33, 3, bg)83+ case 2:84+ art += rect(19, 27, 26, 11, 5, bg) + circle(25, 32, 3, light) + circle(38, 32, 3, light)85+ case 3:86+ for i := 0; i < r.Features.Count; i++ {87+ px := 20 + float64(i)*24/float64(r.Features.Count-1)88+ py := 31 + float64(i%2)*489+ art += circle(px, py, 4, light) + circle(px, py+1, 2, bg)90+ }91+ }92+ if r.Features.Accent != 0 {93+ art += strokePath("M28 43Q32 49 37 42", "none", bg, 3)94+ } else {95+ art += rect(29, 42, 7, 4, 2, bg)96+ }97+ return art98+}99+100+func botanical(r recipe) string {101+ light, hot, cool := r.Palette[1], r.Palette[2], r.Palette[3]102+ w := float64(r.Silhouette.Width)103+ var art string104+ switch r.Layout.Variant {105+ case 0:106+ art = path("M23 43L25 55L40 55L43 43Z", hot)107+ case 1:108+ art = ellipse(32, 48, 18, 7, hot)109+ case 2:110+ art = path("M20 52L25 44L39 44L46 52Z", light)111+ case 3:112+ art = strokePath("M25 54L32 45L39 54M20 50L32 45L45 49", "none", hot, 4)113+ }114+ art += strokePath("M32 46Q28 30 33 14", "none", light, 4)115+ switch r.Silhouette.Variant {116+ case 0:117+ art += path(fmt.Sprintf("M31 35Q%s 37 12 21Q29 17 31 35ZM32 29Q35 12 51 13Q52 29 32 29Z", num(32-w/2)), cool)118+ case 1:119+ art += path("M31 41Q11 45 12 34Q15 28 30 34M32 28Q48 33 51 23Q51 14 33 20", cool)120+ case 2:121+ art += path("M31 39Q8 29 22 13Q35 20 31 39ZM33 37Q55 32 46 19Q32 23 33 37Z", cool)122+ case 3:123+ art += path("M30 40L13 29L27 27L15 16L31 23L38 9L40 26L53 19L45 36Z", cool)124+ }125+ bloomX, bloomY := 29.0, 16.0126+ if r.Features.Variant%2 != 0 {127+ bloomX = 41128+ }129+ if r.Features.Variant >= 2 {130+ bloomY = 24131+ }132+ switch r.Features.Variant {133+ case 0:134+ art += circle(bloomX-6, bloomY, 7, hot) + circle(bloomX+6, bloomY, 7, hot) + circle(bloomX, bloomY-6, 7, hot) + circle(bloomX, bloomY+5, 7, hot) + circle(bloomX, bloomY, 4, light)135+ case 1:136+ art += path("M29 20L30 9L37 14L42 7L47 15L54 11L51 22Q42 32 29 20Z", hot) + circle(42, 21, 4, light)137+ case 2:138+ for i := 0; i < r.Features.Count; i++ {139+ art += circle(19+float64(i)*6, 16+float64(i%2)*6, 4, hot)140+ }141+ case 3:142+ art += ellipse(40, 18, 9, 13, hot) + strokePath("M40 10L40 25", "none", light, 3)143+ }144+ return art145+}146+147+func orbital(r recipe) string {148+ bg, light, hot, cool := r.Palette[0], r.Palette[1], r.Palette[2], r.Palette[3]149+ w := float64(r.Silhouette.Width)150+ radius := 11 + (w-26)/3151+ var art string152+ switch r.Layout.Variant {153+ case 0:154+ art = strokePath("M9 39Q1 18 31 12Q58 6 55 30Q49 53 23 52", "none", cool, 4)155+ case 1:156+ art = strokePath("M12 13L49 50M12 50L49 13", "none", cool, 4)157+ case 2:158+ art = strokePath("M9 32A23 23 0 1 1 32 55", "none", cool, 5)159+ case 3:160+ art = rect(12, 12, 40, 40, 12, cool) + rect(17, 17, 30, 30, 8, bg)161+ }162+ switch r.Silhouette.Variant {163+ case 0:164+ art += circle(32, 32, radius, hot)165+ case 1:166+ art += rect(18, 18, 28, 28, 6, hot)167+ case 2:168+ art += path("M32 14L50 32L32 50L14 32Z", hot)169+ case 3:170+ art += ellipse(32, 32, radius, 19, hot)171+ }172+ switch r.Features.Variant {173+ case 0:174+ art += strokePath("M10 37Q32 46 53 25", "none", light, 5)175+ case 1:176+ art += circle(29, 29, 7, light) + circle(33, 26, 7, hot)177+ case 2:178+ art += strokePath("M21 25L43 25M21 34L43 34M27 42L37 42", "none", light, 4)179+ case 3:180+ art += circle(32, 32, 7, bg) + circle(32, 32, 3, light)181+ }182+ for i := 0; i < r.Features.Count; i++ {183+ px := []float64{13, 49, 16, 46, 32}[i]184+ py := []float64{16, 46, 47, 13, 7}[i]185+ pr := 4.0186+ if i%2 != 0 {187+ pr = 3188+ }189+ art += circle(px, py, pr, light)190+ }191+ return art192+}
A internal/seedmark/families_b.go +162 -0
@@ -0,0 +1,162 @@
1+package seedmark2+3+import "fmt"4+5+func landscapes(r recipe) string {6+ bg, light, hot, cool := r.Palette[0], r.Palette[1], r.Palette[2], r.Palette[3]7+ s, h := r.Silhouette.Variant, r.Silhouette.Height8+ f, accent := r.Features.Variant, r.Features.Accent9+ horizon := 31 + (float64(h)-28)/210+ art := []string{11+ circle(22, 19, 9, hot),12+ circle(43, 19, 10, hot),13+ rect(14, 12, 34, 9, 4, hot),14+ circle(32, 21, 15, hot),15+ }[r.Layout.Variant]16+ art += []string{17+ path("M7 46L21 22L33 37L43 26L57 46V55H7Z", cool),18+ path(fmt.Sprintf("M7 %sQ18 21 31 %sQ46 22 57 35V55H7Z", num(horizon), num(horizon)), cool),19+ path("M8 53V25H20V38H30V19H40V32H55V53Z", cool),20+ path("M7 36L18 28L26 32L39 23L57 33V55H7Z", cool),21+ }[s]22+ if f == 0 {23+ art += path("M31 35L24 45L36 45L28 55H42L45 43L33 43L37 35Z", light)24+ }25+ if f == 1 {26+ art += strokePath("M9 45Q21 40 32 46T55 46M13 53Q23 48 35 53", "none", light, 4)27+ }28+ if f == 2 {29+ art += strokePath("M18 48V35M12 42L18 32L24 42ZM42 50V38M36 43L42 33L48 43Z", light, light, 2)30+ }31+ if f == 3 {32+ art += rect(26, 36, 15, 17, 2, light) + path("M23 36L33 27L44 36Z", light) + rect(31, 43, 5, 10, 0, bg)33+ }34+ if accent != 0 {35+ art += strokePath("M9 18Q13 13 17 18Q21 13 25 18", "none", light, 2)36+ } else {37+ art += circle(51, 12, 3, light)38+ }39+ return art40+}41+42+func glyphs(r recipe) string {43+ bg, light, hot, cool := r.Palette[0], r.Palette[1], r.Palette[2], r.Palette[3]44+ w := float64(r.Silhouette.Width)45+ f, accent := r.Features.Variant, r.Features.Accent46+ art := []string{47+ circle(32, 32, 24, cool) + circle(32, 32, 18, bg),48+ rect(10, 10, 44, 44, 10, cool) + rect(16, 16, 32, 32, 5, bg),49+ strokePath("M10 26L26 10M38 10L54 26M54 38L38 54M26 54L10 38", "none", cool, 6),50+ strokePath("M10 47V17H24M54 17V47H40", "none", cool, 6),51+ }[r.Layout.Variant]52+ a, b := 32-w/2+4, 64-(32-w/2+4)53+ art += []string{54+ strokePath(fmt.Sprintf("M%s 43L32 17L%s 43M24 35H40", num(a), num(b)), "none", light, 7),55+ strokePath(fmt.Sprintf("M%s 22H%sL%s 43H%s", num(a), num(b), num(a), num(b)), "none", light, 7),56+ strokePath(fmt.Sprintf("M%s 19V44H%sV19M32 20V39", num(a), num(b)), "none", light, 7),57+ path("M21 18H37Q49 24 37 32Q50 40 38 46H21Z", light) + rect(27, 24, 9, 5, 2, bg) + rect(27, 36, 10, 5, 2, bg),58+ }[r.Silhouette.Variant]59+ art += []string{60+ circle(32, 32, 5, hot),61+ path("M32 24L40 32L32 40L24 32Z", hot),62+ rect(26, 27, 12, 10, 2, hot),63+ strokePath("M22 37L42 27", "none", hot, 6),64+ }[f]65+ if accent != 0 {66+ art += circle(49, 15, 4, light)67+ } else {68+ art += circle(15, 49, 4, light)69+ }70+ return art71+}72+73+func woven(r recipe) string {74+ bg, light, hot, cool := r.Palette[0], r.Palette[1], r.Palette[2], r.Palette[3]75+ s, w := r.Silhouette.Variant, float64(r.Silhouette.Width)76+ f, count := r.Features.Variant, r.Features.Count77+ gap := 10 + (w-26)/678+ art := ""79+ if s == 0 {80+ for i := 0; i < 3; i++ {81+ fill := light82+ if i%2 != 0 {83+ fill = hot84+ }85+ art += rect(13+float64(i)*gap, 10, 7, 44, 3, fill)86+ }87+ for i := 0; i < 3; i++ {88+ art += rect(9, 16+float64(i)*gap, 45, 6, 2, cool)89+ }90+ art += rect(24, 23, 7, 15, 2, hot)91+ }92+ if s == 1 {93+ for i := 0; i < 3; i++ {94+ colors := []string{light, hot, cool}95+ x := 12 + i*1296+ art += strokePath(fmt.Sprintf("M%d 12L%d 25L%d 39L%d 52", x, x+8, x, x+8), "none", colors[i], 8)97+ }98+ }99+ if s == 2 {100+ art += strokePath("M12 20Q12 11 22 11H43Q52 11 52 21V43Q52 53 42 53H22Q12 53 12 43Z", "none", light, 6)101+ art += strokePath("M21 10V44H43V20H10M54 32H32V54", "none", hot, 7)102+ art += rect(28, 22, 8, 8, 1, cool)103+ }104+ if s == 3 {105+ for i := 0; i < 3; i++ {106+ colors := []string{light, hot, cool}107+ y := 18 + i*13108+ art += path(fmt.Sprintf("M10 %dL22 %dL34 %dL46 %dL54 %dL46 %dL34 %dL22 %dZ", y, 10+i*13, y, 10+i*13, y, 26+i*13, y, 26+i*13), colors[i])109+ }110+ }111+ art = group(art, fmt.Sprintf("rotate(%d 32 32) scale(.88) translate(4.36 4.36)", r.Layout.Variant*45))112+ art += []string{113+ circle(32, 32, 5, bg) + circle(32, 32, 2, light),114+ rect(26, 26, 12, 12, 3, hot),115+ path("M32 24L40 32L32 40L24 32Z", cool),116+ strokePath("M27 28L37 36M37 28L27 36", "none", bg, 4),117+ }[f]118+ for i := 0; i < count; i++ {119+ art += circle(18+float64(i)*7, 56, 2, hot)120+ }121+ return art122+}123+124+func machines(r recipe) string {125+ bg, light, hot, cool := r.Palette[0], r.Palette[1], r.Palette[2], r.Palette[3]126+ s, w := r.Silhouette.Variant, float64(r.Silhouette.Width)127+ f, count, accent := r.Features.Variant, r.Features.Count, r.Features.Accent128+ x := 32 - w/2129+ art := []string{130+ strokePath("M22 23L17 11M42 23L49 11", "none", hot, 4) + circle(17, 11, 4, light),131+ rect(23, 9, 18, 16, 4, hot) + rect(28, 12, 8, 6, 1, bg),132+ strokePath("M12 27L8 39L19 44M51 27L56 39L47 44", "none", hot, 6),133+ circle(22, 49, 8, hot) + circle(43, 49, 8, hot),134+ }[r.Layout.Variant]135+ art += []string{136+ rect(x, 20, w, 30, 5, light),137+ path(fmt.Sprintf("M%s 19H%sL%s 45L41 52H23L%s 45Z", num(x+5), num(59-x), num(64-x), num(x)), light),138+ ellipse(32, 35, w/2, 18, light),139+ path("M23 17H41L52 29V44L41 52H23L12 44V29Z", light),140+ }[s]141+ if f == 0 {142+ art += circle(32, 34, 11, bg) + circle(32, 34, 7, cool) + circle(35, 31, 3, light)143+ }144+ if f == 1 {145+ art += rect(20, 26, 24, 17, 3, bg) + strokePath("M24 37L29 30L34 36L40 29", "none", cool, 3)146+ }147+ if f == 2 {148+ art += rect(19, 25, 26, 15, 2, bg)149+ for i := 0; i < count; i++ {150+ art += rect(22+float64(i)*4, 28, 2, 9, 1, cool)151+ }152+ }153+ if f == 3 {154+ art += circle(25, 32, 6, bg) + circle(39, 32, 6, bg) + strokePath("M20 44H44", "none", hot, 4)155+ }156+ if accent != 0 {157+ art += circle(43, 46, 3, hot)158+ } else {159+ art += circle(21, 46, 3, hot)160+ }161+ return art162+}
A internal/seedmark/primitives.go +27 -0
A internal/seedmark/seedmark.go +109 -0
@@ -0,0 +1,109 @@
1+// Package seedmark renders version 1 of the Seedmark avatar design without2+// JavaScript. It follows the upstream generator at c808a94; fixtures pin its3+// visual output. Avatars are decorative and are not identity verification.4+package seedmark5+6+import (7+ "strconv"8+ "strings"9+ "unicode/utf16"10+)11+12+const Version = 113+14+var families = [...]string{"masks", "creatures", "botanical", "orbital", "landscapes", "glyphs", "woven", "machines"}15+16+var palettes = [...][4]string{17+ {"#142d36", "#f5dfad", "#ed7351", "#65c4ae"},18+ {"#24223c", "#f7dfbf", "#a89cfc", "#f7788b"},19+ {"#163b32", "#f4eacb", "#b4cf66", "#f69c64"},20+ {"#3a2330", "#ffe5b9", "#ef7689", "#8dceba"},21+ {"#1d304c", "#e8eff0", "#f4b855", "#72b9d0"},22+ {"#422d26", "#f8e8c8", "#e8a04d", "#72afa0"},23+ {"#283526", "#f4edcf", "#d3c65e", "#ed8863"},24+ {"#292745", "#efe8f5", "#d89ada", "#8fcabc"},25+ {"#18343c", "#f8e4d0", "#f88c66", "#a7cee0"},26+ {"#393127", "#f4ebd8", "#adb77a", "#da8c6f"},27+ {"#2c2440", "#fbe8c2", "#f2b54f", "#b2a1e2"},28+ {"#1b3537", "#e9f0d7", "#79c7b8", "#e5a4aa"},29+}30+31+type recipe struct {32+ Family string33+ Palette [4]string34+ Layout struct{ Variant, Flip, Tilt int }35+ Silhouette struct{ Variant, Width, Height int }36+ Features struct{ Variant, Count, Accent int }37+ Texture struct{ Variant, Marks int }38+}39+40+type rng struct{ a, b, c, d uint32 }41+42+func newRNG(seed, namespace string) *rng {43+ var framed strings.Builder44+ for _, part := range []string{"seedmark", "1", seed, namespace} {45+ framed.WriteString(strconv.Itoa(len(utf16.Encode([]rune(part)))))46+ framed.WriteByte(':')47+ framed.WriteString(part)48+ }49+ a, b, c, d := uint32(1779033703), uint32(3144134277), uint32(1013904242), uint32(2773480762)50+ for _, unit := range utf16.Encode([]rune(framed.String())) {51+ k := uint32(unit)52+ a = b ^ (a^k)*59739906753+ b = c ^ (b^k)*286986023354+ c = d ^ (c^k)*95127421355+ d = a ^ (d^k)*271604417956+ }57+ a = (c ^ (a >> 18)) * 59739906758+ b = (d ^ (b >> 22)) * 286986023359+ c = (a ^ (c >> 17)) * 95127421360+ d = (b ^ (d >> 19)) * 271604417961+ return &rng{a ^ b ^ c ^ d, b ^ a, c ^ a, d ^ a}62+}63+64+func (r *rng) integer(minimum, maximum int) int {65+ t := r.a + r.b + r.d66+ r.d++67+ r.a = r.b ^ (r.b >> 9)68+ r.b = r.c + (r.c << 3)69+ r.c = ((r.c << 21) | (r.c >> 11)) + t70+ return minimum + int(float64(t)/4294967296*float64(maximum-minimum+1))71+}72+73+func recipeFor(seed string) recipe {74+ var r recipe75+ r.Family = families[newRNG(seed, "family").integer(0, len(families)-1)]76+ r.Palette = palettes[newRNG(seed, "palette").integer(0, len(palettes)-1)]77+ layout, silhouette := newRNG(seed, "layout"), newRNG(seed, "silhouette")78+ features, texture := newRNG(seed, "features"), newRNG(seed, "texture")79+ r.Layout.Variant, r.Layout.Flip = layout.integer(0, 3), layout.integer(0, 1)80+ r.Layout.Tilt = [...]int{-12, 0, 12}[layout.integer(0, 2)]81+ r.Silhouette.Variant = silhouette.integer(0, 3)82+ r.Silhouette.Width, r.Silhouette.Height = silhouette.integer(26, 38), silhouette.integer(28, 42)83+ r.Features.Variant, r.Features.Count = features.integer(0, 3), features.integer(2, 5)84+ r.Features.Accent = features.integer(0, 1)85+ r.Texture.Variant, r.Texture.Marks = texture.integer(0, 2), texture.integer(2, 4)86+ return r87+}88+89+// Avatar returns a standalone 64-pixel SVG for the full seed. The version 190+// geometry remains fixed; CSS may resize the SVG without changing its design.91+func Avatar(seed string) string {92+ r := recipeFor(seed)93+ render := map[string]func(recipe) string{"masks": masks, "creatures": creatures, "botanical": botanical, "orbital": orbital, "landscapes": landscapes, "glyphs": glyphs, "woven": woven, "machines": machines}94+ texture := ""95+ for i := 0; i < r.Texture.Marks; i++ {96+ x := float64(8 + i*6)97+ if r.Texture.Variant == 1 {98+ texture += circle(x, 6, 1.5, r.Palette[3])99+ } else if r.Texture.Variant == 2 {100+ texture += rect(x-1, 5, 3, 3, 0, r.Palette[2])101+ }102+ }103+ flip := "1"104+ if r.Layout.Flip != 0 {105+ flip = "-1"106+ }107+ transform := "translate(32 32) rotate(" + strconv.Itoa(r.Layout.Tilt) + ") scale(" + flip + " 1) translate(-32 -32)"108+ return `<svg xmlns="http://www.w3.org/2000/svg" width="64" height="64" viewBox="0 0 64 64"><rect width="64" height="64" fill="` + r.Palette[0] + `"/>` + texture + group(render[r.Family](r), transform) + `</svg>`109+}
A internal/seedmark/seedmark_test.go +48 -0
@@ -0,0 +1,48 @@
1+package seedmark2+3+import (4+ "crypto/sha256"5+ "encoding/json"6+ "fmt"7+ "os"8+ "strings"9+ "testing"10+)11+12+// TestVersionOneMatchesUpstreamSVG pins the v1 output from Seedmark commit13+// c808a940bc193f97512c02d58da33ecc28ba1f53. Seedmark is MIT licensed; its14+// Go port and upstream license are in this package. The fixture generator is15+// scripts/qa/generate-seedmark-fixture.mjs.16+func TestVersionOneMatchesUpstreamSVG(t *testing.T) {17+ raw, err := os.ReadFile("testdata/v1.json")18+ if err != nil {19+ t.Fatal(err)20+ }21+ var fixtures []struct{ Seed, Family, SHA256 string }22+ if err := json.Unmarshal(raw, &fixtures); err != nil {23+ t.Fatal(err)24+ }25+ families := map[string]bool{}26+ if len(fixtures) != 1010 {27+ t.Fatalf("fixtures contain %d cases, want 1010", len(fixtures))28+ }29+ for _, fixture := range fixtures {30+ families[fixture.Family] = true31+ digest := fmt.Sprintf("%x", sha256.Sum256([]byte(Avatar(fixture.Seed))))32+ if digest != fixture.SHA256 {33+ t.Fatalf("seed %q (%s): got SHA256 %s, want %s", fixture.Seed, fixture.Family, digest, fixture.SHA256)34+ }35+ }36+ if len(families) != 8 {37+ t.Fatalf("fixtures cover %d families", len(families))38+ }39+}40+41+func TestSeedNeverAppearsInMarkup(t *testing.T) {42+ for _, seed := range []string{`<script>alert(1)</script>`, `" onload="alert(1)`, "https://example.test/identity"} {43+ svg := Avatar(seed)44+ if strings.Contains(svg, seed) || strings.Contains(svg, "<script") || strings.Contains(svg, "onload") || strings.Contains(svg, "href=") || strings.Contains(svg, "class=") {45+ t.Fatalf("seed affected SVG markup: %s", svg)46+ }47+ }48+}
A internal/seedmark/testdata/v1.json +5052 -0
@@ -0,0 +1,5052 @@
1+[2+ {3+ "seed": "",4+ "family": "glyphs",5+ "sha256": "94aebee8d0dcbba49bc2e7080ed318f0aa5a466060e8bb5633e2f75ad867472d"6+ },7+ {8+ "seed": "seedmark",9+ "family": "masks",10+ "sha256": "2e47dfd552f5be1d5b9466bb74d4e60b3aff6b8a98488057cedab2160d6641dc"11+ },12+ {13+ "seed": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",14+ "family": "creatures",15+ "sha256": "1e65a63635fb7dbcae3f5da68871dda36b99f4cd97d4e5208be6ba485df546c4"16+ },17+ {18+ "seed": "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA",19+ "family": "glyphs",20+ "sha256": "b5a726748884f0070411d805a94cbb84e9851b7d659c6a4d4b288edbef61c817"21+ },22+ {23+ "seed": "0000000000000000000000000000000000000000000000000000000000000000",24+ "family": "botanical",25+ "sha256": "67adf0a470c3a4817f6c2fd45ef9a7da6459a3ab79fa8b2f90dc7ce1a89ab396"26+ },27+ {28+ "seed": "seed/\u0000part",29+ "family": "botanical",30+ "sha256": "5c46a3bbd0a84363cec9f3d3039569f598af9005b6262694a0b85c35f97a2d11"31+ },32+ {33+ "seed": "é",34+ "family": "glyphs",35+ "sha256": "8e432527cfa52a02c1097a9d41df7fcd915869f404569af4dbd8eddf3f25a832"36+ },37+ {38+ "seed": "é",39+ "family": "botanical",40+ "sha256": "7daa100363c25f8c46be83289cb6d49f4ac6c04077fba448c01d47c8bad4fb1a"41+ },42+ {43+ "seed": "猫🌱",44+ "family": "creatures",45+ "sha256": "dba4d0a680ef2c452e79e8573b0dfcd38fe7c44caad47f43380f203dec81329d"46+ },47+ {48+ "seed": "👩‍💻",49+ "family": "glyphs",50+ "sha256": "0e2ab3646ecc7bb0a17f7a7f87713c43c6cfa9b3421825419a79ede038f6abd0"51+ },52+ {53+ "seed": "sample/0",54+ "family": "machines",55+ "sha256": "f45c9670784bd677175fbfa4e0a80a480e9f28736559188a7d7b64d6c1392206"56+ },57+ {58+ "seed": "sample/1",59+ "family": "woven",60+ "sha256": "4bcaac3b7f45c439c9bf976906d65cfe92a7634a076399e9466b756139d13252"61+ },62+ {63+ "seed": "sample/2",64+ "family": "masks",65+ "sha256": "f2a00bbc4b5b12e0206cc85183684f1017f8d9f438df7afd345ac104524fd232"66+ },67+ {68+ "seed": "sample/3",69+ "family": "botanical",70+ "sha256": "9a23bf4f25a1e89d310e1d57f08c48d6e9959b507ff297746463a0463ad28b71"71+ },72+ {73+ "seed": "sample/4",74+ "family": "machines",75+ "sha256": "178cf45d3ec5f785d55a7d999436a4450d738d1a95e58c49d85732317e4d22e7"76+ },77+ {78+ "seed": "sample/5",79+ "family": "woven",80+ "sha256": "360f04b086fbc37e82951704e0c6601a361055d42baa22c9e5d203d60a23a5c1"81+ },82+ {83+ "seed": "sample/6",84+ "family": "landscapes",85+ "sha256": "6bf1f3bd679a72b3a7d95f84e38580cf32bf55228a6813bf065a8588047f9d86"86+ },87+ {88+ "seed": "sample/7",89+ "family": "glyphs",90+ "sha256": "3146f760179e5821f9bb69d71041335cfb7ca0990d43eee65978e13f2822b63e"91+ },92+ {93+ "seed": "sample/8",94+ "family": "masks",95+ "sha256": "84efc391dfaa92d9015c7c9cf17db02725d2944f20f855bea8670e2e3d65fe59"96+ },97+ {98+ "seed": "sample/9",99+ "family": "woven",100+ "sha256": "412878395eb00ab8b047467bcb624d65590d2bdfe5c3af9b37a808d5d518ae0c"101+ },102+ {103+ "seed": "sample/10",104+ "family": "masks",105+ "sha256": "7988f79494f1ce59cd3dd3cb3c7a2e2b8e52fceaeaab550928530f4ca571da77"106+ },107+ {108+ "seed": "sample/11",109+ "family": "masks",110+ "sha256": "6d7b72f3cc3407f64d0f5b26da94afaa549863667c90e10aa62bcf704e97b7fe"111+ },112+ {113+ "seed": "sample/12",114+ "family": "orbital",115+ "sha256": "26cb8e37ee4d87b5d0e531bea538d6af92ca1c04f1248f341570a0ee663e16d1"116+ },117+ {118+ "seed": "sample/13",119+ "family": "masks",120+ "sha256": "f958eed9b4afd6bc7c2ce2310ca8147f470f5663fdb6da4c8b26edf29f14ed55"121+ },122+ {123+ "seed": "sample/14",124+ "family": "creatures",125+ "sha256": "d360f8e0da1c067e0951be886404a1a01d16e1fb41cc32b3ef54996abcd526c2"126+ },127+ {128+ "seed": "sample/15",129+ "family": "woven",130+ "sha256": "e8246a598c4b8c57d91c88f2849a2c760dedc56bde7edcedc3e58c2022aeb832"131+ },132+ {133+ "seed": "sample/16",134+ "family": "woven",135+ "sha256": "eb5167c97d4df58034d90e062a654eb57894d38a71c1e228b07f11430788e421"136+ },137+ {138+ "seed": "sample/17",139+ "family": "woven",140+ "sha256": "3143ca5c5ba75173e144f292a5231bc726c9f7c4c4e93a17afd7eeb85266d5de"141+ },142+ {143+ "seed": "sample/18",144+ "family": "creatures",145+ "sha256": "f05cc848a3977eb7d0b81e3447d696af93c5da084096db34d7c406d57525c41e"146+ },147+ {148+ "seed": "sample/19",149+ "family": "orbital",150+ "sha256": "fa8707bddd1aa1c4d88076f42abc1a6f29329d65469fdd323d1ec5ad68fe28b0"151+ },152+ {153+ "seed": "sample/20",154+ "family": "landscapes",155+ "sha256": "636a7d87293a2b8228da2edb8511a8dcc695cd0ee4e58973d9827adbbd7cccce"156+ },157+ {158+ "seed": "sample/21",159+ "family": "glyphs",160+ "sha256": "3074353911ac60434236c3ad8fec65435e08d4690790ef5d58751fb559341182"161+ },162+ {163+ "seed": "sample/22",164+ "family": "glyphs",165+ "sha256": "7b15b57e41039ff14ef919910be9f1e347e3cccd855d5a2b76759bdbebddc588"166+ },167+ {168+ "seed": "sample/23",169+ "family": "masks",170+ "sha256": "82eec3e1a1ed3be6e6119c4de90740cee19f63d09f77e2be7e5494b2345e9a98"171+ },172+ {173+ "seed": "sample/24",174+ "family": "machines",175+ "sha256": "362daae2ef3c9cc8670471b2db1357191265463de277268d28d3c30380584c10"176+ },177+ {178+ "seed": "sample/25",179+ "family": "machines",180+ "sha256": "83defb7de5d732c653be310772eeb845009272571e6cbcb56af2da4765919d55"181+ },182+ {183+ "seed": "sample/26",184+ "family": "machines",185+ "sha256": "2a069db22e5d2b251d9d2ce314ec8a7a61676e6b9965dc484e0c5f66f4eb39b4"186+ },187+ {188+ "seed": "sample/27",189+ "family": "landscapes",190+ "sha256": "f8e7d3ccbea0af09a7ecd21546bb3ee48bddc8d706cca4f5149c6a3e22dc1b8e"191+ },192+ {193+ "seed": "sample/28",194+ "family": "creatures",195+ "sha256": "307737a1ae8442937b0dab11dc9bf56791b7e041bd80502246fd0dc3f14d4997"196+ },197+ {198+ "seed": "sample/29",199+ "family": "glyphs",200+ "sha256": "74666daeade8084678a0dc4dc6507674742f453d62f26bbc2a3313816cab1196"201+ },202+ {203+ "seed": "sample/30",204+ "family": "masks",205+ "sha256": "fdc480c964363d6d1fd7eb9e291298ce8038395bbe3c31852f2db5c7e676c7be"206+ },207+ {208+ "seed": "sample/31",209+ "family": "machines",210+ "sha256": "ad82b661abd8c647aca50b88c73d778b989edf12778e1e431e8ac45fec0a769f"211+ },212+ {213+ "seed": "sample/32",214+ "family": "machines",215+ "sha256": "aa581e70d554daa7bcdcd51ed4e1d32df4b8cce57c7b47c966e0899c2277e102"216+ },217+ {218+ "seed": "sample/33",219+ "family": "creatures",220+ "sha256": "d771e226024b91b51d4893d613aaae5e1bb74b5e3e800f841e9b9b07ea8f4593"221+ },222+ {223+ "seed": "sample/34",224+ "family": "glyphs",225+ "sha256": "98c57633fc33d3dac78429ef2c9250ef02f72a0de4027fc4189c306d8ba0d0a7"226+ },227+ {228+ "seed": "sample/35",229+ "family": "woven",230+ "sha256": "8aa1a668deafdbe00059406af4cb6b3f9331bd89019c3bbba6dd298d45c60c36"231+ },232+ {233+ "seed": "sample/36",234+ "family": "machines",235+ "sha256": "10fec47d317af2702cc91152a9a6b1df61d8ff096e1bbafb5a8565a2821eaf12"236+ },237+ {238+ "seed": "sample/37",239+ "family": "landscapes",240+ "sha256": "8f72464e9ea8d001a712144ae1a22549da0f5e24c9de4397e8e36224fe658856"241+ },242+ {243+ "seed": "sample/38",244+ "family": "machines",245+ "sha256": "3bc47742b159bfffff7158206350e162c3d074ebce8bbab67c686cf19fed58eb"246+ },247+ {248+ "seed": "sample/39",249+ "family": "orbital",250+ "sha256": "6161b7dcbe8dd3f79cfaf0c25a6a219fc45878cb3a0b325d5cb92315f68807c3"251+ },252+ {253+ "seed": "sample/40",254+ "family": "woven",255+ "sha256": "e075e859ef9e121fdfb46f3a10f58654f394efbc93451e95ac6821292d1f8752"256+ },257+ {258+ "seed": "sample/41",259+ "family": "glyphs",260+ "sha256": "a92f1b16c9a7d234fa72aa102cb19b805abf20daf289774d26c01f23194bfcc7"261+ },262+ {263+ "seed": "sample/42",264+ "family": "orbital",265+ "sha256": "d4d58e3623b8c4adca5dfb4fd51ba450374e3ef2f930e865522672ecf2d3e3b7"266+ },267+ {268+ "seed": "sample/43",269+ "family": "botanical",270+ "sha256": "cd729ff66613f2bd244dba6215a32da6e4d5b9452c777673c8cad41be294b5e1"271+ },272+ {273+ "seed": "sample/44",274+ "family": "creatures",275+ "sha256": "820b1621176c59928934a74241df5fec7cb285a40832ca10912429611236fafb"276+ },277+ {278+ "seed": "sample/45",279+ "family": "botanical",280+ "sha256": "bce2002b9b7a3d84197f7d54243b9ebd0ccab57520342bd0ef9bbb9530ac2c4a"281+ },282+ {283+ "seed": "sample/46",284+ "family": "botanical",285+ "sha256": "6fe94a4f9f04397da60dd0c3aa38d1d56a4439c412cef6668c3996919c9b8541"286+ },287+ {288+ "seed": "sample/47",289+ "family": "woven",290+ "sha256": "aca9309f3a6ba94ceb1860490ea06aefdd09d9ccd6e11d19ec7fb0d81d66e405"291+ },292+ {293+ "seed": "sample/48",294+ "family": "woven",295+ "sha256": "c4ab9752c7bc24277fee2713610c575d88a71f2d16fb1872205127e8f70d51e7"296+ },297+ {298+ "seed": "sample/49",299+ "family": "woven",300+ "sha256": "e3b2ff606b28ae9393ffcca076409e560dcf916aca3c4ae9d876d94f3ab0c010"301+ },302+ {303+ "seed": "sample/50",304+ "family": "machines",305+ "sha256": "daeb562eb026bb7ee3760343f662630f3a754b14b8521622864359570617e100"306+ },307+ {308+ "seed": "sample/51",309+ "family": "orbital",310+ "sha256": "d54b5cd80ef4204a3578f8e0ca30d4cb483662bb5a09523687c1d1fc1aa6a1a5"311+ },312+ {313+ "seed": "sample/52",314+ "family": "woven",315+ "sha256": "866273f06b447d06437cba2184e4a77dbfbdffb067786476aed880aa2d9a9aa3"316+ },317+ {318+ "seed": "sample/53",319+ "family": "creatures",320+ "sha256": "f3513d1b6b498584e82afdb06b5ee2b59f3a680ac0abcaae4fac8698ddeb430e"321+ },322+ {323+ "seed": "sample/54",324+ "family": "landscapes",325+ "sha256": "8dbad3d1ee71b73f9425506a5f2c9e44ce8c6acf7c2faf02bee582b0ed0f0cdc"326+ },327+ {328+ "seed": "sample/55",329+ "family": "machines",330+ "sha256": "6d799c96fe62f5211183ff3e4c6c817d28bb30a93d891c28dbfabeba84351fad"331+ },332+ {333+ "seed": "sample/56",334+ "family": "botanical",335+ "sha256": "e71ccebceb086d080e41223c4e7a035260430d2219e2a630c34875b6f10ddb0c"336+ },337+ {338+ "seed": "sample/57",339+ "family": "botanical",340+ "sha256": "7a829c4e8ac939f60526e2457f9f52120ef6e4b2e96ebb1666f801cd543e56d6"341+ },342+ {343+ "seed": "sample/58",344+ "family": "machines",345+ "sha256": "d4e4c79250507035126ff36e7cca4408e67423829d7d159375c4548d6c5d6aca"346+ },347+ {348+ "seed": "sample/59",349+ "family": "orbital",350+ "sha256": "4d81d67f82bafa2be012027fcc19caf3ec6c3763e8890ffaecdf4301eee8230e"351+ },352+ {353+ "seed": "sample/60",354+ "family": "landscapes",355+ "sha256": "ba8ad8d23f84c743b76bd969d4cb1cfda7997cb6fed6b569d3fb0b677c0e4a54"356+ },357+ {358+ "seed": "sample/61",359+ "family": "woven",360+ "sha256": "8d33021e42697dc01cd714f9b66f8712849c9a4fa9f7bc9b8644270fd96bb027"361+ },362+ {363+ "seed": "sample/62",364+ "family": "masks",365+ "sha256": "347a18dfe5794a46d5a835efe2d28adc0e29b97e2d09ae799fdbcbd19e07dd83"366+ },367+ {368+ "seed": "sample/63",369+ "family": "masks",370+ "sha256": "b780f518504f925cc6d931c697a8dcf2020165f64fe353929459167bea99cc16"371+ },372+ {373+ "seed": "sample/64",374+ "family": "botanical",375+ "sha256": "a5f29580682cbcae49448b9376cb80240ed36b40cea61b6067a8b495a0b4dcf2"376+ },377+ {378+ "seed": "sample/65",379+ "family": "botanical",380+ "sha256": "b9afa623712f8138ad1e3f1470b2e76f0d66aa4542c15ce39aaa10370275c460"381+ },382+ {383+ "seed": "sample/66",384+ "family": "machines",385+ "sha256": "70a12ec2d582ac9beac513b786204c3a0bb342d42f297450ae73fb3155393048"386+ },387+ {388+ "seed": "sample/67",389+ "family": "woven",390+ "sha256": "a6697371ba9fbc79a5618daa7cb443de17be72556fa7805e9348dd0c70344158"391+ },392+ {393+ "seed": "sample/68",394+ "family": "woven",395+ "sha256": "68b938d2c30c2785a76d70783d9722419e9bbc0a9a68040f583425ea8bd9a7ee"396+ },397+ {398+ "seed": "sample/69",399+ "family": "landscapes",400+ "sha256": "73eb43a6694fcb5185af156ba4bcc65645547427d70f37778ab1fcb7bbdcefb8"401+ },402+ {403+ "seed": "sample/70",404+ "family": "creatures",405+ "sha256": "6874416d4ad64bd2392c65ba1a8d9fbdf3eeceb497da5e0e146dc3a8c4559f9d"406+ },407+ {408+ "seed": "sample/71",409+ "family": "landscapes",410+ "sha256": "dc1174b107b2dc79885453bbe2bca24dc6d2575fbab5651d56e10982f53609b9"411+ },412+ {413+ "seed": "sample/72",414+ "family": "machines",415+ "sha256": "ddac9d5b5137ae91a7d35816dc27382658e12ff05faccc1f350d204ba4742d5c"416+ },417+ {418+ "seed": "sample/73",419+ "family": "landscapes",420+ "sha256": "2b776eb324cb0e2b90b1bf678b61942bd96b5e15a70dbb4a71127f3943d68acc"421+ },422+ {423+ "seed": "sample/74",424+ "family": "woven",425+ "sha256": "c67f05cc0aa9bbda3a36504a77929a2fb9736740c293bcfaef5301a66b94c4b4"426+ },427+ {428+ "seed": "sample/75",429+ "family": "woven",430+ "sha256": "410e470976eacd9ae529ee9551cb1f9e157c9deb4c0aff5639739539bee7a54d"431+ },432+ {433+ "seed": "sample/76",434+ "family": "landscapes",435+ "sha256": "1833c83ca7ae599435dc422421c6d0d4405ba448302c1568c4716594d7940774"436+ },437+ {438+ "seed": "sample/77",439+ "family": "masks",440+ "sha256": "fc2abc9c43c342b05f196e5965f90a85001042114f454d641bebface0e6d644a"441+ },442+ {443+ "seed": "sample/78",444+ "family": "machines",445+ "sha256": "098d40e8331dc790d42585d978747541f3b5915d609f36ae520f0afadb1c83b7"446+ },447+ {448+ "seed": "sample/79",449+ "family": "botanical",450+ "sha256": "def19606031133f58a3f963728e3de8a71d034cd30aa981e62e187bac65812b2"451+ },452+ {453+ "seed": "sample/80",454+ "family": "creatures",455+ "sha256": "c1ce68a624a6b96966f0845b0883125200c621b511da089d19689db30fd2290d"456+ },457+ {458+ "seed": "sample/81",459+ "family": "machines",460+ "sha256": "59cd2f69592c718125e275e52336ddd028aa518f389e4246a1cfa141c78644a6"461+ },462+ {463+ "seed": "sample/82",464+ "family": "glyphs",465+ "sha256": "724aeff5eef8da625f4ee72819b1a244b28361d7422f53fd8a5f5c7900a6212a"466+ },467+ {468+ "seed": "sample/83",469+ "family": "orbital",470+ "sha256": "b2633c506c3d88c0914a460eb383ba5cb87a9865340c8fc729eb3ccf41657215"471+ },472+ {473+ "seed": "sample/84",474+ "family": "orbital",475+ "sha256": "d20b748699562b25a23b8967c381f8ce0a8bacfa7d6e8bd168dab8d9ae6c9a81"476+ },477+ {478+ "seed": "sample/85",479+ "family": "creatures",480+ "sha256": "fd30845e9d0e76fecd3d82ad1bd8c20e8c8dc2c4c8533d7511e817bcc82cdc05"481+ },482+ {483+ "seed": "sample/86",484+ "family": "landscapes",485+ "sha256": "c93060f5f278fa6660a904b9b78bb519f842a8d34bebbe6b713f6a627d8b6428"486+ },487+ {488+ "seed": "sample/87",489+ "family": "masks",490+ "sha256": "c0fc36005e2968787f99e7e6c89a9772e94c98fdab452945f670ce7f613534a8"491+ },492+ {493+ "seed": "sample/88",494+ "family": "machines",495+ "sha256": "aae5442174348459a08a259e68f045156085d5d4b51ba0edf1c62e8603271690"496+ },497+ {498+ "seed": "sample/89",499+ "family": "botanical",500+ "sha256": "b06de3d96baa257da9638c8a95a1a79c47d5b196c0ddc2e2ebc36affba559c6e"501+ },502+ {503+ "seed": "sample/90",504+ "family": "botanical",505+ "sha256": "daf39093fcbfeb492c9b53b097b29285578d1e63d3fa36575e7b5457258af964"506+ },507+ {508+ "seed": "sample/91",509+ "family": "botanical",510+ "sha256": "0b87dd9df9c2f7ac4cefb02b0a37106ce9c911428085fd96dccb2df83b666dee"511+ },512+ {513+ "seed": "sample/92",514+ "family": "machines",515+ "sha256": "432deaeaa5cbd6bde975cc8d17fd4d3f26f04cb1fb33c7136a8429a88f90c7c1"516+ },517+ {518+ "seed": "sample/93",519+ "family": "creatures",520+ "sha256": "08c9bb89bfff4721218873d5b9d66714969cef5c18a7f44aaf0add974a444925"521+ },522+ {523+ "seed": "sample/94",524+ "family": "masks",525+ "sha256": "30be92735fa610c184eeaac1d52ba89415020ffd816e597095685bbae2f79c5e"526+ },527+ {528+ "seed": "sample/95",529+ "family": "machines",530+ "sha256": "0bd768447aefa71454ba984dfe3989a0dafd43185676788eeb6b2298387150fd"531+ },532+ {533+ "seed": "sample/96",534+ "family": "machines",535+ "sha256": "ffa94a32ed88b5f7aba28c29dde32b585d1472961989710b215b204ead29de21"536+ },537+ {538+ "seed": "sample/97",539+ "family": "orbital",540+ "sha256": "18f27c8d66f5e4d1955ce0698ffaf07e4fb50c5ee206cde86c9aadcc0a8ef2f2"541+ },542+ {543+ "seed": "sample/98",544+ "family": "masks",545+ "sha256": "5fcf0654f99f22c55fbd1a648f8991cd2ec53fb4fd3cf71dc0dfc88b1cd5289a"546+ },547+ {548+ "seed": "sample/99",549+ "family": "creatures",550+ "sha256": "cdefdea3581a30e1f26111a5048b59ec2b62a71109c54a2464dcb5d8195bb0fc"551+ },552+ {553+ "seed": "sample/100",554+ "family": "creatures",555+ "sha256": "8362260a72f2f599ef78f81ceba3880d5d7ce34967125a5bfb1859ba831912cd"556+ },557+ {558+ "seed": "sample/101",559+ "family": "creatures",560+ "sha256": "d9c0196345b0fb77ea8923ebbba9f7defb458228f3ecf25beb81a0e9a16ee37f"561+ },562+ {563+ "seed": "sample/102",564+ "family": "woven",565+ "sha256": "3ee62001de8b7c460b8fd0b593fb619a9431bb65008a31a6b2b8dd4259c6a3b8"566+ },567+ {568+ "seed": "sample/103",569+ "family": "machines",570+ "sha256": "7cc84412daab7ea311ab166fc52c9e06a4ecb303bee985f64fdcef378af3e9d3"571+ },572+ {573+ "seed": "sample/104",574+ "family": "glyphs",575+ "sha256": "6056d1dc560015578ce37ec91877119ccf5260a22e25c17cfa12c6545b5e67d9"576+ },577+ {578+ "seed": "sample/105",579+ "family": "creatures",580+ "sha256": "162c9f18e3468d8d9b1d90fb2714886b5615ac797c7a096a0dc4dec1bd61e839"581+ },582+ {583+ "seed": "sample/106",584+ "family": "masks",585+ "sha256": "c65acce1b25f0de486ad7b7e570702582fd80eabb0a9db1facb7b09c02836790"586+ },587+ {588+ "seed": "sample/107",589+ "family": "orbital",590+ "sha256": "bcdc769da640d3fb2db8694295b6f7b89896f40c0686567d702174b6e8dbd66b"591+ },592+ {593+ "seed": "sample/108",594+ "family": "glyphs",595+ "sha256": "8141655b50c6b86d8789d1927c8f3969d405aef855528378717904d735e006ed"596+ },597+ {598+ "seed": "sample/109",599+ "family": "creatures",600+ "sha256": "1e5c790be58c42b4084e0c93f80e15bfadf462d0e8dd892101a3168dc1a6006e"601+ },602+ {603+ "seed": "sample/110",604+ "family": "botanical",605+ "sha256": "44461963c51d8f1e6dc88572f6d8932a77c61e5f155c0e808a9c33bf85dd8058"606+ },607+ {608+ "seed": "sample/111",609+ "family": "woven",610+ "sha256": "030a399bb64859585c75a15f42a08857ac73638041eae09e2dc89f6f4767bfb7"611+ },612+ {613+ "seed": "sample/112",614+ "family": "masks",615+ "sha256": "ddfdcf4d908b97c6a9026dbb2fb8c6d5c0cbc5b780613c52f0f1ac3f113f3109"616+ },617+ {618+ "seed": "sample/113",619+ "family": "machines",620+ "sha256": "5f2f895f016bb08e63aa9d0d4a694d99b5eac2def5a3d555e902715ba3093902"621+ },622+ {623+ "seed": "sample/114",624+ "family": "landscapes",625+ "sha256": "47481fcc040a29c2448d33b31fa959d6b9574c01310b5b180d97138bf7028bb5"626+ },627+ {628+ "seed": "sample/115",629+ "family": "botanical",630+ "sha256": "6bef7b31de34d9d70566fd265bcbde2b87ae9f4dd90f9fd2ae75278b91ac6e15"631+ },632+ {633+ "seed": "sample/116",634+ "family": "orbital",635+ "sha256": "95ae310efbf9460f8ee79a2d55b4aaa9e415a81447c755c6bf3f119d08ba73eb"636+ },637+ {638+ "seed": "sample/117",639+ "family": "masks",640+ "sha256": "ffddde87258e30f6c48e695b6996150f135bbe30f1cdaf771f0cef3e186d46f1"641+ },642+ {643+ "seed": "sample/118",644+ "family": "masks",645+ "sha256": "5d42b3949fb547304cd7301d74a3e63b5092cb18cfd6cd232eebc7ed0932d8e6"646+ },647+ {648+ "seed": "sample/119",649+ "family": "creatures",650+ "sha256": "17f137319319bf7f559d5b851f31d6b753fe95eda5a4ee673f1f26f0e6b950f7"651+ },652+ {653+ "seed": "sample/120",654+ "family": "orbital",655+ "sha256": "48c61af0533c3125f5c884e90359dd5e5295e9f2874a62fd595d6c9d79244ebd"656+ },657+ {658+ "seed": "sample/121",659+ "family": "woven",660+ "sha256": "1dceff3fcd0628295ba5deb0bff221f71d7cfaa1c8b445f9c15111689129d550"661+ },662+ {663+ "seed": "sample/122",664+ "family": "landscapes",665+ "sha256": "55b85c0d5c15e17e3022f6132a584ddcde292c44e3ee8111f6ff7886cbdb26c6"666+ },667+ {668+ "seed": "sample/123",669+ "family": "creatures",670+ "sha256": "f08e598cb535c1929df8501d3572c90ffc7b0eefa83ea62ce43ae74aea94ec49"671+ },672+ {673+ "seed": "sample/124",674+ "family": "masks",675+ "sha256": "261cb5fd1e0ea296dd41265ad272c289bab6e698f39a3948cd411c83eb862204"676+ },677+ {678+ "seed": "sample/125",679+ "family": "machines",680+ "sha256": "8f1db4695a02baafcf8ace4d8d432fd4cf3a873afa48cd40114b39bc2754fee8"681+ },682+ {683+ "seed": "sample/126",684+ "family": "orbital",685+ "sha256": "75ebda39aa0f2346f3f12d4e843f54f602384b0eced122936976ad1a4311fc62"686+ },687+ {688+ "seed": "sample/127",689+ "family": "landscapes",690+ "sha256": "8ed4d29c29673e01d80225f1057caa2bd89bfb7fbbc408b8eaf619a802147115"691+ },692+ {693+ "seed": "sample/128",694+ "family": "machines",695+ "sha256": "7cba12cf3084b692b58b57bc074b56da18140755c048f7c0a9b0e695fbc8f42b"696+ },697+ {698+ "seed": "sample/129",699+ "family": "orbital",700+ "sha256": "72ebda49207d65a4347be1c7d32bf48471a5eebce434338face5c9481d96e7b3"701+ },702+ {703+ "seed": "sample/130",704+ "family": "machines",705+ "sha256": "2d2c700812185507d681e46fb52144c79c3fc8932a03ca297f829cfb45abc1d7"706+ },707+ {708+ "seed": "sample/131",709+ "family": "woven",710+ "sha256": "454350e879d6a7502765ea47c60a4865c5fc677c3a1b110bb41ae440ae842fee"711+ },712+ {713+ "seed": "sample/132",714+ "family": "woven",715+ "sha256": "bb281d63d5b03ebce09fa8cc23e4d22d59ec1ea18548c42608c1248175e3276a"716+ },717+ {718+ "seed": "sample/133",719+ "family": "machines",720+ "sha256": "f26f620b595f3017cb19705b99182dd9014538db88aa44d99d5ecdd28a2cd02a"721+ },722+ {723+ "seed": "sample/134",724+ "family": "landscapes",725+ "sha256": "f0564f568e667d88e13b512443988b163f3ff784a3ca6ff23edf3c5741597c88"726+ },727+ {728+ "seed": "sample/135",729+ "family": "creatures",730+ "sha256": "13c2d24e3c548be99c37c316d68a540b75159fcb881a6cac1aa896a2f0719d42"731+ },732+ {733+ "seed": "sample/136",734+ "family": "creatures",735+ "sha256": "782ef7a8bfa073c3a6e5912af3d58aeec06588ccd7299d03f786846f49962c48"736+ },737+ {738+ "seed": "sample/137",739+ "family": "machines",740+ "sha256": "52b390c4c08a5a2d6264ee8068a7f108db23f2951b755733326eaaf195a0d8df"741+ },742+ {743+ "seed": "sample/138",744+ "family": "botanical",745+ "sha256": "843b437490cf5371117f544307bb8ac6a07aff6f0f9a6aeab9fc3e10be0c00b2"746+ },747+ {748+ "seed": "sample/139",749+ "family": "glyphs",750+ "sha256": "ca7c3976719774e7e00f4647a3a51b4af7c0242e8e6de2b87dc7296c69f8ee6c"751+ },752+ {753+ "seed": "sample/140",754+ "family": "glyphs",755+ "sha256": "f5fa817d897df0f0090824d0feac9404d1361e7a0e33465cd310f72801045d2a"756+ },757+ {758+ "seed": "sample/141",759+ "family": "landscapes",760+ "sha256": "17fc8e3b29d5e47920d5c54147b99b510b98334437503f13783dcc42ca648432"761+ },762+ {763+ "seed": "sample/142",764+ "family": "glyphs",765+ "sha256": "5c21cc12134eb1f3ad147aec7b1965473691dabcbcc0f20ceeb291c2cb624aee"766+ },767+ {768+ "seed": "sample/143",769+ "family": "glyphs",770+ "sha256": "253b45f4c26668cd6da55ad8092d6a353449f1a22bec60a013ab6966d6c9ab21"771+ },772+ {773+ "seed": "sample/144",774+ "family": "machines",775+ "sha256": "bbdbf99b92f63ff229d1807dd2eae2faffa749373557dffd05e8341e542d2b7e"776+ },777+ {778+ "seed": "sample/145",779+ "family": "orbital",780+ "sha256": "3179a33bd34ddf904ab43437e14b7f5306dc5cf8cb694e99b111b9a084dec02e"781+ },782+ {783+ "seed": "sample/146",784+ "family": "landscapes",785+ "sha256": "a03c1bf2608ee7484c41b2e5bcb339b61853d91fa7cd54e7e5e897ebff6087d7"786+ },787+ {788+ "seed": "sample/147",789+ "family": "creatures",790+ "sha256": "174adbb7beaf40034172eb6f1784cd6a5c505e16d89cdc62793c0273fefac962"791+ },792+ {793+ "seed": "sample/148",794+ "family": "masks",795+ "sha256": "28222c41f87e27ee604d2e74ca58a09e34e89382e9af9c182fad8554d837eeb7"796+ },797+ {798+ "seed": "sample/149",799+ "family": "machines",800+ "sha256": "fd3f09016e764e7d70ba2607a266f5986a8553eeacca081a0f42c6191bbcf9ab"801+ },802+ {803+ "seed": "sample/150",804+ "family": "machines",805+ "sha256": "af4e65bd7d470aae43e5031dc21b40935e8a47a2171b54b722ff5665058f38c9"806+ },807+ {808+ "seed": "sample/151",809+ "family": "woven",810+ "sha256": "7283ccf1222427133ee6b188a8be91d9290b23636265a8f0cd713cd85e62d696"811+ },812+ {813+ "seed": "sample/152",814+ "family": "landscapes",815+ "sha256": "784853b214ea3be21c81a846801d48c7c0b5ac6325eb5609ceceb2ce7e5126bb"816+ },817+ {818+ "seed": "sample/153",819+ "family": "creatures",820+ "sha256": "41ea518dd8e8093ec1a0285584c77d82c3c551dd39f015c18dc62fb50551d482"821+ },822+ {823+ "seed": "sample/154",824+ "family": "landscapes",825+ "sha256": "044785917e8ae486f55c562e37a8b3c6eac35d0cf692377f4e5eebebf7ae8f93"826+ },827+ {828+ "seed": "sample/155",829+ "family": "orbital",830+ "sha256": "95f1ca89a1d287563fad925c63006c31f448c487ec808172d07f7cdc656aae20"831+ },832+ {833+ "seed": "sample/156",834+ "family": "botanical",835+ "sha256": "bd5f6d77f0393baef82687aed4fc058ef462bafb0f1163761205f55a2d8c33bf"836+ },837+ {838+ "seed": "sample/157",839+ "family": "masks",840+ "sha256": "2708a5543855803e3476da45e2611b7e999da3ea303b511ba6eb43f1ec61909c"841+ },842+ {843+ "seed": "sample/158",844+ "family": "creatures",845+ "sha256": "cb23dedb951cc97ffd5ac301d2d4c80e59fc32cb6535a298184231be6a9976e0"846+ },847+ {848+ "seed": "sample/159",849+ "family": "masks",850+ "sha256": "31cc7e6912fccdd149c699cd960e190926a9dcab8cdcbe239dd2e99db35c59cb"851+ },852+ {853+ "seed": "sample/160",854+ "family": "machines",855+ "sha256": "f32a0be5575f3829bf7a7733f24b31049a4a9c56933354bc4efe80c0cd7338a3"856+ },857+ {858+ "seed": "sample/161",859+ "family": "glyphs",860+ "sha256": "5928efcfe418b35d0fdaa9d9fcbf78571c058315cab682c4f9f40b92de138cf0"861+ },862+ {863+ "seed": "sample/162",864+ "family": "machines",865+ "sha256": "cde5883f7c8b6cc2f5eb382cca09bd4daa3a95fbf078181bb74871abc1bdbb97"866+ },867+ {868+ "seed": "sample/163",869+ "family": "glyphs",870+ "sha256": "73e394f43e411c4266bd5426fba8705a2806cf61097c22690aae0231f07db89d"871+ },872+ {873+ "seed": "sample/164",874+ "family": "glyphs",875+ "sha256": "d36f4b95b5ac293ba6355f550b46b4d674d66a1d22c5460247673682b1fbd1aa"876+ },877+ {878+ "seed": "sample/165",879+ "family": "machines",880+ "sha256": "a906ce9a545c335e9e6c2b4049ef18ee39fa8c87dc4c6a0c56b7104dfc61d0e2"881+ },882+ {883+ "seed": "sample/166",884+ "family": "botanical",885+ "sha256": "00c9e8030e53d46be0f291ae03644ffcbe96de251186820db849408348dcd501"886+ },887+ {888+ "seed": "sample/167",889+ "family": "glyphs",890+ "sha256": "619bf35a4cfb233371445282f641ea5ebfd5a7376cf439f6dea4cf96924fb735"891+ },892+ {893+ "seed": "sample/168",894+ "family": "landscapes",895+ "sha256": "cb0f763ea5c9b9bab9b3a1a3933700dc7fd11350c1df681b47dd2d214874e81d"896+ },897+ {898+ "seed": "sample/169",899+ "family": "machines",900+ "sha256": "e178765bdbc4a8bc3e61d21ebc5bfb47f8400fee03dab157d7fb06f89d9695bd"901+ },902+ {903+ "seed": "sample/170",904+ "family": "woven",905+ "sha256": "be37bad3415ca4d067bdefe46745aa1bb5851de0417d2007235db320f76a0415"906+ },907+ {908+ "seed": "sample/171",909+ "family": "glyphs",910+ "sha256": "8d5d5e80accd08b0e985b173d12d37f2261761575776c4e05c3fcd8e3e0bf2b7"911+ },912+ {913+ "seed": "sample/172",914+ "family": "machines",915+ "sha256": "c3d7bfb5c65d0358dbbd4bbb606a77300d14953d35eb28542b2695736b8d53cd"916+ },917+ {918+ "seed": "sample/173",919+ "family": "glyphs",920+ "sha256": "4f32fa3c7251cfa5c0ad85c90e79b36ddf3dc37f1857659f4415b66301d6b37f"921+ },922+ {923+ "seed": "sample/174",924+ "family": "glyphs",925+ "sha256": "f6e8a7811dceee13da5cd045d0e00822378738074d74e63ce622820b83215e3c"926+ },927+ {928+ "seed": "sample/175",929+ "family": "orbital",930+ "sha256": "b5b97ac74904dbd8e71a560d3633c12b82531b57b347ccb1775b6b721358292b"931+ },932+ {933+ "seed": "sample/176",934+ "family": "creatures",935+ "sha256": "6c74ab52671122d394013219d912cbae3f797e40c42d89bc7c19ad3bbab81a4a"936+ },937+ {938+ "seed": "sample/177",939+ "family": "woven",940+ "sha256": "34be6446216fd248de85f56d8e064e535fea058890622ff58d16082868059fde"941+ },942+ {943+ "seed": "sample/178",944+ "family": "glyphs",945+ "sha256": "bfb8f66e9dfe4cffd6af6b185ce976d006994a5efbab4b40ccea07f598d88518"946+ },947+ {948+ "seed": "sample/179",949+ "family": "orbital",950+ "sha256": "d7dc337fc810c6cd89d22ec21d2795da6424c334a763f5a7ecdedd1a5955a153"951+ },952+ {953+ "seed": "sample/180",954+ "family": "orbital",955+ "sha256": "cc92db0e351d9e3973715553647e42b9527b21f5619edcd696c3f16c38b686e8"956+ },957+ {958+ "seed": "sample/181",959+ "family": "botanical",960+ "sha256": "3cf61849fae7cc4aa390b87d62e52a1bded9b2f79ee3ca06f4e9b67ed885d378"961+ },962+ {963+ "seed": "sample/182",964+ "family": "orbital",965+ "sha256": "3df71c77bbd697f13ee07c7dcbd6c212e844b349e14745ca1a232bc8fc39a086"966+ },967+ {968+ "seed": "sample/183",969+ "family": "landscapes",970+ "sha256": "1589dcc0f25a6ff26235cf72dea39b3becca4605e507f5d86867579dc37246cc"971+ },972+ {973+ "seed": "sample/184",974+ "family": "orbital",975+ "sha256": "8489a9f60f888030bfb324f1984827ab6c9f6d5e923dd0f37eb93d48d53aca94"976+ },977+ {978+ "seed": "sample/185",979+ "family": "machines",980+ "sha256": "076643082ee6923f0add08b76b2d2ee8e2893774f9fab7a62f4d0834ca3cfe0c"981+ },982+ {983+ "seed": "sample/186",984+ "family": "creatures",985+ "sha256": "393bfbcab2092d694f8066eee9b32c07a0169e2a6f0ff74d717d1a6cd082f3e2"986+ },987+ {988+ "seed": "sample/187",989+ "family": "botanical",990+ "sha256": "85ed55cacfa7f9d1482e8a8bda199c7500634b7c64fe27a491e7cd800b426dd2"991+ },992+ {993+ "seed": "sample/188",994+ "family": "machines",995+ "sha256": "742a90355d1336c41c8be23b3a32b98700c46c1c7960882679f979c9a6f570ac"996+ },997+ {998+ "seed": "sample/189",999+ "family": "orbital",1000+ "sha256": "d39840560b3b2db9c0406e552606fa1cc3b4409db356e4ab33ab37dddff84f13"1001+ },1002+ {1003+ "seed": "sample/190",1004+ "family": "orbital",1005+ "sha256": "3ca993f0006e72d84f250115ded5e6e3f5173a27b12037fa5036202ee1a181b7"1006+ },1007+ {1008+ "seed": "sample/191",1009+ "family": "woven",1010+ "sha256": "719b4be81e1c97af42d96bef8f35f0182b533010d64f01fc02c57ccba5ad3b7b"1011+ },1012+ {1013+ "seed": "sample/192",1014+ "family": "glyphs",1015+ "sha256": "bd97d5995ed28fed93421b70d3b651e61ee4443810d80cc5ff20d93e19d63db0"1016+ },1017+ {1018+ "seed": "sample/193",1019+ "family": "botanical",1020+ "sha256": "af9215cfd96d0c71ef856dd3f17ef0806c58bdc36027b380c217d1b29a079006"1021+ },1022+ {1023+ "seed": "sample/194",1024+ "family": "glyphs",1025+ "sha256": "b9efff1c21cc32075a063d95db2ac9d8c04b4be3dcd41785be348010ac118ccc"1026+ },1027+ {1028+ "seed": "sample/195",1029+ "family": "machines",1030+ "sha256": "b1c601fe88a53433c73ac66f74fad5094971b303b249803507fe3b39a50f02ce"1031+ },1032+ {1033+ "seed": "sample/196",1034+ "family": "woven",1035+ "sha256": "84b0bf3691bf442181c220423fa871f713cb50e9e1375a5187c5bbe575324388"1036+ },1037+ {1038+ "seed": "sample/197",1039+ "family": "glyphs",1040+ "sha256": "8aa4fa2a39d7ff53c6fa34789975c3881209903e1e6bb02e97b6a2151eb3ef30"1041+ },1042+ {1043+ "seed": "sample/198",1044+ "family": "creatures",1045+ "sha256": "4dfb3b3c8fc4f544370da80caa546b0811447aca85a903de3c87c63731c5c750"1046+ },1047+ {1048+ "seed": "sample/199",1049+ "family": "machines",1050+ "sha256": "e5b475e3d00f6b9f861757a33911772828ba6523ee4ee13be8eec64f36333c11"1051+ },1052+ {1053+ "seed": "sample/200",1054+ "family": "creatures",1055+ "sha256": "e173ce9f4a26299f5e05ab09a3fd93d951076db23869773252ba2f984cb765f5"1056+ },1057+ {1058+ "seed": "sample/201",1059+ "family": "landscapes",1060+ "sha256": "15af0538a7859ddd5b8e44c0a07f32c517a3dda97bf94b90b3d4e891c323eb76"1061+ },1062+ {1063+ "seed": "sample/202",1064+ "family": "orbital",1065+ "sha256": "adb5b541244e58bd6735605ca7db014102bdc6fb7e4a98104b37d9122d78d2e3"1066+ },1067+ {1068+ "seed": "sample/203",1069+ "family": "botanical",1070+ "sha256": "c8f0381f3e7b8b92b7f3d9e030c4308a5d98488778d9f1aaed50645cc272d451"1071+ },1072+ {1073+ "seed": "sample/204",1074+ "family": "machines",1075+ "sha256": "f797ac06ce4affa5b6722591f95a613a188f03caa153a8068c212ee68070a69b"1076+ },1077+ {1078+ "seed": "sample/205",1079+ "family": "landscapes",1080+ "sha256": "9360507bdcb843616efbbdc53b62da6cefe283414dadfc4ccbd040a009088015"1081+ },1082+ {1083+ "seed": "sample/206",1084+ "family": "creatures",1085+ "sha256": "e99cef80229c1057fec5e22ba4ffb742a425e9be96f17dda49842086c8cdab7c"1086+ },1087+ {1088+ "seed": "sample/207",1089+ "family": "landscapes",1090+ "sha256": "22bc556880a411b12155f8068a79ee5b53eee288fd83fb3fc80d921c47f3c81c"1091+ },1092+ {1093+ "seed": "sample/208",1094+ "family": "masks",1095+ "sha256": "4cc4d1266604124d93ae7d1145a648245427559ad35e6834fe9dc30f1cf1fdf8"1096+ },1097+ {1098+ "seed": "sample/209",1099+ "family": "masks",1100+ "sha256": "71a874a109fd209d6b94cfacf18bf0b7a9d1fb8a8fc47aa01f5bb2ce0340f1a6"1101+ },1102+ {1103+ "seed": "sample/210",1104+ "family": "orbital",1105+ "sha256": "5d137f4a5bda5292a84944649c5e97e73321e7742b12cad4d91296f763d747d4"1106+ },1107+ {1108+ "seed": "sample/211",1109+ "family": "botanical",1110+ "sha256": "74df6847499fe13630364c3f25af1d06b11e58f76824f24b49613d7ae5197e93"1111+ },1112+ {1113+ "seed": "sample/212",1114+ "family": "machines",1115+ "sha256": "36dc23006d7948b1befb136e970be4360219b912ac48a0e468a58e576c22aea2"1116+ },1117+ {1118+ "seed": "sample/213",1119+ "family": "masks",1120+ "sha256": "d38fece2dc9b32caf8e61cb6d003f2b4e937d25da32b15650a730d005ff5bf21"1121+ },1122+ {1123+ "seed": "sample/214",1124+ "family": "woven",1125+ "sha256": "beba8aacef4b3e828f743eaf7291286894b9b9fe13a605c008abd7280386faa2"1126+ },1127+ {1128+ "seed": "sample/215",1129+ "family": "masks",1130+ "sha256": "bf04a2e24f466262695551cc0f2024e9d2dc067364d8a8931c1240bcf530307f"1131+ },1132+ {1133+ "seed": "sample/216",1134+ "family": "machines",1135+ "sha256": "645bd7fb6bdcee56fca3de50079f7f270903c7aa35f58ea51dfefd60aab94aa8"1136+ },1137+ {1138+ "seed": "sample/217",1139+ "family": "woven",1140+ "sha256": "76dab525874a019da971ef502c83f2c9a7b49be688ebecad1cfd1b9703902278"1141+ },1142+ {1143+ "seed": "sample/218",1144+ "family": "creatures",1145+ "sha256": "eb1efc4822942b502e15eb2b3a5be438a4efdf0adc35b8fb152e77ba3e1235ce"1146+ },1147+ {1148+ "seed": "sample/219",1149+ "family": "glyphs",1150+ "sha256": "4ec8607d03d1b4dbdbb22227169ce4a6c848735e692c0cbd2cf038c0279ab030"1151+ },1152+ {1153+ "seed": "sample/220",1154+ "family": "creatures",1155+ "sha256": "d8807b70d2ce6ff4feefd61fddc77d6e156c6ac2ca81ddbd81c724b5c795e497"1156+ },1157+ {1158+ "seed": "sample/221",1159+ "family": "creatures",1160+ "sha256": "5ffa32ed530c45a8187e390626d0203233ee4590dbaeced3ac6d6d558465b4d8"1161+ },1162+ {1163+ "seed": "sample/222",1164+ "family": "creatures",1165+ "sha256": "d2a35956a819b3f40bd7b00f4c15acd58232d12efee7f93b64e3c6ab953390e8"1166+ },1167+ {1168+ "seed": "sample/223",1169+ "family": "masks",1170+ "sha256": "a6a5f628c917f9c68cde30e09a222a478013becbcb39ab403898569cc1cb0ea3"1171+ },1172+ {1173+ "seed": "sample/224",1174+ "family": "landscapes",1175+ "sha256": "0723b4ef6071d73414ad83c915e8fdfa34a28a98b62e666fcac4d983c11c3ccf"1176+ },1177+ {1178+ "seed": "sample/225",1179+ "family": "woven",1180+ "sha256": "6059d4ee15f9909e6e58bd1a4d8e724ae523911ffeeec368ad4c18805c95938b"1181+ },1182+ {1183+ "seed": "sample/226",1184+ "family": "machines",1185+ "sha256": "44cde40c0cd3befd578d8e8687dd7da0b1fbdd7299f45aa7139833ee8cd7a132"1186+ },1187+ {1188+ "seed": "sample/227",1189+ "family": "glyphs",1190+ "sha256": "4d7b75fd3936b36c0679d8d06195c34d452f4b30e39c975caa813442107ea9c6"1191+ },1192+ {1193+ "seed": "sample/228",1194+ "family": "masks",1195+ "sha256": "cbef4fe57c3fb627905964aacbb07313421372dc78c3a10cafff66c4d2ef43f2"1196+ },1197+ {1198+ "seed": "sample/229",1199+ "family": "masks",1200+ "sha256": "107c57eb7c5f0d24074f3aae3730fdefb305e1b44a3e51a539b6107f4f1a022a"1201+ },1202+ {1203+ "seed": "sample/230",1204+ "family": "masks",1205+ "sha256": "e0f1fd357e640f8d84520b2d599f8e4c8ab4ed58f770cae164f021c4ee1341d1"1206+ },1207+ {1208+ "seed": "sample/231",1209+ "family": "masks",1210+ "sha256": "88e17effe9e45d67b329bb68f6b8b635a3c5d370d4aae9ddba7098688c8ebfc0"1211+ },1212+ {1213+ "seed": "sample/232",1214+ "family": "woven",1215+ "sha256": "45137046e990d23c394ca7e43ff164259f3f3cd2b486842e814d9ca37d86f71a"1216+ },1217+ {1218+ "seed": "sample/233",1219+ "family": "orbital",1220+ "sha256": "a8bc465222b10915b560f472fe011aefa1b657e69a37f445376d28e03860456c"1221+ },1222+ {1223+ "seed": "sample/234",1224+ "family": "masks",1225+ "sha256": "a81241eeeb6094196613d3ea8788e0f777334579ab7ae45958605f137393f4f5"1226+ },1227+ {1228+ "seed": "sample/235",1229+ "family": "botanical",1230+ "sha256": "fa62272d79a795a84376815140d49b8d1dfb22f5f733d308547fa13a836dd9ed"1231+ },1232+ {1233+ "seed": "sample/236",1234+ "family": "creatures",1235+ "sha256": "d1f986704090d15daa096623cdab1c71ec44abee42162471146eb2d790ff4036"1236+ },1237+ {1238+ "seed": "sample/237",1239+ "family": "orbital",1240+ "sha256": "b12e45060cc73a0dfb251976baad312d9f69e31e186556c9c64c82b4b010d31d"1241+ },1242+ {1243+ "seed": "sample/238",1244+ "family": "landscapes",1245+ "sha256": "b171657de2ecebe579a205e05bf59b6a8c1998aeab9cb7981bbecd7466fc8892"1246+ },1247+ {1248+ "seed": "sample/239",1249+ "family": "creatures",1250+ "sha256": "6a71a042ad8b4cfc8f9cbddad52ffe148a0d2ad263a793a233d1ad874e182707"1251+ },1252+ {1253+ "seed": "sample/240",1254+ "family": "masks",1255+ "sha256": "63ebe2166fb22a6ac5e180aefd6d9c96df71c0e9e9e78892375a1f30ec117316"1256+ },1257+ {1258+ "seed": "sample/241",1259+ "family": "creatures",1260+ "sha256": "bbee402bda2ae7399ab1eb426db8e2f190e5d26577a3aaa38e3808d729897465"1261+ },1262+ {1263+ "seed": "sample/242",1264+ "family": "orbital",1265+ "sha256": "033844ef263cf6669ed82d7a935f4d6f6d1c9bfe2dec504e7f8a09ca87072f1b"1266+ },1267+ {1268+ "seed": "sample/243",1269+ "family": "landscapes",1270+ "sha256": "3d9dcd34efeff07839b5ba9488eefe31334cf09143875aea54ccf9b112a5f3ac"1271+ },1272+ {1273+ "seed": "sample/244",1274+ "family": "landscapes",1275+ "sha256": "1f3c24a6945a61e149e02a6b49c2f4e42a1e274dcc49da8b6a0d49d38aec1aad"1276+ },1277+ {1278+ "seed": "sample/245",1279+ "family": "orbital",1280+ "sha256": "62d5d9be3910bf0344d23aaa54ba762d99e78ce4db604c4f50ee08cd2ead2b76"1281+ },1282+ {1283+ "seed": "sample/246",1284+ "family": "masks",1285+ "sha256": "691290f3f71d29bf7cf61faa932ce776d7f6379c992d910e8077bc63a102eb04"1286+ },1287+ {1288+ "seed": "sample/247",1289+ "family": "woven",1290+ "sha256": "de768a973af9e7bbcef7ad330fedb50f1987392f02f9fc8a2207b4dd18029f3c"1291+ },1292+ {1293+ "seed": "sample/248",1294+ "family": "botanical",1295+ "sha256": "ce3e9e5d3d2770daf54b1bfa238f987fa0f06ba68997fbfb6950f9d34bd88c47"1296+ },1297+ {1298+ "seed": "sample/249",1299+ "family": "orbital",1300+ "sha256": "24a1d207189a834fdf25cc9e31a55bcae5bbb53f389ba76a33980dae10a68f0a"1301+ },1302+ {1303+ "seed": "sample/250",1304+ "family": "orbital",1305+ "sha256": "24271e83191a4d7ffe4518d35cce9198d3bd75a7d8b6efed1ddede1da893ee9a"1306+ },1307+ {1308+ "seed": "sample/251",1309+ "family": "glyphs",1310+ "sha256": "a836c4584d66dc043172f3f3f54d144c138d0ba979064edccaaa28af2856bc7d"1311+ },1312+ {1313+ "seed": "sample/252",1314+ "family": "masks",1315+ "sha256": "2302a394e7bbbb3c83ed270e8c346d59ddb80e56d8b3710e2f6fdd1724c1abce"1316+ },1317+ {1318+ "seed": "sample/253",1319+ "family": "glyphs",1320+ "sha256": "4bbca31ce643d2e8a34d9ffb47b2f58781bb4e95d448d523bfffb6bfae64e519"1321+ },1322+ {1323+ "seed": "sample/254",1324+ "family": "botanical",1325+ "sha256": "8c1fec78ddf203b7315f25f9faa1557c07baf3b95bf75db67bd30e3f06e59d7f"1326+ },1327+ {1328+ "seed": "sample/255",1329+ "family": "glyphs",1330+ "sha256": "5be613c7e0f7ede9072a641f5ac3aad7b1e54373805c042c2f81f21541df7f8d"1331+ },1332+ {1333+ "seed": "sample/256",1334+ "family": "orbital",1335+ "sha256": "de0a69bf620d0ee40f0933ff54e438a8b063c6658b2114fbf01a9fce8d100f45"1336+ },1337+ {1338+ "seed": "sample/257",1339+ "family": "creatures",1340+ "sha256": "2c525819f6aa1fddab14ef50a5e9e1e6f384c4ece1b4989999853b9bb906f493"1341+ },1342+ {1343+ "seed": "sample/258",1344+ "family": "landscapes",1345+ "sha256": "e5d78a00f43c43c6d0de870800c35c766f5c3f9a36cea050b6dd13e95dfa8337"1346+ },1347+ {1348+ "seed": "sample/259",1349+ "family": "masks",1350+ "sha256": "c4888975b581899c64179a2b4d78348519b443a8dc66b0b1255837a592a0bbcf"1351+ },1352+ {1353+ "seed": "sample/260",1354+ "family": "woven",1355+ "sha256": "00cc1cee52416f0bc107fa843f62dc317fb388fab92eaa69a08bd1e58aaa399d"1356+ },1357+ {1358+ "seed": "sample/261",1359+ "family": "masks",1360+ "sha256": "f5b98697e504c7bead86f68c3ffbeaef8d8e16eadd155afbf13f9935ce63dfa1"1361+ },1362+ {1363+ "seed": "sample/262",1364+ "family": "glyphs",1365+ "sha256": "7fc002089ab22c3273e4b8e6a3a9cb28f4d8356575a6ea22c8ebf02817e71449"1366+ },1367+ {1368+ "seed": "sample/263",1369+ "family": "orbital",1370+ "sha256": "76958c271ba7cb7ccbdc8b574ca46cc3229e4a33056f7a2a6bb9db60bc2e17ba"1371+ },1372+ {1373+ "seed": "sample/264",1374+ "family": "masks",1375+ "sha256": "c57ce953beb27e78771c9043909c92af3e863c3fa152c6e95159add4dd54285f"1376+ },1377+ {1378+ "seed": "sample/265",1379+ "family": "machines",1380+ "sha256": "b74563f343239b689ba17a1d3a8ee42c1bb4fb59e1170875a28db3f85b811006"1381+ },1382+ {1383+ "seed": "sample/266",1384+ "family": "masks",1385+ "sha256": "aeee6abb83aafe7758bbab6fc826e918e4c9758ee39bd4740beadc6981ff9868"1386+ },1387+ {1388+ "seed": "sample/267",1389+ "family": "glyphs",1390+ "sha256": "d43ea0b7baf7b64ccc80b4ece91983a9511393d45a5ad9c69e1668c48e18720f"1391+ },1392+ {1393+ "seed": "sample/268",1394+ "family": "landscapes",1395+ "sha256": "6645a94132610248bed4b9f0d29b4bd1a24b43088f4dc7cb90b3fbbfadebb7e1"1396+ },1397+ {1398+ "seed": "sample/269",1399+ "family": "botanical",1400+ "sha256": "94478c95570a9075b1888074269c37624611e43cb8690fdf38e32e42d5931236"1401+ },1402+ {1403+ "seed": "sample/270",1404+ "family": "landscapes",1405+ "sha256": "f038b343ec50801aff10e8af8d9ddaf0168e53c1db991285a8612baa7ff5e982"1406+ },1407+ {1408+ "seed": "sample/271",1409+ "family": "glyphs",1410+ "sha256": "bf6d448d97d8d6b1d9c29932709d480feb9f722884d45ea0c24d58cf992a890f"1411+ },1412+ {1413+ "seed": "sample/272",1414+ "family": "woven",1415+ "sha256": "057b7285bcab85bc32655b143ceab35a59f827f820470fba9219398f060de0e5"1416+ },1417+ {1418+ "seed": "sample/273",1419+ "family": "botanical",1420+ "sha256": "e4882c69d7119a03d31a91808c77f1c2abd039d668aa6fa6c72ac09695d6ba72"1421+ },1422+ {1423+ "seed": "sample/274",1424+ "family": "botanical",1425+ "sha256": "da29c9987c8115cbccd17e0eac7d363f766f5fbb4086e673f97b392e07f96bc3"1426+ },1427+ {1428+ "seed": "sample/275",1429+ "family": "creatures",1430+ "sha256": "b5da8f8cb303e131259cf62e65f168507af3971de429ac0dce4c7dc76627ae83"1431+ },1432+ {1433+ "seed": "sample/276",1434+ "family": "creatures",1435+ "sha256": "8b1e904a04cd2819bf6fdd92be270eb2108b0f66c3f4abf1984df9cb81c4ff9b"1436+ },1437+ {1438+ "seed": "sample/277",1439+ "family": "woven",1440+ "sha256": "85fee3bbeb78549a7c10fc3745b6fb83374e6084b9abb3f0f01cedc92ba10390"1441+ },1442+ {1443+ "seed": "sample/278",1444+ "family": "glyphs",1445+ "sha256": "ce3f7214f6c2d25d3565ef35b06554bc3864f2e66caa5678f5d7d5abc8d1edd0"1446+ },1447+ {1448+ "seed": "sample/279",1449+ "family": "machines",1450+ "sha256": "efbd4e3b0e8ff6b6226a91f1a1b97764b5dcbd7fd5d703d67c71f3223afc0218"1451+ },1452+ {1453+ "seed": "sample/280",1454+ "family": "glyphs",1455+ "sha256": "d49870e6ad94f31e7bbfd69c41a12d563c5f9f2e69a00d02f147677ee96bc260"1456+ },1457+ {1458+ "seed": "sample/281",1459+ "family": "machines",1460+ "sha256": "2734a80f81e0f6aba8742bd4d66d4ad0751476c1e6138c3968473a860fd1b7a8"1461+ },1462+ {1463+ "seed": "sample/282",1464+ "family": "woven",1465+ "sha256": "b28bfbefc42f604b09256350406e94c6b2fe65c0cefd504dcaec0338a09c74f5"1466+ },1467+ {1468+ "seed": "sample/283",1469+ "family": "creatures",1470+ "sha256": "5ff5ea1ed549f46cc64c5e39715efd9104eeed96c85773f2fd4c93c4be4a3ce2"1471+ },1472+ {1473+ "seed": "sample/284",1474+ "family": "landscapes",1475+ "sha256": "c1718913b227bdf760afff5247c023ea4dcb9a90a2b86a82428aa90bcabfabba"1476+ },1477+ {1478+ "seed": "sample/285",1479+ "family": "masks",1480+ "sha256": "e303d06cd64f0713a1b9ed157bbd3408e205f0a12c897bff4c19c6465515fb36"1481+ },1482+ {1483+ "seed": "sample/286",1484+ "family": "masks",1485+ "sha256": "d91ebda0b72e06a40616ba65f0d99359bc85fadb74863a7a8e92b9355042f110"1486+ },1487+ {1488+ "seed": "sample/287",1489+ "family": "botanical",1490+ "sha256": "f7ce059834078642f37139dcd3665ed4b0d34fdea8f7a2bac4d3cf78ac476c99"1491+ },1492+ {1493+ "seed": "sample/288",1494+ "family": "orbital",1495+ "sha256": "898b8a19724ca331cba87aa4eab056053124ca0d5e475d87cd4342956117894c"1496+ },1497+ {1498+ "seed": "sample/289",1499+ "family": "botanical",1500+ "sha256": "9641f22a050db580ca5f5de274a7574ab3056de69f38a3ad225fa0e0f64e6b5a"1501+ },1502+ {1503+ "seed": "sample/290",1504+ "family": "botanical",1505+ "sha256": "bddfcd6aa1c7ef8bec7d7b7cd3c71b7870abac14c8add0b9c8f78e176eaa5df8"1506+ },1507+ {1508+ "seed": "sample/291",1509+ "family": "machines",1510+ "sha256": "2194430be7c24caf34d1227a63e291d53763d4b61c7212509e15e39c17be6370"1511+ },1512+ {1513+ "seed": "sample/292",1514+ "family": "woven",1515+ "sha256": "838c48567b9f60a49545ab0241cc0da84f5f3942ffd681fe190eb8f0a00bd715"1516+ },1517+ {1518+ "seed": "sample/293",1519+ "family": "botanical",1520+ "sha256": "bcaddf982aa8775533e7a7842a0a871d633848522a223fcbc1e9e5958de7c5b0"1521+ },1522+ {1523+ "seed": "sample/294",1524+ "family": "landscapes",1525+ "sha256": "963f9d941c4ce68debf8475eaf4f43d3795a21b741b28124f648d7f138f695e8"1526+ },1527+ {1528+ "seed": "sample/295",1529+ "family": "machines",1530+ "sha256": "1c2c3296d59ddb2d67a967ddb63d35a803ea040e5697e7255c89ef0635e63ccc"1531+ },1532+ {1533+ "seed": "sample/296",1534+ "family": "machines",1535+ "sha256": "23084231347c18601dd566dbfc712fdf907174849544fe704fde4c11ec5490d1"1536+ },1537+ {1538+ "seed": "sample/297",1539+ "family": "machines",1540+ "sha256": "b61ced8e7ba2f61eae3b4480f962ad78e85f0b4c1bca59184bf6447fdf09c845"1541+ },1542+ {1543+ "seed": "sample/298",1544+ "family": "glyphs",1545+ "sha256": "2edc34201a3dd4bf81fabc9b3fac660d37bfecaf8ad4c4a907a42ce4f955df4e"1546+ },1547+ {1548+ "seed": "sample/299",1549+ "family": "glyphs",1550+ "sha256": "e1e749cc53462a26f1d58716de01ff6d6b45829e8a2db9e5804a3fef9cdbf2bd"1551+ },1552+ {1553+ "seed": "sample/300",1554+ "family": "botanical",1555+ "sha256": "c0838849d95c13be3fe4d40ece7ce4226357937b2e43c3ee7b4d7c99057d53b8"1556+ },1557+ {1558+ "seed": "sample/301",1559+ "family": "landscapes",1560+ "sha256": "4f2f33c87d03dca41ed48cc94365ee9cf1b3f23072871ee57b92b8211cef284c"1561+ },1562+ {1563+ "seed": "sample/302",1564+ "family": "landscapes",1565+ "sha256": "4d66ba07ebba60c88353cb01a987bdd1ac966bfdadc569a2b8bc988430b04341"1566+ },1567+ {1568+ "seed": "sample/303",1569+ "family": "landscapes",1570+ "sha256": "7c86c9be7b2c5c4ee6e1ebf72def7c50f0002d5a16499f6c0b8d5172322c460e"1571+ },1572+ {1573+ "seed": "sample/304",1574+ "family": "botanical",1575+ "sha256": "2049c8fc9039b96bc1c5a256451425d6b28cb9fbef3a4a6fd21345f8646c9b4d"1576+ },1577+ {1578+ "seed": "sample/305",1579+ "family": "machines",1580+ "sha256": "34b5cc8574cc04f8bcef37520a47ad0db6b878a55cad298af8ec92cc7a067af5"1581+ },1582+ {1583+ "seed": "sample/306",1584+ "family": "creatures",1585+ "sha256": "073e28b0903e3c64c22ed80e2861d672ee2abcf24ea7efbe02ec5da68f383f5d"1586+ },1587+ {1588+ "seed": "sample/307",1589+ "family": "masks",1590+ "sha256": "a210ce3728f7b32c091d05904d55460fb30e1ea9f8da7fb86c9f8113461697fd"1591+ },1592+ {1593+ "seed": "sample/308",1594+ "family": "botanical",1595+ "sha256": "05961b0dc4d7c2fcdcf17a214745c5fef35320ecc75f0f1d6292635f7b5cc113"1596+ },1597+ {1598+ "seed": "sample/309",1599+ "family": "masks",1600+ "sha256": "73978f8ee1245438e69e5f029122d8f794aed28d90b6ca1a7a25918568c8ae46"1601+ },1602+ {1603+ "seed": "sample/310",1604+ "family": "woven",1605+ "sha256": "7027b9ea7c0a22f2bde9aa93387a94dda22d4cf258eabbe46260d32e4e959db5"1606+ },1607+ {1608+ "seed": "sample/311",1609+ "family": "landscapes",1610+ "sha256": "8a6f74aec960183bb28626265c882e59e116a41c216a26687fc2d5183d31572a"1611+ },1612+ {1613+ "seed": "sample/312",1614+ "family": "glyphs",1615+ "sha256": "ebcae3dce034850bcc03db79d4ec3ecfb30767b7e6627ea770613a72911b63d6"1616+ },1617+ {1618+ "seed": "sample/313",1619+ "family": "woven",1620+ "sha256": "b71c333f798182a01182c61f0ecd7c4e13fae187a9ec106701c6a2f3ff9db7b0"1621+ },1622+ {1623+ "seed": "sample/314",1624+ "family": "landscapes",1625+ "sha256": "1860dfec4d58eac09ec0907aec820cfd195ecceb67d8315a581a0e2f8f618bdf"1626+ },1627+ {1628+ "seed": "sample/315",1629+ "family": "masks",1630+ "sha256": "96ca992a5d8b7b36c8165c06e9417cadb45b71a8b710bbca609a73feed7dd565"1631+ },1632+ {1633+ "seed": "sample/316",1634+ "family": "orbital",1635+ "sha256": "39396c5da766ca320a3c379259d52b70f1d5cc41f7901a02844de214c8091b3b"1636+ },1637+ {1638+ "seed": "sample/317",1639+ "family": "machines",1640+ "sha256": "bef8132ae48cf9bfdb7d6bca348d1d7414bb56222a1b8d9e1eee978b8f50ea00"1641+ },1642+ {1643+ "seed": "sample/318",1644+ "family": "machines",1645+ "sha256": "11e8453d3525e5c9b896ec29f558f616f85991b490148e89f6feced501b242dd"1646+ },1647+ {1648+ "seed": "sample/319",1649+ "family": "glyphs",1650+ "sha256": "8df7ca35caae7f026836d90dfedd64f9dff060bb022b52df825d7593957198ca"1651+ },1652+ {1653+ "seed": "sample/320",1654+ "family": "creatures",1655+ "sha256": "e50784984da226c8c28d222d731da5aaa43781c5572e63e305f39b3e492fbaa5"1656+ },1657+ {1658+ "seed": "sample/321",1659+ "family": "woven",1660+ "sha256": "6f9e68793b3caf3c2eaebfc87b592bf4f3a4430d837bf6612e0cb6186eabbf1a"1661+ },1662+ {1663+ "seed": "sample/322",1664+ "family": "botanical",1665+ "sha256": "f7c2dc2a0d328dee67202afcce9af4b399a5f2eaeb02646f58840f5c67e6fe70"1666+ },1667+ {1668+ "seed": "sample/323",1669+ "family": "botanical",1670+ "sha256": "3a2227f020574734fb578e0ac284848842f657392afe5580c17e0a374a3d4e0a"1671+ },1672+ {1673+ "seed": "sample/324",1674+ "family": "masks",1675+ "sha256": "1f9304349e135f840687ea093c1c3c58bb2dcd9c3a73211a8be765087ea7daf5"1676+ },1677+ {1678+ "seed": "sample/325",1679+ "family": "masks",1680+ "sha256": "e7bc66cbfae4b19e5b4201b0e643a6ba73e0dcac5ed9648d0ddfebc9a6273ef4"1681+ },1682+ {1683+ "seed": "sample/326",1684+ "family": "botanical",1685+ "sha256": "9e7d647a0b5b799846efb1eb62778a0b9ab2d77a64ce2ec2758226c27eda419a"1686+ },1687+ {1688+ "seed": "sample/327",1689+ "family": "botanical",1690+ "sha256": "9dacdab8e918de3550b7e203647e5f9e9ac2ada4c3d061f13edc48515066101c"1691+ },1692+ {1693+ "seed": "sample/328",1694+ "family": "glyphs",1695+ "sha256": "31e397683d6e37368b7079a1aa4156b41ffc356765c4cb7345a4bc56f32675a0"1696+ },1697+ {1698+ "seed": "sample/329",1699+ "family": "glyphs",1700+ "sha256": "646269249ed9a5a58e17b826a50994c773a91bbea82e54b271df0b0f694e4f03"1701+ },1702+ {1703+ "seed": "sample/330",1704+ "family": "masks",1705+ "sha256": "e2cb2321f6edd21861c30f782aab1d9e1700c0afe855e9401a6482a44c53a8e1"1706+ },1707+ {1708+ "seed": "sample/331",1709+ "family": "creatures",1710+ "sha256": "9dbd50216a8079afb439d9c71675a03158b04adc0cc58588dc9afe52313910bd"1711+ },1712+ {1713+ "seed": "sample/332",1714+ "family": "machines",1715+ "sha256": "f34c7d55b56d5abf3f14480a86c33492307057372e85d036533a76dae461ed66"1716+ },1717+ {1718+ "seed": "sample/333",1719+ "family": "creatures",1720+ "sha256": "b8dfb4df1b003cb3f16246e8052b747a491dc2ffde5704aaf2f31a24c1ffc035"1721+ },1722+ {1723+ "seed": "sample/334",1724+ "family": "glyphs",1725+ "sha256": "59fe06d3c287f3a1389a681a70a088a2d6c1d1422759730c87117a79ba37c8a0"1726+ },1727+ {1728+ "seed": "sample/335",1729+ "family": "glyphs",1730+ "sha256": "d8810997f449c6e08d4d9540aa388e47e704b482b9d1a51806131bb80d5292eb"1731+ },1732+ {1733+ "seed": "sample/336",1734+ "family": "botanical",1735+ "sha256": "9641f22a050db580ca5f5de274a7574ab3056de69f38a3ad225fa0e0f64e6b5a"1736+ },1737+ {1738+ "seed": "sample/337",1739+ "family": "landscapes",1740+ "sha256": "dc2953c4a8cfeecde1c05a7eca7eccc186f48ce44150fdb029dd683d51ba9470"1741+ },1742+ {1743+ "seed": "sample/338",1744+ "family": "botanical",1745+ "sha256": "4ade9335c966eed0a9a4c008d2023a14929981488e43d593dd3eff7e4fb4b5a2"1746+ },1747+ {1748+ "seed": "sample/339",1749+ "family": "machines",1750+ "sha256": "dc374b26384b692b7de8fc56b314c66593da9bde5e15cffb22d4ad3f7d093657"1751+ },1752+ {1753+ "seed": "sample/340",1754+ "family": "machines",1755+ "sha256": "4200fa4ef7d552248f2f8da720bed54e5d4fcdc56a50dd00f0c3d79a9af78726"1756+ },1757+ {1758+ "seed": "sample/341",1759+ "family": "machines",1760+ "sha256": "ef67cfe67710a27aaf742dcbb17e8beb1085c339afc21e867d68e1cb5d9809f6"1761+ },1762+ {1763+ "seed": "sample/342",1764+ "family": "glyphs",1765+ "sha256": "28aac8f8901322a9ce6f2f84e2f59dac550f13bec06cb35f7bdb523e2eaaca5a"1766+ },1767+ {1768+ "seed": "sample/343",1769+ "family": "botanical",1770+ "sha256": "c508580fdd9ea87ab8eeb7f12128d802ff0f569a849f6e7379102c8fdab47889"1771+ },1772+ {1773+ "seed": "sample/344",1774+ "family": "machines",1775+ "sha256": "2aa5a26af8a91789ce2bb2342d9454617a70f3d37a6122d1796f0a1ab0609fa2"1776+ },1777+ {1778+ "seed": "sample/345",1779+ "family": "orbital",1780+ "sha256": "eaa918a182a3f95ab6765ca67baa3251ac3f0d847e595882804beffda95b70ed"1781+ },1782+ {1783+ "seed": "sample/346",1784+ "family": "masks",1785+ "sha256": "374c128951e94362fc59acf92e3216b903ab0984077e1e27d3ff22e240686784"1786+ },1787+ {1788+ "seed": "sample/347",1789+ "family": "orbital",1790+ "sha256": "4ace27fe0b15717cb91fcf81121e09093cfcb27c56af929ef9b6f03ea2229bb1"1791+ },1792+ {1793+ "seed": "sample/348",1794+ "family": "orbital",1795+ "sha256": "bdeedb47f6ad528f77edf31e77c7056e4b8f9110482fab91c2cac65c04c9a824"1796+ },1797+ {1798+ "seed": "sample/349",1799+ "family": "landscapes",1800+ "sha256": "04869f87b60ddbe67adb73a63dd033eb3bfb3e7567d1f7f00f1c255007e8f030"1801+ },1802+ {1803+ "seed": "sample/350",1804+ "family": "masks",1805+ "sha256": "dfd1c6bccd16a608f3db022453fe5e93eb7798ae4c73a1c36de80374624d632e"1806+ },1807+ {1808+ "seed": "sample/351",1809+ "family": "creatures",1810+ "sha256": "848b3a0465d4e9b1d7e584d7db9490015ea1f80e9ebe42dbf22a935d94780f3c"1811+ },1812+ {1813+ "seed": "sample/352",1814+ "family": "machines",1815+ "sha256": "4003dd2f4cce713fea0f27cc5302f7360113c49e76776c1ab5772185bd726064"1816+ },1817+ {1818+ "seed": "sample/353",1819+ "family": "machines",1820+ "sha256": "53bbaa818e35a0cebb4f194d47a54ad3d2234f8e35deedd8e962aa103b33200b"1821+ },1822+ {1823+ "seed": "sample/354",1824+ "family": "creatures",1825+ "sha256": "0383636677120983e7db18a97f974db8a41cf4897daeb6a4fc4b1bd49ea38348"1826+ },1827+ {1828+ "seed": "sample/355",1829+ "family": "machines",1830+ "sha256": "046f85621b45be751deeef3bf49a1f1994e929a1223ebe5b409fea2336aaed7b"1831+ },1832+ {1833+ "seed": "sample/356",1834+ "family": "glyphs",1835+ "sha256": "c3b7edca11850e981af45130c385f471c57862b4f071e9488af3609ba29ab18f"1836+ },1837+ {1838+ "seed": "sample/357",1839+ "family": "woven",1840+ "sha256": "445fef1e213ca83999e006e06dee97a35647147db4e5df4cfc2b0166e4419a42"1841+ },1842+ {1843+ "seed": "sample/358",1844+ "family": "woven",1845+ "sha256": "d3203791411ddc8446a053f10ec53909804cef4fec9a9739a61c133dc6682f1a"1846+ },1847+ {1848+ "seed": "sample/359",1849+ "family": "creatures",1850+ "sha256": "95df192849e7b65bac2dcc6a5948e949c5488a99707f4b4cd6bfeb0b9380f278"1851+ },1852+ {1853+ "seed": "sample/360",1854+ "family": "creatures",1855+ "sha256": "9576ee31f49826377522baa35c74c0ab18dc6ca5d6f23e78af28cd1a9277962e"1856+ },1857+ {1858+ "seed": "sample/361",1859+ "family": "landscapes",1860+ "sha256": "b12176062dfa97f8eb6f0e316d306a2a557bbff241e8a21292b9e203ef933175"1861+ },1862+ {1863+ "seed": "sample/362",1864+ "family": "woven",1865+ "sha256": "180fbde97af4dfb30cd6866869529d3070954e5a7d02a20448d9a6662cd99a0c"1866+ },1867+ {1868+ "seed": "sample/363",1869+ "family": "botanical",1870+ "sha256": "2ac62a6ea02da232c4e791bf1e82d3820b0209b63f4c429d443e71ab782f7f5d"1871+ },1872+ {1873+ "seed": "sample/364",1874+ "family": "glyphs",1875+ "sha256": "79f70b7511c3c74ec60b1d25c37faf2e030b05c3a9a2c05b6773f5d706e3e4a8"1876+ },1877+ {1878+ "seed": "sample/365",1879+ "family": "glyphs",1880+ "sha256": "a781d97060e40579223c4fa9afefaf230372267a97906f25095e6b350bbd187a"1881+ },1882+ {1883+ "seed": "sample/366",1884+ "family": "glyphs",1885+ "sha256": "28380302d3be2708dbead049ff1b2cd25d7fd3ff5e2a93a7d97da7cbc8ffacf6"1886+ },1887+ {1888+ "seed": "sample/367",1889+ "family": "creatures",1890+ "sha256": "58955a9f0b61b4b4bce1bbcc555990a24ad6e2893e45a476dbf70b98a53b3cb9"1891+ },1892+ {1893+ "seed": "sample/368",1894+ "family": "orbital",1895+ "sha256": "5e190a3d0fa89a29fb2a974c5882cc2433468e3ae9e38741a547cda9f96e2aa2"1896+ },1897+ {1898+ "seed": "sample/369",1899+ "family": "creatures",1900+ "sha256": "b6038cf6570d998cf5669a8b6f03bae751305a0b2c656a208465da177351e3d7"1901+ },1902+ {1903+ "seed": "sample/370",1904+ "family": "botanical",1905+ "sha256": "7eeb94d4c7beecaeefd7391eebecf9cfcc3f5baa9de73a40946962fc6b09a6c9"1906+ },1907+ {1908+ "seed": "sample/371",1909+ "family": "woven",1910+ "sha256": "79f86240970dff05fed35bfb329ef5b2d17dbf88ff3b37851ee2748a9d6959d7"1911+ },1912+ {1913+ "seed": "sample/372",1914+ "family": "landscapes",1915+ "sha256": "d6d09ffb49c997bcac2f1444eb5e0ce547fb8c3b8d5a15398e932d3b6ca56957"1916+ },1917+ {1918+ "seed": "sample/373",1919+ "family": "woven",1920+ "sha256": "32e453510d29fd736efff8a517ce2cdfdabfae30298c14074d76f21d99f3be49"1921+ },1922+ {1923+ "seed": "sample/374",1924+ "family": "creatures",1925+ "sha256": "3103c9f468f6456283654963919657fcb60b2418dc9acf697f89b8dcac1c2b2c"1926+ },1927+ {1928+ "seed": "sample/375",1929+ "family": "botanical",1930+ "sha256": "f60c9631956c136bfe9e00898d06ef1954070ec80567a8b1f94b4f5c78011b11"1931+ },1932+ {1933+ "seed": "sample/376",1934+ "family": "creatures",1935+ "sha256": "e96f5b3c29fbb12a45eaa3ccf311a26f5a8b467b067f830dbce52b3ac435e447"1936+ },1937+ {1938+ "seed": "sample/377",1939+ "family": "botanical",1940+ "sha256": "43512249c5b9231147bc2b7a8ff205fb56f45edb4f956386a54ad25a6ac07fd2"1941+ },1942+ {1943+ "seed": "sample/378",1944+ "family": "creatures",1945+ "sha256": "91cb9533775025d1e8d92b356df4d88ecbe67ae2e635178b89d3d679a5409110"1946+ },1947+ {1948+ "seed": "sample/379",1949+ "family": "landscapes",1950+ "sha256": "cab3c5201c4a3824cf38d5028279b681d0cf634069006eccdc39d52d95642620"1951+ },1952+ {1953+ "seed": "sample/380",1954+ "family": "glyphs",1955+ "sha256": "1667666556815317496743baf0b309d188968e3255d18e5179ba97511db2b32b"1956+ },1957+ {1958+ "seed": "sample/381",1959+ "family": "creatures",1960+ "sha256": "ebeb0406f7d2ea5d1a29c20b3dede781c7af84bbea5159f5b29e1b7c06ad66a6"1961+ },1962+ {1963+ "seed": "sample/382",1964+ "family": "landscapes",1965+ "sha256": "63c17af13153875a94464e87de3c30a59b6f4306c01c742f908d5cc75fb5f39c"1966+ },1967+ {1968+ "seed": "sample/383",1969+ "family": "masks",1970+ "sha256": "705b29f9be57d4b2f7fb561a7357934cb3c591cbe83c1a2feef3b52e60e0c0d6"1971+ },1972+ {1973+ "seed": "sample/384",1974+ "family": "machines",1975+ "sha256": "1b415b2a6db8b3883352a0ae0bfb07a54214d6eb1f189a2924a91129426033cb"1976+ },1977+ {1978+ "seed": "sample/385",1979+ "family": "orbital",1980+ "sha256": "9536bbc19c29c0951457081b29b02d02b8e5f5fd7e7a7705e7e29b6fce8b69ff"1981+ },1982+ {1983+ "seed": "sample/386",1984+ "family": "machines",1985+ "sha256": "e12dd5747f6a24b0c175a90edc151157395478a967cce3683ac35c8cdfd32fdd"1986+ },1987+ {1988+ "seed": "sample/387",1989+ "family": "orbital",1990+ "sha256": "713d354413f2b264d0725b3d4536b6fce3d669bfdce54b051b977d08257b74ed"1991+ },1992+ {1993+ "seed": "sample/388",1994+ "family": "glyphs",1995+ "sha256": "fc63885c70a97b8f1f267b2faedf881ef3cc3de9e1b9c0c03618eeafb1c44587"1996+ },1997+ {1998+ "seed": "sample/389",1999+ "family": "creatures",2000+ "sha256": "2d98c37ec5fcfd7edd5a53a18ac52229046479c629a5ae5586131dd2733c5447"2001+ },2002+ {2003+ "seed": "sample/390",2004+ "family": "machines",2005+ "sha256": "24c0bb7258a7ec176fc667204a7727ad849b5088e57f44f16bf5f14181a306a1"2006+ },2007+ {2008+ "seed": "sample/391",2009+ "family": "masks",2010+ "sha256": "d58cd087b7fde2b5cea8b944487ee432e37f19733d557b6e97b1e5e9d0fb1106"2011+ },2012+ {2013+ "seed": "sample/392",2014+ "family": "machines",2015+ "sha256": "3d83e84cd3ae99b609af767a485023d538dd89637f9771103083d9749bf845bf"2016+ },2017+ {2018+ "seed": "sample/393",2019+ "family": "glyphs",2020+ "sha256": "1fad412500597cc29c58f13a0a8963723ea1f32742335477429a7ec76b0ccc12"2021+ },2022+ {2023+ "seed": "sample/394",2024+ "family": "landscapes",2025+ "sha256": "bfa830602aae3c76506f359782bf4cc523f635ffe66bc2ea4f1f2e66e64d91ce"2026+ },2027+ {2028+ "seed": "sample/395",2029+ "family": "machines",2030+ "sha256": "10e8c712dea997037ce00036f0b16284e0d6b1a6fc45ec53e1705ff542e063c8"2031+ },2032+ {2033+ "seed": "sample/396",2034+ "family": "masks",2035+ "sha256": "e9728193a3dbb1d3c1211f1ff03b016b7b3d1c0c07151db62b9984678ce1eed7"2036+ },2037+ {2038+ "seed": "sample/397",2039+ "family": "machines",2040+ "sha256": "461faa9be17f459899b9dc4c8f218f51e11713a911bcd02f2b6bb2eac9d2db2c"2041+ },2042+ {2043+ "seed": "sample/398",2044+ "family": "machines",2045+ "sha256": "84ae00ce18559dec8a1c02cf93566c13d4f1627330c6184da717ec62600d524a"2046+ },2047+ {2048+ "seed": "sample/399",2049+ "family": "creatures",2050+ "sha256": "233e1682bc9388f25ecbe4be156acbf7b56e5f2d7ba6b6deb2b8829fe7a79e57"2051+ },2052+ {2053+ "seed": "sample/400",2054+ "family": "machines",2055+ "sha256": "b3921068084ebac7ab1864186b6470e0be8ca540f5454720b2144c979df61954"2056+ },2057+ {2058+ "seed": "sample/401",2059+ "family": "glyphs",2060+ "sha256": "5b0df6f62069536f6160d3525db78f95c1527f045ceb90a5701851e4cf5493f4"2061+ },2062+ {2063+ "seed": "sample/402",2064+ "family": "machines",2065+ "sha256": "8ae81a2fc5ede16a2437940226a46685be50338337b82d80312e3352a261448d"2066+ },2067+ {2068+ "seed": "sample/403",2069+ "family": "glyphs",2070+ "sha256": "a296c0a542254207a45667d60820fefdb971cf5b8ac3f5afa4efd0175531c5e1"2071+ },2072+ {2073+ "seed": "sample/404",2074+ "family": "woven",2075+ "sha256": "dc6757d9358c5d321702269f0aee73910ae80bbaa3da3c0e1a099f91b3b96247"2076+ },2077+ {2078+ "seed": "sample/405",2079+ "family": "landscapes",2080+ "sha256": "8af9afed6623253cde9c6e58377fc3395a926e56254c2ce1c776485d0bf73fd1"2081+ },2082+ {2083+ "seed": "sample/406",2084+ "family": "creatures",2085+ "sha256": "78f775d7f38deb68e122592123a6fbcbd6fdc86f79e0c908647315ab3a42f322"2086+ },2087+ {2088+ "seed": "sample/407",2089+ "family": "creatures",2090+ "sha256": "3090d757edefe05b6af419e3df731f72ac40b516280d662955903e1afbfc24ca"2091+ },2092+ {2093+ "seed": "sample/408",2094+ "family": "creatures",2095+ "sha256": "1dc06a17548a8b4c1b825a78770713cddff0182148b2e91b13c34c3b2f82d55e"2096+ },2097+ {2098+ "seed": "sample/409",2099+ "family": "woven",2100+ "sha256": "0213a00b44b3d85c389f1d871fb3f1490b0bee5975337e0f19a9d6d4dc4028b2"2101+ },2102+ {2103+ "seed": "sample/410",2104+ "family": "creatures",2105+ "sha256": "a4a21c5de88f6e0b4f1ef25801d7c728592c790c5f8c7cc664703200b4dbecad"2106+ },2107+ {2108+ "seed": "sample/411",2109+ "family": "masks",2110+ "sha256": "0bdf88195e80738db23cd1d1aeb7393910820c7526fbaaa574d37c996a877eac"2111+ },2112+ {2113+ "seed": "sample/412",2114+ "family": "landscapes",2115+ "sha256": "18698d9708f8f63503fd9da412a9f995bb1058317750da2bb32387c5eca66301"2116+ },2117+ {2118+ "seed": "sample/413",2119+ "family": "machines",2120+ "sha256": "dcd1789e739b9aa1ae9772c5e7c7cbddbd8436a6b25f56e8ae30e71dd08be761"2121+ },2122+ {2123+ "seed": "sample/414",2124+ "family": "botanical",2125+ "sha256": "09be9aca2970b4b03193ee61a4a18e867df250ad173ae75e3d24b203aa54680d"2126+ },2127+ {2128+ "seed": "sample/415",2129+ "family": "orbital",2130+ "sha256": "07b207ff47395c40eea2a1ac8105c8e469db6d9195e07aa00333fb9ad8f04b92"2131+ },2132+ {2133+ "seed": "sample/416",2134+ "family": "machines",2135+ "sha256": "b12a13e35e71de4a16c14fd4213004e91cb997ac0906a5f0bb23bc0344cf4232"2136+ },2137+ {2138+ "seed": "sample/417",2139+ "family": "creatures",2140+ "sha256": "333a3ae7555aba45e251219850884a356a583cfeac9c0ed5acd5b4313447dced"2141+ },2142+ {2143+ "seed": "sample/418",2144+ "family": "woven",2145+ "sha256": "cd4c9dbee4656a856e35951f5ce07e123334c9e11a3c57906b99cc06b956d454"2146+ },2147+ {2148+ "seed": "sample/419",2149+ "family": "machines",2150+ "sha256": "3d698e49bfed25a628416cc27243097f2719669785ec07d4fb8ee579ecb758d8"2151+ },2152+ {2153+ "seed": "sample/420",2154+ "family": "glyphs",2155+ "sha256": "a2785df8a6f572786d59862832e345823a33476ed6cef98c32ec5dea081b2cfd"2156+ },2157+ {2158+ "seed": "sample/421",2159+ "family": "machines",2160+ "sha256": "00fcd336020675a1d4c9109f659582142ca1e2813468274c7cbd949c30e4bfe9"2161+ },2162+ {2163+ "seed": "sample/422",2164+ "family": "botanical",2165+ "sha256": "d478ac7e4338cbda5e6efc6bf21bff788ca56a10763831fbc503536c4808a6fe"2166+ },2167+ {2168+ "seed": "sample/423",2169+ "family": "glyphs",2170+ "sha256": "fd6a7af6d8d4ad9811f44f569f5d1fbef4ee441c9c08f5f658361ba5f2211f4a"2171+ },2172+ {2173+ "seed": "sample/424",2174+ "family": "machines",2175+ "sha256": "261448e7e9e781ed09511521da41eb727ce68ac4141c0dc9a3b0cebd2b370bd7"2176+ },2177+ {2178+ "seed": "sample/425",2179+ "family": "botanical",2180+ "sha256": "71178affcceef3155645037360d4d50a24535d482d6463636f1c8fcad965401f"2181+ },2182+ {2183+ "seed": "sample/426",2184+ "family": "woven",2185+ "sha256": "7c992c10b38439a5fda4e0546a9201957890b2d4e4825746367489096d6bf4cb"2186+ },2187+ {2188+ "seed": "sample/427",2189+ "family": "machines",2190+ "sha256": "219cb7d3c337ee1085853ae3e51dfbb1eace354874c7ddae2c2c03afde858923"2191+ },2192+ {2193+ "seed": "sample/428",2194+ "family": "glyphs",2195+ "sha256": "62e07e913ef987216822332ee973f7765243cf0e4c226fdc16d6ef9ce015e59c"2196+ },2197+ {2198+ "seed": "sample/429",2199+ "family": "glyphs",2200+ "sha256": "7749bafcb53243ec35af733a36cdb2d884897f470c549f33e8fc9029c41c7740"2201+ },2202+ {2203+ "seed": "sample/430",2204+ "family": "masks",2205+ "sha256": "e864f456211b84c1336b0040765ba938972804a24efc602cf9688923f247de84"2206+ },2207+ {2208+ "seed": "sample/431",2209+ "family": "woven",2210+ "sha256": "4e925f4fefe199f618c8c068d01114c9d80f0744be066884cf9f30637ca05088"2211+ },2212+ {2213+ "seed": "sample/432",2214+ "family": "orbital",2215+ "sha256": "3a2f15eeb7fb87f3a2288725a61ad16ef551d934e814bf7c135863f5151f6905"2216+ },2217+ {2218+ "seed": "sample/433",2219+ "family": "orbital",2220+ "sha256": "7950e99e6658ab8e6bb49a20a3d666bb670ff03bea50fba736ea067d8f32ee22"2221+ },2222+ {2223+ "seed": "sample/434",2224+ "family": "creatures",2225+ "sha256": "ecfdde445e9968f2b1ebcdc83f362bd120b3ce7be91fe3a7a32827beb7a66e81"2226+ },2227+ {2228+ "seed": "sample/435",2229+ "family": "creatures",2230+ "sha256": "9cf47ddd540cf15f8766575f62cf6fe0e535df9c6b398f66cd6373d45cdc2f9a"2231+ },2232+ {2233+ "seed": "sample/436",2234+ "family": "woven",2235+ "sha256": "ffefbe30c27fb23f5ce875441f5a351ae87a55bb8f46483e7001bc7874d3e387"2236+ },2237+ {2238+ "seed": "sample/437",2239+ "family": "glyphs",2240+ "sha256": "658d939740fdaf56882d65ac87a31da784046a8d8793964d043538a935c3c5d4"2241+ },2242+ {2243+ "seed": "sample/438",2244+ "family": "creatures",2245+ "sha256": "c5f9c1116d1607fc2957d4afa406020cfe7d54b351a7b96e9d1d89cd1060f48c"2246+ },2247+ {2248+ "seed": "sample/439",2249+ "family": "creatures",2250+ "sha256": "b4167bbecc660070eaa06d8a065f6c3a1973ec26ba427083b1c48c77450c0bb0"2251+ },2252+ {2253+ "seed": "sample/440",2254+ "family": "creatures",2255+ "sha256": "4629ec2ba0cc5d0716059c6f3952ffe6a24195b01877825785633b36a3bb20c5"2256+ },2257+ {2258+ "seed": "sample/441",2259+ "family": "woven",2260+ "sha256": "9724d2a42b14bfee1999a6c558ae3c73fec0a5c742885a07a7c4df1d2e12b781"2261+ },2262+ {2263+ "seed": "sample/442",2264+ "family": "glyphs",2265+ "sha256": "491bd4832107285feed5aa45efe079c9c7a675a3e15804cb0b97cadc55e27bbe"2266+ },2267+ {2268+ "seed": "sample/443",2269+ "family": "machines",2270+ "sha256": "c648bb2566f99bc2e617a202132ce9f5a79c000518573d2b10ad5f7511d861e7"2271+ },2272+ {2273+ "seed": "sample/444",2274+ "family": "masks",2275+ "sha256": "70e970d07f69ece20a17ed703238cb5f9a3bd576dad6362f0ac7cec5ac0e00ef"2276+ },2277+ {2278+ "seed": "sample/445",2279+ "family": "orbital",2280+ "sha256": "c45c8169b5cfc17a713c6727867a3ec2cf2e851ed94ad2d57c3511fcbc7c6068"2281+ },2282+ {2283+ "seed": "sample/446",2284+ "family": "masks",2285+ "sha256": "a188d34b8251f7951db87bbc2653ff154115b02fd5ac86ba6e3c0d251bd2369b"2286+ },2287+ {2288+ "seed": "sample/447",2289+ "family": "orbital",2290+ "sha256": "045469e7b44c573f10eb851d370b0629dab1be166e1a1acf156ad77b369cf8bf"2291+ },2292+ {2293+ "seed": "sample/448",2294+ "family": "woven",2295+ "sha256": "618bd0d9f89f4558b69d3ab294769689a382a06c901aef2b1b8c592c373935fc"2296+ },2297+ {2298+ "seed": "sample/449",2299+ "family": "glyphs",2300+ "sha256": "4b4260f5fdaf2c554c2ee5153ea1c1e1a90ffb81ebc6fdbdb9b16a1810acbf79"2301+ },2302+ {2303+ "seed": "sample/450",2304+ "family": "landscapes",2305+ "sha256": "a5ce00f63692d7a89ba774b1ec95d5776742ef971bf9c633867703b3e46cc99e"2306+ },2307+ {2308+ "seed": "sample/451",2309+ "family": "masks",2310+ "sha256": "08483129874b8c3008ac8e61edb009a7f825d34a7adb7bdc68835161b9c843f3"2311+ },2312+ {2313+ "seed": "sample/452",2314+ "family": "creatures",2315+ "sha256": "94897c5ebdcabb30e7a20af650b845c505665a488bacf2e0149ae854f993b6d9"2316+ },2317+ {2318+ "seed": "sample/453",2319+ "family": "glyphs",2320+ "sha256": "22649352b24c774abbbe1d3ee3db7c2758b2a8cb573ff21c27e262b79f3b5bb7"2321+ },2322+ {2323+ "seed": "sample/454",2324+ "family": "masks",2325+ "sha256": "dadfc85ea8ca6c469200bbe510d97d55c6e1b30a05109077b72abf81dc37f45e"2326+ },2327+ {2328+ "seed": "sample/455",2329+ "family": "masks",2330+ "sha256": "9fd2465bf1cd4dc0fff538021a3a01ad155ff57614c64b66913cd6623c7e2261"2331+ },2332+ {2333+ "seed": "sample/456",2334+ "family": "woven",2335+ "sha256": "4e57ec8783bbd3434c44ec5a5a302b9d3b246af62ae7077c40fe86c7a898ddd3"2336+ },2337+ {2338+ "seed": "sample/457",2339+ "family": "orbital",2340+ "sha256": "3adfd2c9b76e31ff00c9aa5c53a02393d50832c1d10248b20a2d6acd76c8f409"2341+ },2342+ {2343+ "seed": "sample/458",2344+ "family": "masks",2345+ "sha256": "dcf690f67417cf31b9f7ed5a2d1cfd92569ef28fb8137a72ad9e6ef957839349"2346+ },2347+ {2348+ "seed": "sample/459",2349+ "family": "landscapes",2350+ "sha256": "89c81b253854409ba65f96834e3ca46afd3de025946e989e6b9c9317365bf4e2"2351+ },2352+ {2353+ "seed": "sample/460",2354+ "family": "orbital",2355+ "sha256": "70ecff7d5592bc89e657abf5d373fdffa52f9f3f2c7d83fd2c16e6d440049569"2356+ },2357+ {2358+ "seed": "sample/461",2359+ "family": "botanical",2360+ "sha256": "4a0b35f8aff7606ea24afa1ca54b18f1bb843c14f812a14635ac48f5a1af3487"2361+ },2362+ {2363+ "seed": "sample/462",2364+ "family": "machines",2365+ "sha256": "3010430393de25939225746e82684a545ed6091488d5733fec36b87ced7ef96a"2366+ },2367+ {2368+ "seed": "sample/463",2369+ "family": "woven",2370+ "sha256": "de7f15d7a69cae9ba64951c6d36af79f305b0b8cbe9529956cf82d6f33285b00"2371+ },2372+ {2373+ "seed": "sample/464",2374+ "family": "woven",2375+ "sha256": "03e11b38db2181ed26b3d9c5863fa4d14cbaaa47f95bc4cb5909f7ec24dd8c83"2376+ },2377+ {2378+ "seed": "sample/465",2379+ "family": "orbital",2380+ "sha256": "85de34968e94cb69c9d8c8a3c164afa1d12f31182e0507f781a4190be5d6e907"2381+ },2382+ {2383+ "seed": "sample/466",2384+ "family": "glyphs",2385+ "sha256": "80605b58420078e4a60596e4b74f1b17edb6719de671943408af0b028bee8c30"2386+ },2387+ {2388+ "seed": "sample/467",2389+ "family": "creatures",2390+ "sha256": "1db7080c2a6d7c07c87efcd1570d93224363bc7d9e7b0d0b3a649200c324c4fd"2391+ },2392+ {2393+ "seed": "sample/468",2394+ "family": "masks",2395+ "sha256": "12933976822f6e184a9a06486a9da1b318d4b128468706e419297f01c2306495"2396+ },2397+ {2398+ "seed": "sample/469",2399+ "family": "machines",2400+ "sha256": "5e43b5fe4cb2e70fc26b219f5d479925fac3b213bed51fff25c5ed7a35542689"2401+ },2402+ {2403+ "seed": "sample/470",2404+ "family": "botanical",2405+ "sha256": "3572709ba57f1db1683362ac55dce216cd2f1db78679c73dda090def53735adf"2406+ },2407+ {2408+ "seed": "sample/471",2409+ "family": "masks",2410+ "sha256": "aa635f70036b0aebec379bd8026de7497a82c7d90fade10a9159f0d9151913f7"2411+ },2412+ {2413+ "seed": "sample/472",2414+ "family": "glyphs",2415+ "sha256": "45e528ec2da8e09d1f86e5a4ef2a72c93f288579320bb20bab38380f82e70ac6"2416+ },2417+ {2418+ "seed": "sample/473",2419+ "family": "creatures",2420+ "sha256": "c17c3b4750e14b2627a93cfb6cecec5cc5bbcef238e7a26ef3c2295b6de7d3e1"2421+ },2422+ {2423+ "seed": "sample/474",2424+ "family": "masks",2425+ "sha256": "d192f82231f4a1d5687f046daf58d1d61b3608ed6769c137f3d13542a5c0b7e7"2426+ },2427+ {2428+ "seed": "sample/475",2429+ "family": "landscapes",2430+ "sha256": "c4cee12e0cac6446c8f7c4f1b2588f425ef9202b81fb58a4a76724cbba8a3478"2431+ },2432+ {2433+ "seed": "sample/476",2434+ "family": "botanical",2435+ "sha256": "62393ad3591ff060d6aafca4ebe51e6203b1e1f8aeca4d88580d30d3bfaa2276"2436+ },2437+ {2438+ "seed": "sample/477",2439+ "family": "machines",2440+ "sha256": "2dc40f2a406e1589d63923a4a1c099024b4e2ed31bbb24a2abc06c3d731be743"2441+ },2442+ {2443+ "seed": "sample/478",2444+ "family": "botanical",2445+ "sha256": "561eef1c603762f16fdcfe208221323937708ff94581a37812e55130af07a404"2446+ },2447+ {2448+ "seed": "sample/479",2449+ "family": "machines",2450+ "sha256": "57a3b822744fcd4b1f3a64aec839daef8c44fa904ff4d1590549e1fdcb5d3b57"2451+ },2452+ {2453+ "seed": "sample/480",2454+ "family": "landscapes",2455+ "sha256": "e6ef941a1482cbfa7d8e7d4c6d6a7f501b8277bd112a3135246ac334da4c3a69"2456+ },2457+ {2458+ "seed": "sample/481",2459+ "family": "machines",2460+ "sha256": "33d08a736b75a3fd64d3bb2a6bcd14e2a1df1008830231d559b1039c77b7f06d"2461+ },2462+ {2463+ "seed": "sample/482",2464+ "family": "masks",2465+ "sha256": "fe5b3222ac56035171b9e563075b4c5755f781c572ec131759c3c038c87ab75f"2466+ },2467+ {2468+ "seed": "sample/483",2469+ "family": "creatures",2470+ "sha256": "2ce07685d2d23efc5e0ca44eda42cc6a01440bb731b0b3170c6228f564d2f119"2471+ },2472+ {2473+ "seed": "sample/484",2474+ "family": "masks",2475+ "sha256": "1531279e1fd679c82ed64974eb043f546380698b1f3b93ef88ed6394cc8aa8fd"2476+ },2477+ {2478+ "seed": "sample/485",2479+ "family": "woven",2480+ "sha256": "af0f8a9d3119395d35b0aa07f9459fb080bb5b5c68e14e8ed1df0c4e6f5e47c7"2481+ },2482+ {2483+ "seed": "sample/486",2484+ "family": "woven",2485+ "sha256": "f38a460844364912d63001e8ec46ce789e96be5d630b4a2f7594a5179a31e0fb"2486+ },2487+ {2488+ "seed": "sample/487",2489+ "family": "creatures",2490+ "sha256": "ad1b994a985f1d86a82bfb2ac18b4248c944fa725ba1d17140c75eec75aa569b"2491+ },2492+ {2493+ "seed": "sample/488",2494+ "family": "creatures",2495+ "sha256": "85c0ab9b6f57a2d3bcc87e085907e3f6da7044098bca67a6fbb0c675eab48cf3"2496+ },2497+ {2498+ "seed": "sample/489",2499+ "family": "orbital",2500+ "sha256": "04b3f5192b245367bf2b3189105d4d85a002df60487bf736ae979ebc160c0086"2501+ },2502+ {2503+ "seed": "sample/490",2504+ "family": "machines",2505+ "sha256": "8d042d37129a5d4f64c020aa6b2403f6b23df50a4baa1a03a8d44654645bb093"2506+ },2507+ {2508+ "seed": "sample/491",2509+ "family": "woven",2510+ "sha256": "411920cdbf139126a29fe919463d8dad1c95139286e6208778dd69b114c753b0"2511+ },2512+ {2513+ "seed": "sample/492",2514+ "family": "landscapes",2515+ "sha256": "ef4b48a5f4c15b46e6e50577693f2a80dc1886a51f065aafaa129e5450f32cf0"2516+ },2517+ {2518+ "seed": "sample/493",2519+ "family": "woven",2520+ "sha256": "9444f99a10ee66a977084bc7ce357b6e001761605868fe85e788f31ab876e279"2521+ },2522+ {2523+ "seed": "sample/494",2524+ "family": "masks",2525+ "sha256": "8f88901ba302799620dc80b87b934e3cd0440fb334ca80589a203a4eb23fd124"2526+ },2527+ {2528+ "seed": "sample/495",2529+ "family": "landscapes",2530+ "sha256": "3b1f129d55c710ccc5e2f6b24d46aa29a74830c84624059e7eb8808f3d4491a6"2531+ },2532+ {2533+ "seed": "sample/496",2534+ "family": "landscapes",2535+ "sha256": "f56d52a26f12a38e09dc96f3d2b46ee40b970a68ae88d052c7fbbdd32bbe1ebd"2536+ },2537+ {2538+ "seed": "sample/497",2539+ "family": "orbital",2540+ "sha256": "0c7745f1295ad996add5cb1b215215958809a7542a647ee61cf486843c7c5ee5"2541+ },2542+ {2543+ "seed": "sample/498",2544+ "family": "botanical",2545+ "sha256": "3e1bd3938f12e2b225dbb57f3a8a63372100779f7ff3479d7692f3abe74b623c"2546+ },2547+ {2548+ "seed": "sample/499",2549+ "family": "masks",2550+ "sha256": "da35ba4b35c5fa032f050872c070ea71566aa401e708f9acaada13164206a294"2551+ },2552+ {2553+ "seed": "sample/500",2554+ "family": "woven",2555+ "sha256": "9daf25fab9c30917652a4872afc8ff6ca5355c788540c87807aebc07f06884d9"2556+ },2557+ {2558+ "seed": "sample/501",2559+ "family": "glyphs",2560+ "sha256": "c30db93cac8569f88ecc2aaa299c5c520614d39b70e01f37bd5e72530f680b68"2561+ },2562+ {2563+ "seed": "sample/502",2564+ "family": "botanical",2565+ "sha256": "e7a53b3ebe126a4f289f3a66f98a5d466af71f2046144e5b52cdf30ed915b3a4"2566+ },2567+ {2568+ "seed": "sample/503",2569+ "family": "landscapes",2570+ "sha256": "c5d67fb20b889e4c80cfec004b20ca6e3028f3245e8a5653bf3514da874334bc"2571+ },2572+ {2573+ "seed": "sample/504",2574+ "family": "machines",2575+ "sha256": "b360529a914df3e1663bdce16f4e4bdf7f31d9ff14524f47cc908eea9ed99526"2576+ },2577+ {2578+ "seed": "sample/505",2579+ "family": "orbital",2580+ "sha256": "65a5fe457b436df1fdc5aa3b4fccb78445ca0c092fb00b44687b6614df9144ff"2581+ },2582+ {2583+ "seed": "sample/506",2584+ "family": "glyphs",2585+ "sha256": "807a2dbdf9e6ca7bca1332e9153d105113ca9ea0cf40b6720ee4774a64486edd"2586+ },2587+ {2588+ "seed": "sample/507",2589+ "family": "masks",2590+ "sha256": "7ed207e49a8413ceeb2faf7405f5eb4c9169b9b037669e755ed6d992e79cd9b5"2591+ },2592+ {2593+ "seed": "sample/508",2594+ "family": "creatures",2595+ "sha256": "1797f8ad8e784b16e78cdc24c2eb2f3b6451c84107cbf6b948ba151e30cdef05"2596+ },2597+ {2598+ "seed": "sample/509",2599+ "family": "orbital",2600+ "sha256": "8b9ea8b74272f9bf84dd6e1d8a45af7833ca250fd38d6d41e8f44a396bd273ef"2601+ },2602+ {2603+ "seed": "sample/510",2604+ "family": "botanical",2605+ "sha256": "9c0bd9a4f914d6b795fea46436563d4a56507b6d05595699561a285a9e4078a0"2606+ },2607+ {2608+ "seed": "sample/511",2609+ "family": "glyphs",2610+ "sha256": "cafd8280e234ff833a612d6c932fec4e5699023f0ed27bdaadf1ba099e7d3cfc"2611+ },2612+ {2613+ "seed": "sample/512",2614+ "family": "machines",2615+ "sha256": "65d89391f30cdf37796b870475ed2bf9ab14ee7124b86f1e7bd599d6795d74f0"2616+ },2617+ {2618+ "seed": "sample/513",2619+ "family": "masks",2620+ "sha256": "be14b6ffdd6cd47ede9b98b0bc15a8e5782addb9b35e5c3299b0ed2dbbed0c5e"2621+ },2622+ {2623+ "seed": "sample/514",2624+ "family": "machines",2625+ "sha256": "7371bdd5a27b082fceb8a6ff036d5ec620642190fc04709fd975a8ce34dc4eb1"2626+ },2627+ {2628+ "seed": "sample/515",2629+ "family": "orbital",2630+ "sha256": "a97dc9a09865ce3a1b8db4f2df87c2e7ebac4dd520a4c6badbcb1045dccfcf20"2631+ },2632+ {2633+ "seed": "sample/516",2634+ "family": "masks",2635+ "sha256": "b0f711220d323f51023d70990071a9c6933a659f553d096d0c58bf310be35959"2636+ },2637+ {2638+ "seed": "sample/517",2639+ "family": "masks",2640+ "sha256": "6231e2346a5dc1d3cb1ff03a33e4efd913e640cd56959e0420a643700975aec8"2641+ },2642+ {2643+ "seed": "sample/518",2644+ "family": "woven",2645+ "sha256": "8eb795d0cfb0b44d5f75926290ca343323a197a9776349cf8d05ba35a22d69c9"2646+ },2647+ {2648+ "seed": "sample/519",2649+ "family": "glyphs",2650+ "sha256": "80250713480385f6aff70a252ceb2a08d03317b48fb5ae2d8f4377519956b844"2651+ },2652+ {2653+ "seed": "sample/520",2654+ "family": "glyphs",2655+ "sha256": "b261f431b5d2f2a09875380c2d23117e3433e37a21ca6e1dc73cde51afb1d34d"2656+ },2657+ {2658+ "seed": "sample/521",2659+ "family": "botanical",2660+ "sha256": "5092ecf3b26533f126ae1ef3a946b2c38a47dbe4e7d650af60464b4e7361e9ba"2661+ },2662+ {2663+ "seed": "sample/522",2664+ "family": "orbital",2665+ "sha256": "3a8929b0217b4947920c6a6cfcc49010cae9edce2d0d8193825bf872126db775"2666+ },2667+ {2668+ "seed": "sample/523",2669+ "family": "woven",2670+ "sha256": "82c42cf3ff27afe1b66beee048671a2a1636046882c668a63ef6440984a2473e"2671+ },2672+ {2673+ "seed": "sample/524",2674+ "family": "botanical",2675+ "sha256": "764cc89a523ba9c41933231c6f0a9d5e60d887b3544d19d435cae47d6ff37655"2676+ },2677+ {2678+ "seed": "sample/525",2679+ "family": "creatures",2680+ "sha256": "44b99c0ff5abcdeb5ce0593e8976a64c526cca7485c41e23ffc55ced725b91b6"2681+ },2682+ {2683+ "seed": "sample/526",2684+ "family": "machines",2685+ "sha256": "67e7f261dba543fab053ead76b3e2ce2b636b5c21da8e2e0c81b86a5296a1089"2686+ },2687+ {2688+ "seed": "sample/527",2689+ "family": "machines",2690+ "sha256": "252ae78445c5b79ef03015b51602934cad239b8b0a1bb86d22b800d00b7f49bf"2691+ },2692+ {2693+ "seed": "sample/528",2694+ "family": "masks",2695+ "sha256": "bc592373f97593cf997524bb61d1fadc45a5c027616a47e64e3a59742f67223f"2696+ },2697+ {2698+ "seed": "sample/529",2699+ "family": "masks",2700+ "sha256": "ad5d3375b22f2ebe64535e0893479408b0f519ff0c0a77e2e1e8b1dac557846f"2701+ },2702+ {2703+ "seed": "sample/530",2704+ "family": "masks",2705+ "sha256": "3bcc5c84bd5161905dc3c931557d349b5b216224e43e0eb1eceb070aad50b841"2706+ },2707+ {2708+ "seed": "sample/531",2709+ "family": "landscapes",2710+ "sha256": "853387fe6633e2275bc181429dfc637d1cb6f08dde007cd09a63b62da3e94934"2711+ },2712+ {2713+ "seed": "sample/532",2714+ "family": "machines",2715+ "sha256": "1a635606b4bd55b22d7b86b5c62ece5d9d7b7b1678cb38de765bda80192fa513"2716+ },2717+ {2718+ "seed": "sample/533",2719+ "family": "orbital",2720+ "sha256": "dcf4c58bc6e8d1a930ca047dc233ec40627901d37eb993b50b636e788b108e19"2721+ },2722+ {2723+ "seed": "sample/534",2724+ "family": "creatures",2725+ "sha256": "95b316f729d1392edd19cee3ddb8b2c5c26c079645a3d314141dac45077f7c54"2726+ },2727+ {2728+ "seed": "sample/535",2729+ "family": "masks",2730+ "sha256": "8beb37722a38b9558b2e9b9523231d07ad6a1b015bcd8953aea18b45dcdc7b2f"2731+ },2732+ {2733+ "seed": "sample/536",2734+ "family": "woven",2735+ "sha256": "ba82c20346d49ace1306ea705b5972e0f162a5804aac4d177a3310e0b5155427"2736+ },2737+ {2738+ "seed": "sample/537",2739+ "family": "woven",2740+ "sha256": "4bdbc35493551c1d597de1a7f360846ce89ee92b6d80f6c3bb6ceb7bca3458db"2741+ },2742+ {2743+ "seed": "sample/538",2744+ "family": "glyphs",2745+ "sha256": "6303ebd983c5c8b71551a0ac8d99ccc6e1b49c8b2a01b225fbfeedffe58b0fb9"2746+ },2747+ {2748+ "seed": "sample/539",2749+ "family": "machines",2750+ "sha256": "2ce174b4f75b9cadf4f7ba1f6388339efdf750c917c9f940fbe37b66499fdcf9"2751+ },2752+ {2753+ "seed": "sample/540",2754+ "family": "glyphs",2755+ "sha256": "dbcd0fb5f2973bfebc834ece42c19dcd2f0fdbdf450fc4abc68d07c6dde91a7b"2756+ },2757+ {2758+ "seed": "sample/541",2759+ "family": "landscapes",2760+ "sha256": "395cec829a3129f42aec2a2ac50706565b7154e70f16bf054f3a2bef245e7e8d"2761+ },2762+ {2763+ "seed": "sample/542",2764+ "family": "machines",2765+ "sha256": "fba7cf34ebdb580604d11828ae140d28a14a5c0ab25e5e8811e0fcf7219d866e"2766+ },2767+ {2768+ "seed": "sample/543",2769+ "family": "woven",2770+ "sha256": "ccff27e0a60ea35ca13c53742d40aefd50e93729c9f7ecf08b70d9d0cec50bdc"2771+ },2772+ {2773+ "seed": "sample/544",2774+ "family": "botanical",2775+ "sha256": "55578cfca3297db150c2b601e6bdae30d9935fa1951bf3865cf96e814e9e993d"2776+ },2777+ {2778+ "seed": "sample/545",2779+ "family": "botanical",2780+ "sha256": "4e16657d3839c21d9f76fb7b080bab3749341683ba14ed12df3025a87bc8f9ab"2781+ },2782+ {2783+ "seed": "sample/546",2784+ "family": "machines",2785+ "sha256": "11f56a59275cf413dae60bd79a30d613df0ad3b8c236996dccb74ec55bd45653"2786+ },2787+ {2788+ "seed": "sample/547",2789+ "family": "creatures",2790+ "sha256": "bc0504e1781465c330afc83f72611bcbdafd607a0f266196e55c6ef109324719"2791+ },2792+ {2793+ "seed": "sample/548",2794+ "family": "glyphs",2795+ "sha256": "fa9f36d4d61c727beb99cd92fcd79022bd5e25c885c3cf5f65f95c29288cb7f1"2796+ },2797+ {2798+ "seed": "sample/549",2799+ "family": "landscapes",2800+ "sha256": "743ac1d443238188d33db09a3a08e76962db62a51bd42c21c76a22a03e0ffd57"2801+ },2802+ {2803+ "seed": "sample/550",2804+ "family": "creatures",2805+ "sha256": "dc5a3dd6b9f47ab8d4e81f7e901f102ba9670bfe1b0933f894c2e864efa8da37"2806+ },2807+ {2808+ "seed": "sample/551",2809+ "family": "masks",2810+ "sha256": "f3c7eae70cc86ee5c1f1755913d84bb68da43b1bf5f28577443220f1a632d13b"2811+ },2812+ {2813+ "seed": "sample/552",2814+ "family": "orbital",2815+ "sha256": "f86f182aec008991549019666afcd0e82d024dfe25388ccb19d068d42b83628e"2816+ },2817+ {2818+ "seed": "sample/553",2819+ "family": "orbital",2820+ "sha256": "0cafb1f0b2dd89ef6d8ab0d17e68bf48bd5c5148c4a86fe78a3f27128086b565"2821+ },2822+ {2823+ "seed": "sample/554",2824+ "family": "woven",2825+ "sha256": "2f4d42daeb6470bc39189178d395de1c400ae2792768932b3b598981ebad70cd"2826+ },2827+ {2828+ "seed": "sample/555",2829+ "family": "machines",2830+ "sha256": "dc0356bd0940dae678db535ff38a10a42b8a788cbad01bd5feac15a1697ec63b"2831+ },2832+ {2833+ "seed": "sample/556",2834+ "family": "botanical",2835+ "sha256": "7573cca261d15096424a8bcfca8016c577e8f4a2812be147943ff0bfd6be1466"2836+ },2837+ {2838+ "seed": "sample/557",2839+ "family": "landscapes",2840+ "sha256": "cd762519507f60e6af276363f4f5d40290b7530f4b249d62db9b33ab2cc65c0f"2841+ },2842+ {2843+ "seed": "sample/558",2844+ "family": "woven",2845+ "sha256": "623c6db20aa108dbb8e470e1ff511f17ef080b16e7e566fc0ab3c80e759265fc"2846+ },2847+ {2848+ "seed": "sample/559",2849+ "family": "woven",2850+ "sha256": "780e1bebe2c31c46df89c52aac027c894797cd284e50fe0a00ce5747b97a905b"2851+ },2852+ {2853+ "seed": "sample/560",2854+ "family": "glyphs",2855+ "sha256": "23345d30d04ac7b6bd29467aac06d000646e4bbd328c28edfc101f1cbbac6ec4"2856+ },2857+ {2858+ "seed": "sample/561",2859+ "family": "woven",2860+ "sha256": "4018ceea49e083b58c227b2deaec447fb8693bbd6533c37bc7e7b63dfe1213f9"2861+ },2862+ {2863+ "seed": "sample/562",2864+ "family": "woven",2865+ "sha256": "b105ca78dd206b26e42fb778c811502b408fc917d9768db830d02bd913d425df"2866+ },2867+ {2868+ "seed": "sample/563",2869+ "family": "landscapes",2870+ "sha256": "c565d9f32634efa07c64cc9f0eaf8a46d2b0af376664d6d99348640b16e0e5c1"2871+ },2872+ {2873+ "seed": "sample/564",2874+ "family": "botanical",2875+ "sha256": "4af76e37bb9363bc315673661bf2d72e8cd28c77b5e18be38ba0087731059cea"2876+ },2877+ {2878+ "seed": "sample/565",2879+ "family": "masks",2880+ "sha256": "02dc9cdcce4a1d4c53c7bd71ac4ec170922816845219762e8b6920b5009e1b86"2881+ },2882+ {2883+ "seed": "sample/566",2884+ "family": "glyphs",2885+ "sha256": "128c1eb111ff671fc9409bb8776eedea1f6c0864e92b1335d8159842fc3620ce"2886+ },2887+ {2888+ "seed": "sample/567",2889+ "family": "landscapes",2890+ "sha256": "423b35cf0678a14ea937b45b22b7e75411d736fd3b299266dcdc1b8b64ee942f"2891+ },2892+ {2893+ "seed": "sample/568",2894+ "family": "creatures",2895+ "sha256": "3007f9ef23c9f2654eed19eeeb9af398b5fa20817d044698176686299c85be57"2896+ },2897+ {2898+ "seed": "sample/569",2899+ "family": "glyphs",2900+ "sha256": "93c2280fad97567c6b2b9087863e85c1ead23633a069190a98da957bfdde1318"2901+ },2902+ {2903+ "seed": "sample/570",2904+ "family": "woven",2905+ "sha256": "242da5ffec852d3e9f7148a4b265ada34972e2f9a5c731cff8a217f57122f465"2906+ },2907+ {2908+ "seed": "sample/571",2909+ "family": "machines",2910+ "sha256": "c2d3603b818d78b2a6683e9306a221d32e998165b361dc4774fe20335f5ed4c4"2911+ },2912+ {2913+ "seed": "sample/572",2914+ "family": "orbital",2915+ "sha256": "bc16c2847a2637a3e00dbef9c32846dbfb74cf9d690887b72470d982e04c4a03"2916+ },2917+ {2918+ "seed": "sample/573",2919+ "family": "masks",2920+ "sha256": "80852e3e517ac520a9290aea020e75fd3b36acc3200ab97b0774591c0b519d8a"2921+ },2922+ {2923+ "seed": "sample/574",2924+ "family": "masks",2925+ "sha256": "a12bfb4f097e22b34a6584a03d67d3e985e654c9bf41fdee9f5995a23a226edc"2926+ },2927+ {2928+ "seed": "sample/575",2929+ "family": "landscapes",2930+ "sha256": "81f410f74c90b35b6a0539db5b733ad43556489ffae524fbac515d2b00e907b3"2931+ },2932+ {2933+ "seed": "sample/576",2934+ "family": "woven",2935+ "sha256": "554e7ea914213c9753f103e97fa328afd42c97859321ec16520985660be5d687"2936+ },2937+ {2938+ "seed": "sample/577",2939+ "family": "landscapes",2940+ "sha256": "6a4f955ebd66977f0453f4ee80300389cd11e31b4c59af20349f1d909851d3f5"2941+ },2942+ {2943+ "seed": "sample/578",2944+ "family": "creatures",2945+ "sha256": "397d9749f0df004689f0ee2d23179d73bd5825c7390c1524a51a526e48ef5c34"2946+ },2947+ {2948+ "seed": "sample/579",2949+ "family": "glyphs",2950+ "sha256": "4d07015e8b5cd4363676895aa229e503259bf9728df6a3c2f8fdf3d4c9815dca"2951+ },2952+ {2953+ "seed": "sample/580",2954+ "family": "creatures",2955+ "sha256": "1fd43ef3315e26198f756a043b4d72573a282021d5d864e6cf727c20553dad49"2956+ },2957+ {2958+ "seed": "sample/581",2959+ "family": "creatures",2960+ "sha256": "c0efa9eafc155da7b3fe3d2952d691d1b86f79ce29949f05dcb790525f89d7be"2961+ },2962+ {2963+ "seed": "sample/582",2964+ "family": "machines",2965+ "sha256": "674cbdbc43a561fb425778dd29909e03db45ecc470366f4cfa3f59d21bea8c44"2966+ },2967+ {2968+ "seed": "sample/583",2969+ "family": "landscapes",2970+ "sha256": "87389a83ea52f43da997c2daba4c141877142f5081e7e7fb32698262adbb862d"2971+ },2972+ {2973+ "seed": "sample/584",2974+ "family": "masks",2975+ "sha256": "d603ea5d874bce827d9cbfd7d527a18f8778999520596e1189b40cf23b9e6012"2976+ },2977+ {2978+ "seed": "sample/585",2979+ "family": "creatures",2980+ "sha256": "1a863f789ad440d892c364bcfbc209c86d0bb16804482d4f2aa2d145d937afaf"2981+ },2982+ {2983+ "seed": "sample/586",2984+ "family": "orbital",2985+ "sha256": "439c7757c46cf61fd4ac078963e052ef0d638dedd3d747d80c88cfa111344d23"2986+ },2987+ {2988+ "seed": "sample/587",2989+ "family": "woven",2990+ "sha256": "b9e3474b19592f572926614dd7c00296f8dc0f9114d7fd1d17eb25eee1da8a95"2991+ },2992+ {2993+ "seed": "sample/588",2994+ "family": "machines",2995+ "sha256": "4a0658790a1ef139b7512cf9c5c5a20663a2120f617cdb7bf52d6438db2eec9d"2996+ },2997+ {2998+ "seed": "sample/589",2999+ "family": "botanical",3000+ "sha256": "d6c1b6a49029240dd60de32e435d11bb176483c4cdf8b15c0b7336f01f5340b3"
diff truncated
A internal/webui/avatars.go +47 -0
@@ -0,0 +1,47 @@
1+package webui2+3+import (4+ "crypto/sha256"5+ "fmt"6+ "net/http"7+ "regexp"8+ "strings"9+ "time"10+11+ "github.com/FelineStateMachine/tinyrelay/internal/seedmark"12+)13+14+var avatarPath = regexp.MustCompile(`^/avatars/v1/([0-9a-f]{64})\.svg$`)15+16+func avatarURL(pubkey string) string {17+ key := strings.ToLower(pubkey)18+ if !eventIDPattern.MatchString(key) {19+ key = strings.Repeat("0", 64)20+ }21+ return "/avatars/v1/" + key + ".svg"22+}23+24+func roomAvatarURL(relay, id string) string {25+ seed := sha256.Sum256([]byte("tinyrelay:room:v1\x00" + strings.TrimRight(relay, "/") + "\x00" + id))26+ return avatarURL(fmt.Sprintf("%x", seed))27+}28+29+// Avatars depend only on the supplied seed and never look up tenant, room or30+// profile data. Versioned URLs keep each identity's appearance stable.31+func serveAvatar(w http.ResponseWriter, r *http.Request) {32+ if r.Method != http.MethodGet && r.Method != http.MethodHead {33+ w.Header().Set("Allow", "GET, HEAD")34+ http.Error(w, "method not allowed", http.StatusMethodNotAllowed)35+ return36+ }37+ match := avatarPath.FindStringSubmatch(r.URL.Path)38+ if match == nil {39+ http.NotFound(w, r)40+ return41+ }42+ w.Header().Set("Content-Type", "image/svg+xml; charset=utf-8")43+ w.Header().Set("X-Content-Type-Options", "nosniff")44+ w.Header().Set("Cache-Control", "public, max-age=31536000, immutable")45+ w.Header().Set("ETag", `"seedmark-v1-`+match[1]+`"`)46+ http.ServeContent(w, r, "avatar.svg", time.Time{}, strings.NewReader(seedmark.Avatar(match[1])))47+}
A internal/webui/avatars_test.go +78 -0
@@ -0,0 +1,78 @@
1+package webui2+3+import (4+ "net/http"5+ "net/http/httptest"6+ "strings"7+ "testing"8+9+ "github.com/FelineStateMachine/tinyrelay/internal/policy"10+ "github.com/FelineStateMachine/tinyrelay/internal/seedmark"11+)12+13+func TestAvatarEndpointRendersWithoutProfileAccess(t *testing.T) {14+ backend := &fakeBackend{policy: policy.Defaults(strings.Repeat("a", 64))}15+ backend.policy.Features.Grasp08 = true16+ app, err := New(backend, Options{})17+ if err != nil {18+ t.Fatal(err)19+ }20+ key := strings.Repeat("b", 64)21+ path := avatarURL(key)22+ response := httptest.NewRecorder()23+ app.ServeHTTP(response, httptest.NewRequest(http.MethodGet, path, nil))24+ if response.Code != http.StatusOK || response.Body.String() != seedmark.Avatar(key) {25+ t.Fatalf("avatar: status=%d body=%s", response.Code, response.Body.String())26+ }27+ if backend.call != "" || response.Header().Get("Content-Type") != "image/svg+xml; charset=utf-8" || !strings.Contains(response.Header().Get("Cache-Control"), "immutable") {28+ t.Fatalf("avatar accessed relay data or lacks image caching: call=%q headers=%v", backend.call, response.Header())29+ }30+ for _, method := range []string{http.MethodHead, http.MethodGet} {31+ request := httptest.NewRequest(method, path, nil)32+ if method == http.MethodGet {33+ request.Header.Set("If-None-Match", response.Header().Get("ETag"))34+ }35+ next := httptest.NewRecorder()36+ app.ServeHTTP(next, request)37+ if next.Body.Len() != 0 || (method == http.MethodHead && next.Code != http.StatusOK) || (method == http.MethodGet && next.Code != http.StatusNotModified) {38+ t.Fatalf("%s cache response: status=%d body=%s", method, next.Code, next.Body.String())39+ }40+ }41+}42+43+func TestAvatarEndpointRejectsOtherVersionsSeedsAndWrites(t *testing.T) {44+ app, err := New(&fakeBackend{policy: policy.Defaults(strings.Repeat("a", 64))}, Options{})45+ if err != nil {46+ t.Fatal(err)47+ }48+ for _, tc := range []struct {49+ method, path string50+ status int51+ }{52+ {http.MethodGet, "/avatars/v2/" + strings.Repeat("a", 64) + ".svg", http.StatusNotFound},53+ {http.MethodGet, "/avatars/v1/short.svg", http.StatusNotFound},54+ {http.MethodGet, "/avatars/v1/" + strings.Repeat("g", 64) + ".svg", http.StatusNotFound},55+ {http.MethodGet, "/avatars/v1/" + strings.Repeat("a", 64) + ".png", http.StatusNotFound},56+ {http.MethodPost, avatarURL(strings.Repeat("a", 64)), http.StatusMethodNotAllowed},57+ } {58+ response := httptest.NewRecorder()59+ app.ServeHTTP(response, httptest.NewRequest(tc.method, tc.path, nil))60+ if response.Code != tc.status {61+ t.Errorf("%s %s: status=%d, want %d", tc.method, tc.path, response.Code, tc.status)62+ }63+ }64+}65+66+func TestAvatarSeedsUseFullKeysAndRoomIdentity(t *testing.T) {67+ key := strings.Repeat("ab", 32)68+ if avatarURL(strings.ToUpper(key)) != avatarURL(key) || avatarURL(key) == avatarURL(key[:62]+"cd") {69+ t.Fatal("profile seed must use the full normalized key")70+ }71+ if avatarURL("invalid") != "/avatars/v1/"+strings.Repeat("0", 64)+".svg" {72+ t.Fatal("invalid keys should use the anonymous fallback")73+ }74+ room := roomAvatarURL("https://relay.test/r/team", "general")75+ if room != roomAvatarURL("https://relay.test/r/team/", "general") || room == roomAvatarURL("https://relay.test/r/other", "general") || room == roomAvatarURL("https://relay.test/r/team", "random") {76+ t.Fatal("room avatars must follow stable relay and room identity")77+ }78+}
M internal/webui/chat.js +1 -1
M internal/webui/components.js +69 -4
@@ -768,18 +768,32 @@
768768 769769 render() {770770 const pubkey = String(this.getAttribute("pubkey") || "").toLowerCase();771- const fallback = pubkey.slice(0, 2).toUpperCase();771+ const valid = /^[0-9a-f]{64}$/.test(pubkey);772+ const seed = valid ? pubkey : "0".repeat(64);773+ const fallback = tiny.localPath ? tiny.localPath("/avatars/v1/" + seed + ".svg") : "/avatars/v1/" + seed + ".svg";772774 const serial = (this.serial || 0) + 1;773775 this.serial = serial;776+ const changed = this.renderedPubkey !== undefined && this.renderedPubkey !== pubkey;777+ this.renderedPubkey = pubkey;774778 const restore = () => {775779 if (serial !== this.serial) return;776- this.replaceChildren(document.createTextNode(fallback));780+ const image = el("img");781+ image.src = fallback;782+ image.alt = "";783+ image.loading = "lazy";784+ image.referrerPolicy = "no-referrer";785+ image.setAttribute("aria-hidden", "true");786+ this.replaceChildren(image);777787 };778- const load = window.tinyNames?.load;779- if (!load || !/^[0-9a-f]{64}$/.test(pubkey)) {788+ const current = this.querySelector("img");789+ if (!valid) {780790 restore();781791 return;782792 }793+ if (changed || !current) restore();794+ else current.addEventListener("error", restore, {once: true});795+ const load = window.tinyNames?.load;796+ if (!load) return;783797 Promise.resolve(load({pubkey, refresh: true})).then(user => {784798 const raw = user?.image;785799 let url;
@@ -800,6 +814,56 @@
800814 }801815 }802816 817+ class RoomAvatar extends HTMLElement {818+ static get observedAttributes() { return ["picture", "fallback"]; }819+820+ connectedCallback() { this.render(); }821+822+ attributeChangedCallback() { this.render(); }823+824+ render(forceFallback = false) {825+ const serial = (this.serial || 0) + 1;826+ this.serial = serial;827+ let picture;828+ try {829+ const raw = this.getAttribute("picture") || "";830+ if (/^https?:\/\/\S+$/i.test(raw)) {831+ const candidate = new URL(raw);832+ picture = candidate.href;833+ }834+ } catch {}835+ let fallback;836+ try {837+ const candidate = new URL(this.getAttribute("fallback") || "", location.href);838+ if (candidate.origin === location.origin && /^(?:\/avatars|\/r\/[^/]+\/avatars)\/v1\/[0-9a-f]{64}\.svg$/.test(candidate.pathname)) fallback = candidate.href;839+ } catch {}840+ const src = forceFallback ? fallback : picture || fallback;841+ const current = this.querySelector("img");842+ if (current && current.src === src) {843+ if (picture && !forceFallback) {844+ if (current.complete && current.naturalWidth === 0) this.render(true);845+ else current.addEventListener("error", () => this.restore(serial), {once: true});846+ }847+ return;848+ }849+ this.replaceChildren();850+ if (!src) return;851+ const image = el("img");852+ image.src = src;853+ image.alt = "";854+ image.loading = "lazy";855+ image.referrerPolicy = "no-referrer";856+ image.setAttribute("aria-hidden", "true");857+ if (picture && !forceFallback) image.addEventListener("error", () => this.restore(serial), {once: true});858+ this.append(image);859+ }860+861+ restore(serial) {862+ if (serial !== this.serial) return;863+ this.render(true);864+ }865+ }866+803867 // PageLink is the crumb that names the current page. A tap copies the804868 // page's public address, the one the server put in url, and shows805869 // "copied" for a moment. Without JavaScript it is plain text.
@@ -1639,6 +1703,7 @@
16391703 customElements.define("share-link", ShareLink);16401704 customElements.define("nostr-key", NostrKey);16411705 customElements.define("nostr-avatar", NostrAvatar);1706+ customElements.define("room-avatar", RoomAvatar);16421707 customElements.define("page-link", PageLink);16431708 customElements.define("json-view", JsonView);16441709
A internal/webui/group_avatars_test.go +76 -0
@@ -0,0 +1,76 @@
1+package webui2+3+import (4+ "context"5+ "encoding/json"6+ "net/http"7+ "net/http/httptest"8+ "strings"9+ "testing"10+11+ "github.com/FelineStateMachine/tinyrelay/internal/policy"12+)13+14+type pictureRoomsBackend struct{ *roomsBackend }15+16+func (b *pictureRoomsBackend) Query(ctx context.Context, method string, params []json.RawMessage, actor string) (any, error) {17+ result, err := b.roomsBackend.Query(ctx, method, params, actor)18+ if err != nil {19+ return result, err20+ }21+ page, ok := result.(map[string]any)22+ if !ok {23+ return result, nil24+ }25+ if method == "browserooms" {26+ for _, item := range page["items"].([]any) {27+ item.(map[string]any)["picture"] = "https://cdn.example/general.png"28+ }29+ } else if room, ok := page["room"].(map[string]any); ok {30+ room["picture"] = "https://cdn.example/general.png"31+ }32+ return page, nil33+}34+35+func TestRoomSurfacesRenderStableSeedmarkFallbackAvatars(t *testing.T) {36+ app, _ := roomsApp(t, roomOwner)37+ for _, path := range []string{"/chat", "/rooms/general", "/rooms/general/thread/" + roomThread} {38+ body := roomsPage(t, app, path)39+ if !strings.Contains(body, `fallback="/avatars/v1/`) {40+ t.Fatalf("%s has no Seedmark room fallback: %s", path, body)41+ }42+ if strings.Count(body, "<room-avatar") == 0 {43+ t.Fatalf("%s has no room avatar element", path)44+ }45+ }46+}47+48+func TestRoomAvatarSeedAndTenantScoping(t *testing.T) {49+ first := roomAvatarURL("https://relay.example", "general")50+ if first == "" || first != roomAvatarURL("https://relay.example", "general") {51+ t.Fatalf("room avatar URL is not stable: %q", first)52+ }53+ if first == roomAvatarURL("https://relay.example", "build") {54+ t.Fatal("different room IDs share an avatar URL")55+ }56+ if scoped := injectBase(`<room-avatar fallback="`+first+`"><img src="`+first+`"></room-avatar>`, "/r/alice"); !strings.Contains(scoped, `fallback="/r/alice`+first+`"`) || !strings.Contains(scoped, `src="/r/alice`+first+`"`) {57+ t.Fatalf("tenant prefix missed room avatar URL: %s", scoped)58+ }59+}60+61+func TestRoomAvatarUsesValidatedPictureForSSR(t *testing.T) {62+ base := &roomsBackend{fakeBackend: fakeBackend{policy: policy.Defaults(roomOwner)}}63+ app, err := New(&pictureRoomsBackend{roomsBackend: base}, Options{Actor: func(*http.Request) (string, error) { return roomOwner, nil }})64+ if err != nil {65+ t.Fatal(err)66+ }67+ recorder := httptest.NewRecorder()68+ app.ServeHTTP(recorder, httptest.NewRequest(http.MethodGet, "/rooms/general", nil))69+ if recorder.Code != http.StatusOK {70+ t.Fatalf("status = %d", recorder.Code)71+ }72+ body := recorder.Body.String()73+ if !strings.Contains(body, `picture="https://cdn.example/general.png"`) || !strings.Contains(body, `<img src="https://cdn.example/general.png"`) {74+ t.Fatalf("validated room picture was not preferred in SSR: %s", body)75+ }76+}
M internal/webui/page.html +1 -1
@@ -110,7 +110,7 @@ then feature modules are loaded for the current page. */}}
110110 {{if .Actor}}<h4>Direct chats</h4><direct-chats actor="{{.Actor}}" relay="{{wsURL .URL}}" compact><p>Connect your signer to see direct chats.</p></direct-chats>111111 <p><a href="/chat#new-chat">+ new chat</a></p>{{end}}112112 <h4>Rooms</h4>113-<nav id="room-list">{{range .Rooms}}{{$r := asMap .}}<a href="/rooms/{{str (index $r "id")}}"{{if eq (str (index $r "id")) $.View}} aria-current="page"{{end}}>{{or (str (index $r "name")) (str (index $r "id"))}}{{with age (index $r "last_message_at")}} <small>{{.}}</small>{{end}}</a>{{else}}<a href="/chat">Browse rooms</a>{{end}}</nav>113+<nav id="room-list">{{range .Rooms}}{{$r := asMap .}}{{$id := str (index $r "id")}}{{$picture := socialMediaURL (str (index $r "picture"))}}<a href="/rooms/{{$id}}"{{if eq $id $.View}} aria-current="page"{{end}}><room-avatar picture="{{$picture}}" fallback="{{roomAvatarURL $.URL $id}}" aria-hidden="true"><img src="{{if $picture}}{{$picture}}{{else}}{{roomAvatarURL $.URL $id}}{{end}}" alt="" width="24" height="24" loading="lazy"></room-avatar>{{or (str (index $r "name")) $id}}{{with age (index $r "last_message_at")}} <small>{{.}}</small>{{end}}</a>{{else}}<a href="/chat">Browse rooms</a>{{end}}</nav>114114 <p><a href="/chat#new-room">+ new room</a></p>115115 {{else}}<header><span><a id="mark" href="/" title="{{.Slug}}"><i>t</i><i>i</i><i>n</i><i>y</i></a></span></header>116116 {{range $group := navGroups relayItems}}<nav>{{range $item := $group}}<a href="{{$item.Href}}"{{if eq $item.Tab $.Tab}} aria-current="page"{{end}}>{{$item.Label}}</a>{{end}}</nav>{{end}}{{if .Actor}}<p id="personal-nav"><a href="/account"{{if eq .Tab "account"}} aria-current="page"{{end}}>account</a></p>{{end}}
M internal/webui/prefix.go +1 -1
M internal/webui/prefix_test.go +1 -0
M internal/webui/rooms.go +7 -10
M internal/webui/rooms.html +6 -6
@@ -9,7 +9,7 @@ establishes the initial state. */}}
99 {{else if .Feed}}<table id="rows">1010 <thead><tr><th>room</th><th>access</th><th>members</th><th>last message</th></tr></thead>1111 <tbody>12-{{range .Feed}}{{$r := asMap .}}<tr><td><a href="/rooms/{{str (index $r "id")}}">{{or (str (index $r "name")) (str (index $r "id"))}}</a></td><td>{{if eq (str (index $r "access")) "members"}}members only{{else}}open{{end}}{{with str (index $r "about")}} <small>{{.}}</small>{{end}}</td><td>{{str (index $r "members")}}</td><td>{{with index $r "last_message_at"}}<time datetime="{{datetime .}}">{{when .}}</time>{{else}}no messages{{end}}</td></tr>12+{{range .Feed}}{{$r := asMap .}}{{$id := str (index $r "id")}}{{$picture := socialMediaURL (str (index $r "picture"))}}<tr><td><a href="/rooms/{{$id}}"><room-avatar picture="{{$picture}}" fallback="{{roomAvatarURL $.URL $id}}" aria-hidden="true"><img src="{{if $picture}}{{$picture}}{{else}}{{roomAvatarURL $.URL $id}}{{end}}" alt="" width="32" height="32" loading="lazy"></room-avatar>{{or (str (index $r "name")) $id}}</a></td><td>{{if eq (str (index $r "access")) "members"}}members only{{else}}open{{end}}{{with str (index $r "about")}} <small>{{.}}</small>{{end}}</td><td>{{str (index $r "members")}}</td><td>{{with index $r "last_message_at"}}<time datetime="{{datetime .}}">{{when .}}</time>{{else}}no messages{{end}}</td></tr>1313 {{end}}</tbody>1414 </table>1515 {{else}}<p>No rooms yet.</p>
@@ -26,8 +26,8 @@ establishes the initial state. */}}
2626 {{template "alert" .Error}}2727 {{if not .Actor}}<p><a href="{{signinURL .Base .Path .Query}}">Sign in</a> to read this room.</p>{{end}}2828 </section>29-{{else}}{{$data := asMap .Event}}{{$room := asMap (index $data "room")}}{{$id := str (index $room "id")}}<section id="room" data-viewer="{{.Actor}}">30-<h1>{{or (str (index $room "name")) $id}} <small>{{if eq (str (index $room "access")) "members"}}members only{{else}}open room{{end}}</small></h1>29+{{else}}{{$data := asMap .Event}}{{$room := asMap (index $data "room")}}{{$id := str (index $room "id")}}{{$picture := socialMediaURL (str (index $room "picture"))}}<section id="room" data-viewer="{{.Actor}}">30+<h1><room-avatar picture="{{$picture}}" fallback="{{roomAvatarURL $.URL $id}}" aria-hidden="true"><img src="{{if $picture}}{{$picture}}{{else}}{{roomAvatarURL $.URL $id}}{{end}}" alt="" width="40" height="40" loading="lazy"></room-avatar>{{or (str (index $room "name")) $id}} <small>{{if eq (str (index $room "access")) "members"}}members only{{else}}open room{{end}}</small></h1>3131 {{with str (index $room "about")}}<p>{{.}}</p>{{end}}3232 {{with index $data "next_cursor"}}<p id="pages"><a href="/rooms/{{$id}}?cursor={{urlquery (str .)}}">load earlier</a></p>{{end}}3333 {{if .Query.Get "cursor"}}<p id="newest"><a href="/rooms/{{$id}}">newest messages</a></p>{{end}}
@@ -47,8 +47,8 @@ establishes the initial state. */}}
4747 <h1>Thread</h1>4848 {{template "alert" .Error}}4949 </section>50-{{else}}{{$data := asMap .Event}}{{$room := asMap (index $data "room")}}{{$id := str (index $room "id")}}{{$root := roomRoot .Event .Actor}}<section id="thread" data-viewer="{{.Actor}}">51-<p id="crumbs"><a href="/rooms/{{$id}}">{{or (str (index $room "name")) $id}}</a></p>50+{{else}}{{$data := asMap .Event}}{{$room := asMap (index $data "room")}}{{$id := str (index $room "id")}}{{$picture := socialMediaURL (str (index $room "picture"))}}{{$root := roomRoot .Event .Actor}}<section id="thread" data-viewer="{{.Actor}}">51+<p id="crumbs"><a href="/rooms/{{$id}}"><room-avatar picture="{{$picture}}" fallback="{{roomAvatarURL $.URL $id}}" aria-hidden="true"><img src="{{if $picture}}{{$picture}}{{else}}{{roomAvatarURL $.URL $id}}{{end}}" alt="" width="28" height="28" loading="lazy"></room-avatar>{{or (str (index $room "name")) $id}}</a></p>5252 <h1>Thread <small>{{$root.Replies}} {{if eq $root.Replies 1}}reply{{else}}replies{{end}}</small></h1>5353 <div id="root">{{template "roomMessage" $root}}</div>5454 {{template "chatActivity" .Activity}}
@@ -68,7 +68,7 @@ establishes the initial state. */}}
6868 {{define "roomSendIcon"}}<svg viewBox="0 0 24 24" width="22" height="22" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M12 19V5m-7 7 7-7 7 7"/></svg>{{end}}6969 7070 {{/* One message. room-live builds the same markup for messages that arrive on the stream. */}}71-{{define "roomMessage"}}<room-message id="msg-{{.ID}}" data-id="{{.ID}}" data-kind="{{.Kind}}" data-updated-at="{{.UpdatedAt}}" data-pubkey="{{.PubKey}}"{{if .Own}} data-own{{end}}{{if .Agent}} data-agent{{end}}{{if .Notice}} data-notice{{end}}{{if .Edited}} data-edited{{end}}><nostr-avatar pubkey="{{.PubKey}}" aria-hidden="true">{{.Avatar}}</nostr-avatar>71+{{define "roomMessage"}}<room-message id="msg-{{.ID}}" data-id="{{.ID}}" data-kind="{{.Kind}}" data-updated-at="{{.UpdatedAt}}" data-pubkey="{{.PubKey}}"{{if .Own}} data-own{{end}}{{if .Agent}} data-agent{{end}}{{if .Notice}} data-notice{{end}}{{if .Edited}} data-edited{{end}}><nostr-avatar pubkey="{{.PubKey}}" aria-hidden="true"><img src="{{avatarURL .PubKey}}" alt="" width="32" height="32" loading="lazy"></nostr-avatar>7272 <header><b>{{template "nostrName" .PubKey}}</b>{{if .Own}}<span data-own-label>you</span>{{end}}{{if .Agent}} | {{if .Operator}}{{template "nostrName" .Operator}}'s agent{{else}}agent{{end}}{{else if .Role}} | {{.Role}}{{end}}<small><time datetime="{{datetime .CreatedAt}}" title="{{when .CreatedAt}}">{{clock .CreatedAt}}</time></small></header>7373 {{if .Notice}}<p>{{.Notice}}</p>{{else}}{{if .Quote}}<blockquote data-quote><a href="/open?target={{urlquery (printf "nostr:%s" .Quote)}}">quoted event {{shortID .Quote}}</a></blockquote>{{end}}<div>{{chatMarkdown (roomAttachmentContent .Content .Attachments)}}</div>{{roomAttachmentMarkup .Attachments}}{{end}}7474 {{if or .Mentions (and (not .InThread) (or (eq .Kind "11") (eq .Kind "9") (eq .Kind "40002") .Root)) .Reactions .Edited}}<footer>{{if .Mentions}}<span>to {{range .Mentions}}{{template "nostrName" .}} {{end}}</span>{{end}}{{if not .InThread}}{{if or (eq .Kind "11") (eq .Kind "9") (eq .Kind "40002")}}<a href="/rooms/{{.Room}}/thread/{{.ID}}">thread{{if .Replies}} | {{.Replies}} {{if eq .Replies 1}}reply{{else}}replies{{end}}{{end}}</a>{{else if .Root}}<a href="/rooms/{{.Room}}/thread/{{.Root}}">in thread</a>{{end}}{{end}}{{range .Reactions}}<span data-reaction="{{.Content}}">{{.Content}} {{.Count}}</span>{{end}}{{if .Edited}}<span data-edited>edited</span>{{end}}</footer>{{end}}
M internal/webui/rooms.js +1 -1
M internal/webui/rooms_test.go +7 -7
@@ -115,7 +115,7 @@ func roomsPage(t *testing.T, app *App, path string) string {
115115 func TestRoomsListRendersRoomsRailAndCreateForm(t *testing.T) {116116 app, backend := roomsApp(t, roomOwner)117117 body := roomsPage(t, app, "/chat")118- for _, want := range []string{`<table id="rows">`, `<a href="/rooms/general">General</a>`, `<a href="/rooms/build">Build</a>`, `members only <small>Where Build happens.</small>`, `<room-create>`, `<a href="/chat"><b>chat</b></a>`, `<nav id="room-list"><a href="/rooms/general">General`, `<a href="/chat#new-room">+ new room</a>`, `<p id="rail-tools"><a href="/">&larr; relay</a>`, `<h4>Rooms</h4>`, `<td>2</td>`} {118+ for _, want := range []string{`<table id="rows">`, `</room-avatar>General</a>`, `</room-avatar>Build</a>`, `members only <small>Where Build happens.</small>`, `<room-create>`, `<a href="/chat"><b>chat</b></a>`, `<nav id="room-list"><a href="/rooms/general"><room-avatar`, `<a href="/chat#new-room">+ new room</a>`, `<p id="rail-tools"><a href="/">&larr; relay</a>`, `<h4>Rooms</h4>`, `<td>2</td>`} {119119 if !strings.Contains(body, want) {120120 t.Errorf("rooms list missing %q", want)121121 }
@@ -141,11 +141,11 @@ func TestRoomPageRendersMessagesOldestFirstWithMarkers(t *testing.T) {
141141 app, backend := roomsApp(t, roomOwner)142142 body := roomsPage(t, app, "/rooms/general")143143 for _, want := range []string{144- `<h1>General <small>open room</small></h1>`, `<p>Where General happens.</p>`,144+ `</room-avatar>General <small>open room</small></h1>`, `<p>Where General happens.</p>`,145145 `<a href="/rooms/general?cursor=cursor-1">load earlier</a>`,146146 `<room-message id="msg-` + roomHello + `" data-id="` + roomHello + `" data-kind="9" data-updated-at="1757203650" data-pubkey="` + roomAgent + `" data-agent data-edited>`,147- `<nostr-avatar pubkey="` + roomAgent + `" aria-hidden="true">BB</nostr-avatar>`,148- `<room-message id="msg-` + roomChat + `" data-id="` + roomChat + `" data-kind="9" data-updated-at="1757202000" data-pubkey="` + roomOwner + `" data-own><nostr-avatar pubkey="` + roomOwner + `" aria-hidden="true">AA</nostr-avatar>`,147+ `<nostr-avatar pubkey="` + roomAgent + `" aria-hidden="true"><img src="` + avatarURL(roomAgent) + `" alt="" width="32" height="32" loading="lazy"></nostr-avatar>`,148+ `<room-message id="msg-` + roomChat + `" data-id="` + roomChat + `" data-kind="9" data-updated-at="1757202000" data-pubkey="` + roomOwner + `" data-own><nostr-avatar pubkey="` + roomOwner + `" aria-hidden="true"><img src="` + avatarURL(roomOwner) + `" alt="" width="32" height="32" loading="lazy"></nostr-avatar>`,149149 `</b><span data-own-label>you</span>`,150150 `<section id="room" data-viewer="` + roomOwner + `">`,151151 `<a href="https://example.com/docs" rel="noopener">https://example.com/docs</a>.`,
@@ -161,7 +161,7 @@ func TestRoomPageRendersMessagesOldestFirstWithMarkers(t *testing.T) {
161161 `<noscript><p>Sending needs JavaScript and a connected signer.</p></noscript>`,162162 `<ul id="members"><li><nostr-name pubkey="` + roomOwner + `"`, `<li data-agent data-operator="` + roomOwner + `"><nostr-name pubkey="` + roomAgent + `"`, `<small><nostr-name pubkey="` + roomOwner + `" title="` + roomOwner + `">` + shortID(roomOwner) + `</nostr-name>'s agent</small>`,163163 `<room-action room="general" kind="9000">`, `<room-action room="general" kind="9002">`, `<room-action room="general" kind="9022">`,164- `<nav id="room-list"><a href="/rooms/general" aria-current="page">General`,164+ `<nav id="room-list"><a href="/rooms/general" aria-current="page"><room-avatar`,165165 `&raquo; <page-link url="http://relay.example/rooms/general" title="Copy the page address">rooms/general</page-link></span>`,166166 } {167167 if !strings.Contains(body, want) {
@@ -196,7 +196,7 @@ func TestMembersOnlyRoomIsHiddenFromGuests(t *testing.T) {
196196 }197197 app, _ := roomsApp(t, roomOwner)198198 body = roomsPage(t, app, "/rooms/build")199- if !strings.Contains(body, `<h1>Build <small>members only</small></h1>`) || !strings.Contains(body, `id="msg-`+roomHello) {199+ if !strings.Contains(body, `</room-avatar>Build <small>members only</small></h1>`) || !strings.Contains(body, `id="msg-`+roomHello) {200200 t.Fatal("owner did not see the members-only room")201201 }202202 }
@@ -205,7 +205,7 @@ func TestThreadPageRendersRootRepliesAndReplyCompose(t *testing.T) {
205205 app, _ := roomsApp(t, roomOwner)206206 body := roomsPage(t, app, "/rooms/general/thread/"+roomThread)207207 for _, want := range []string{208- `<p id="crumbs"><a href="/rooms/general">General</a></p>`, `<h1>Thread <small>1 reply</small></h1>`,208+ `<p id="crumbs"><a href="/rooms/general"><room-avatar`, `</room-avatar>General</a></p>`, `<h1>Thread <small>1 reply</small></h1>`,209209 `<div id="root"><room-message id="msg-` + roomThread + `"`, `<div id="messages">`, `id="msg-` + roomReply + `"`,210210 `data-viewer="` + roomOwner + `"`, `<span data-own-label>you</span>`,211211 `<room-live room="general" root="` + roomThread + `"></room-live>`,
M internal/webui/social.html +1 -1
@@ -34,7 +34,7 @@
3434 {{template "socialActions" $p}}3535 </li>{{end}}3636 37-{{define "socialByline"}}{{$p := asMap .}}<header data-social-byline><span data-social-avatar><nostr-avatar pubkey="{{index $p "pubkey"}}" aria-hidden="true">{{with socialMediaURL (str (index $p "author_picture"))}}<img src="{{.}}" alt="" loading="lazy" width="36" height="36" referrerpolicy="no-referrer">{{else}}<span aria-hidden="true">@</span>{{end}}</nostr-avatar></span><span><a href="/social?author={{str (index $p "pubkey")}}"><b>{{if index $p "author_name"}}{{str (index $p "author_name")}}{{else}}{{template "nostrName" (str (index $p "pubkey"))}}{{end}}</b></a> <a href="{{socialURL $p}}"><time datetime="{{datetime (index $p "created_at")}}">{{when (index $p "created_at")}}</time></a>{{if eq (str (index $p "kind")) "30023"}} <small>Article</small>{{end}}{{with index $p "parent_url"}}<small data-social-parent>↳ <a href="{{.}}">Replying{{if index $p "parent_author"}} to {{template "nostrName" (str (index $p "parent_author"))}}{{else}} to a post{{end}}</a></small>{{end}}</span></header>{{end}}37+{{define "socialByline"}}{{$p := asMap .}}<header data-social-byline><span data-social-avatar><nostr-avatar pubkey="{{index $p "pubkey"}}" aria-hidden="true">{{with socialMediaURL (str (index $p "author_picture"))}}<img src="{{.}}" alt="" loading="lazy" width="36" height="36" referrerpolicy="no-referrer">{{else}}<img src="{{avatarURL (str (index $p "pubkey"))}}" alt="" width="36" height="36" loading="lazy">{{end}}</nostr-avatar></span><span><a href="/social?author={{str (index $p "pubkey")}}"><b>{{if index $p "author_name"}}{{str (index $p "author_name")}}{{else}}{{template "nostrName" (str (index $p "pubkey"))}}{{end}}</b></a> <a href="{{socialURL $p}}"><time datetime="{{datetime (index $p "created_at")}}">{{when (index $p "created_at")}}</time></a>{{if eq (str (index $p "kind")) "30023"}} <small>Article</small>{{end}}{{with index $p "parent_url"}}<small data-social-parent>↳ <a href="{{.}}">Replying{{if index $p "parent_author"}} to {{template "nostrName" (str (index $p "parent_author"))}}{{else}} to a post{{end}}</a></small>{{end}}</span></header>{{end}}3838 3939 {{define "socialActions"}}{{$p := asMap .}}<footer data-social-actions>4040 <a href="{{socialURL $p}}#reply">{{if index $p "reply_count"}}{{index $p "reply_count"}} {{if eq (str (index $p "reply_count")) "1"}}reply{{else}}replies{{end}}{{else}}Reply{{end}}</a>
M internal/webui/style.css +10 -2
@@ -299,17 +299,25 @@ wiki-compose output{display:block;font:12px var(--mono);color:var(--muted);margi
299299 /* Rooms: the rail lists rooms, messages read oldest first, and the compose bar stays at the bottom of the column. */300300 #rail h4{margin:16px 0 4px;padding:0 2ch 0 2.5ch;font:11px var(--mono);color:var(--muted);text-transform:uppercase;letter-spacing:.04em}301301 #room-list a{white-space:nowrap;overflow:hidden;text-overflow:ellipsis}#room-list a small{float:right;color:var(--muted);font-size:11px}302+#room-list room-avatar{display:inline-block;vertical-align:middle;width:24px;height:24px;margin-right:7px}303+#room-list room-avatar img{display:block;width:100%;height:100%;border-radius:50%;object-fit:cover}304+#rooms #rows room-avatar,#chat #rows room-avatar{display:inline-block;vertical-align:middle;width:32px;height:32px;margin-right:8px}305+#rooms #rows room-avatar img,#chat #rows room-avatar img{display:block;width:100%;height:100%;border-radius:50%;object-fit:cover}302306 #rooms #rows td:first-child{width:220px}#rooms #rows td:nth-child(3){text-align:right;width:80px;font:12px var(--mono)}303307 #content>section#room,#content>section#thread{padding-bottom:0}304308 #room h1 small,#thread h1 small{font:12px var(--mono);color:var(--muted)}309+#room h1 room-avatar{display:inline-block;vertical-align:middle;width:40px;height:40px;margin-right:10px}310+#room h1 room-avatar img{display:block;width:100%;height:100%;border-radius:50%;object-fit:cover}311+#thread #crumbs room-avatar{display:inline-block;vertical-align:middle;width:28px;height:28px;margin-right:7px}312+#thread #crumbs room-avatar img{display:block;width:100%;height:100%;border-radius:50%;object-fit:cover}305313 #crumbs,#newest,#empty{font:12px var(--mono);color:var(--muted)}306314 /* Rooms read as a centered conversation column; the compose bar spans the307315 content column but its controls sit in the same 720px lane. */308316 #room>*:not(room-compose),#thread>*:not(room-compose),room-compose>*{max-width:720px;margin-left:auto;margin-right:auto}309317 room-message{display:block;position:relative;min-width:0;margin:10px 0;padding:12px 0 14px 44px;border-bottom:1px solid var(--line)}310318 room-message>nostr-avatar{position:absolute;top:12px;left:0}311-nostr-avatar{display:inline-block;box-sizing:border-box;width:32px;height:32px;overflow:hidden;border:1px solid var(--line);border-radius:50%;background:var(--rail);color:var(--muted);text-align:center;font:11px/30px var(--mono);user-select:none;-webkit-user-select:none}312-nostr-avatar img{display:block;width:100%;height:100%;object-fit:cover}319+nostr-avatar,room-avatar{display:inline-block;box-sizing:border-box;width:32px;height:32px;overflow:hidden;border:1px solid var(--line);border-radius:50%;background:var(--rail);color:var(--muted);text-align:center;font:11px/30px var(--mono);user-select:none;-webkit-user-select:none}320+nostr-avatar img,room-avatar img{display:block;width:100%;height:100%;object-fit:cover}313321 room-message>header{font:12px/1.5 var(--mono);color:var(--muted);margin-bottom:6px;overflow-wrap:anywhere;user-select:none;-webkit-user-select:none}314322 room-message>header b{color:var(--ink);font-weight:500}room-message>header b nostr-key{cursor:copy}315323 room-message>header small{float:right;margin-left:8px;white-space:nowrap}
M internal/webui/templates.go +2 -0
M internal/webui/webui.go +4 -0
M scripts/chat-identity.test.mjs +1 -1
M scripts/nostr-avatar.test.mjs +54 -22
@@ -6,32 +6,35 @@ import vm from "node:vm";
66 const source = fs.readFileSync("internal/webui/components.js", "utf8");77 const avatarSource = source.slice(source.indexOf(" class NostrAvatar "), source.indexOf(" // PageLink"));88 const key = "a".repeat(64);9+const location = {href: "https://relay.example/r/demo/chat", origin: "https://relay.example"};910 10-function setup(userPromise) {11+function setup(userPromise, tag = "nostr-avatar", children = []) {1112 const images = [], requests = [];1213 const document = {13- createTextNode: text => ({textContent: text}),1414 createElement: name => {15- const image = {nodeName: name, addEventListener: (event, callback) => { image.handlers ??= {}; image.handlers[event] = callback; }, setAttribute() {}};16- images.push(image);17- return image;15+ const node = {nodeName: name, children: [], setAttribute(name, value) { this[name] = value; }, append(...nodes) { this.children.push(...nodes); }, addEventListener(event, callback) { this.handlers ??= {}; this.handlers[event] = callback; }};16+ if (name === "img") images.push(node);17+ return node;1818 }1919 };2020 const window = {tinyNames: {load: request => { requests.push(request); return typeof userPromise === "function" ? userPromise() : userPromise; }}};2121 const HTMLElement = class {22- constructor() { this.attributes = {}; this.children = []; }22+ constructor() { this.attributes = {}; this.children = children; }2323 getAttribute(name) { return this.attributes[name] ?? null; }24- replaceChildren(...children) { this.children = children; }24+ setAttribute(name, value) { this.attributes[name] = value; }25+ replaceChildren(...nodes) { this.children = nodes; }26+ append(...nodes) { this.children.push(...nodes); }2527 querySelector(name) { return this.children.find(child => child.nodeName === name) || null; }2628 };27- const el = (name, text) => { const node = document.createElement(name); if (text !== undefined) node.textContent = text; return node; };28- const Constructor = vm.runInNewContext(`${avatarSource}\nNostrAvatar`, {HTMLElement, window, document, URL, Promise, String, setTimeout, el});29+ const el = name => document.createElement(name);30+ const tiny = {localPath: path => "/r/demo" + path};31+ const Constructor = vm.runInNewContext(`${avatarSource}\n${tag === "room-avatar" ? "RoomAvatar" : "NostrAvatar"}`, {HTMLElement, window, document, URL, Promise, String, setTimeout, el, tiny, location});2932 const component = new Constructor();3033 component.attributes.pubkey = key;3134 return {component, images, requests};3235 }3336 34-test("loads a fresh profile image with privacy attributes", async () => {37+test("loads a profile image with a tenant scoped generated fallback", async () => {3538 const {component, requests} = setup(Promise.resolve({image: "https://cdn.example/avatar.png"}));3639 component.connectedCallback();3740 await new Promise(resolve => setTimeout(resolve, 0));
@@ -44,24 +47,28 @@ test("loads a fresh profile image with privacy attributes", async () => {
4447 assert.equal(requests[0].refresh, true);4548 });4649 47-test("keeps the two character fallback for missing or unsafe pictures", async () => {50+test("uses generated avatars for missing or unsafe pictures", async () => {4851 for (const image of [undefined, "javascript:alert(1)", "/avatar.png"]) {4952 const {component} = setup(Promise.resolve(image === undefined ? {} : {image}));5053 component.connectedCallback();5154 await new Promise(resolve => setTimeout(resolve, 0));52- assert.equal(component.children[0].textContent, "AA");55+ assert.equal(component.children[0].src, "/r/demo/avatars/v1/" + key + ".svg");5356 }57+ const {component} = setup(Promise.resolve({}));58+ component.attributes.pubkey = "bad";59+ component.connectedCallback();60+ assert.equal(component.children[0].src, "/r/demo/avatars/v1/" + "0".repeat(64) + ".svg");5461 });5562 56-test("restores the fallback after an image load failure", async () => {63+test("restores the generated fallback after an image load failure", async () => {5764 const {component, images} = setup(Promise.resolve({image: "https://cdn.example/avatar.png"}));5865 component.connectedCallback();5966 await new Promise(resolve => setTimeout(resolve, 0));60- images[0].handlers.error();61- assert.equal(component.children[0].textContent, "AA");67+ images.at(-1).handlers.error();68+ assert.equal(component.children[0].src, "/r/demo/avatars/v1/" + key + ".svg");6269 });6370 64-test("ignores a stale profile result after the pubkey changes", async () => {71+test("ignores stale profile results after the pubkey changes", async () => {6572 let resolve, calls = 0;6673 const {component} = setup(() => calls++ === 0 ? new Promise(done => { resolve = done; }) : Promise.resolve({}));6774 component.connectedCallback();
@@ -69,14 +76,39 @@ test("ignores a stale profile result after the pubkey changes", async () => {
6976 component.attributeChangedCallback();7077 resolve({image: "https://cdn.example/avatar.png"});7178 await new Promise(resolve => setTimeout(resolve, 0));72- assert.equal(component.children[0].textContent, "BB");79+ assert.equal(component.children[0].src, "/r/demo/avatars/v1/" + "b".repeat(64) + ".svg");7380 });7481 75-test("replaces a prior image when the pubkey becomes invalid", async () => {76- const {component} = setup(Promise.resolve({image: "https://cdn.example/avatar.png"}));82+test("room avatars prefer safe pictures and reject unsafe or cross origin fallbacks", () => {83+ const fallback = "/r/demo/avatars/v1/" + key + ".svg";84+ const {component} = setup(undefined, "room-avatar");85+ component.attributes.picture = "https://cdn.example/room.png";86+ component.attributes.fallback = fallback;7787 component.connectedCallback();78- await new Promise(resolve => setTimeout(resolve, 0));79- component.attributes.pubkey = "bad";88+ assert.equal(component.children[0].src, "https://cdn.example/room.png");89+ component.attributes.picture = "javascript:alert(1)";90+ component.attributeChangedCallback();91+ assert.equal(component.children[0].src, "https://relay.example/r/demo/avatars/v1/" + key + ".svg");92+ component.attributes.fallback = "https://evil.example/avatars/v1/" + key + ".svg";8093 component.attributeChangedCallback();81- assert.equal(component.children[0].textContent, "BA");94+ assert.equal(component.children.length, 0);95+});96+97+test("room avatars accept the root fallback path", () => {98+ const fallback = "/avatars/v1/" + key + ".svg";99+ const {component} = setup(undefined, "room-avatar");100+ component.attributes.fallback = fallback;101+ component.connectedCallback();102+ assert.equal(component.children[0].src, "https://relay.example" + fallback);103+});104+105+test("room avatars restore their fallback after a picture fails", () => {106+ const fallback = "/r/demo/avatars/v1/" + key + ".svg";107+ const {component, images} = setup(undefined, "room-avatar");108+ component.attributes.picture = "https://cdn.example/room.png";109+ component.attributes.fallback = fallback;110+ component.connectedCallback();111+ images[0].handlers.error();112+ assert.equal(component.children[0].src, "https://relay.example" + fallback);113+ assert.equal(images.at(-1).handlers?.error, undefined);82114 });
A scripts/qa/generate-seedmark-fixture.mjs +42 -0
@@ -0,0 +1,42 @@
1+#!/usr/bin/env node2+3+// Regenerate the Seedmark v1 parity fixture from a local upstream checkout.4+// Usage: node scripts/qa/generate-seedmark-fixture.mjs ../seedmark [output]5+import {execFileSync} from 'node:child_process';6+import {createHash} from 'node:crypto';7+import {writeFile} from 'node:fs/promises';8+import {pathToFileURL} from 'node:url';9+import path from 'node:path';10+11+const upstream = path.resolve(process.argv[2] || '../seedmark');12+const output = path.resolve(process.argv[3] || 'internal/seedmark/testdata/v1.json');13+const expectedCommit = 'c808a940bc193f97512c02d58da33ecc28ba1f53';14+const commit = execFileSync('git', ['-C', upstream, 'rev-parse', 'HEAD'], {encoding: 'utf8'}).trim();15+if (commit !== expectedCommit) {16+ throw new Error(`Seedmark checkout is ${commit}; expected ${expectedCommit}`);17+}18+19+const {avatar, recipe} = await import(pathToFileURL(path.join(upstream, 'src/index.js')).href);20+const seeds = [21+ '',22+ 'seedmark',23+ 'a'.repeat(64),24+ 'A'.repeat(64),25+ '0'.repeat(64),26+ 'seed/\u0000part',27+ 'é',28+ 'e\u0301',29+ '猫🌱',30+ '👩‍💻',31+ ...Array.from({length: 1000}, (_, index) => `sample/${index}`),32+];33+const fixtures = seeds.map(seed => {34+ const svg = avatar(seed);35+ return {36+ seed,37+ family: recipe(seed).family,38+ sha256: createHash('sha256').update(svg).digest('hex'),39+ };40+});41+await writeFile(output, `${JSON.stringify(fixtures, null, 2)}\n`);42+process.stderr.write(`wrote ${fixtures.length} cases from Seedmark ${commit} to ${output}\n`);