agent-avatars 1.0.0-rc.2

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/dist/png.mjs ADDED
@@ -0,0 +1,340 @@
1
+ import { deflateSync } from "node:zlib";
2
+ import { mkdirSync, writeFileSync } from "node:fs";
3
+ import { join, resolve } from "node:path";
4
+ import { createAvatarDescriptor } from "./index.mjs";
5
+ import { snapshotRenderableDescriptor } from "./render-descriptor.mjs";
6
+ import { normalizePngSize, normalizeSupersample } from "./png-options.mjs";
7
+
8
+ const PLATFORM_PNG_SIZES = Object.freeze([32, 64, 192, 200]);
9
+ const GRID_W = 5;
10
+ const GRID_H = 4;
11
+ const CELL = 10;
12
+ const GLYPH_X = 39;
13
+ const GLYPH_Y = 44;
14
+ const MAX_PNG_SET_SIZES = 64;
15
+ const MAX_PNG_SET_RENDER_PIXELS = 16_777_216;
16
+
17
+ const CRC_TABLE = (() => {
18
+ const table = new Uint32Array(256);
19
+ for (let index = 0; index < 256; index++) {
20
+ let value = index;
21
+ for (let bit = 0; bit < 8; bit++) {
22
+ value = (value & 1) ? (0xedb88320 ^ (value >>> 1)) : (value >>> 1);
23
+ }
24
+ table[index] = value >>> 0;
25
+ }
26
+ return table;
27
+ })();
28
+
29
+ function parseColor(hex) {
30
+ if (typeof hex !== "string" || !/^#[0-9A-Fa-f]{6}$/.test(hex)) {
31
+ throw new TypeError(`Invalid render color: ${hex}`);
32
+ }
33
+ return [
34
+ Number.parseInt(hex.slice(1, 3), 16),
35
+ Number.parseInt(hex.slice(3, 5), 16),
36
+ Number.parseInt(hex.slice(5, 7), 16),
37
+ ];
38
+ }
39
+
40
+ function blendPixel(buffer, width, x, y, color, alpha = 1) {
41
+ if (x < 0 || y < 0 || x >= width || y >= width || alpha <= 0) return;
42
+ const offset = (y * width + x) * 4;
43
+ const sourceAlpha = Math.max(0, Math.min(1, alpha));
44
+ const destinationAlpha = buffer[offset + 3] / 255;
45
+ const outputAlpha = sourceAlpha + destinationAlpha * (1 - sourceAlpha);
46
+ if (outputAlpha <= 0) return;
47
+
48
+ for (let channel = 0; channel < 3; channel++) {
49
+ const source = color[channel] / 255;
50
+ const destination = buffer[offset + channel] / 255;
51
+ const output = (source * sourceAlpha + destination * destinationAlpha * (1 - sourceAlpha)) / outputAlpha;
52
+ buffer[offset + channel] = Math.round(output * 255);
53
+ }
54
+ buffer[offset + 3] = Math.round(outputAlpha * 255);
55
+ }
56
+
57
+ function fillCircle(buffer, width, scale, centerX, centerY, radius, color, alpha = 1) {
58
+ const cx = centerX * scale;
59
+ const cy = centerY * scale;
60
+ const r = radius * scale;
61
+ const minX = Math.max(0, Math.floor(cx - r));
62
+ const maxX = Math.min(width - 1, Math.ceil(cx + r));
63
+ const minY = Math.max(0, Math.floor(cy - r));
64
+ const maxY = Math.min(width - 1, Math.ceil(cy + r));
65
+ const r2 = r * r;
66
+
67
+ for (let y = minY; y <= maxY; y++) {
68
+ const py = y + 0.5 - cy;
69
+ for (let x = minX; x <= maxX; x++) {
70
+ const px = x + 0.5 - cx;
71
+ if (px * px + py * py <= r2) blendPixel(buffer, width, x, y, color, alpha);
72
+ }
73
+ }
74
+ }
75
+
76
+ function pointInsideRoundedCell(px, py, x, y, size, radius, corners) {
77
+ if (px < x || px > x + size || py < y || py > y + size) return false;
78
+ const [topLeft, topRight, bottomRight, bottomLeft] = corners;
79
+
80
+ if (topLeft && px < x + radius && py < y + radius) {
81
+ const dx = px - (x + radius);
82
+ const dy = py - (y + radius);
83
+ return dx * dx + dy * dy <= radius * radius;
84
+ }
85
+ if (topRight && px > x + size - radius && py < y + radius) {
86
+ const dx = px - (x + size - radius);
87
+ const dy = py - (y + radius);
88
+ return dx * dx + dy * dy <= radius * radius;
89
+ }
90
+ if (bottomRight && px > x + size - radius && py > y + size - radius) {
91
+ const dx = px - (x + size - radius);
92
+ const dy = py - (y + size - radius);
93
+ return dx * dx + dy * dy <= radius * radius;
94
+ }
95
+ if (bottomLeft && px < x + radius && py > y + size - radius) {
96
+ const dx = px - (x + radius);
97
+ const dy = py - (y + size - radius);
98
+ return dx * dx + dy * dy <= radius * radius;
99
+ }
100
+ return true;
101
+ }
102
+
103
+ function fillRoundedCell(buffer, width, scale, x, y, size, radius, corners, color) {
104
+ const scaledX = x * scale;
105
+ const scaledY = y * scale;
106
+ const scaledSize = size * scale;
107
+ const scaledRadius = radius * scale;
108
+ const minX = Math.max(0, Math.floor(scaledX));
109
+ const maxX = Math.min(width - 1, Math.ceil(scaledX + scaledSize));
110
+ const minY = Math.max(0, Math.floor(scaledY));
111
+ const maxY = Math.min(width - 1, Math.ceil(scaledY + scaledSize));
112
+
113
+ for (let py = minY; py <= maxY; py++) {
114
+ for (let px = minX; px <= maxX; px++) {
115
+ if (pointInsideRoundedCell(px + 0.5, py + 0.5, scaledX, scaledY, scaledSize, scaledRadius, corners)) {
116
+ blendPixel(buffer, width, px, py, color, 1);
117
+ }
118
+ }
119
+ }
120
+ }
121
+
122
+ function cellOn(rows, x, y) {
123
+ return y >= 0 && y < GRID_H && x >= 0 && x < GRID_W && ((rows[y] >>> (GRID_W - 1 - x)) & 1) === 1;
124
+ }
125
+
126
+ function renderHighResolution(descriptor, targetSize, supersample) {
127
+ const width = targetSize * supersample;
128
+ const designScale = width / 128;
129
+ const buffer = new Uint8Array(width * width * 4);
130
+ const background = parseColor(descriptor.colors.background);
131
+ const foreground = parseColor(descriptor.colors.foreground);
132
+ fillCircle(buffer, width, designScale, 64, 64, 48, background, 1);
133
+
134
+ const cellRadius = 2;
135
+ for (let y = 0; y < GRID_H; y++) {
136
+ for (let x = 0; x < GRID_W; x++) {
137
+ if (!cellOn(descriptor.rows, x, y)) continue;
138
+ const up = cellOn(descriptor.rows, x, y - 1);
139
+ const down = cellOn(descriptor.rows, x, y + 1);
140
+ const left = cellOn(descriptor.rows, x - 1, y);
141
+ const right = cellOn(descriptor.rows, x + 1, y);
142
+ fillRoundedCell(
143
+ buffer,
144
+ width,
145
+ designScale,
146
+ GLYPH_X + x * CELL,
147
+ GLYPH_Y + y * CELL,
148
+ CELL,
149
+ cellRadius,
150
+ [!up && !left, !up && !right, !down && !right, !down && !left],
151
+ foreground
152
+ );
153
+ }
154
+ }
155
+
156
+ return { buffer, width };
157
+ }
158
+
159
+ function downsample(source, sourceWidth, targetSize, supersample) {
160
+ if (supersample === 1) return source;
161
+ const output = new Uint8Array(targetSize * targetSize * 4);
162
+ const samples = supersample * supersample;
163
+
164
+ for (let y = 0; y < targetSize; y++) {
165
+ for (let x = 0; x < targetSize; x++) {
166
+ let sumAlpha = 0;
167
+ let sumRed = 0;
168
+ let sumGreen = 0;
169
+ let sumBlue = 0;
170
+ for (let sampleY = 0; sampleY < supersample; sampleY++) {
171
+ for (let sampleX = 0; sampleX < supersample; sampleX++) {
172
+ const sourceX = x * supersample + sampleX;
173
+ const sourceY = y * supersample + sampleY;
174
+ const sourceOffset = (sourceY * sourceWidth + sourceX) * 4;
175
+ const alpha = source[sourceOffset + 3] / 255;
176
+ sumAlpha += alpha;
177
+ sumRed += source[sourceOffset] * alpha;
178
+ sumGreen += source[sourceOffset + 1] * alpha;
179
+ sumBlue += source[sourceOffset + 2] * alpha;
180
+ }
181
+ }
182
+
183
+ const outputOffset = (y * targetSize + x) * 4;
184
+ const averageAlpha = sumAlpha / samples;
185
+ if (sumAlpha > 0) {
186
+ output[outputOffset] = Math.round(sumRed / sumAlpha);
187
+ output[outputOffset + 1] = Math.round(sumGreen / sumAlpha);
188
+ output[outputOffset + 2] = Math.round(sumBlue / sumAlpha);
189
+ }
190
+ output[outputOffset + 3] = Math.round(averageAlpha * 255);
191
+ }
192
+ }
193
+
194
+ return output;
195
+ }
196
+
197
+ function crc32(buffer) {
198
+ let crc = 0xffffffff;
199
+ for (const byte of buffer) crc = CRC_TABLE[(crc ^ byte) & 0xff] ^ (crc >>> 8);
200
+ return (crc ^ 0xffffffff) >>> 0;
201
+ }
202
+
203
+ function pngChunk(type, data = Buffer.alloc(0)) {
204
+ const typeBuffer = Buffer.from(type, "ascii");
205
+ const length = Buffer.alloc(4);
206
+ length.writeUInt32BE(data.length, 0);
207
+ const checksum = Buffer.alloc(4);
208
+ checksum.writeUInt32BE(crc32(Buffer.concat([typeBuffer, data])), 0);
209
+ return Buffer.concat([length, typeBuffer, data, checksum]);
210
+ }
211
+
212
+ function encodePng(rgba, width, height) {
213
+ const raw = Buffer.alloc((width * 4 + 1) * height);
214
+ for (let y = 0; y < height; y++) {
215
+ const rowOffset = y * (width * 4 + 1);
216
+ raw[rowOffset] = 0; // PNG filter: None
217
+ Buffer.from(rgba.buffer, rgba.byteOffset + y * width * 4, width * 4).copy(raw, rowOffset + 1);
218
+ }
219
+
220
+ const header = Buffer.alloc(13);
221
+ header.writeUInt32BE(width, 0);
222
+ header.writeUInt32BE(height, 4);
223
+ header[8] = 8; // bit depth
224
+ header[9] = 6; // RGBA
225
+ header[10] = 0;
226
+ header[11] = 0;
227
+ header[12] = 0;
228
+
229
+ return Buffer.concat([
230
+ Buffer.from([137, 80, 78, 71, 13, 10, 26, 10]),
231
+ pngChunk("IHDR", header),
232
+ pngChunk("IDAT", deflateSync(raw, { level: 9 })),
233
+ pngChunk("IEND"),
234
+ ]);
235
+ }
236
+
237
+ function createAvatarPngFromDescriptor(descriptor, size = 96, options = {}) {
238
+ const snapshot = snapshotRenderableDescriptor(descriptor);
239
+ const targetSize = normalizePngSize(size);
240
+ const supersample = normalizeSupersample(options.supersample, targetSize);
241
+ const highResolution = renderHighResolution(snapshot, targetSize, supersample);
242
+ const rgba = downsample(highResolution.buffer, highResolution.width, targetSize, supersample);
243
+ return encodePng(rgba, targetSize, targetSize);
244
+ }
245
+
246
+ function optionsFromArgs(sizeOrOptions, explicitOptions) {
247
+ if (typeof sizeOrOptions === "object" && sizeOrOptions !== null) {
248
+ return {
249
+ size: sizeOrOptions.size ?? 96,
250
+ options: { ...sizeOrOptions },
251
+ };
252
+ }
253
+ return {
254
+ size: sizeOrOptions ?? 96,
255
+ options: { ...(explicitOptions ?? {}) },
256
+ };
257
+ }
258
+
259
+ function createAvatarPng(seed, sizeOrOptions = 96, explicitOptions = {}) {
260
+ const { size, options } = optionsFromArgs(sizeOrOptions, explicitOptions);
261
+ const descriptor = createAvatarDescriptor(seed, options);
262
+ return createAvatarPngFromDescriptor(descriptor, size, options);
263
+ }
264
+
265
+ function avatarPngDataUri(seed, sizeOrOptions = 96, explicitOptions = {}) {
266
+ return `data:image/png;base64,${createAvatarPng(seed, sizeOrOptions, explicitOptions).toString("base64")}`;
267
+ }
268
+
269
+ function normalizeSizes(value, supersampleValue) {
270
+ const sizes = value ?? PLATFORM_PNG_SIZES;
271
+ if (!Array.isArray(sizes) || sizes.length === 0) throw new TypeError("sizes must be a non-empty array.");
272
+ if (sizes.length > MAX_PNG_SET_SIZES) {
273
+ throw new RangeError(`sizes must contain at most ${MAX_PNG_SET_SIZES} entries.`);
274
+ }
275
+ const normalized = sizes.map(normalizePngSize);
276
+ if (new Set(normalized).size !== normalized.length) throw new TypeError("sizes must not contain duplicates.");
277
+ let renderPixels = 0;
278
+ for (const size of normalized) {
279
+ const supersample = normalizeSupersample(supersampleValue, size);
280
+ const renderWidth = size * supersample;
281
+ renderPixels += renderWidth * renderWidth;
282
+ if (renderPixels > MAX_PNG_SET_RENDER_PIXELS) {
283
+ throw new RangeError(`PNG set exceeds the ${MAX_PNG_SET_RENDER_PIXELS} render-pixel budget.`);
284
+ }
285
+ }
286
+ return normalized;
287
+ }
288
+
289
+ function createAvatarPngSet(seed, options = {}) {
290
+ const sizes = normalizeSizes(options.sizes, options.supersample);
291
+ const descriptor = createAvatarDescriptor(seed, options);
292
+ const files = {};
293
+ for (const size of sizes) files[size] = createAvatarPngFromDescriptor(descriptor, size, options);
294
+ return { descriptor, files };
295
+ }
296
+
297
+ function writeAvatarPngSet(seed, directory, options = {}) {
298
+ if (typeof directory !== "string" || directory.trim() === "") {
299
+ throw new TypeError("directory must be a non-empty path string.");
300
+ }
301
+ const baseName = String(options.baseName ?? "avatar");
302
+ if (!/^[A-Za-z0-9._-]{1,96}$/.test(baseName)) {
303
+ throw new TypeError("baseName may contain only letters, digits, periods, underscores, and hyphens.");
304
+ }
305
+
306
+ const outputDirectory = resolve(directory);
307
+ mkdirSync(outputDirectory, { recursive: true });
308
+ const generated = createAvatarPngSet(seed, options);
309
+ const paths = {};
310
+
311
+ for (const [size, data] of Object.entries(generated.files)) {
312
+ const path = join(outputDirectory, `${baseName}-${size}.png`);
313
+ writeFileSync(path, data);
314
+ paths[size] = path;
315
+ }
316
+
317
+ const manifest = {
318
+ schema: "deterministic-agent-avatars-png-export/v1",
319
+ styleVersion: generated.descriptor.styleVersion,
320
+ identityKey: generated.descriptor.identityKey,
321
+ signature: generated.descriptor.signature,
322
+ namespace: generated.descriptor.namespace,
323
+ theme: generated.descriptor.theme,
324
+ paletteId: generated.descriptor.paletteId,
325
+ files: Object.fromEntries(Object.entries(paths).map(([size, path]) => [size, path.split(/[\\/]/).pop()])),
326
+ };
327
+ const manifestPath = join(outputDirectory, `${baseName}-manifest.json`);
328
+ writeFileSync(manifestPath, `${JSON.stringify(manifest, null, 2)}\n`, "utf8");
329
+
330
+ return { ...generated, paths, manifestPath, manifest };
331
+ }
332
+
333
+ export {
334
+ PLATFORM_PNG_SIZES,
335
+ createAvatarPngFromDescriptor,
336
+ createAvatarPng,
337
+ avatarPngDataUri,
338
+ createAvatarPngSet,
339
+ writeAvatarPngSet,
340
+ };
@@ -0,0 +1,77 @@
1
+ const { webcrypto } = require("node:crypto");
2
+ const { STYLE_VERSION, canonicalSeed, createAvatarDescriptor, createHashAvatarFromDescriptor } = require("./index.cjs");
3
+
4
+ // Compatibility identifier: changing this would change deterministic outputs.
5
+ const LIBRARY_ID = "deterministic-agent-avatars";
6
+ const DEFAULT_NAMESPACE = "default";
7
+ const UTF8 = new TextEncoder();
8
+ const MIN_SECRET_BYTES = 32;
9
+
10
+ function assertSecretLength(secret) {
11
+ if (secret.byteLength < MIN_SECRET_BYTES) {
12
+ throw new TypeError(`secret must contain at least ${MIN_SECRET_BYTES} encoded bytes.`);
13
+ }
14
+ return secret;
15
+ }
16
+
17
+ function encodePart(value) {
18
+ return `${value.length}:${value}`;
19
+ }
20
+
21
+ function canonicalNamespace(value, mode = "human") {
22
+ const normalized = canonicalSeed(value ?? DEFAULT_NAMESPACE, mode);
23
+ if (normalized.length === 0) {
24
+ throw new TypeError("namespace must not be empty after canonicalization.");
25
+ }
26
+ return normalized;
27
+ }
28
+
29
+ function domainMessage(domain, canonical, namespace, nonce = 0) {
30
+ return `${LIBRARY_ID}\u0000${STYLE_VERSION}\u0000${domain}\u0000${encodePart(namespace)}\u0000${encodePart(canonical)}\u0000${nonce}`;
31
+ }
32
+
33
+ function secretBytes(secret) {
34
+ if (typeof secret === "string") {
35
+ if (secret.length === 0) throw new TypeError("secret must not be empty.");
36
+ return assertSecretLength(UTF8.encode(secret));
37
+ }
38
+ if (secret instanceof Uint8Array) {
39
+ if (secret.byteLength === 0) throw new TypeError("secret must not be empty.");
40
+ return assertSecretLength(secret);
41
+ }
42
+ throw new TypeError("secret must be a non-empty string or Uint8Array.");
43
+ }
44
+
45
+ function bytesToHex(bytes) {
46
+ return Array.from(bytes, (byte) => byte.toString(16).padStart(2, "0")).join("");
47
+ }
48
+
49
+ async function derivePrivateSeed(value, options = {}) {
50
+ const secret = secretBytes(options.secret);
51
+ const seedMode = options.seedMode ?? "human";
52
+ const namespaceMode = options.namespaceMode ?? "human";
53
+ const canonical = canonicalSeed(value, seedMode);
54
+ const namespace = canonicalNamespace(options.namespace ?? DEFAULT_NAMESPACE, namespaceMode);
55
+ const key = await webcrypto.subtle.importKey("raw", secret, { name: "HMAC", hash: "SHA-256" }, false, ["sign"]);
56
+ const message = UTF8.encode(domainMessage("private-seed", canonical, namespace, 0));
57
+ const signature = await webcrypto.subtle.sign("HMAC", key, message);
58
+ return `hmac-sha256:${bytesToHex(new Uint8Array(signature))}`;
59
+ }
60
+
61
+ async function createPrivateAvatarDescriptor(seed, options = {}) {
62
+ const privateSeed = await derivePrivateSeed(seed, options);
63
+ const avatarOptions = { ...options, seedMode: "raw" };
64
+ delete avatarOptions.secret;
65
+ return createAvatarDescriptor(privateSeed, avatarOptions);
66
+ }
67
+
68
+ async function createPrivateHashAvatar(seed, options = {}) {
69
+ const descriptor = await createPrivateAvatarDescriptor(seed, options);
70
+ return createHashAvatarFromDescriptor(descriptor, options.size ?? 96);
71
+ }
72
+
73
+ module.exports = {
74
+ derivePrivateSeed,
75
+ createPrivateAvatarDescriptor,
76
+ createPrivateHashAvatar,
77
+ };
@@ -0,0 +1,9 @@
1
+ import type { AvatarDescriptor, AvatarOptions } from "./index.cjs";
2
+
3
+ export interface PrivateAvatarOptions extends AvatarOptions {
4
+ secret: string | Uint8Array;
5
+ }
6
+
7
+ export function derivePrivateSeed(value: unknown, options: PrivateAvatarOptions): Promise<string>;
8
+ export function createPrivateAvatarDescriptor(seed: unknown, options: PrivateAvatarOptions): Promise<AvatarDescriptor>;
9
+ export function createPrivateHashAvatar(seed: unknown, options: PrivateAvatarOptions): Promise<string>;
@@ -0,0 +1,9 @@
1
+ import type { AvatarDescriptor, AvatarOptions } from "./index.mjs";
2
+
3
+ export interface PrivateAvatarOptions extends AvatarOptions {
4
+ secret: string | Uint8Array;
5
+ }
6
+
7
+ export function derivePrivateSeed(value: unknown, options: PrivateAvatarOptions): Promise<string>;
8
+ export function createPrivateAvatarDescriptor(seed: unknown, options: PrivateAvatarOptions): Promise<AvatarDescriptor>;
9
+ export function createPrivateHashAvatar(seed: unknown, options: PrivateAvatarOptions): Promise<string>;
@@ -0,0 +1,82 @@
1
+ import { webcrypto } from "node:crypto";
2
+ import {
3
+ STYLE_VERSION,
4
+ canonicalSeed,
5
+ createAvatarDescriptor,
6
+ createHashAvatarFromDescriptor,
7
+ } from "./index.mjs";
8
+
9
+ // Compatibility identifier: changing this would change deterministic outputs.
10
+ const LIBRARY_ID = "deterministic-agent-avatars";
11
+ const DEFAULT_NAMESPACE = "default";
12
+ const UTF8 = new TextEncoder();
13
+ const MIN_SECRET_BYTES = 32;
14
+
15
+ function assertSecretLength(secret) {
16
+ if (secret.byteLength < MIN_SECRET_BYTES) {
17
+ throw new TypeError(`secret must contain at least ${MIN_SECRET_BYTES} encoded bytes.`);
18
+ }
19
+ return secret;
20
+ }
21
+
22
+ function encodePart(value) {
23
+ return `${value.length}:${value}`;
24
+ }
25
+
26
+ function canonicalNamespace(value, mode = "human") {
27
+ const normalized = canonicalSeed(value ?? DEFAULT_NAMESPACE, mode);
28
+ if (normalized.length === 0) {
29
+ throw new TypeError("namespace must not be empty after canonicalization.");
30
+ }
31
+ return normalized;
32
+ }
33
+
34
+ function domainMessage(domain, canonical, namespace, nonce = 0) {
35
+ return `${LIBRARY_ID}\u0000${STYLE_VERSION}\u0000${domain}\u0000${encodePart(namespace)}\u0000${encodePart(canonical)}\u0000${nonce}`;
36
+ }
37
+
38
+ function secretBytes(secret) {
39
+ if (typeof secret === "string") {
40
+ if (secret.length === 0) throw new TypeError("secret must not be empty.");
41
+ return assertSecretLength(UTF8.encode(secret));
42
+ }
43
+ if (secret instanceof Uint8Array) {
44
+ if (secret.byteLength === 0) throw new TypeError("secret must not be empty.");
45
+ return assertSecretLength(secret);
46
+ }
47
+ throw new TypeError("secret must be a non-empty string or Uint8Array.");
48
+ }
49
+
50
+ function bytesToHex(bytes) {
51
+ return Array.from(bytes, (byte) => byte.toString(16).padStart(2, "0")).join("");
52
+ }
53
+
54
+ async function derivePrivateSeed(value, options = {}) {
55
+ const secret = secretBytes(options.secret);
56
+ const seedMode = options.seedMode ?? "human";
57
+ const namespaceMode = options.namespaceMode ?? "human";
58
+ const canonical = canonicalSeed(value, seedMode);
59
+ const namespace = canonicalNamespace(options.namespace ?? DEFAULT_NAMESPACE, namespaceMode);
60
+ const key = await webcrypto.subtle.importKey("raw", secret, { name: "HMAC", hash: "SHA-256" }, false, ["sign"]);
61
+ const message = UTF8.encode(domainMessage("private-seed", canonical, namespace, 0));
62
+ const signature = await webcrypto.subtle.sign("HMAC", key, message);
63
+ return `hmac-sha256:${bytesToHex(new Uint8Array(signature))}`;
64
+ }
65
+
66
+ async function createPrivateAvatarDescriptor(seed, options = {}) {
67
+ const privateSeed = await derivePrivateSeed(seed, options);
68
+ const avatarOptions = { ...options, seedMode: "raw" };
69
+ delete avatarOptions.secret;
70
+ return createAvatarDescriptor(privateSeed, avatarOptions);
71
+ }
72
+
73
+ async function createPrivateHashAvatar(seed, options = {}) {
74
+ const descriptor = await createPrivateAvatarDescriptor(seed, options);
75
+ return createHashAvatarFromDescriptor(descriptor, options.size ?? 96);
76
+ }
77
+
78
+ export {
79
+ derivePrivateSeed,
80
+ createPrivateAvatarDescriptor,
81
+ createPrivateHashAvatar,
82
+ };
package/dist/react.cjs ADDED
@@ -0,0 +1,32 @@
1
+ const React = require("react");
2
+ const { avatarDataUri } = require("./index.cjs");
3
+
4
+ const AgentAvatar = React.forwardRef(function AgentAvatar(props, ref) {
5
+ const {
6
+ seed,
7
+ size = 96,
8
+ options = {},
9
+ alt = "",
10
+ width = size,
11
+ height = size,
12
+ ...imageProps
13
+ } = props;
14
+
15
+ const src = avatarDataUri(seed, { ...options, size });
16
+ return React.createElement("img", {
17
+ ...imageProps,
18
+ ref,
19
+ src,
20
+ alt,
21
+ width,
22
+ height,
23
+ });
24
+ });
25
+
26
+ AgentAvatar.displayName = "AgentAvatar";
27
+ const HashAvatar = AgentAvatar;
28
+
29
+ module.exports = {
30
+ AgentAvatar,
31
+ HashAvatar,
32
+ };
@@ -0,0 +1,13 @@
1
+ import type * as React from "react";
2
+ import type { AvatarOptions } from "./index.cjs";
3
+
4
+ export interface AgentAvatarProps extends Omit<React.ImgHTMLAttributes<HTMLImageElement>, "src" | "width" | "height"> {
5
+ seed: unknown;
6
+ size?: number;
7
+ options?: Omit<AvatarOptions, "size">;
8
+ width?: number | string;
9
+ height?: number | string;
10
+ }
11
+
12
+ export const AgentAvatar: React.ForwardRefExoticComponent<AgentAvatarProps & React.RefAttributes<HTMLImageElement>>;
13
+ export const HashAvatar: typeof AgentAvatar;
@@ -0,0 +1,13 @@
1
+ import type * as React from "react";
2
+ import type { AvatarOptions } from "./index.mjs";
3
+
4
+ export interface AgentAvatarProps extends Omit<React.ImgHTMLAttributes<HTMLImageElement>, "src" | "width" | "height"> {
5
+ seed: unknown;
6
+ size?: number;
7
+ options?: Omit<AvatarOptions, "size">;
8
+ width?: number | string;
9
+ height?: number | string;
10
+ }
11
+
12
+ export const AgentAvatar: React.ForwardRefExoticComponent<AgentAvatarProps & React.RefAttributes<HTMLImageElement>>;
13
+ export const HashAvatar: typeof AgentAvatar;
package/dist/react.mjs ADDED
@@ -0,0 +1,29 @@
1
+ import * as React from "react";
2
+ import { avatarDataUri } from "./index.mjs";
3
+
4
+ const AgentAvatar = React.forwardRef(function AgentAvatar(props, ref) {
5
+ const {
6
+ seed,
7
+ size = 96,
8
+ options = {},
9
+ alt = "",
10
+ width = size,
11
+ height = size,
12
+ ...imageProps
13
+ } = props;
14
+
15
+ const src = avatarDataUri(seed, { ...options, size });
16
+ return React.createElement("img", {
17
+ ...imageProps,
18
+ ref,
19
+ src,
20
+ alt,
21
+ width,
22
+ height,
23
+ });
24
+ });
25
+
26
+ AgentAvatar.displayName = "AgentAvatar";
27
+ const HashAvatar = AgentAvatar;
28
+
29
+ export { AgentAvatar, HashAvatar };
@@ -0,0 +1,43 @@
1
+ const AVATAR_STYLE_VERSION = "1";
2
+ const GRID_HEIGHT = 4;
3
+ const MAX_ROW_VALUE = 31;
4
+
5
+ function normalizeHexColor(value, label) {
6
+ if (typeof value !== "string" || !/^#[0-9a-fA-F]{6}$/.test(value)) {
7
+ throw new TypeError(label + " must be a six-digit hexadecimal color such as #EAF3F8.");
8
+ }
9
+ return value.toUpperCase();
10
+ }
11
+
12
+ function snapshotRenderableDescriptor(descriptor) {
13
+ if (!descriptor || descriptor.styleVersion !== AVATAR_STYLE_VERSION) {
14
+ throw new TypeError("descriptor must be a " + AVATAR_STYLE_VERSION + " avatar descriptor.");
15
+ }
16
+
17
+ const rows = descriptor.rows;
18
+ if (
19
+ !Array.isArray(rows)
20
+ || rows.length !== GRID_HEIGHT
21
+ || rows.some((row) => !Number.isInteger(row) || row < 0 || row > MAX_ROW_VALUE)
22
+ ) {
23
+ throw new TypeError(
24
+ "descriptor.rows must contain " + GRID_HEIGHT + " integers in [0, " + MAX_ROW_VALUE + "]."
25
+ );
26
+ }
27
+
28
+ // Snapshot rows before reading colors so accessors cannot mutate the rendered bitmap after validation.
29
+ const safeRows = rows.slice();
30
+ const colors = descriptor.colors;
31
+ const background = normalizeHexColor(colors?.background, "descriptor.colors.background");
32
+ const foreground = normalizeHexColor(colors?.foreground, "descriptor.colors.foreground");
33
+
34
+ return {
35
+ rows: safeRows,
36
+ colors: { background, foreground },
37
+ };
38
+ }
39
+
40
+ module.exports = {
41
+ normalizeHexColor,
42
+ snapshotRenderableDescriptor,
43
+ };