mirror of
https://github.com/4gray/iptvnator.git
synced 2026-10-12 03:56:15 -08:00
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:
1 parent
23512411a0
commit
1ab82b04a1
11 files changed
+143
-39
No files matched your search
@@ -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: [],
|
||||
|
||||
+3
-4
@@ -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: '',
|
||||
|
||||
+3
-3
@@ -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;
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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 } : {}),
|
||||
};
|
||||
}),
|
||||
|
||||
@@ -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",
|
||||
|
||||
Generated
+2
-21
@@ -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:
|
||||
|
||||
Reference in new issue
Block a user