Add random.ts helpers, replace all raw crypto.getRandomValues calls

New module packages/core/src/random.ts with randomHex, randomInt,
randomIndex — centralizes all crypto-based randomness.

Replaces:
- Date.now() IDs in VariablesDialog (not unique, not random)
- Hand-rolled hex token in automation/server.ts
- Inline crypto.getRandomValues in clipboard.ts, use-collab.ts

Also adds --color-component Tailwind theme color (#9747ff) and
replaces all 6 hardcoded occurrences across DesignPanel, LayerTree,
NodeContextMenuContent, and menu.ts.
This commit is contained in:
Danila Poyarkov 2026-03-16 14:13:36 +03:00
parent 921d2d35c5
commit 8d7dd0a36b
12 changed files with 35 additions and 23 deletions

View file

@ -1,5 +1,7 @@
import { inflateSync, deflateSync } from 'fflate'
import { randomInt } from './random'
import {
sceneNodeToKiwi,
buildFigKiwi,
@ -313,7 +315,7 @@ export function buildFigmaClipboardHTML(nodes: SceneNode[], graph: SceneGraph):
type: 'NODE_CHANGES',
sessionID: 0,
ackID: 0,
pasteID: crypto.getRandomValues(new Int32Array(1))[0],
pasteID: randomInt(),
pasteFileKey: 'openpencil',
nodeChanges
}

View file

@ -281,3 +281,4 @@ export const CJK_FALLBACK_FAMILIES_LINUX = [
]
export const CJK_GOOGLE_FONT = 'Noto Sans SC'

View file

@ -1,5 +1,6 @@
export type { GUID, Color, Vector, Matrix, Rect } from './types'
export { degToRad, radToDeg, rotatePoint, rotatedCorners, rotatedBBox } from './geometry'
export { randomHex, randomInt, randomIndex } from './random'
export * from './constants'

View file

@ -0,0 +1,14 @@
export function randomHex(bytes = 16): string {
const buf = crypto.getRandomValues(new Uint8Array(bytes))
let hex = ''
for (const b of buf) hex += b.toString(16).padStart(2, '0')
return hex
}
export function randomInt(): number {
return crypto.getRandomValues(new Int32Array(1))[0]
}
export function randomIndex(length: number): number {
return crypto.getRandomValues(new Uint8Array(1))[0] % length
}

View file

@ -37,6 +37,7 @@
--color-surface: #e0e0e0;
--color-muted: #888888;
--color-input: #1e1e1e;
--color-component: #9747ff;
}
html,

View file

@ -12,22 +12,15 @@ import {
renderTreeNode,
computeAllLayouts,
selectionToJSX,
sceneNodeToJSX
sceneNodeToJSX,
randomHex
} from '@open-pencil/core'
import type { EditorStore } from '@/stores/editor'
import type { ExportFormat } from '@open-pencil/core'
const TOKEN_LENGTH = 32
function generateToken(): string {
const bytes = new Uint8Array(TOKEN_LENGTH)
crypto.getRandomValues(bytes)
return Array.from(bytes, (b) => b.toString(16).padStart(2, '0')).join('')
}
export function connectAutomation(getStore: () => EditorStore) {
const token = generateToken()
const token = randomHex(32)
let ws: WebSocket | null = null
let reconnectTimer: ReturnType<typeof setTimeout> | undefined

View file

@ -57,7 +57,7 @@ const isComponentType = computed(() => {
data-test-id="design-node-header"
class="flex items-center gap-1.5 border-b border-border px-3 py-2"
>
<span class="text-[11px]" :class="isComponentType ? 'text-[#9747ff]' : 'text-muted'">{{
<span class="text-[11px]" :class="isComponentType ? 'text-component' : 'text-muted'">{{
node.type
}}</span>
<span class="text-xs font-semibold">{{ node.name }}</span>
@ -70,7 +70,7 @@ const isComponentType = computed(() => {
>
<button
data-test-id="design-go-to-component"
class="rounded bg-[#9747ff]/10 px-2 py-1 text-left text-[11px] text-[#9747ff] hover:bg-[#9747ff]/20"
class="rounded bg-component/10 px-2 py-1 text-left text-[11px] text-component hover:bg-component/20"
@click="store.goToMainComponent()"
>
Go to Main Component

View file

@ -392,7 +392,7 @@ function updateDropTarget(ev: PointerEvent) {
class="size-3 shrink-0"
:class="
COMPONENT_TYPES.has(item.value.type)
? 'text-[#9747ff] opacity-100'
? 'text-component opacity-100'
: 'opacity-70'
"
/>

View file

@ -234,7 +234,7 @@ const separatorClass = menuSeparator({ class: 'my-1' })
@select="store.createComponentFromSelection()"
>
<span>Create component</span>
<span class="text-[11px] text-[#9747ff]/60">⌥⌘K</span>
<span class="text-[11px] text-component/60">⌥⌘K</span>
</ContextMenuItem>
<ContextMenuItem
v-if="canCreateComponentSet"
@ -243,7 +243,7 @@ const separatorClass = menuSeparator({ class: 'my-1' })
@select="store.createComponentSetFromComponents()"
>
<span>Create component set</span>
<span class="text-[11px] text-[#9747ff]/60">⇧⌘K</span>
<span class="text-[11px] text-component/60">⇧⌘K</span>
</ContextMenuItem>
<ContextMenuItem
v-if="isComponent"

View file

@ -24,7 +24,7 @@ import IconType from '~icons/lucide/type'
import IconToggleLeft from '~icons/lucide/toggle-left'
import IconX from '~icons/lucide/x'
import ColorInput from './ColorInput.vue'
import { colorToHexRaw, parseColor } from '@open-pencil/core'
import { colorToHexRaw, parseColor, randomHex } from '@open-pencil/core'
import { useEditorStore } from '@/stores/editor'
import type { Variable, VariableCollection, VariableValue, Color } from '@open-pencil/core'
@ -186,7 +186,7 @@ function addVariable() {
const col = store.graph.variableCollections.get(activeTab.value)
if (!col) return
const id = `var:${Date.now()}`
const id = `var:${randomHex(8)}`
const valuesByMode: Record<string, VariableValue> = {}
for (const mode of col.modes) {
valuesByMode[mode.modeId] = { r: 0, g: 0, b: 0, a: 1 }
@ -217,7 +217,7 @@ function addVariable() {
}
function addCollection() {
const id = `col:${Date.now()}`
const id = `col:${randomHex(8)}`
const collection: VariableCollection = {
id,
name: 'New collection',

View file

@ -11,7 +11,7 @@ const item = tv({
tone: {
default: 'text-surface',
component:
'text-[#9747ff] data-[disabled]:text-[#9747ff]/40 data-[highlighted]:bg-[#9747ff]/12'
'text-component data-[disabled]:text-component/40 data-[highlighted]:bg-component/12'
},
justify: {
between: 'justify-between gap-6',

View file

@ -14,6 +14,7 @@ import {
} from '@/constants'
import type { EditorStore } from '@/stores/editor'
import { randomIndex } from '@open-pencil/core'
import type { Color, SceneNode } from '@open-pencil/core'
import type { Room } from 'trystero'
@ -40,7 +41,7 @@ export function useCollab(store: EditorStore) {
roomId: null,
peers: [],
localName: storedName.value,
localColor: PEER_COLORS[crypto.getRandomValues(new Uint8Array(1))[0] % PEER_COLORS.length]
localColor: PEER_COLORS[randomIndex(PEER_COLORS.length)]
})
let ydoc: Y.Doc | null = null
@ -458,10 +459,9 @@ export function useCollab(store: EditorStore) {
}
function generateRoomId(): string {
const bytes = crypto.getRandomValues(new Uint8Array(ROOM_ID_LENGTH))
let result = ''
for (let i = 0; i < ROOM_ID_LENGTH; i++) {
result += ROOM_ID_CHARS[bytes[i] % ROOM_ID_CHARS.length]
result += ROOM_ID_CHARS[randomIndex(ROOM_ID_CHARS.length)]
}
return result
}