@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 +123 -8
- package/README.md +54 -8
- package/dist/index.cjs +169 -2
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +55 -2
- package/dist/index.d.ts +55 -2
- package/dist/index.js +167 -3
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.d.cts
CHANGED
|
@@ -82,6 +82,28 @@ declare function renderGroup(seeds: string[], options?: GroupOptions): string;
|
|
|
82
82
|
* it with `createdAt` makes the result globally unique while remaining
|
|
83
83
|
* stable across renders (assuming `createdAt` is set once at signup).
|
|
84
84
|
*/
|
|
85
|
+
/**
|
|
86
|
+
* Normalize an email the same way Gravatar does — trim + lowercase, NFC.
|
|
87
|
+
* Exported so callers can reproduce the canonical form before hashing.
|
|
88
|
+
*/
|
|
89
|
+
declare function normalizeEmail(email: string): string;
|
|
90
|
+
/**
|
|
91
|
+
* Turn an email into a stable, opaque seed using Gravatar's scheme:
|
|
92
|
+
* `sha256(trim(lowercase(email)))` → lowercase hex.
|
|
93
|
+
*
|
|
94
|
+
* Why: passing raw emails as seeds leaks them through URLs (server logs,
|
|
95
|
+
* Referer headers, browser history, CDN cache keys, analytics). The hash
|
|
96
|
+
* is stable across systems that normalize the same way, so two products
|
|
97
|
+
* looking up the same person get the same avatar.
|
|
98
|
+
*
|
|
99
|
+
* @example
|
|
100
|
+
* ```ts
|
|
101
|
+
* const s = seedFromEmail(user.email);
|
|
102
|
+
* createAvatar(s); // safe to log
|
|
103
|
+
* // or hit the API: `/avatar/${s}.svg` // no plaintext email on the wire
|
|
104
|
+
* ```
|
|
105
|
+
*/
|
|
106
|
+
declare function seedFromEmail(email: string): string;
|
|
85
107
|
interface SeedFields {
|
|
86
108
|
/** Stable primary key (database id, UUID, OAuth sub). Best choice. */
|
|
87
109
|
id?: string | number | null | undefined;
|
|
@@ -92,6 +114,18 @@ interface SeedFields {
|
|
|
92
114
|
/** Account creation time. Combined with `name` to bake uniqueness in at signup. */
|
|
93
115
|
createdAt?: string | number | Date | null | undefined;
|
|
94
116
|
}
|
|
117
|
+
interface SeedOptions {
|
|
118
|
+
/**
|
|
119
|
+
* When the email branch is used, hash the email instead of returning it
|
|
120
|
+
* raw. Hashing keeps the seed stable but stops the address from leaking
|
|
121
|
+
* into URLs, server logs, and Referer headers. Default `true` from v1.
|
|
122
|
+
*
|
|
123
|
+
* Set to `false` to opt back into the legacy plaintext-email behavior —
|
|
124
|
+
* useful for migrations where existing avatars are keyed off the raw
|
|
125
|
+
* email and you don't want every user's face to change.
|
|
126
|
+
*/
|
|
127
|
+
hashEmail?: boolean;
|
|
128
|
+
}
|
|
95
129
|
/**
|
|
96
130
|
* Compose a stable seed string from the most unique field available.
|
|
97
131
|
*
|
|
@@ -107,9 +141,27 @@ interface SeedFields {
|
|
|
107
141
|
* // → "Alice|1700000000000"
|
|
108
142
|
* ```
|
|
109
143
|
*
|
|
144
|
+
* @example Opt out of email hashing (legacy behavior):
|
|
145
|
+
* ```ts
|
|
146
|
+
* seed({ email: 'a@b.c' }, { hashEmail: false });
|
|
147
|
+
* // → "a@b.c" — avoid; only for migrating off the old default.
|
|
148
|
+
* ```
|
|
149
|
+
*
|
|
110
150
|
* @throws if no usable field is provided.
|
|
111
151
|
*/
|
|
112
|
-
declare function seed(fields: SeedFields): string;
|
|
152
|
+
declare function seed(fields: SeedFields, options?: SeedOptions): string;
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Sync SHA-256 (FIPS 180-4) → lowercase hex.
|
|
156
|
+
*
|
|
157
|
+
* Used by `seedFromEmail()` to match Gravatar's seed format
|
|
158
|
+
* (sha256 of trimmed, lowercased email). Sync API on purpose —
|
|
159
|
+
* a seed helper that returns a Promise is a footgun in render code.
|
|
160
|
+
*
|
|
161
|
+
* ~50 lines, no deps, zero allocations after init. Not for crypto
|
|
162
|
+
* use beyond opaque-identifier hashing.
|
|
163
|
+
*/
|
|
164
|
+
declare function sha256Hex(input: string): string;
|
|
113
165
|
|
|
114
166
|
/**
|
|
115
167
|
* Direct construction of an avatar from explicit part choices — no seed.
|
|
@@ -382,7 +434,8 @@ declare const Navii: {
|
|
|
382
434
|
readonly select: typeof selectAvatar;
|
|
383
435
|
readonly group: typeof renderGroup;
|
|
384
436
|
readonly seed: typeof seed;
|
|
437
|
+
readonly seedFromEmail: typeof seedFromEmail;
|
|
385
438
|
readonly build: typeof build;
|
|
386
439
|
};
|
|
387
440
|
|
|
388
|
-
export { AccessoryId, AntennaStyleId, AvatarOptions, AvatarSpec, BUILT_IN_PACKS, BackgroundId, BodyShapeId, type BuildSpec, EyeStyleId, type GroupOptions, MouthStyleId, Navii, OutfitId, PACK_REGISTRY, type Pack, type PackRegistry, Palette, type SeedFields, TopperId, build, createAvatar, createRng, cyrb53, random, renderAvatar, renderAvatarInner, renderGroup, resolvePacks, seed, selectAvatar };
|
|
441
|
+
export { AccessoryId, AntennaStyleId, AvatarOptions, AvatarSpec, BUILT_IN_PACKS, BackgroundId, BodyShapeId, type BuildSpec, EyeStyleId, type GroupOptions, MouthStyleId, Navii, OutfitId, PACK_REGISTRY, type Pack, type PackRegistry, Palette, type SeedFields, type SeedOptions, TopperId, build, createAvatar, createRng, cyrb53, normalizeEmail, random, renderAvatar, renderAvatarInner, renderGroup, resolvePacks, seed, seedFromEmail, selectAvatar, sha256Hex };
|
package/dist/index.d.ts
CHANGED
|
@@ -82,6 +82,28 @@ declare function renderGroup(seeds: string[], options?: GroupOptions): string;
|
|
|
82
82
|
* it with `createdAt` makes the result globally unique while remaining
|
|
83
83
|
* stable across renders (assuming `createdAt` is set once at signup).
|
|
84
84
|
*/
|
|
85
|
+
/**
|
|
86
|
+
* Normalize an email the same way Gravatar does — trim + lowercase, NFC.
|
|
87
|
+
* Exported so callers can reproduce the canonical form before hashing.
|
|
88
|
+
*/
|
|
89
|
+
declare function normalizeEmail(email: string): string;
|
|
90
|
+
/**
|
|
91
|
+
* Turn an email into a stable, opaque seed using Gravatar's scheme:
|
|
92
|
+
* `sha256(trim(lowercase(email)))` → lowercase hex.
|
|
93
|
+
*
|
|
94
|
+
* Why: passing raw emails as seeds leaks them through URLs (server logs,
|
|
95
|
+
* Referer headers, browser history, CDN cache keys, analytics). The hash
|
|
96
|
+
* is stable across systems that normalize the same way, so two products
|
|
97
|
+
* looking up the same person get the same avatar.
|
|
98
|
+
*
|
|
99
|
+
* @example
|
|
100
|
+
* ```ts
|
|
101
|
+
* const s = seedFromEmail(user.email);
|
|
102
|
+
* createAvatar(s); // safe to log
|
|
103
|
+
* // or hit the API: `/avatar/${s}.svg` // no plaintext email on the wire
|
|
104
|
+
* ```
|
|
105
|
+
*/
|
|
106
|
+
declare function seedFromEmail(email: string): string;
|
|
85
107
|
interface SeedFields {
|
|
86
108
|
/** Stable primary key (database id, UUID, OAuth sub). Best choice. */
|
|
87
109
|
id?: string | number | null | undefined;
|
|
@@ -92,6 +114,18 @@ interface SeedFields {
|
|
|
92
114
|
/** Account creation time. Combined with `name` to bake uniqueness in at signup. */
|
|
93
115
|
createdAt?: string | number | Date | null | undefined;
|
|
94
116
|
}
|
|
117
|
+
interface SeedOptions {
|
|
118
|
+
/**
|
|
119
|
+
* When the email branch is used, hash the email instead of returning it
|
|
120
|
+
* raw. Hashing keeps the seed stable but stops the address from leaking
|
|
121
|
+
* into URLs, server logs, and Referer headers. Default `true` from v1.
|
|
122
|
+
*
|
|
123
|
+
* Set to `false` to opt back into the legacy plaintext-email behavior —
|
|
124
|
+
* useful for migrations where existing avatars are keyed off the raw
|
|
125
|
+
* email and you don't want every user's face to change.
|
|
126
|
+
*/
|
|
127
|
+
hashEmail?: boolean;
|
|
128
|
+
}
|
|
95
129
|
/**
|
|
96
130
|
* Compose a stable seed string from the most unique field available.
|
|
97
131
|
*
|
|
@@ -107,9 +141,27 @@ interface SeedFields {
|
|
|
107
141
|
* // → "Alice|1700000000000"
|
|
108
142
|
* ```
|
|
109
143
|
*
|
|
144
|
+
* @example Opt out of email hashing (legacy behavior):
|
|
145
|
+
* ```ts
|
|
146
|
+
* seed({ email: 'a@b.c' }, { hashEmail: false });
|
|
147
|
+
* // → "a@b.c" — avoid; only for migrating off the old default.
|
|
148
|
+
* ```
|
|
149
|
+
*
|
|
110
150
|
* @throws if no usable field is provided.
|
|
111
151
|
*/
|
|
112
|
-
declare function seed(fields: SeedFields): string;
|
|
152
|
+
declare function seed(fields: SeedFields, options?: SeedOptions): string;
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Sync SHA-256 (FIPS 180-4) → lowercase hex.
|
|
156
|
+
*
|
|
157
|
+
* Used by `seedFromEmail()` to match Gravatar's seed format
|
|
158
|
+
* (sha256 of trimmed, lowercased email). Sync API on purpose —
|
|
159
|
+
* a seed helper that returns a Promise is a footgun in render code.
|
|
160
|
+
*
|
|
161
|
+
* ~50 lines, no deps, zero allocations after init. Not for crypto
|
|
162
|
+
* use beyond opaque-identifier hashing.
|
|
163
|
+
*/
|
|
164
|
+
declare function sha256Hex(input: string): string;
|
|
113
165
|
|
|
114
166
|
/**
|
|
115
167
|
* Direct construction of an avatar from explicit part choices — no seed.
|
|
@@ -382,7 +434,8 @@ declare const Navii: {
|
|
|
382
434
|
readonly select: typeof selectAvatar;
|
|
383
435
|
readonly group: typeof renderGroup;
|
|
384
436
|
readonly seed: typeof seed;
|
|
437
|
+
readonly seedFromEmail: typeof seedFromEmail;
|
|
385
438
|
readonly build: typeof build;
|
|
386
439
|
};
|
|
387
440
|
|
|
388
|
-
export { AccessoryId, AntennaStyleId, AvatarOptions, AvatarSpec, BUILT_IN_PACKS, BackgroundId, BodyShapeId, type BuildSpec, EyeStyleId, type GroupOptions, MouthStyleId, Navii, OutfitId, PACK_REGISTRY, type Pack, type PackRegistry, Palette, type SeedFields, TopperId, build, createAvatar, createRng, cyrb53, random, renderAvatar, renderAvatarInner, renderGroup, resolvePacks, seed, selectAvatar };
|
|
441
|
+
export { AccessoryId, AntennaStyleId, AvatarOptions, AvatarSpec, BUILT_IN_PACKS, BackgroundId, BodyShapeId, type BuildSpec, EyeStyleId, type GroupOptions, MouthStyleId, Navii, OutfitId, PACK_REGISTRY, type Pack, type PackRegistry, Palette, type SeedFields, type SeedOptions, TopperId, build, createAvatar, createRng, cyrb53, normalizeEmail, random, renderAvatar, renderAvatarInner, renderGroup, resolvePacks, seed, seedFromEmail, selectAvatar, sha256Hex };
|
package/dist/index.js
CHANGED
|
@@ -1666,13 +1666,176 @@ function clamp(n, lo, hi) {
|
|
|
1666
1666
|
return Math.max(lo, Math.min(hi, n));
|
|
1667
1667
|
}
|
|
1668
1668
|
|
|
1669
|
+
// src/sha256.ts
|
|
1670
|
+
var K = new Uint32Array([
|
|
1671
|
+
1116352408,
|
|
1672
|
+
1899447441,
|
|
1673
|
+
3049323471,
|
|
1674
|
+
3921009573,
|
|
1675
|
+
961987163,
|
|
1676
|
+
1508970993,
|
|
1677
|
+
2453635748,
|
|
1678
|
+
2870763221,
|
|
1679
|
+
3624381080,
|
|
1680
|
+
310598401,
|
|
1681
|
+
607225278,
|
|
1682
|
+
1426881987,
|
|
1683
|
+
1925078388,
|
|
1684
|
+
2162078206,
|
|
1685
|
+
2614888103,
|
|
1686
|
+
3248222580,
|
|
1687
|
+
3835390401,
|
|
1688
|
+
4022224774,
|
|
1689
|
+
264347078,
|
|
1690
|
+
604807628,
|
|
1691
|
+
770255983,
|
|
1692
|
+
1249150122,
|
|
1693
|
+
1555081692,
|
|
1694
|
+
1996064986,
|
|
1695
|
+
2554220882,
|
|
1696
|
+
2821834349,
|
|
1697
|
+
2952996808,
|
|
1698
|
+
3210313671,
|
|
1699
|
+
3336571891,
|
|
1700
|
+
3584528711,
|
|
1701
|
+
113926993,
|
|
1702
|
+
338241895,
|
|
1703
|
+
666307205,
|
|
1704
|
+
773529912,
|
|
1705
|
+
1294757372,
|
|
1706
|
+
1396182291,
|
|
1707
|
+
1695183700,
|
|
1708
|
+
1986661051,
|
|
1709
|
+
2177026350,
|
|
1710
|
+
2456956037,
|
|
1711
|
+
2730485921,
|
|
1712
|
+
2820302411,
|
|
1713
|
+
3259730800,
|
|
1714
|
+
3345764771,
|
|
1715
|
+
3516065817,
|
|
1716
|
+
3600352804,
|
|
1717
|
+
4094571909,
|
|
1718
|
+
275423344,
|
|
1719
|
+
430227734,
|
|
1720
|
+
506948616,
|
|
1721
|
+
659060556,
|
|
1722
|
+
883997877,
|
|
1723
|
+
958139571,
|
|
1724
|
+
1322822218,
|
|
1725
|
+
1537002063,
|
|
1726
|
+
1747873779,
|
|
1727
|
+
1955562222,
|
|
1728
|
+
2024104815,
|
|
1729
|
+
2227730452,
|
|
1730
|
+
2361852424,
|
|
1731
|
+
2428436474,
|
|
1732
|
+
2756734187,
|
|
1733
|
+
3204031479,
|
|
1734
|
+
3329325298
|
|
1735
|
+
]);
|
|
1736
|
+
function rotr(x, n) {
|
|
1737
|
+
return (x >>> n | x << 32 - n) >>> 0;
|
|
1738
|
+
}
|
|
1739
|
+
function utf8Encode(str) {
|
|
1740
|
+
if (typeof TextEncoder !== "undefined") return new TextEncoder().encode(str);
|
|
1741
|
+
const out = [];
|
|
1742
|
+
for (let i = 0; i < str.length; i++) {
|
|
1743
|
+
let c = str.charCodeAt(i);
|
|
1744
|
+
if (c < 128) out.push(c);
|
|
1745
|
+
else if (c < 2048) {
|
|
1746
|
+
out.push(192 | c >> 6, 128 | c & 63);
|
|
1747
|
+
} else if (c < 55296 || c >= 57344) {
|
|
1748
|
+
out.push(224 | c >> 12, 128 | c >> 6 & 63, 128 | c & 63);
|
|
1749
|
+
} else {
|
|
1750
|
+
i++;
|
|
1751
|
+
c = 65536 + ((c & 1023) << 10 | str.charCodeAt(i) & 1023);
|
|
1752
|
+
out.push(
|
|
1753
|
+
240 | c >> 18,
|
|
1754
|
+
128 | c >> 12 & 63,
|
|
1755
|
+
128 | c >> 6 & 63,
|
|
1756
|
+
128 | c & 63
|
|
1757
|
+
);
|
|
1758
|
+
}
|
|
1759
|
+
}
|
|
1760
|
+
return new Uint8Array(out);
|
|
1761
|
+
}
|
|
1762
|
+
function sha256Hex(input) {
|
|
1763
|
+
const msg = utf8Encode(input);
|
|
1764
|
+
const bitLen = msg.length * 8;
|
|
1765
|
+
const padLen = (msg.length + 9 + 63 & -64) - msg.length;
|
|
1766
|
+
const buf = new Uint8Array(msg.length + padLen);
|
|
1767
|
+
buf.set(msg);
|
|
1768
|
+
buf[msg.length] = 128;
|
|
1769
|
+
const dv = new DataView(buf.buffer);
|
|
1770
|
+
dv.setUint32(buf.length - 4, bitLen >>> 0, false);
|
|
1771
|
+
dv.setUint32(buf.length - 8, Math.floor(bitLen / 4294967296), false);
|
|
1772
|
+
const H = new Uint32Array([
|
|
1773
|
+
1779033703,
|
|
1774
|
+
3144134277,
|
|
1775
|
+
1013904242,
|
|
1776
|
+
2773480762,
|
|
1777
|
+
1359893119,
|
|
1778
|
+
2600822924,
|
|
1779
|
+
528734635,
|
|
1780
|
+
1541459225
|
|
1781
|
+
]);
|
|
1782
|
+
const W = new Uint32Array(64);
|
|
1783
|
+
for (let chunk = 0; chunk < buf.length; chunk += 64) {
|
|
1784
|
+
for (let i = 0; i < 16; i++) W[i] = dv.getUint32(chunk + i * 4, false);
|
|
1785
|
+
for (let i = 16; i < 64; i++) {
|
|
1786
|
+
const s0 = rotr(W[i - 15], 7) ^ rotr(W[i - 15], 18) ^ W[i - 15] >>> 3;
|
|
1787
|
+
const s1 = rotr(W[i - 2], 17) ^ rotr(W[i - 2], 19) ^ W[i - 2] >>> 10;
|
|
1788
|
+
W[i] = W[i - 16] + s0 + W[i - 7] + s1 >>> 0;
|
|
1789
|
+
}
|
|
1790
|
+
let a = H[0], b = H[1], c = H[2], d = H[3];
|
|
1791
|
+
let e = H[4], f = H[5], g = H[6], h = H[7];
|
|
1792
|
+
for (let i = 0; i < 64; i++) {
|
|
1793
|
+
const S1 = rotr(e, 6) ^ rotr(e, 11) ^ rotr(e, 25);
|
|
1794
|
+
const ch = e & f ^ ~e & g;
|
|
1795
|
+
const t1 = h + S1 + ch + K[i] + W[i] >>> 0;
|
|
1796
|
+
const S0 = rotr(a, 2) ^ rotr(a, 13) ^ rotr(a, 22);
|
|
1797
|
+
const mj = a & b ^ a & c ^ b & c;
|
|
1798
|
+
const t2 = S0 + mj >>> 0;
|
|
1799
|
+
h = g;
|
|
1800
|
+
g = f;
|
|
1801
|
+
f = e;
|
|
1802
|
+
e = d + t1 >>> 0;
|
|
1803
|
+
d = c;
|
|
1804
|
+
c = b;
|
|
1805
|
+
b = a;
|
|
1806
|
+
a = t1 + t2 >>> 0;
|
|
1807
|
+
}
|
|
1808
|
+
H[0] = H[0] + a >>> 0;
|
|
1809
|
+
H[1] = H[1] + b >>> 0;
|
|
1810
|
+
H[2] = H[2] + c >>> 0;
|
|
1811
|
+
H[3] = H[3] + d >>> 0;
|
|
1812
|
+
H[4] = H[4] + e >>> 0;
|
|
1813
|
+
H[5] = H[5] + f >>> 0;
|
|
1814
|
+
H[6] = H[6] + g >>> 0;
|
|
1815
|
+
H[7] = H[7] + h >>> 0;
|
|
1816
|
+
}
|
|
1817
|
+
let out = "";
|
|
1818
|
+
for (let i = 0; i < 8; i++) out += H[i].toString(16).padStart(8, "0");
|
|
1819
|
+
return out;
|
|
1820
|
+
}
|
|
1821
|
+
|
|
1669
1822
|
// src/seed.ts
|
|
1670
|
-
function
|
|
1823
|
+
function normalizeEmail(email) {
|
|
1824
|
+
return email.trim().toLowerCase().normalize("NFC");
|
|
1825
|
+
}
|
|
1826
|
+
function seedFromEmail(email) {
|
|
1827
|
+
if (typeof email !== "string" || email.length === 0) {
|
|
1828
|
+
throw new Error("navii: seedFromEmail() requires a non-empty string");
|
|
1829
|
+
}
|
|
1830
|
+
return sha256Hex(normalizeEmail(email));
|
|
1831
|
+
}
|
|
1832
|
+
function seed(fields, options = {}) {
|
|
1833
|
+
const hashEmail = options.hashEmail ?? true;
|
|
1671
1834
|
if (fields.id !== null && fields.id !== void 0 && String(fields.id).length > 0) {
|
|
1672
1835
|
return String(fields.id);
|
|
1673
1836
|
}
|
|
1674
1837
|
if (fields.email && fields.email.length > 0) {
|
|
1675
|
-
return fields.email;
|
|
1838
|
+
return hashEmail ? seedFromEmail(fields.email) : fields.email;
|
|
1676
1839
|
}
|
|
1677
1840
|
if (fields.name && fields.name.length > 0) {
|
|
1678
1841
|
if (fields.createdAt !== null && fields.createdAt !== void 0) {
|
|
@@ -1731,9 +1894,10 @@ var Navii = {
|
|
|
1731
1894
|
select: selectAvatar,
|
|
1732
1895
|
group: renderGroup,
|
|
1733
1896
|
seed,
|
|
1897
|
+
seedFromEmail,
|
|
1734
1898
|
build
|
|
1735
1899
|
};
|
|
1736
1900
|
|
|
1737
|
-
export { BUILT_IN_PACKS, Navii, PACK_REGISTRY, build, createAvatar, createRng, cyrb53, random, renderAvatar, renderAvatarInner, renderGroup, resolvePacks, seed, selectAvatar };
|
|
1901
|
+
export { BUILT_IN_PACKS, Navii, PACK_REGISTRY, build, createAvatar, createRng, cyrb53, normalizeEmail, random, renderAvatar, renderAvatarInner, renderGroup, resolvePacks, seed, seedFromEmail, selectAvatar, sha256Hex };
|
|
1738
1902
|
//# sourceMappingURL=index.js.map
|
|
1739
1903
|
//# sourceMappingURL=index.js.map
|