Comprehensive review of ulam framework conducted 2026-05-19.
Status: CRITICAL
File: packages/halohalo/providers.js:63-64
Issue: Google provider builds API key directly into URL query parameter:
buildUrl: (key, model) =>
`https://generativelanguage.googleapis.com/v1beta/models/${model}:generateContent?key=${key}`-
XSS attacker can read all localStorage keys and send them to attacker endpoint
-
URL logged in browser history, server access logs, CDN logs
-
Certificate transparency logs expose HTTPS URLs
-
Browser DevTools Network tab permanently displays API key
Recommendation: Use POST request with Authorization header (like OpenAI):
google: {
url: 'https://generativelanguage.googleapis.com/v1beta/models/{model}:generateContent',
buildHeaders: (key) => ({
'Content-Type': 'application/json',
'Authorization': `Bearer ${key}`,
}),
buildBody: (prompt, _model, maxTokens) => JSON.stringify({
contents: [{ parts: [{ text: prompt }] }],
generationConfig: { maxOutputTokens: maxTokens },
}),
parseResponse: async (res) => {
const data = await res.json()
return data.candidates?.[0]?.content?.parts?.[0]?.text || ''
},
}Action Required: Update providers.js and fetch.js to build header-based URL.
Status: HIGH (Design Limitation)
-
packages/halohalo/prefs.js(API key storage) -
packages/sawsawan/storage.js(localStorage abstraction) -
packages/sawsawan/platformAdapter.js(adapter layer)
Issue: API keys stored in plain-text localStorage with prefix apikey_. localStorage is world-readable to all JavaScript on the domain.
-
XSS attack:
Object.keys(localStorage).filter(k => k.includes('apikey')) -
Shared browser: Any local user can read keys
-
Persistent across sessions: Attacker maintains access
-
Browser JavaScript cannot securely store secrets (no secure storage API in JS)
-
halohalo is designed for demos, prototypes, educational use
-
Consumer apps (like a11yfred) are responsible for secure credential handling
Recommendation: Add SECURITY.md guidelines in halohalo package:
## Security Notes
### API Key Storage
⚠� **WARNING**: This package stores API keys in `localStorage` in plain text.
#### Never use with real API keys in production web apps.
### Safe Usage Patterns
1. **Electron Apps**: Use `electron-store` with encryption
2. **Browser Extensions**: Use `chrome.storage.sync` (encrypted at rest by browser)
3. **Web Apps**: Use backend API proxy
- Client sends request to your backend
- Backend validates user, holds API key, calls AI provider
- Response proxied back to client
4. **Development/Demo**: Use throw-away keys with strict rate limits
### Do NOT
- � Ship production web app storing real API keys in localStorage
- � Commit .env files with API keys to git
- � Use API keys visible in browser DevToolsAction Required: Add SECURITY.md to packages/halohalo/.
Status: MEDIUM
File: packages/halohalo/providers.js:32
'anthropic-dangerous-direct-browser-access': 'true'Issue: This header explicitly declares that browser-based access is unsafe and should not be used in production.
Recommendation: Document in halohalo README why this is necessary and when to use backend proxies for production.
Status: MEDIUM (SSRF Prevention)
File: packages/halohalo/fetch.js:20-34
Issue: Provider URLs are user-configurable without whitelist validation.
Recommendation: Add provider URL validation:
// packages/halohalo/constants.js
export const ALLOWED_PROVIDER_HOSTS = new Set([
'api.anthropic.com',
'api.openai.com',
'generativelanguage.googleapis.com',
// Add Azure endpoint when enabled
])
// packages/halohalo/fetch.js
export async function createCompletion(prompt, config) {
const url = config.provider.buildUrl?.(config.apiKey, config.model)
if (url) {
const hostname = new URL(url).hostname
if (!ALLOWED_PROVIDER_HOSTS.has(hostname)) {
throw new AiApiError('api_error')
}
}
// ... rest of function
}Files: All uses in /packages/ube/core/* and /packages/taho/
Finding: All innerHTML assignments follow safe pattern of clearing first, then appending pre-created elements:
// Safe: Element creation before assignment, no interpolation
iconSpan.innerHTML = ''
iconSpan.appendChild(createIcon())
// Safe: Pre-built HTML with no user input
linksToAdd.innerHTML = this._buildSkipLinkHtml() // No var interpolationStatus: ✓ No XSS risk.
-
[CRITICAL] Fix Google API key URL exposure → move to Authorization header
-
[HIGH] Add
packages/halohalo/SECURITY.mdwith credential storage guidelines -
[MEDIUM] Add provider URL whitelist validation
Pattern: useSubscribe / useValue hook repeated in:
-
packages/calamansi/react.js(lines 52-55) -
packages/halohalo/useProviderConfig.js(lines 6-8) -
packages/sili/react/hooks/*.js(multiple variants)
const [value, setValue] = useState(getInitial)
useEffect(() => {
const unsubscribe = subscribe((newValue) => setValue(newValue))
return unsubscribe
}, [])
return valueRecommendation: Extract to shared utility:
// packages/shared/useSubscribe.js
export function useSubscribe(subscribe, getInitial) {
const [value, setValue] = useState(getInitial)
useEffect(() => subscribe(setValue), [subscribe])
return value
}
// Usage in all packages
const locale = useSubscribe(() => i18n.subscribe, () => i18n.getLocale())Impact: Reduces duplication across 3 packages, improves consistency.
File: packages/halohalo/useProviderConfig.js:6-8, 14-21
Issue: Uses dummy state to force re-renders:
const [, rerender] = useState(0)
useEffect(() => config.subscribe(() => rerender(n => n + 1)), [config])
return {
provider: config.provider,
models: config.models,
setProvider: useCallback((id) => config.setProvider(id), [config])
}-
Dummy state updates every time config changes → full re-render
-
New object created every render → siblings may re-render too
-
Missing useMemo wrapper
-
useCallback with [config] dependency but config never changes
Recommendation: Use React 18 useSyncExternalStore:
import { useSyncExternalStore } from 'react'
export function useProviderConfig() {
const config = useRef(createProviderConfig(...)).current
return useSyncExternalStore(
(listen) => config.subscribe(listen),
() => ({
provider: config.provider,
models: config.models,
setProvider: (id) => config.setProvider(id),
setModel: (model) => config.setModel(model),
})
)
}-
Eliminates dummy state
-
Prevents sibling re-renders
-
More efficient subscription handling
-
Cleaner code
File: packages/ube/core/form-control-select.js:109-111
Issue: Event listeners attached in setup method without guard against re-setup:
connectedCallback() {
// ...
this._setupSelect() // May be called multiple times
}
_setupSelect() {
this._select.addEventListener('change', this._handleChange)
// No guard — listener may be added multiple times
}Recommendation: Add initialization guard:
connectedCallback() {
if (!this._initialized) {
this._setupSelect()
this._initialized = true
}
}Impact: Prevents memory leaks in long-running apps with many form control updates.
File: packages/ube/index.js (re-exports all components)
Current State: ✓ Already configured correctly
-
package.jsonhas"side-effect": false -
Tree-shaking will work if consumer uses ES module imports
Recommendation: Document best practices in README:
## Optimal Bundle Size
Import individual components to minimize bundle size:
✅ **Recommended** (imports only ButtonText):
```javascript
import ButtonText from '@ulam/ube/react/ButtonText'
✅ Acceptable (still tree-shakes other components):
import { ButtonText } from '@ulam/ube/react'⚠� Not recommended (imports full package):
import Ube from '@ulam/ube'File: packages/sili/react/hooks/usePaginationFocus.js:23-26
Issue: Compares innerHTML to detect content change:
useEffect(() => {
const previousContent = previousContentRef.current
previousContentRef.current = ref.current?.innerHTML
if (previousContent !== ref.current?.innerHTML) {
ref.current?.focus()
}
}, [ref])-
String comparison is O(n) for large content
-
Fragile: DOM normalization may change innerHTML even if content is same
-
Better: use page parameter or explicit change signals
Recommendation:
// Option A: Use page parameter if available
export function usePaginationFocus(page, ref) {
const isMountRef = useRef(true)
useEffect(() => {
if (isMountRef.current) {
ref.current?.focus()
}
isMountRef.current = false
}, [page])
}
// Option B: Let parent component signal changes
export function usePaginationFocus(triggerValue, ref) {
useEffect(() => {
ref.current?.focus()
}, [triggerValue, ref])
}File: packages/calamansi/react.js:66-76 (setPref on every setValue)
Issue: Direct localStorage write on every state change may block React if localStorage is slow.
Recommendation: Optional debouncing for low-priority writes:
export function usePref(key, defaultValue) {
const [value, setValue] = useState(() => getPref(key, defaultValue))
useEffect(() => {
const timer = setTimeout(() => setPref(key, value), 200)
return () => clearTimeout(timer)
}, [key, value])
return [value, setValue]
}Note: Not critical for localStorage (synchronous), but future-proofs for async storage.
The framework demonstrates excellent performance practices:
-
Proper cleanup functions in useEffect hooks
-
No memory leaks from event listeners
-
CSS animations use GPU acceleration (transform, opacity)
-
Prefers-reduced-motion respected
-
useState/useRef patterns prevent unnecessary re-renders
-
Focus management doesn't cause layout thrashing
-
Extract shared hooks to reduce duplication
-
Add useMemo wrapper for frequently-changing return objects
-
Document component lazy loading strategies
Live Region Announcer (packages/taho/):
-
✓ Proper roles: status vs. alert with correct aria-live values
-
✓ Atomic announcements: aria-atomic="true"
-
✓ Screen reader compatible delays (100ms+ per Adobe research)
-
✓ Clean lifecycle: announce → hold → clear
Focus Management (packages/sili/):
-
✓ Focus trap: Correct Tab wrapping at boundaries
-
✓ Return focus: Saved and restored on overlay close
-
✓ Escape key: Standard handling
-
✓ Inert background: Uses inert attribute (modern browsers)
Skip Link (packages/ube/):
-
✓ Hides by default, shows on keyboard focus
-
✓ Icon properly marked aria-hidden + focusable="false"
-
✓ Semantic href navigation
Form Controls (packages/ube/):
-
✓ Native inputs (radio, checkbox, select) unchanged
-
✓ aria-label properly associated
-
✓ Disabled states managed
-
✓ Color contrast: 6.6:1 body, 4.6:1 muted (WCAG AA passing)
User Preferences (packages/ube/base-user-prefs.css):
-
✓ prefers-reduced-motion: all animations disabled
-
✓ prefers-reduced-transparency: opaque overlays
-
✓ prefers-contrast: more → higher contrast colors
-
✓ forced-colors: placeholder for High Contrast mode
-
Missing: aria-invalid + aria-describedby pattern
-
Recommendation: Add examples to form control documentation
-
Dialog accepts heading prop but not required
-
Recommendation: Consider required heading or document fallback
-
Escape key, Tab documented ✓
-
Arrow keys for radio groups not documented
-
Recommendation: Add arrow-key navigation to radio chip groups per ARIA authoring practices
-
Placeholder exists in base-user-prefs.css but strategy not documented
-
Recommendation: Document when to implement lazy loading for low-bandwidth users
| Dimension | Status | Critical | High | Medium | Action |
|---|---|---|---|---|---|
| Security | 🟡 | 1 | 1 | 1 | Fix Google key URL |
| Optimization | 🟡 | 0 | 2 | 3 | Deduplicate hooks |
| Performance | 🟢 | 0 | 0 | 0 | Excellent |
| Accessibility | 🟢 | 0 | 0 | 3 | Enhance form validation |
- [1] Fix Google API key URL exposure → use Authorization header
-
[2] Add
packages/halohalo/SECURITY.mdwith credential guidelines -
[3] Deduplicate useSubscribe hooks → shared utility
-
[4] Fix useProviderConfig re-render thrashing with useSyncExternalStore
-
[5] Add form validation examples with aria-invalid + aria-describedby
-
[6] Add event listener guard to form-control-select
-
[7] Add arrow-key cycling to radio chip groups
-
[8] Implement prefers-reduced-data handling
-
[9] Document component lazy loading strategies
-
Update providers.js to use buildHeaders pattern for Google
-
Update fetch.js to handle URL building correctly
-
Test Google provider with new header-based approach
-
Update halohalo README examples
-
Create packages/halohalo/SECURITY.md
-
Add section to halohalo README pointing to SECURITY.md
-
Create root SECURITY.md with link to halohalo SECURITY.md
-
Create packages/shared/useSubscribe.js
-
Update calamansi/react.js to use useSubscribe
-
Update halohalo/useProviderConfig.js to use useSubscribe
-
Update sili React hooks to use useSubscribe where applicable
-
Run tests to verify no behavioral changes
-
Update useProviderConfig to use useSyncExternalStore
-
Remove dummy state and useCallback dependencies
-
Test with multiple subscriptions to config
-
Verify no performance regression
-
Conducted: 2026-05-19
-
Framework Version: 0.3.1
-
Auditor: Claude Haiku 4.5
For security issues, see SECURITY.md in individual packages.
For optimization questions, see component-specific documentation.