@usenavii/core 0.6.0 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -6,6 +6,125 @@ Format: [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). Versioning: [S
6
6
 
7
7
  ## [Unreleased]
8
8
 
9
+ ## [0.26.1] - 2026-05-31
10
+
11
+ ### Changed (docs)
12
+
13
+ - Removed Gravatar comparisons from user-facing docs (README, package READMEs, `/docs/recipes`, `/docs/sdk-core`). Functionality unchanged — `seedFromEmail()` still hashes `sha256(email.trim().toLowerCase())`, so cross-product seed parity still holds for any caller using the same scheme.
14
+
15
+ ## [0.26.0] - 2026-05-30
16
+
17
+ ### Added (`@usenavii/core` 0.7.0)
18
+
19
+ - **`seedFromEmail(email)`** — Gravatar-compatible seed helper. Returns `sha256` hex of the trimmed + lowercased email so the raw address never reaches URLs, server access logs, `Referer` headers, browser history, CDN cache keys, or analytics pixels. Two services hashing the same email produce the same seed → drop-in compatible with Gravatar's lookup scheme.
20
+ - **`normalizeEmail(email)`** — exported canonicalization step (trim + lowercase + NFC) for callers who need to reproduce the form before hashing.
21
+ - **`sha256Hex(input)`** — sync SHA-256 (FIPS 180-4) primitive used by `seedFromEmail`. Pure JS, no deps; available for callers that want to hash other inputs in the same scheme.
22
+ - **`SeedOptions`** type + `hashEmail` option on `Navii.seed()`.
23
+
24
+ ### Changed (`@usenavii/core` 0.7.0) — breaking
25
+
26
+ - `Navii.seed({ email })` now returns `sha256(normalizeEmail(email))` instead of the raw address. **Migration:** if your existing avatars were keyed on raw emails and you need them to stay stable, pass `{ hashEmail: false }` until you can re-key. New deployments should leave the default.
27
+ - `Navii.seedFromEmail` exposed on the `Navii` namespace.
28
+
29
+ ### Added (`@usenavii/react` 0.8.0)
30
+
31
+ - Re-exports `seed`, `seedFromEmail`, `normalizeEmail`, `SeedFields`, `SeedOptions` from `@usenavii/core` so `<Navii seed={seedFromEmail(user.email)} />` works without a second import.
32
+
33
+ ### Added (API host)
34
+
35
+ - `/avatar/:seed` sets `x-navii-warning: plaintext-email-seed; hash with seedFromEmail()` when the seed matches an email pattern. Render still succeeds — the header is a client-side nudge. Also logged at `warn` level for ops visibility.
36
+
37
+ ### Changed (API host)
38
+
39
+ - `/docs/recipes` gets a new "Using emails as seeds (Gravatar-style)" section.
40
+ - `/docs/sdk-core#seed` documents `seedFromEmail`, `normalizeEmail`, and the `hashEmail` option on `Navii.seed()`.
41
+ - `/docs/http-api#headers` documents the new `x-navii-warning` response header.
42
+
43
+ ## [0.25.2] - 2026-05-28
44
+
45
+ ### Changed (API host)
46
+ - Landing hero CTAs reworked. New triple: `Install Navii →` (primary, scrolls to install snippets), `Get Figma Plugin →` (secondary, opens Figma community), `Try the Builder` (tertiary text link → `/builder`). Old single-purpose pair (`Try it` + `Customize a face`) replaced — plugin is now visible from the hero.
47
+ - `/docs/sdk-core` SDK options table picks up `packs` + `style` rows (matches the engine surface that's been live since v0.23.0).
48
+
49
+ ## [0.25.1] - 2026-05-27
50
+
51
+ ### Added (API host)
52
+ - **`/avatar/:seed?packs=…&style=…`** — `/avatar/:seed` now parses `packs` (comma-separated pack ids) and `style` (`masc | femme | neutral`) query params. Unknown pack ids are silently skipped (engine ignores them). Pack order does not affect cached output — the PNG cache key normalizes by sorting.
53
+
54
+ ### Fixed (API host)
55
+ - PNG cache key extended to include `packs` + `style` so cached renders no longer collide across different pack/style combinations of the same seed.
56
+
57
+ ### Changed (API host)
58
+ - `/docs/http-api` documents the new `packs` and `style` rows in the `/avatar/:seed` query table, plus new example URLs (`?packs=halloween`, `?packs=office,mono&style=neutral`).
59
+ - `/random` inherits the new params (it forwards every `/avatar/:seed` query through unchanged).
60
+
61
+ ### Notes
62
+ - Required by the upcoming Figma plugin update (fixes fill-mode rendering — plugin was sending the right options to the main thread but `buildUrl()` was stripping `packs`/`mood`/`style` before the HTTP request).
63
+ - Backward compatible. Existing `<img src="https://api.navii.dev/avatar/alice">` URLs unchanged.
64
+
65
+ ## [0.25.0] - 2026-05-27
66
+
67
+ ### Added (`@usenavii/react` 0.7.0)
68
+ - **`<NaviiGroup>`** — overlapping avatar stack, thin React wrapper around `@usenavii/core`'s `renderGroup()`. Props: `seeds`, `size`, `overlap`, `max` (overflow → `+N` counter tile), `ring`, `tileBg`, `counterFill`, `counterInk`, plus all per-tile options (`paletteId`, `palette`, `mood`, `background`, `animated`, `styleHint`). `<img>` width is computed from `size + overlap + max` so layout is stable on load.
69
+ - `renderGroup` + `GroupOptions` re-exported from `@usenavii/react`.
70
+
71
+ ### Changed (`@usenavii/core`)
72
+ - No source changes. Stays at `0.6.0` — react `0.7.0` ships independently. Lockstep convention relaxed when only one package has source changes.
73
+
74
+ ### Changed (tooling)
75
+ - `scripts/release-audit.mjs` — core/react version mismatch downgraded from `error` to `warn`. Lockstep stays the default expectation, but the audit no longer forces a no-op publish on the unchanged package.
76
+
77
+ ### Added (API host)
78
+ - `/docs/sdk-react` documents `<NaviiGroup>` props + behavior, plus the new `renderGroup`/`GroupOptions` re-exports.
79
+
80
+ ## [0.24.2] - 2026-05-27
81
+
82
+ ### Added (API host)
83
+ - The Skill Club and ForYu logos in the landing "built with navii" wall.
84
+
85
+ ## [0.24.1] - 2026-05-27
86
+
87
+ ### Added (Figma plugin)
88
+ - **Sign out of Pro** — Pro pill in header now opens upgrade modal even when Pro. Modal footer shows the signed-in email + a Sign-out button that clears the cached license via `license-clear`. Lets users test the free-tier flow on the same device without losing access (the underlying Polar license is unchanged).
89
+
90
+ ### Changed (Figma plugin)
91
+ - Pro pill click now always opens the upgrade modal (was: free-only). Pro view exposes account info + sign-out.
92
+ - Footer (Insert / Fill random) hidden on **Packs** and **Mascots** tabs — those are browsing surfaces with their own inline actions (Enable button in pack-modal, card-click action modal in Mascots). Footer remains on Seed + Build where Insert is the primary CTA.
93
+
94
+ ### Fixed (Figma plugin)
95
+ - Pro user's usage chip stayed stuck at "10 of 10 left today" on plugin open due to a race between `usage-get` (UI request) and `doLicenseRestore` (main thread state). Restore now pushes a fresh usage snapshot via `doUsageGet()` after setting `cachedLicenseOk`, so the chip flips to "Pro · Unlimited" reliably.
96
+
97
+ ## [0.24.0] - 2026-05-27
98
+
99
+ ### Added (API host)
100
+ - **Per-release OG cards** — `GET /og/blog/v<X.Y.Z>.png` composes a 1200×630 card for each minor+ release: dark radial background, hero avatar (deterministic from `navii <version>` seed + `mood: happy`, transparent so the mascot floats on the gradient), version pill, headline parsed from CHANGELOG, date, and `navii.dev/blog` brand mark. Cached per version. `/blog/v<X.Y.Z>` now sets `og:image` + `twitter:image` to this URL so social previews show the release-specific card instead of the generic landing OG image.
101
+
102
+ ## [0.23.6] - 2026-05-27
103
+
104
+ ### Removed (API host)
105
+ - Ghana Duty logo from the landing "built with navii" wall (asset + markup). Wall keeps Elorm UI, Golly Express, Fleetlinq.
106
+
107
+ ## [0.23.5] - 2026-05-27
108
+
109
+ ### Added (API host)
110
+ - Ghana Duty logo (PNG) in the landing "built with navii" wall, alongside Elorm UI.
111
+
112
+ ## [0.23.4] - 2026-05-26
113
+
114
+ ### Fixed (API tests)
115
+ - `license/verify` "route not mounted" test expected `404` but the app's catchall `notFound` handler returns `302 → /`. Test updated to match actual behavior. v0.23.3 release pipeline failed on this stale expectation; v0.23.4 reships the `/blog` timeline with the fix.
116
+
117
+ ## [0.23.3] - 2026-05-26 (yanked — test pipeline failed)
118
+
119
+ ### Added (API host)
120
+ - **`/blog` — release timeline.** New page parses `CHANGELOG.md` at request time, surfaces minor+ releases (`x.y.0`) only, links to GitHub release notes per entry. Per-release permalinks at `/blog/v<x.y.z>`. Hero avatar per release derived from the version string. Sitemap includes every release URL. Linked from landing + docs nav.
121
+
122
+ ## [0.23.2] - 2026-05-26
123
+
124
+ ### Changed (docs)
125
+ - Public READMEs (root, `@usenavii/core`, `@usenavii/react`) and the live API reference at `/docs/http-api` + `/docs/sdk-core` now document `AvatarOptions.mood`, the new `Palette` object override on `build()`, and the `?mood=` query param on `/avatar/:seed`. React README memo-deps line corrected to match source (`mood`, `palette`, `styleHint` in; stale `tileBg` claim out).
126
+ - Chore release — image rebuild deploys the updated landing page and `/docs/*` pages to `api.navii.dev`. No npm package contents changed (publish step no-ops on existing versions).
127
+
9
128
  ## [0.23.1] - 2026-05-26
10
129
 
11
130
  ### Fixed (release)
@@ -20,10 +139,6 @@ Format: [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). Versioning: [S
20
139
 
21
140
  ### Added (API host)
22
141
  - `GET /avatar/:seed?mood=happy|serious|sleepy|wink|neutral` — server-side mood overlay. PNG cache key extended with `m=` so moods don't collide.
23
-
24
-
25
-
26
- ### Added (API host)
27
142
  - Elorm UI logo (inline SVG, `currentColor`) in the landing "built with navii" wall, sized via new `.logo svg.lg` rule.
28
143
 
29
144
  ## [0.22.4] - 2026-05-26
@@ -83,7 +198,7 @@ Format: [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). Versioning: [S
83
198
  - Mono switched to full-bleed squircle (was contained orb) for editorial tile look.
84
199
 
85
200
  ### Added (API host)
86
- - **Polar.sh license verification** — `POST /license/verify` now proxies to Polar's `/v1/customer-portal/license-keys/validate` (replaces Gumroad). Validates `status === 'granted'`, expiry, and optional benefit-id match.
201
+ - **Polar.sh license verification** — `POST /license/verify` now proxies to Polar's `/v1/customer-portal/license-keys/validate` (replaces Polar.sh). Validates `status === 'granted'`, expiry, and optional benefit-id match.
87
202
  - **`GET /checkout`** — redirects to Polar checkout w/ configured product preselected. Powered by `@polar-sh/hono`.
88
203
  - **`GET /portal`** — Polar customer portal proxy (license re-fetch, refund request).
89
204
  - **`POST /polar/webhooks`** — signature-verified webhook receiver, logs events.
@@ -92,12 +207,12 @@ Format: [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). Versioning: [S
92
207
  - 8 new license unit tests w/ fetch mock covering granted/revoked/expired/wrong-product/upstream-error paths.
93
208
 
94
209
  ### Changed (API host)
95
- - Privacy page swapped Gumroad references for Polar.sh.
96
- - `AppOptions` renamed `gumroadProductPermalink` → `polarOrganizationId` + related Polar fields.
210
+ - Privacy page swapped Polar.sh references for Polar.sh.
211
+ - `AppOptions` renamed `Polar.shProductPermalink` → `polarOrganizationId` + related Polar fields.
97
212
 
98
213
  ### Added (Figma plugin)
99
214
  - **Style hint pill row** in Packs panel — Auto / Masc / Femme / Neutral toggle. Persisted via `navii.style` localStorage. Disabled when no pack active.
100
- - Plugin checkout URL now points at `${API_BASE}/checkout` (instead of hardcoded Gumroad link), letting us swap payment providers without re-publishing.
215
+ - Plugin checkout URL now points at `${API_BASE}/checkout` (instead of hardcoded Polar.sh link), letting us swap payment providers without re-publishing.
101
216
 
102
217
  ### Fixed (Figma plugin)
103
218
  - Left column in Packs panel was not scrollable when content overflowed (e.g. with new Style hint section). Added `overflow-y: auto` + `min-height: 0` to `.col-left`.
package/README.md CHANGED
@@ -43,13 +43,14 @@ Navii.group([user1.id, user2.id, user3.id]);
43
43
 
44
44
  Same seed always produces the same avatar — that's the contract.
45
45
 
46
- | Pass | Result |
47
- | -------------------------- | ------------------------------------------------------- |
48
- | `user.id` / UUID | ✅ Best. Stable and globally unique. |
49
- | `user.email` | ✅ Good. Stable, unique per user. |
50
- | `user.name` alone | ⚠️ Names collide. Two "Alice"s get the same avatar. |
51
- | `${name}-${createdAt}` | Fine fallback if no ID exists. Bake at signup. |
52
- | `Date.now()` at render | **Don't.** Breaks determinism changes on reload. |
46
+ | Pass | Result |
47
+ | -------------------------- | ----------------------------------------------------------- |
48
+ | `user.id` / UUID | ✅ Best. Stable and globally unique. |
49
+ | `seedFromEmail(email)` | ✅ Good. Hashed email — stable, unique, no PII on the wire. |
50
+ | `user.email` (raw) | ⚠️ Leaks email into URLs, logs, Referer headers. Hash it. |
51
+ | `user.name` alone | ⚠️ Names collide. Two "Alice"s get the same avatar. |
52
+ | `${name}-${createdAt}` | Fine fallback if no ID exists. Bake at signup. |
53
+ | `Date.now()` at render | ❌ **Don't.** Breaks determinism — changes on reload. |
53
54
 
54
55
  If your shape is uncertain, use the helper:
55
56
 
@@ -58,7 +59,20 @@ const s = Navii.seed({ id: user.id, email: user.email, name: user.name, createdA
58
59
  const svg = Navii.create(s);
59
60
  ```
60
61
 
61
- It picks the most unique field automatically: `id` → `email` → `name + createdAt` → `name`.
62
+ It picks the most unique field automatically: `id` → `email` → `name + createdAt` → `name`. When the email branch is used it hashes via `seedFromEmail()` by default — pass `{ hashEmail: false }` only to preserve existing raw-email seeds during a migration.
63
+
64
+ ### Using emails as seeds
65
+
66
+ Raw emails in URLs leak through server logs, Referer headers, browser history, and CDN cache keys. Hash the email — `sha256` of the trimmed + lowercased address:
67
+
68
+ ```ts
69
+ import { seedFromEmail, createAvatar } from '@usenavii/core';
70
+
71
+ const s = seedFromEmail(user.email); // sha256 hex, 64 chars
72
+ createAvatar(s);
73
+ ```
74
+
75
+ Two services that both hash with `seedFromEmail()` get the same seed for the same person, so avatars stay consistent across products.
62
76
 
63
77
  ## API
64
78
 
@@ -81,11 +95,43 @@ Navii.{ create, random, render, select, group, seed, build }
81
95
  | ------------ | ----------------------------------------------------- | ------------ |
82
96
  | `size` | `number` (px) | `96` |
83
97
  | `paletteId` | known palette id (e.g. `'mint'`) | seed-derived |
98
+ | `palette` | `Palette` object — runtime/brand palette, no registration in `PALETTES` needed. Wins over `paletteId`. | none |
84
99
  | `background` | `'none' \| 'solid' \| 'ring'` or `{ color }` | seed-derived |
100
+ | `mood` | `'neutral' \| 'happy' \| 'serious' \| 'sleepy' \| 'wink'` — overrides seed-derived eyes + mouth with a curated pair. Same seed + same mood = byte-identical. Bypasses pack eye/mouth constraints by design. | `'neutral'` |
85
101
  | `tileBg` | CSS color or `'auto'` (palette accent) | none |
86
102
  | `title` | accessible label (sets `<title>` + `aria-label`) | none |
87
103
  | `animated` | `boolean` — idle float / blink / sway / pulse / twinkle | `false` |
88
104
 
105
+ #### Mood overlay
106
+
107
+ Same seed, four expressions — body / palette / topper stay identical:
108
+
109
+ ```ts
110
+ import { createAvatar } from '@usenavii/core';
111
+
112
+ createAvatar('alice'); // neutral (seed-derived)
113
+ createAvatar('alice', { mood: 'happy' }); // wide eyes + smile
114
+ createAvatar('alice', { mood: 'serious' }); // squint + flat mouth
115
+ createAvatar('alice', { mood: 'sleepy' }); // sleepy + dot
116
+ createAvatar('alice', { mood: 'wink' }); // wink + smirk
117
+ ```
118
+
119
+ #### Runtime palette injection (`build()` only)
120
+
121
+ Pass a `Palette` object (e.g. pulled from Figma variables or design tokens) without registering it in the global `PALETTES` lookup:
122
+
123
+ ```ts
124
+ import { build } from '@usenavii/core';
125
+
126
+ const acmePalette = {
127
+ id: 'acme', name: 'Acme Brand',
128
+ bodyFrom: '#FF5722', bodyTo: '#FFA726',
129
+ feature: '#1A1A1A', background: '#FFF8F0',
130
+ };
131
+
132
+ build({ body: 'tall', eyes: 'wide' }, { palette: acmePalette, size: 128 });
133
+ ```
134
+
89
135
  ### `random()` — pick a seed for you
90
136
 
91
137
  For "spin again" UX, onboarding before the user picks an avatar, dev/demo seeding. Returns the chosen seed so you can persist it:
package/dist/index.cjs CHANGED
@@ -1668,13 +1668,176 @@ function clamp(n, lo, hi) {
1668
1668
  return Math.max(lo, Math.min(hi, n));
1669
1669
  }
1670
1670
 
1671
+ // src/sha256.ts
1672
+ var K = new Uint32Array([
1673
+ 1116352408,
1674
+ 1899447441,
1675
+ 3049323471,
1676
+ 3921009573,
1677
+ 961987163,
1678
+ 1508970993,
1679
+ 2453635748,
1680
+ 2870763221,
1681
+ 3624381080,
1682
+ 310598401,
1683
+ 607225278,
1684
+ 1426881987,
1685
+ 1925078388,
1686
+ 2162078206,
1687
+ 2614888103,
1688
+ 3248222580,
1689
+ 3835390401,
1690
+ 4022224774,
1691
+ 264347078,
1692
+ 604807628,
1693
+ 770255983,
1694
+ 1249150122,
1695
+ 1555081692,
1696
+ 1996064986,
1697
+ 2554220882,
1698
+ 2821834349,
1699
+ 2952996808,
1700
+ 3210313671,
1701
+ 3336571891,
1702
+ 3584528711,
1703
+ 113926993,
1704
+ 338241895,
1705
+ 666307205,
1706
+ 773529912,
1707
+ 1294757372,
1708
+ 1396182291,
1709
+ 1695183700,
1710
+ 1986661051,
1711
+ 2177026350,
1712
+ 2456956037,
1713
+ 2730485921,
1714
+ 2820302411,
1715
+ 3259730800,
1716
+ 3345764771,
1717
+ 3516065817,
1718
+ 3600352804,
1719
+ 4094571909,
1720
+ 275423344,
1721
+ 430227734,
1722
+ 506948616,
1723
+ 659060556,
1724
+ 883997877,
1725
+ 958139571,
1726
+ 1322822218,
1727
+ 1537002063,
1728
+ 1747873779,
1729
+ 1955562222,
1730
+ 2024104815,
1731
+ 2227730452,
1732
+ 2361852424,
1733
+ 2428436474,
1734
+ 2756734187,
1735
+ 3204031479,
1736
+ 3329325298
1737
+ ]);
1738
+ function rotr(x, n) {
1739
+ return (x >>> n | x << 32 - n) >>> 0;
1740
+ }
1741
+ function utf8Encode(str) {
1742
+ if (typeof TextEncoder !== "undefined") return new TextEncoder().encode(str);
1743
+ const out = [];
1744
+ for (let i = 0; i < str.length; i++) {
1745
+ let c = str.charCodeAt(i);
1746
+ if (c < 128) out.push(c);
1747
+ else if (c < 2048) {
1748
+ out.push(192 | c >> 6, 128 | c & 63);
1749
+ } else if (c < 55296 || c >= 57344) {
1750
+ out.push(224 | c >> 12, 128 | c >> 6 & 63, 128 | c & 63);
1751
+ } else {
1752
+ i++;
1753
+ c = 65536 + ((c & 1023) << 10 | str.charCodeAt(i) & 1023);
1754
+ out.push(
1755
+ 240 | c >> 18,
1756
+ 128 | c >> 12 & 63,
1757
+ 128 | c >> 6 & 63,
1758
+ 128 | c & 63
1759
+ );
1760
+ }
1761
+ }
1762
+ return new Uint8Array(out);
1763
+ }
1764
+ function sha256Hex(input) {
1765
+ const msg = utf8Encode(input);
1766
+ const bitLen = msg.length * 8;
1767
+ const padLen = (msg.length + 9 + 63 & -64) - msg.length;
1768
+ const buf = new Uint8Array(msg.length + padLen);
1769
+ buf.set(msg);
1770
+ buf[msg.length] = 128;
1771
+ const dv = new DataView(buf.buffer);
1772
+ dv.setUint32(buf.length - 4, bitLen >>> 0, false);
1773
+ dv.setUint32(buf.length - 8, Math.floor(bitLen / 4294967296), false);
1774
+ const H = new Uint32Array([
1775
+ 1779033703,
1776
+ 3144134277,
1777
+ 1013904242,
1778
+ 2773480762,
1779
+ 1359893119,
1780
+ 2600822924,
1781
+ 528734635,
1782
+ 1541459225
1783
+ ]);
1784
+ const W = new Uint32Array(64);
1785
+ for (let chunk = 0; chunk < buf.length; chunk += 64) {
1786
+ for (let i = 0; i < 16; i++) W[i] = dv.getUint32(chunk + i * 4, false);
1787
+ for (let i = 16; i < 64; i++) {
1788
+ const s0 = rotr(W[i - 15], 7) ^ rotr(W[i - 15], 18) ^ W[i - 15] >>> 3;
1789
+ const s1 = rotr(W[i - 2], 17) ^ rotr(W[i - 2], 19) ^ W[i - 2] >>> 10;
1790
+ W[i] = W[i - 16] + s0 + W[i - 7] + s1 >>> 0;
1791
+ }
1792
+ let a = H[0], b = H[1], c = H[2], d = H[3];
1793
+ let e = H[4], f = H[5], g = H[6], h = H[7];
1794
+ for (let i = 0; i < 64; i++) {
1795
+ const S1 = rotr(e, 6) ^ rotr(e, 11) ^ rotr(e, 25);
1796
+ const ch = e & f ^ ~e & g;
1797
+ const t1 = h + S1 + ch + K[i] + W[i] >>> 0;
1798
+ const S0 = rotr(a, 2) ^ rotr(a, 13) ^ rotr(a, 22);
1799
+ const mj = a & b ^ a & c ^ b & c;
1800
+ const t2 = S0 + mj >>> 0;
1801
+ h = g;
1802
+ g = f;
1803
+ f = e;
1804
+ e = d + t1 >>> 0;
1805
+ d = c;
1806
+ c = b;
1807
+ b = a;
1808
+ a = t1 + t2 >>> 0;
1809
+ }
1810
+ H[0] = H[0] + a >>> 0;
1811
+ H[1] = H[1] + b >>> 0;
1812
+ H[2] = H[2] + c >>> 0;
1813
+ H[3] = H[3] + d >>> 0;
1814
+ H[4] = H[4] + e >>> 0;
1815
+ H[5] = H[5] + f >>> 0;
1816
+ H[6] = H[6] + g >>> 0;
1817
+ H[7] = H[7] + h >>> 0;
1818
+ }
1819
+ let out = "";
1820
+ for (let i = 0; i < 8; i++) out += H[i].toString(16).padStart(8, "0");
1821
+ return out;
1822
+ }
1823
+
1671
1824
  // src/seed.ts
1672
- function seed(fields) {
1825
+ function normalizeEmail(email) {
1826
+ return email.trim().toLowerCase().normalize("NFC");
1827
+ }
1828
+ function seedFromEmail(email) {
1829
+ if (typeof email !== "string" || email.length === 0) {
1830
+ throw new Error("navii: seedFromEmail() requires a non-empty string");
1831
+ }
1832
+ return sha256Hex(normalizeEmail(email));
1833
+ }
1834
+ function seed(fields, options = {}) {
1835
+ const hashEmail = options.hashEmail ?? true;
1673
1836
  if (fields.id !== null && fields.id !== void 0 && String(fields.id).length > 0) {
1674
1837
  return String(fields.id);
1675
1838
  }
1676
1839
  if (fields.email && fields.email.length > 0) {
1677
- return fields.email;
1840
+ return hashEmail ? seedFromEmail(fields.email) : fields.email;
1678
1841
  }
1679
1842
  if (fields.name && fields.name.length > 0) {
1680
1843
  if (fields.createdAt !== null && fields.createdAt !== void 0) {
@@ -1733,6 +1896,7 @@ var Navii = {
1733
1896
  select: selectAvatar,
1734
1897
  group: renderGroup,
1735
1898
  seed,
1899
+ seedFromEmail,
1736
1900
  build
1737
1901
  };
1738
1902
 
@@ -1743,12 +1907,15 @@ exports.build = build;
1743
1907
  exports.createAvatar = createAvatar;
1744
1908
  exports.createRng = createRng;
1745
1909
  exports.cyrb53 = cyrb53;
1910
+ exports.normalizeEmail = normalizeEmail;
1746
1911
  exports.random = random;
1747
1912
  exports.renderAvatar = renderAvatar;
1748
1913
  exports.renderAvatarInner = renderAvatarInner;
1749
1914
  exports.renderGroup = renderGroup;
1750
1915
  exports.resolvePacks = resolvePacks;
1751
1916
  exports.seed = seed;
1917
+ exports.seedFromEmail = seedFromEmail;
1752
1918
  exports.selectAvatar = selectAvatar;
1919
+ exports.sha256Hex = sha256Hex;
1753
1920
  //# sourceMappingURL=index.cjs.map
1754
1921
  //# sourceMappingURL=index.cjs.map