refactor(deps): drop uuid for a shared crypto-based id helper (#1266)

Supersedes #1252 and #872. uuid 14 is ESM-only, apps/web/jest.config.ts only
kept v9 working by mapping `^uuid$` at a `wrapper.mjs` that v14 no longer
ships, and the specifier also has to be synced in
libs/shared/m3u-utils/package.json or @nx/dependency-checks fails lint.

All four call sites only used `v4()`, so the dependency goes away instead.
`createRandomId()` prefers `crypto.randomUUID()` and falls back to building the
same v4 shape from `crypto.getRandomValues()` — that fallback is load-bearing,
because randomUUID is only exposed in secure contexts and the self-hosted PWA
is regularly served over plain http on a LAN address. getRandomValues stays
available there, and it is what uuid's own v4 used.

`@types/uuid` goes too; it only existed for the untyped v9 package.
This commit is contained in:
4gray authored and GitHub committed 2026-07-26 22:27:31 +02:00
1 parent 23512411a0
commit 1ab82b04a1
11 files changed
+143 -39

No files matched your search

+1
View File
@@ -30,6 +30,7 @@ export * from './lib/playlist.interface';
export * from './lib/portal-activity-item.interface';
export * from './lib/portal-debug.interface';
export * from './lib/portal-playback.interface';
export * from './lib/random-id.util';
export * from './lib/security-policy-error.utils';
export * from './lib/settings.interface';
export * from './lib/stalker-portal-actions.enum';
@@ -0,0 +1,84 @@
import { createRandomId } from './random-id.util';
const UUID_V4 =
/^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/;
describe('createRandomId', () => {
const nativeRandomUUID = crypto.randomUUID;
afterEach(() => {
Object.defineProperty(crypto, 'randomUUID', {
configurable: true,
value: nativeRandomUUID,
});
});
it('returns a v4 uuid', () => {
expect(createRandomId()).toMatch(UUID_V4);
});
it('returns unique ids', () => {
const ids = new Set(
Array.from({ length: 500 }, () => createRandomId())
);
expect(ids.size).toBe(500);
});
it('uses the native randomUUID when the context exposes it', () => {
const randomUUID = jest
.fn()
.mockReturnValue('11111111-1111-4111-8111-111111111111');
Object.defineProperty(crypto, 'randomUUID', {
configurable: true,
value: randomUUID,
});
expect(createRandomId()).toBe('11111111-1111-4111-8111-111111111111');
expect(randomUUID).toHaveBeenCalledTimes(1);
});
describe('without randomUUID (insecure context)', () => {
beforeEach(() => {
Object.defineProperty(crypto, 'randomUUID', {
configurable: true,
value: undefined,
});
});
it('still returns a v4 uuid from getRandomValues', () => {
expect(createRandomId()).toMatch(UUID_V4);
});
it('returns unique ids', () => {
const ids = new Set(
Array.from({ length: 500 }, () => createRandomId())
);
expect(ids.size).toBe(500);
});
it('sets the version and variant bits on all-zero randomness', () => {
jest.spyOn(crypto, 'getRandomValues').mockImplementation(
(buffer) => buffer
);
expect(createRandomId()).toBe(
'00000000-0000-4000-8000-000000000000'
);
});
it('keeps the remaining bits from getRandomValues', () => {
jest.spyOn(crypto, 'getRandomValues').mockImplementation(
(buffer) => {
(buffer as Uint8Array).fill(0xff);
return buffer;
}
);
expect(createRandomId()).toBe(
'ffffffff-ffff-4fff-bfff-ffffffffffff'
);
});
});
});
@@ -0,0 +1,43 @@
/**
* Random RFC 4122 v4 identifier used for playlist `_id` values, shared by the
* renderer, the Electron backend and the web backend. Pure function — no
* Angular/Node dependencies.
*
* `crypto.randomUUID()` is only exposed in secure contexts, and the
* self-hosted PWA is regularly served over plain http on a LAN address. So the
* fallback builds the same v4 shape from `crypto.getRandomValues()`, which
* insecure contexts do expose. This mirrors what `uuid`'s `v4()` did before it
* was dropped as a dependency.
*/
const HEX_BY_BYTE = Array.from({ length: 256 }, (_unused, byte) =>
byte.toString(16).padStart(2, '0')
);
function randomUuidFromBytes(): string {
const bytes = new Uint8Array(16);
crypto.getRandomValues(bytes);
// Version 4 and the RFC 4122 variant bits.
bytes[6] = (bytes[6] & 0x0f) | 0x40;
bytes[8] = (bytes[8] & 0x3f) | 0x80;
const hex = Array.from(bytes, (byte) => HEX_BY_BYTE[byte]);
return [
hex.slice(0, 4).join(''),
hex.slice(4, 6).join(''),
hex.slice(6, 8).join(''),
hex.slice(8, 10).join(''),
hex.slice(10, 16).join(''),
].join('-');
}
/** Returns a lowercase RFC 4122 v4 UUID. */
export function createRandomId(): string {
if (typeof crypto.randomUUID === 'function') {
return crypto.randomUUID();
}
return randomUuidFromBytes();
}