diff --git a/apps/web/jest.config.ts b/apps/web/jest.config.ts index fe4335511..d218a0165 100644 --- a/apps/web/jest.config.ts +++ b/apps/web/jest.config.ts @@ -42,7 +42,6 @@ export default { '^shaka-player$': '/src/test-stubs/shaka-player.js', '^video.js$': '/src/test-stubs/video-js.js', '^rxjs': '/../../node_modules/rxjs/dist/bundles/rxjs.umd.js', - '^uuid$': '/../../node_modules/uuid/wrapper.mjs', }, transform: angularEsmPreset.transform, transformIgnorePatterns: [], diff --git a/libs/playlist/import/feature/src/lib/stalker-portal-import/stalker-portal-import.component.ts b/libs/playlist/import/feature/src/lib/stalker-portal-import/stalker-portal-import.component.ts index 266a7b40b..0345965ad 100644 --- a/libs/playlist/import/feature/src/lib/stalker-portal-import/stalker-portal-import.component.ts +++ b/libs/playlist/import/feature/src/lib/stalker-portal-import/stalker-portal-import.component.ts @@ -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: '', diff --git a/libs/playlist/import/feature/src/lib/xtream-code-import/xtream-code-import.component.ts b/libs/playlist/import/feature/src/lib/xtream-code-import/xtream-code-import.component.ts index 77d1a8497..f55115de3 100644 --- a/libs/playlist/import/feature/src/lib/xtream-code-import/xtream-code-import.component.ts +++ b/libs/playlist/import/feature/src/lib/xtream-code-import/xtream-code-import.component.ts @@ -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: '', diff --git a/libs/services/src/lib/playlist-backup.service.ts b/libs/services/src/lib/playlist-backup.service.ts index 9a18097fb..26cd646bf 100644 --- a/libs/services/src/lib/playlist-backup.service.ts +++ b/libs/services/src/lib/playlist-backup.service.ts @@ -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; diff --git a/libs/shared/interfaces/src/index.ts b/libs/shared/interfaces/src/index.ts index f1a1660ea..f4d51e0bc 100644 --- a/libs/shared/interfaces/src/index.ts +++ b/libs/shared/interfaces/src/index.ts @@ -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'; diff --git a/libs/shared/interfaces/src/lib/random-id.util.spec.ts b/libs/shared/interfaces/src/lib/random-id.util.spec.ts new file mode 100644 index 000000000..776dfdcd4 --- /dev/null +++ b/libs/shared/interfaces/src/lib/random-id.util.spec.ts @@ -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' + ); + }); + }); +}); diff --git a/libs/shared/interfaces/src/lib/random-id.util.ts b/libs/shared/interfaces/src/lib/random-id.util.ts new file mode 100644 index 000000000..fa0e78b2a --- /dev/null +++ b/libs/shared/interfaces/src/lib/random-id.util.ts @@ -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(); +} diff --git a/libs/shared/m3u-utils/package.json b/libs/shared/m3u-utils/package.json index 221ae65e9..2771f45b4 100644 --- a/libs/shared/m3u-utils/package.json +++ b/libs/shared/m3u-utils/package.json @@ -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" } } diff --git a/libs/shared/m3u-utils/src/lib/playlist.utils.ts b/libs/shared/m3u-utils/src/lib/playlist.utils.ts index 1361d13a7..5688c077e 100644 --- a/libs/shared/m3u-utils/src/lib/playlist.utils.ts +++ b/libs/shared/m3u-utils/src/lib/playlist.utils.ts @@ -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 } : {}), }; }), diff --git a/package.json b/package.json index 0228fc507..8fa71c67d 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 43dca8014..619e3b961 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -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: