Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
112 changes: 112 additions & 0 deletions packages/browser/scripts/generate-anonymous-animal-icons.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
/**
* Copyright (c) 2026, WSO2 LLC. (https://www.wso2.com).
*
* WSO2 LLC. licenses this file to you under the Apache License,
* Version 2.0 (the "License"); you may not use this file except
* in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/

/**
* Regenerates `src/utils/anonymousAnimalIcons.generated.ts` from the SVG files in
* `static/images/anonymous`. Each source SVG is a self-contained badge (a colored
* rounded-square background + white icon paths). This script strips the background
* `<rect>`, records its fill color as the animal's default color, and replaces any
* reuse of that same color elsewhere in the markup (accent details like eyes/nostrils)
* with a `%COLOR%` placeholder so `generateAvatarDataUri` can re-color the whole icon
* (background + accents) together when a caller supplies an explicit `bg` override.
*
* Run this whenever an icon is added, removed, or replaced under `static/images/anonymous`.
*
* Usage: node scripts/generate-anonymous-animal-icons.mjs
*/

import {readdirSync, readFileSync, writeFileSync} from 'node:fs';
import {dirname, join} from 'node:path';
import {fileURLToPath} from 'node:url';

const __dirname = dirname(fileURLToPath(import.meta.url));
const root = join(__dirname, '..');
const dir = join(root, 'static', 'images', 'avatars', 'anonymous_animals');

const escapeRegExp = (str) => str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');

const names = readdirSync(dir)
.filter((file) => file.endsWith('.svg'))
.map((file) => file.replace(/\.svg$/, ''))
.sort();

const icons = names.map((name) => {
const svg = readFileSync(join(dir, `${name}.svg`), 'utf-8');

const rectMatch = svg.match(/<rect[^>]*fill="(#[0-9a-fA-F]{3,8})"[^>]*\/>/);
if (!rectMatch) {
throw new Error(`${name}.svg: could not find a background <rect fill="#...">`);
}
const color = rectMatch[1];

const withoutRect = svg.replace(rectMatch[0], '');
const colorPattern = new RegExp(escapeRegExp(color), 'gi');
const markup = withoutRect
.replace(/<svg[^>]*>/, '')
.replace('</svg>', '')
.replace(colorPattern, '%COLOR%')
.trim();

return {color, markup, name};
});

const banner = `/**
* Copyright (c) 2026, WSO2 LLC. (https://www.wso2.com).
*
* WSO2 LLC. licenses this file to you under the Apache License,
* Version 2.0 (the "License"); you may not use this file except
* in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/

// AUTO-GENERATED FILE. Do not edit directly.
// Regenerate with: node scripts/generate-anonymous-animal-icons.mjs
// Source SVGs live in static/images/anonymous.

export interface AnonymousAnimalIcon {
/** The animal's curated default background color (hex). */
color: string;
/** Inner SVG markup (no outer <svg>/<rect>). Any "%COLOR%" token is the animal's
* color, substitutable at render time so a custom "bg" recolors accents too. */
markup: string;
}

`;

const entries = icons
.map(
({name, color, markup}) =>
` ${JSON.stringify(name)}: {color: ${JSON.stringify(color)}, markup: ${JSON.stringify(markup)}},`,
)
.join('\n');

const output = `${banner}export const ANONYMOUS_ANIMAL_ICONS: Record<string, AnonymousAnimalIcon> = {\n${entries}\n};\n`;

writeFileSync(join(root, 'src', 'utils', 'anonymousAnimalIcons.generated.ts'), output);

// eslint-disable-next-line no-console
console.log(`Generated anonymousAnimalIcons.generated.ts with ${icons.length} anonymous animal icon(s).`);
113 changes: 113 additions & 0 deletions packages/browser/scripts/generate-anonymous-entity-icons.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
/**
* Copyright (c) 2026, WSO2 LLC. (https://www.wso2.com).
*
* WSO2 LLC. licenses this file to you under the Apache License,
* Version 2.0 (the "License"); you may not use this file except
* in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/

/**
* Regenerates `src/utils/anonymousEntityIcons.generated.ts` from the SVG files in
* `static/images/entity`. Each source SVG is a self-contained badge (a colored
* rounded-square background + white icon paths) representing a non-human entity
* (application, organization, or resource server). This script strips the background
* `<rect>`, records its fill color as the entity icon's default color, and replaces any
* reuse of that same color elsewhere in the markup with a `%COLOR%` placeholder so
* `generateAvatarDataUri` can re-color the whole icon (background + accents) together
* when a caller supplies an explicit `bg` override.
*
* Run this whenever an icon is added, removed, or replaced under `static/images/entity`.
*
* Usage: node scripts/generate-anonymous-entity-icons.mjs
*/

import {readdirSync, readFileSync, writeFileSync} from 'node:fs';
import {dirname, join} from 'node:path';
import {fileURLToPath} from 'node:url';

const __dirname = dirname(fileURLToPath(import.meta.url));
const root = join(__dirname, '..');
const dir = join(root, 'static', 'images', 'avatars', 'anonymous_entity');

const escapeRegExp = (str) => str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');

const names = readdirSync(dir)
.filter((file) => file.endsWith('.svg'))
.map((file) => file.replace(/\.svg$/, ''))
.sort();

const icons = names.map((name) => {
const svg = readFileSync(join(dir, `${name}.svg`), 'utf-8');

const rectMatch = svg.match(/<rect[^>]*fill="(#[0-9a-fA-F]{3,8})"[^>]*\/>/);
if (!rectMatch) {
throw new Error(`${name}.svg: could not find a background <rect fill="#...">`);
}
const color = rectMatch[1];

const withoutRect = svg.replace(rectMatch[0], '');
const colorPattern = new RegExp(escapeRegExp(color), 'gi');
const markup = withoutRect
.replace(/<svg[^>]*>/, '')
.replace('</svg>', '')
.replace(colorPattern, '%COLOR%')
.trim();

return {color, markup, name};
});

const banner = `/**
* Copyright (c) 2026, WSO2 LLC. (https://www.wso2.com).
*
* WSO2 LLC. licenses this file to you under the Apache License,
* Version 2.0 (the "License"); you may not use this file except
* in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/

// AUTO-GENERATED FILE. Do not edit directly.
// Regenerate with: node scripts/generate-anonymous-entity-icons.mjs
// Source SVGs live in static/images/entity.

export interface AnonymousEntityIcon {
/** The entity icon's curated default background color (hex). */
color: string;
/** Inner SVG markup (no outer <svg>/<rect>). Any "%COLOR%" token is the icon's
* color, substitutable at render time so a custom "bg" recolors accents too. */
markup: string;
}

`;

const entries = icons
.map(
({name, color, markup}) =>
` ${JSON.stringify(name)}: {color: ${JSON.stringify(color)}, markup: ${JSON.stringify(markup)}},`,
)
.join('\n');

const output = `${banner}export const ANONYMOUS_ENTITY_ICONS: Record<string, AnonymousEntityIcon> = {\n${entries}\n};\n`;

writeFileSync(join(root, 'src', 'utils', 'anonymousEntityIcons.generated.ts'), output);

// eslint-disable-next-line no-console
console.log(`Generated anonymousEntityIcons.generated.ts with ${icons.length} anonymous entity icon(s).`);
16 changes: 16 additions & 0 deletions packages/browser/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,22 @@ export {default as navigate} from './utils/navigate';
export {default as http} from './utils/http';
export {default as handleWebAuthnAuthentication} from './utils/handleWebAuthnAuthentication';
export {default as resolveEmojiUrisInHtml} from './utils/resolveEmojiUrisInHtml';
export {default as isAvatarUri, AVATAR_URI_SCHEME} from './utils/isAvatarUri';
export {default as extractAvatarParamsFromUri} from './utils/extractAvatarParamsFromUri';
export type {AvatarParams, AvatarShape, AvatarVariant} from './utils/extractAvatarParamsFromUri';
export {
default as generateAvatarDataUri,
deriveAvatarContent,
AVATAR_GRADIENT_COUNT,
} from './utils/generateAvatarDataUri';
export {default as pickAnonymousAvatarName} from './utils/pickAnonymousAvatarName';
export {default as pickAnonymousEntityName} from './utils/pickAnonymousEntityName';
export {ANONYMOUS_ANIMAL_ICONS} from './utils/anonymousAnimalIcons.generated';
export type {AnonymousAnimalIcon} from './utils/anonymousAnimalIcons.generated';
export {ANONYMOUS_ENTITY_ICONS} from './utils/anonymousEntityIcons.generated';
export type {AnonymousEntityIcon} from './utils/anonymousEntityIcons.generated';
export {default as resolveLogoUri} from './utils/resolveLogoUri';
export type {ResolvedLogo, ResolvedLogoKind} from './utils/resolveLogoUri';

// Theme
export {detectThemeMode, createClassObserver, createMediaQueryListener} from './theme/themeDetection';
Expand Down
12 changes: 10 additions & 2 deletions packages/browser/src/utils/SPACryptoUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,9 @@ import {ThunderIDAuthException, Crypto, JWKInterface} from '@thunderid/javascrip
import base64url from 'base64url';
import sha256 from 'fast-sha256';
import {createLocalJWKSet, jwtVerify, JWTVerifyOptions} from 'jose';
import randombytes from 'randombytes';

// `crypto.getRandomValues` throws once a single request exceeds this many bytes.
const MAX_RANDOM_VALUES_BYTES = 65_536;

/**
* Browser-side `Crypto` implementation using native Web Crypto APIs and `jose` for JWT verification.
Expand Down Expand Up @@ -64,7 +66,13 @@ class SPACryptoUtils implements Crypto<Buffer | string> {
* @returns A `Buffer` of random bytes.
*/
public generateRandomBytes(length: number): string | Buffer {
return randombytes(length);
const bytes: Uint8Array = new Uint8Array(length);

for (let offset = 0; offset < length; offset += MAX_RANDOM_VALUES_BYTES) {
crypto.getRandomValues(bytes.subarray(offset, offset + MAX_RANDOM_VALUES_BYTES));
}

return Buffer.from(bytes);
}

/**
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
/**
* Copyright (c) 2026, WSO2 LLC. (https://www.wso2.com).
*
* WSO2 LLC. licenses this file to you under the Apache License,
* Version 2.0 (the "License"); you may not use this file except
* in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/

import {describe, it, expect} from 'vitest';
import extractAvatarParamsFromUri from '../extractAvatarParamsFromUri';

describe('extractAvatarParamsFromUri', () => {
it('parses shape, variant, content, and colors', () => {
expect(extractAvatarParamsFromUri('avatar:shape=circle,variant=two_letter,content=BM,colors=2')).toEqual({
colors: 2,
content: 'BM',
shape: 'circle',
variant: 'two_letter',
});
});

it('parses an anonymous_animal spec', () => {
expect(extractAvatarParamsFromUri('avatar:shape=rounded,variant=anonymous_animal,content=jackalope')).toEqual({
colors: 0,
content: 'jackalope',
shape: 'rounded',
variant: 'anonymous_animal',
});
});

it('parses an anonymous_entity spec', () => {
expect(extractAvatarParamsFromUri('avatar:shape=rounded,variant=anonymous_entity,content=hexagon')).toEqual({
colors: 0,
content: 'hexagon',
shape: 'rounded',
variant: 'anonymous_entity',
});
});

it('parses a blank spec', () => {
expect(extractAvatarParamsFromUri('avatar:shape=circle,variant=blank,content=,colors=3')).toEqual({
colors: 3,
content: '',
shape: 'circle',
variant: 'blank',
});
});

it('parses an explicit bg override', () => {
expect(extractAvatarParamsFromUri('avatar:shape=circle,variant=one_letter,content=A,bg=#FF5733')).toEqual({
bg: '#FF5733',
colors: 0,
content: 'A',
shape: 'circle',
variant: 'one_letter',
});
});

it('falls back to defaults for missing or invalid params', () => {
expect(extractAvatarParamsFromUri('avatar:')).toEqual({
colors: 0,
content: '',
shape: 'rounded',
variant: 'two_letter',
});
expect(extractAvatarParamsFromUri('avatar:shape=bogus,variant=bogus,colors=notanumber')).toEqual({
colors: 0,
content: '',
shape: 'rounded',
variant: 'two_letter',
});
});

it('keeps = characters inside the content value', () => {
expect(extractAvatarParamsFromUri('avatar:shape=circle,content=a=b,colors=1').content).toBe('a=b');
});
});
Loading
Loading