@angular-helpers/security 21.1.0 → 21.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.es.md CHANGED
@@ -4,7 +4,11 @@
4
4
 
5
5
  # Angular Security Helpers
6
6
 
7
- Paquete de seguridad para aplicaciones Angular que previene ataques comunes como ReDoS (Regular Expression Denial of Service) usando Web Workers para ejecución segura.
7
+ Paquete de seguridad para aplicaciones Angular que previene ataques comunes como ReDoS (Regular Expression Denial of Service), XSS, y proporciona utilidades criptográficas usando Web Workers y Web Crypto API para ejecución segura.
8
+
9
+ > **Versión**: 21.2.0 — [CHANGELOG](./CHANGELOG.md)
10
+
11
+ ---
8
12
 
9
13
  ## 🛡️ Características
10
14
 
@@ -18,9 +22,63 @@ Paquete de seguridad para aplicaciones Angular que previene ataques comunes como
18
22
  ### **Web Crypto API**
19
23
 
20
24
  - **Cifrado/Descifrado**: Soporte AES-GCM para manejo seguro de datos
25
+ - **Firmas HMAC**: Firmar y verificar datos con HMAC-SHA256/384/512
21
26
  - **Hashing**: SHA-256 y otros algoritmos
22
27
  - **Gestión de Claves**: Generar, importar y exportar claves criptográficas
23
28
  - **Aleatorio Seguro**: Valores aleatorios criptográficamente seguros
29
+ - **Generación UUID**: UUIDs RFC4122 v4
30
+
31
+ ### **Almacenamiento Seguro**
32
+
33
+ - **Cifrado Transparente**: Almacenamiento cifrado AES-GCM sobre localStorage/sessionStorage
34
+ - **Modo Efímero**: Claves en memoria para seguridad de sesión única
35
+ - **Modo Passphrase**: Claves derivadas PBKDF2 para persistencia entre sesiones
36
+ - **Aislamiento por Namespace**: Organiza datos almacenados con prefijos
37
+
38
+ ### **Sanitización de Input**
39
+
40
+ - **Prevención XSS**: Limpieza de HTML con lista de permitidos
41
+ - **Validación de URLs**: Esquemas de URL seguros
42
+ - **Escape de HTML**: Caracteres especiales para interpolación segura
43
+ - **JSON Seguro**: Parseo seguro sin eval
44
+
45
+ ### **Fuerza de Contraseña**
46
+
47
+ - **Puntuación basada en Entropía**: Calcular fuerza en bits de entropía
48
+ - **Detección de Contraseñas Comunes**: Bloquear contraseñas débiles conocidas
49
+ - **Detección de Patrones**: Detectar patrones de teclado y secuencias
50
+ - **Feedback Accionable**: Sugerencias específicas para mejorar
51
+
52
+ ### **Validators de Formularios (sub-entries)**
53
+
54
+ - **`@angular-helpers/security/forms`**: puente para Reactive Forms — `SecurityValidators.strongPassword`, `safeHtml`, `safeUrl`, `noScriptInjection`, `noSqlInjectionHints`.
55
+ - **`@angular-helpers/security/signal-forms`**: puente para Signal Forms de Angular v21 — `strongPassword`, `safeHtml`, `safeUrl`, `noScriptInjection`, `noSqlInjectionHints`, y el async `hibpPassword`.
56
+ - **Core compartido**: ambos paradigmas delegan en los mismos helpers puros para garantizar paridad de comportamiento.
57
+
58
+ ### **Inspección de JWT**
59
+
60
+ - **Decodificación client-side**: `decode`, `claim`, `isExpired`, `expiresIn`.
61
+ - **Explícitamente NO verifica firma**: la validación criptográfica se hace server-side.
62
+
63
+ ### **Protección CSRF**
64
+
65
+ - **`CsrfService`**: double-submit con tokens generados por `WebCryptoService.generateRandomBytes`.
66
+ - **`withCsrfHeader()`**: interceptor funcional que inyecta el header en POST/PUT/PATCH/DELETE.
67
+
68
+ ### **Rate Limiter**
69
+
70
+ - **Token-bucket** y **sliding-window**, configurables por clave.
71
+ - **Estado basado en signals**: `canExecute(key)` y `remaining(key)` devuelven `Signal<T>`.
72
+
73
+ ### **HIBP Leaked Password Check**
74
+
75
+ - **k-anonymity**: sólo los primeros 5 caracteres hex del SHA-1 salen del navegador.
76
+ - **Fail-open**: los errores de red nunca bloquean el envío del formulario.
77
+
78
+ ### **Clipboard Sensible**
79
+
80
+ - **Auto-clear verificado**: lee el clipboard antes de limpiar para no pisar contenido ajeno.
81
+ - **Estilo password-manager**: default 15 segundos, configurable.
24
82
 
25
83
  ### **Patrón Builder**
26
84
 
@@ -44,7 +102,16 @@ import { provideSecurity } from '@angular-helpers/security';
44
102
  bootstrapApplication(AppComponent, {
45
103
  providers: [
46
104
  provideSecurity({
105
+ // Servicios core (habilitados por defecto)
47
106
  enableRegexSecurity: true,
107
+ enableWebCrypto: true,
108
+
109
+ // Nuevos servicios (opt-in, deshabilitados por defecto)
110
+ enableSecureStorage: true,
111
+ enableInputSanitizer: true,
112
+ enablePasswordStrength: true,
113
+
114
+ // Configuración global
48
115
  defaultTimeout: 5000,
49
116
  safeMode: false,
50
117
  }),
@@ -52,6 +119,27 @@ bootstrapApplication(AppComponent, {
52
119
  });
53
120
  ```
54
121
 
122
+ ### **Providers Individuales**
123
+
124
+ ```typescript
125
+ import {
126
+ provideRegexSecurity,
127
+ provideWebCrypto,
128
+ provideSecureStorage,
129
+ provideInputSanitizer,
130
+ providePasswordStrength,
131
+ } from '@angular-helpers/security';
132
+
133
+ // Usar solo los servicios que necesites
134
+ bootstrapApplication(AppComponent, {
135
+ providers: [
136
+ provideSecureStorage({ storage: 'session', pbkdf2Iterations: 600_000 }),
137
+ provideInputSanitizer({ allowedTags: ['b', 'i', 'em', 'strong'] }),
138
+ providePasswordStrength(),
139
+ ],
140
+ });
141
+ ```
142
+
55
143
  ### **Inyección de Servicios**
56
144
 
57
145
  ```typescript
@@ -142,9 +230,276 @@ export class SecureStorageComponent {
142
230
  generateUUID(): string {
143
231
  return this.cryptoService.randomUUID();
144
232
  }
233
+
234
+ async signAndVerify(data: string): Promise<boolean> {
235
+ // Generar clave HMAC para SHA-256
236
+ const key = await this.cryptoService.generateHmacKey('HMAC-SHA-256');
237
+
238
+ // Firmar los datos
239
+ const signature = await this.cryptoService.sign(key, data);
240
+
241
+ // Verificar la firma
242
+ return await this.cryptoService.verify(key, data, signature);
243
+ }
145
244
  }
146
245
  ```
147
246
 
247
+ ### **SecureStorageService — Almacenamiento Cifrado**
248
+
249
+ ```typescript
250
+ import { SecureStorageService } from '@angular-helpers/security';
251
+
252
+ export class UserSettingsComponent {
253
+ private storage = inject(SecureStorageService);
254
+
255
+ async saveUserToken(token: string): Promise<void> {
256
+ // Modo efímero (por defecto): datos sobreviven solo esta sesión
257
+ await this.storage.set('authToken', { token, createdAt: Date.now() });
258
+ }
259
+
260
+ async getUserToken(): Promise<{ token: string; createdAt: number } | null> {
261
+ return await this.storage.get<{ token: string; createdAt: number }>('authToken');
262
+ }
263
+
264
+ async initWithPassphrase(passphrase: string): Promise<void> {
265
+ // Modo passphrase: datos sobreviven recargas de página
266
+ await this.storage.initWithPassphrase(passphrase);
267
+ }
268
+
269
+ async saveWithNamespace(userId: string, data: unknown): Promise<void> {
270
+ // Aislamiento por namespace
271
+ await this.storage.set('profile', data, `user:${userId}`);
272
+ }
273
+
274
+ clearUserData(userId: string): void {
275
+ // Limpiar solo los datos de este usuario
276
+ this.storage.clear(`user:${userId}`);
277
+ }
278
+ }
279
+ ```
280
+
281
+ ### **InputSanitizerService — Prevención XSS**
282
+
283
+ ```typescript
284
+ import { InputSanitizerService } from '@angular-helpers/security';
285
+
286
+ export class CommentComponent {
287
+ private sanitizer = inject(InputSanitizerService);
288
+
289
+ sanitizeUserComment(html: string): string {
290
+ // Limpiar tags peligrosos, mantener seguros (b, i, em, a, etc.)
291
+ return this.sanitizer.sanitizeHtml(html);
292
+ // Ejemplo: '<b>Hola</b><script>alert(1)</script>' → '<b>Hola</b>'
293
+ }
294
+
295
+ validateUserLink(url: string): string | null {
296
+ // Solo permitir URLs http/https
297
+ return this.sanitizer.sanitizeUrl(url);
298
+ // Ejemplo: 'javascript:alert(1)' → null
299
+ // Ejemplo: 'https://example.com' → 'https://example.com/'
300
+ }
301
+
302
+ escapeForDisplay(text: string): string {
303
+ // Seguro para nodos de texto HTML
304
+ return this.sanitizer.escapeHtml(text);
305
+ // Ejemplo: '<b>hola</b>' → '&lt;b&gt;hola&lt;/b&gt;'
306
+ }
307
+
308
+ parseUserJson(json: string): unknown | null {
309
+ // Parseo seguro de JSON sin eval
310
+ return this.sanitizer.sanitizeJson(json);
311
+ }
312
+ }
313
+ ```
314
+
315
+ ### **PasswordStrengthService — Evaluación de Contraseñas**
316
+
317
+ ```typescript
318
+ import { PasswordStrengthService } from '@angular-helpers/security';
319
+
320
+ export class RegistrationComponent {
321
+ private passwordStrength = inject(PasswordStrengthService);
322
+
323
+ checkPasswordStrength(password: string): void {
324
+ const result = this.passwordStrength.assess(password);
325
+
326
+ console.log(`Score: ${result.score}/4`); // 0-4
327
+ console.log(`Label: ${result.label}`); // 'very-weak' a 'very-strong'
328
+ console.log(`Entropía: ${result.entropy} bits`); // entropía calculada
329
+ console.log('Feedback:', result.feedback); // sugerencias de mejora
330
+
331
+ // Ejemplos de resultados:
332
+ // 'password' → score: 0, label: 'very-weak', feedback: ['Contraseña común']
333
+ // 'P@ssw0rd!' → score: 2, label: 'fair', feedback: ['Evita patrones de teclado']
334
+ // 'xK#9mZ$vLq2@rBnT7' → score: 4, label: 'very-strong', feedback: []
335
+ }
336
+ }
337
+ ```
338
+
339
+ ### **SecurityValidators — Reactive Forms**
340
+
341
+ ```typescript
342
+ import { FormControl, FormGroup, Validators } from '@angular/forms';
343
+ import { SecurityValidators } from '@angular-helpers/security/forms';
344
+
345
+ export class SignupFormComponent {
346
+ form = new FormGroup({
347
+ password: new FormControl('', [
348
+ Validators.required,
349
+ SecurityValidators.strongPassword({ minScore: 3 }),
350
+ ]),
351
+ bio: new FormControl('', [SecurityValidators.safeHtml()]),
352
+ homepage: new FormControl('', [SecurityValidators.safeUrl({ schemes: ['https:'] })]),
353
+ });
354
+ }
355
+ ```
356
+
357
+ Los validators son factory functions estáticas — no hace falta registrar providers. Delegan en los
358
+ mismos helpers puros que usa la versión Signal Forms, así que ambos paradigmas devuelven resultados
359
+ equivalentes para el mismo input.
360
+
361
+ ### **Validators para Signal Forms**
362
+
363
+ ```typescript
364
+ import { signal } from '@angular/core';
365
+ import { form, required } from '@angular/forms/signals';
366
+ import {
367
+ strongPassword,
368
+ hibpPassword,
369
+ safeHtml,
370
+ safeUrl,
371
+ } from '@angular-helpers/security/signal-forms';
372
+
373
+ export class SignupSignalFormsComponent {
374
+ model = signal({ email: '', password: '', bio: '', homepage: '' });
375
+
376
+ f = form(this.model, (p) => {
377
+ required(p.email);
378
+ required(p.password);
379
+ strongPassword(p.password, { minScore: 3 });
380
+ hibpPassword(p.password); // async — valida contra HIBP
381
+ safeHtml(p.bio);
382
+ safeUrl(p.homepage, { schemes: ['https:'] });
383
+ });
384
+ }
385
+ ```
386
+
387
+ **Requisito del sub-entry**: instalar `@angular/forms`. El entry principal
388
+ `@angular-helpers/security` no depende de `@angular/forms`.
389
+
390
+ **Regla HIBP async**: `hibpPassword` requiere `provideHibp()` en la jerarquía del inyector. Falla
391
+ en modo open — errores de red nunca bloquean el submit del formulario.
392
+
393
+ ### **JwtService — Inspección Client-Side**
394
+
395
+ ```typescript
396
+ import { JwtService } from '@angular-helpers/security';
397
+
398
+ export class SessionGuard {
399
+ private jwt = inject(JwtService);
400
+
401
+ isAuthenticated(): boolean {
402
+ const token = localStorage.getItem('access_token');
403
+ if (!token) return false;
404
+ return !this.jwt.isExpired(token, 30); // 30 segundos de leeway
405
+ }
406
+
407
+ currentUserId(): string | null {
408
+ const token = localStorage.getItem('access_token');
409
+ return token ? this.jwt.claim<string>(token, 'sub') : null;
410
+ }
411
+ }
412
+ ```
413
+
414
+ > **Nota de seguridad**: `JwtService` sólo decodifica payloads para inspección. **Nunca** confíes
415
+ > en el contenido para decisiones de autorización — la verificación de la firma va siempre
416
+ > server-side.
417
+
418
+ ### **CsrfService + `withCsrfHeader()`**
419
+
420
+ ```typescript
421
+ import { bootstrapApplication } from '@angular/platform-browser';
422
+ import { provideHttpClient, withInterceptors } from '@angular/common/http';
423
+ import { provideSecurity, CsrfService, withCsrfHeader } from '@angular-helpers/security';
424
+
425
+ bootstrapApplication(App, {
426
+ providers: [
427
+ provideSecurity({ enableCsrf: true }),
428
+ provideHttpClient(withInterceptors([withCsrfHeader()])),
429
+ ],
430
+ });
431
+
432
+ // Tras el login:
433
+ const csrf = inject(CsrfService);
434
+ csrf.storeToken(response.csrfToken);
435
+ // A partir de ahora, todas las requests POST/PUT/PATCH/DELETE llevan X-CSRF-Token.
436
+ ```
437
+
438
+ ### **RateLimiterService**
439
+
440
+ ```typescript
441
+ import { RateLimiterService, RateLimitExceededError } from '@angular-helpers/security';
442
+
443
+ export class SearchComponent {
444
+ private rateLimiter = inject(RateLimiterService);
445
+
446
+ constructor() {
447
+ this.rateLimiter.configure('search', {
448
+ type: 'token-bucket',
449
+ capacity: 5,
450
+ refillPerSecond: 1,
451
+ });
452
+ }
453
+
454
+ canSearch = this.rateLimiter.canExecute('search'); // Signal<boolean>
455
+ remaining = this.rateLimiter.remaining('search'); // Signal<number>
456
+
457
+ async search(query: string) {
458
+ try {
459
+ await this.rateLimiter.consume('search');
460
+ return this.api.search(query);
461
+ } catch (err) {
462
+ if (err instanceof RateLimitExceededError) {
463
+ // Mostrar countdown usando err.retryAfterMs
464
+ }
465
+ }
466
+ }
467
+ }
468
+ ```
469
+
470
+ ### **HibpService**
471
+
472
+ ```typescript
473
+ import { HibpService } from '@angular-helpers/security';
474
+
475
+ export class RegistrationComponent {
476
+ private hibp = inject(HibpService);
477
+
478
+ async checkPassword(password: string) {
479
+ const { leaked, count, error } = await this.hibp.isPasswordLeaked(password);
480
+ if (error) return; // fail-open en errores de red
481
+ if (leaked) alert(`Esta contraseña apareció en ${count} brechas.`);
482
+ }
483
+ }
484
+ ```
485
+
486
+ ### **SensitiveClipboardService**
487
+
488
+ ```typescript
489
+ import { SensitiveClipboardService } from '@angular-helpers/security';
490
+
491
+ export class ApiKeyPanel {
492
+ private sensitiveClipboard = inject(SensitiveClipboardService);
493
+
494
+ async copy(value: string) {
495
+ await this.sensitiveClipboard.copy(value, { clearAfterMs: 15_000 });
496
+ }
497
+ }
498
+ ```
499
+
500
+ El servicio lee el clipboard antes de limpiarlo y omite el clear si el contenido ya no coincide con
501
+ lo que escribió — así evita pisar copias que el usuario haya hecho en otra parte.
502
+
148
503
  ## 📊 Niveles de Riesgo
149
504
 
150
505
  | Nivel | Descripción | Acción |
package/README.md CHANGED
@@ -45,6 +45,37 @@ Security package for Angular applications that prevents common attacks like ReDo
45
45
  - **Common Password Check**: Blocks frequently used passwords
46
46
  - **Feedback Messages**: Actionable improvement suggestions
47
47
 
48
+ ### **Forms Validators (sub-entries)**
49
+
50
+ - **`@angular-helpers/security/forms`**: Reactive Forms bridge — `SecurityValidators.strongPassword`, `safeHtml`, `safeUrl`, `noScriptInjection`, `noSqlInjectionHints`.
51
+ - **`@angular-helpers/security/signal-forms`**: Angular v21 Signal Forms bridge — `strongPassword`, `safeHtml`, `safeUrl`, `noScriptInjection`, `noSqlInjectionHints`, and async `hibpPassword`.
52
+ - **Shared core**: both paradigms delegate to the same pure helpers for guaranteed behavioural parity.
53
+
54
+ ### **JWT Inspection**
55
+
56
+ - **Client-side decode**: `decode`, `claim`, `isExpired`, `expiresIn`.
57
+ - **Explicit non-verifying**: signature validation must happen server-side.
58
+
59
+ ### **CSRF Protection**
60
+
61
+ - **`CsrfService`**: double-submit token helper backed by `WebCryptoService.generateRandomBytes`.
62
+ - **`withCsrfHeader()`**: functional HTTP interceptor that injects the token on POST/PUT/PATCH/DELETE.
63
+
64
+ ### **Rate Limiter**
65
+
66
+ - **Token-bucket** and **sliding-window** policies.
67
+ - **Signal-based state**: `canExecute(key)`, `remaining(key)` return `Signal<T>`.
68
+
69
+ ### **HIBP Leaked-Password Check**
70
+
71
+ - **k-anonymity**: only the first 5 hex chars of SHA-1 leave the browser.
72
+ - **Fail-open**: network errors never block form submissions.
73
+
74
+ ### **Sensitive Clipboard**
75
+
76
+ - **Verified auto-clear**: reads back the clipboard before clearing to avoid clobbering unrelated content.
77
+ - **Password-manager semantics**: default 15-second clear, configurable.
78
+
48
79
  ### **Builder Pattern**
49
80
 
50
81
  - **Fluent API**: Intuitively build regular expressions.
@@ -371,6 +402,172 @@ export class RegistrationComponent {
371
402
  }
372
403
  ```
373
404
 
405
+ ### **SecurityValidators (Reactive Forms)**
406
+
407
+ ```typescript
408
+ import { FormControl, FormGroup, Validators } from '@angular/forms';
409
+ import { SecurityValidators } from '@angular-helpers/security/forms';
410
+
411
+ export class SignupFormComponent {
412
+ form = new FormGroup({
413
+ password: new FormControl('', [
414
+ Validators.required,
415
+ SecurityValidators.strongPassword({ minScore: 3 }),
416
+ ]),
417
+ bio: new FormControl('', [SecurityValidators.safeHtml()]),
418
+ homepage: new FormControl('', [SecurityValidators.safeUrl({ schemes: ['https:'] })]),
419
+ query: new FormControl('', [
420
+ SecurityValidators.noScriptInjection(),
421
+ SecurityValidators.noSqlInjectionHints(),
422
+ ]),
423
+ });
424
+ }
425
+ ```
426
+
427
+ The validators are static factory functions — no provider registration required. They delegate to
428
+ shared pure helpers, so the Signal Forms variant below produces equivalent results for the same input.
429
+
430
+ ### **Signal Forms validators**
431
+
432
+ ```typescript
433
+ import { signal } from '@angular/core';
434
+ import { form, required } from '@angular/forms/signals';
435
+ import {
436
+ strongPassword,
437
+ hibpPassword,
438
+ safeHtml,
439
+ safeUrl,
440
+ } from '@angular-helpers/security/signal-forms';
441
+
442
+ export class SignupSignalFormsComponent {
443
+ model = signal({ email: '', password: '', bio: '', homepage: '' });
444
+
445
+ f = form(this.model, (p) => {
446
+ required(p.email);
447
+ required(p.password);
448
+ strongPassword(p.password, { minScore: 3 });
449
+ hibpPassword(p.password); // async — calls HIBP via validateAsync
450
+ safeHtml(p.bio);
451
+ safeUrl(p.homepage, { schemes: ['https:'] });
452
+ });
453
+ }
454
+ ```
455
+
456
+ **Sub-entry requirement**: ensure `@angular/forms` is installed. The main entry has zero runtime
457
+ dependency on `@angular/forms`; only the sub-entries need it.
458
+
459
+ **Async HIBP rule**: `hibpPassword` requires `provideHibp()` in the injector hierarchy. The rule
460
+ fails open — network errors never block form submission.
461
+
462
+ ### **JwtService**
463
+
464
+ ```typescript
465
+ import { JwtService } from '@angular-helpers/security';
466
+
467
+ export class SessionGuard {
468
+ private jwt = inject(JwtService);
469
+
470
+ isAuthenticated(): boolean {
471
+ const token = localStorage.getItem('access_token');
472
+ if (!token) return false;
473
+ return !this.jwt.isExpired(token, /* leewaySeconds */ 30);
474
+ }
475
+
476
+ currentUserId(): string | null {
477
+ const token = localStorage.getItem('access_token');
478
+ return token ? this.jwt.claim<string>(token, 'sub') : null;
479
+ }
480
+ }
481
+ ```
482
+
483
+ > **Security note**: `JwtService` decodes payloads for client-side inspection only. **Never** trust
484
+ > the decoded contents for authorization decisions — signature verification must happen server-side.
485
+
486
+ ### **CsrfService + `withCsrfHeader()`**
487
+
488
+ ```typescript
489
+ import { bootstrapApplication } from '@angular/platform-browser';
490
+ import { provideHttpClient, withInterceptors } from '@angular/common/http';
491
+ import { provideSecurity, CsrfService, withCsrfHeader } from '@angular-helpers/security';
492
+
493
+ bootstrapApplication(App, {
494
+ providers: [
495
+ provideSecurity({ enableCsrf: true }),
496
+ provideHttpClient(withInterceptors([withCsrfHeader()])),
497
+ ],
498
+ });
499
+
500
+ // After login:
501
+ const csrf = inject(CsrfService);
502
+ csrf.storeToken(response.csrfToken);
503
+ // Subsequent POST/PUT/PATCH/DELETE requests automatically carry X-CSRF-Token.
504
+ ```
505
+
506
+ ### **RateLimiterService**
507
+
508
+ ```typescript
509
+ import { RateLimiterService, RateLimitExceededError } from '@angular-helpers/security';
510
+
511
+ export class SearchComponent {
512
+ private rateLimiter = inject(RateLimiterService);
513
+
514
+ constructor() {
515
+ this.rateLimiter.configure('search', {
516
+ type: 'token-bucket',
517
+ capacity: 5,
518
+ refillPerSecond: 1,
519
+ });
520
+ }
521
+
522
+ canSearch = this.rateLimiter.canExecute('search'); // Signal<boolean>
523
+ remaining = this.rateLimiter.remaining('search'); // Signal<number>
524
+
525
+ async search(query: string) {
526
+ try {
527
+ await this.rateLimiter.consume('search');
528
+ return this.api.search(query);
529
+ } catch (err) {
530
+ if (err instanceof RateLimitExceededError) {
531
+ // Show countdown using err.retryAfterMs
532
+ }
533
+ }
534
+ }
535
+ }
536
+ ```
537
+
538
+ ### **HibpService**
539
+
540
+ ```typescript
541
+ import { HibpService } from '@angular-helpers/security';
542
+
543
+ export class RegistrationComponent {
544
+ private hibp = inject(HibpService);
545
+
546
+ async checkPassword(password: string) {
547
+ const { leaked, count, error } = await this.hibp.isPasswordLeaked(password);
548
+ if (error) return; // fail-open on network failures
549
+ if (leaked) alert(`This password has appeared in ${count} data breaches.`);
550
+ }
551
+ }
552
+ ```
553
+
554
+ ### **SensitiveClipboardService**
555
+
556
+ ```typescript
557
+ import { SensitiveClipboardService } from '@angular-helpers/security';
558
+
559
+ export class ApiKeyPanel {
560
+ private sensitiveClipboard = inject(SensitiveClipboardService);
561
+
562
+ async copy(value: string) {
563
+ await this.sensitiveClipboard.copy(value, { clearAfterMs: 15_000 });
564
+ }
565
+ }
566
+ ```
567
+
568
+ The service reads the clipboard before clearing and skips the clear if the content no longer
569
+ matches what was copied — so third-party copies by the user are never overwritten.
570
+
374
571
  ## 🔧 Advanced Configuration
375
572
 
376
573
  ### **Security Options**