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
@@ -42,7 +42,6 @@ export default {
'^shaka-player$': '<rootDir>/src/test-stubs/shaka-player.js',
'^video.js$': '<rootDir>/src/test-stubs/video-js.js',
'^rxjs': '<rootDir>/../../node_modules/rxjs/dist/bundles/rxjs.umd.js',
'^uuid$': '<rootDir>/../../node_modules/uuid/wrapper.mjs',
},
transform: angularEsmPreset.transform,
transformIgnorePatterns: [],
@@ -17,8 +17,7 @@ import {
StalkerSessionService,
normalizeStalkerPortalIdentity,
} from '@iptvnator/portal/stalker/data-access';
import { Playlist } from '@iptvnator/shared/interfaces';
import { v4 as uuid } from 'uuid';
import { createRandomId, Playlist } from '@iptvnator/shared/interfaces';
@Component({
imports: [
@@ -55,7 +54,7 @@ export class StalkerPortalImportComponent {
readonly URL_REGEX = /^(http|https|file):\/\/[^ "]+$/;
readonly form = new FormGroup({
_id: new FormControl(uuid()),
_id: new FormControl(createRandomId()),
title: new FormControl('', [Validators.required]),
macAddress: new FormControl('', [Validators.required]),
serialNumber: new FormControl(''),
@@ -82,7 +81,7 @@ export class StalkerPortalImportComponent {
clearForm(): void {
this.form.reset({
_id: uuid(),
_id: createRandomId(),
title: '',
macAddress: '',
serialNumber: '',
@@ -16,11 +16,11 @@ import { TranslatePipe } from '@ngx-translate/core';
import { PlaylistActions } from '@iptvnator/m3u-state';
import { PortalStatus, PortalStatusService } from '@iptvnator/services';
import {
createRandomId,
extractXtreamCredentialsFromUrl,
normalizeXtreamServerUrl,
Playlist,
} from '@iptvnator/shared/interfaces';
import { v4 as uuid } from 'uuid';
function xtreamServerUrlValidator(
control: AbstractControl
@@ -91,7 +91,7 @@ export class XtreamCodeImportComponent {
URL_REGEX = /^\s*https?:\/\/[^ "]+\s*$/;
form = new FormGroup({
_id: new FormControl(uuid()),
_id: new FormControl(createRandomId()),
title: new FormControl('', [Validators.required]),
password: new FormControl('', [Validators.required]),
username: new FormControl('', [Validators.required]),
@@ -149,7 +149,7 @@ export class XtreamCodeImportComponent {
clearForm(): void {
this.form.reset({
_id: uuid(),
_id: createRandomId(),
title: '',
password: '',
username: '',
@@ -1,6 +1,5 @@
import { inject, Injectable } from '@angular/core';
import { firstValueFrom } from 'rxjs';
import { v4 as uuidv4 } from 'uuid';
import { PlaylistsService } from './playlists.service';
import { SettingsStore } from './settings-store.service';
import { DatabaseService } from './database-electron.service';
@@ -23,6 +22,7 @@ import {
XtreamBackupContentType,
XtreamPlaylistBackupEntry,
XtreamPendingRestoreState,
createRandomId,
} from '@iptvnator/shared/interfaces';
export interface PlaylistBackupExportPayload {
@@ -585,10 +585,10 @@ export class PlaylistBackupService {
return entry.exportedId;
}
let nextId = uuidv4();
let nextId = createRandomId();
while (existingIds.has(nextId)) {
nextId = uuidv4();
nextId = createRandomId();
}
return nextId;
+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();
}
+1 -2
View File
@@ -7,7 +7,6 @@
"types": "./src/index.d.ts",
"dependencies": {
"@iptvnator/shared/interfaces": "0.0.1",
"tslib": "^2.3.0",
"uuid": "^9.0.0"
"tslib": "^2.3.0"
}
}
@@ -1,10 +1,10 @@
import {
Channel,
createRandomId,
ParsedPlaylist,
ParsedPlaylistItem,
Playlist,
} from '@iptvnator/shared/interfaces';
import { v4 as uuidv4 } from 'uuid';
import { extractDrmFromRaw } from './kodiprop.utils';
/**
@@ -387,7 +387,7 @@ export const createPlaylistObject = (
});
return {
_id: uuidv4(),
_id: createRandomId(),
filename: name,
title: name,
count: playlist.items.length,
@@ -397,7 +397,7 @@ export const createPlaylistObject = (
const drm = extractDrmFromRaw(item.raw);
return {
...item,
id: uuidv4(),
id: createRandomId(),
...(drm ? { drm } : {}),
};
}),
-2
View File
@@ -110,7 +110,6 @@
"rxjs": "7.8.2",
"saxes": "6.0.0",
"shaka-player": "5.2.2",
"uuid": "9.0.0",
"video.js": "8.23.9",
"videojs-contrib-quality-levels": "4.1.0",
"videojs-quality-selector-hls": "1.1.1",
@@ -172,7 +171,6 @@
"@types/jest": "^30.0.0",
"@types/mocha": "9.0.0",
"@types/node": "20.19.9",
"@types/uuid": "^10.0.0",
"@types/video.js": "7.3.58",
"@typescript-eslint/eslint-plugin": "^8.65.0",
"@typescript-eslint/parser": "^8.65.0",
+2 -21
View File
@@ -154,9 +154,6 @@ importers:
shaka-player:
specifier: 5.2.2
version: 5.2.2
uuid:
specifier: 9.0.0
version: 9.0.0
video.js:
specifier: 8.23.9
version: 8.23.9
@@ -314,9 +311,6 @@ importers:
'@types/node':
specifier: 20.19.9
version: 20.19.9
'@types/uuid':
specifier: ^10.0.0
version: 10.0.0
'@types/video.js':
specifier: 7.3.58
version: 7.3.58
@@ -4801,9 +4795,6 @@ packages:
'@types/unist@3.0.3':
resolution: {integrity: sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==}
'@types/uuid@10.0.0':
resolution: {integrity: sha512-7gqG38EyHgyP1S+7+xomFtL+ZNHcKv6DwNaCZmJmo1vgMugyF3TCnXVg4t1uk89mLNwnLtnY3TpOpCOyp1/xHQ==}
'@types/verror@1.10.11':
resolution: {integrity: sha512-RlDm9K7+o5stv0Co8i8ZRGxDbrTxhJtgjqjFyVh/tXQyl/rYtTKlnTvZ88oSTeYREWurwx20Js4kTuKCsFkUtg==}
@@ -10323,7 +10314,6 @@ packages:
shaka-player@5.2.2:
resolution: {integrity: sha512-1Z5Sxqz7nEAtvp4zlZKAHQSEq6cUlw5CKAdUxqmuMNeZgu23090VXMcEDdiSLU5qtMn1skFOzq+KKCVX71bHqw==}
engines: {node: '>=18'}
shallow-clone@3.0.1:
resolution: {integrity: sha512-/6KqX+GVUdqPuPPd2LxDDxzX6CAbjJehAAOKlNpqqUpAqPM6HeL8f+o3a+JsyGjn2lv0WY8UsTgUJjU9Ok55NA==}
@@ -11215,11 +11205,6 @@ packages:
deprecated: uuid@10 and below is no longer supported. For ESM codebases, update to uuid@latest. For CommonJS codebases, use uuid@11 (but be aware this version will likely be deprecated in 2028).
hasBin: true
uuid@9.0.0:
resolution: {integrity: sha512-MXcSTerfPa4uqyzStbRoTgt5XIe3x5+42+q1sDuy3R5MDk66URdLMOZe5aPX/SQd+kuYAh0FdP/pO28IkQyTeg==}
deprecated: uuid@10 and below is no longer supported. For ESM codebases, update to uuid@latest. For CommonJS codebases, use uuid@11 (but be aware this version will likely be deprecated in 2028).
hasBin: true
v8-compile-cache-lib@3.0.1:
resolution: {integrity: sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==}
@@ -13491,7 +13476,7 @@ snapshots:
make-fetch-happen: 10.2.1
nopt: 6.0.0
proc-log: 2.0.1
semver: 7.7.4
semver: 7.8.5
tar: 6.2.1
which: 2.0.2
transitivePeerDependencies:
@@ -15454,7 +15439,7 @@ snapshots:
'@npmcli/fs@2.1.2':
dependencies:
'@gar/promisify': 1.1.3
semver: 7.7.4
semver: 7.8.5
'@npmcli/fs@5.0.0':
dependencies:
@@ -16795,8 +16780,6 @@ snapshots:
'@types/unist@3.0.3': {}
'@types/uuid@10.0.0': {}
'@types/verror@1.10.11':
optional: true
@@ -25001,8 +24984,6 @@ snapshots:
uuid@8.3.2: {}
uuid@9.0.0: {}
v8-compile-cache-lib@3.0.1: {}
v8-to-istanbul@9.3.0: