@angular-helpers/security 21.3.0 → 21.4.2

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.md CHANGED
@@ -76,6 +76,19 @@ Security package for Angular applications that prevents common attacks like ReDo
76
76
  - **Verified auto-clear**: reads back the clipboard before clearing to avoid clobbering unrelated content.
77
77
  - **Password-manager semantics**: default 15-second clear, configurable.
78
78
 
79
+ ### **Session Inactivity Monitor**
80
+
81
+ - **NgZone-optimized**: DOM events tracked outside Angular change detection.
82
+ - **Security interop**: Can automatically clear SecureStorage and SensitiveClipboard upon timeout.
83
+ - **Warning states**: Configurable thresholds to warn users before expiration.
84
+
85
+ ### **Secure Cross-Window Messaging**
86
+
87
+ - **HMAC-SHA-256 signatures**: Every message is signed; tampered payloads are discarded.
88
+ - **Origin whitelist**: Messages from non-allowed origins are rejected before any crypto work.
89
+ - **Anti-replay protection**: Envelope includes `timestamp + nonce`; messages older than 30s are discarded.
90
+ - **SSR-safe**: No-op on the server — no `window` access.
91
+
79
92
  ### **Builder Pattern**
80
93
 
81
94
  - **Fluent API**: Intuitively build regular expressions.
@@ -85,7 +98,7 @@ Security package for Angular applications that prevents common attacks like ReDo
85
98
  ## 📦 Installation
86
99
 
87
100
  ```bash
88
- npm install @angular-helpers/security
101
+ pnpm add @angular-helpers/security
89
102
  ```
90
103
 
91
104
  ## 🚀 Basic Usage
@@ -568,6 +581,87 @@ export class ApiKeyPanel {
568
581
  The service reads the clipboard before clearing and skips the clear if the content no longer
569
582
  matches what was copied — so third-party copies by the user are never overwritten.
570
583
 
584
+ ### **SessionIdleService**
585
+
586
+ ```typescript
587
+ import { SessionIdleService } from '@angular-helpers/security';
588
+
589
+ export class AppComponent {
590
+ private sessionIdle = inject(SessionIdleService);
591
+
592
+ ngOnInit() {
593
+ this.sessionIdle.start({
594
+ timeoutMs: 15 * 60 * 1000, // 15 minutes
595
+ warningThresholdMs: 60 * 1000, // 1 minute warning
596
+ autoClearStorage: true,
597
+ autoClearClipboard: true,
598
+ });
599
+
600
+ // React to states
601
+ effect(() => {
602
+ if (this.sessionIdle.isWarning()) {
603
+ console.warn(`Session will expire in ${this.sessionIdle.timeRemaining()}ms`);
604
+ }
605
+ });
606
+
607
+ // React to timeout
608
+ this.sessionIdle.onTimeout.subscribe(() => {
609
+ this.authService.logout();
610
+ });
611
+ }
612
+ }
613
+ ```
614
+
615
+ The service tracks DOM events (`mousemove`, `keydown`, etc.) outside the Angular Zone to prevent change detection spam. It can automatically clear `SecureStorageService` and `SensitiveClipboardService` when the session times out.
616
+
617
+ ### **SecureMessageService**
618
+
619
+ ```typescript
620
+ import { SecureMessageService, provideSecureMessage } from '@angular-helpers/security';
621
+
622
+ // app.config.ts
623
+ bootstrapApplication(AppComponent, {
624
+ providers: [provideSecureMessage()],
625
+ });
626
+
627
+ // parent-app.component.ts
628
+ export class ParentComponent {
629
+ private channel = inject(SecureMessageService);
630
+
631
+ async ngOnInit() {
632
+ // 1. Generate a shared key (transport it to the iframe via a secure channel)
633
+ const key = await this.channel.generateChannelKey();
634
+
635
+ // 2. Configure: only accept messages from the iframe origin
636
+ this.channel.configure({
637
+ allowedOrigins: ['https://child-app.example.com'],
638
+ signingKey: key,
639
+ });
640
+
641
+ // 3. React to incoming messages
642
+ this.channel.messages$<{ type: string; payload: unknown }>().subscribe(({ data, origin }) => {
643
+ console.log('Verified message from', origin, data);
644
+ });
645
+
646
+ // 4. Or use the Signal for reactive UI
647
+ effect(() => {
648
+ const msg = this.channel.lastMessage()();
649
+ if (msg) console.log('Last message:', msg.data);
650
+ });
651
+ }
652
+
653
+ async sendToChild(iframe: HTMLIFrameElement) {
654
+ await this.channel.send(
655
+ iframe.contentWindow!,
656
+ { type: 'INIT', payload: { userId: 42 } },
657
+ 'https://child-app.example.com', // targetOrigin — never '*'
658
+ );
659
+ }
660
+ }
661
+ ```
662
+
663
+ > **Key exchange**: `SecureMessageService` does not perform automatic key negotiation — transport the `CryptoKey` to the other context via a secure out-of-band channel (e.g., derive it from a shared secret using `WebCryptoService.generateHmacKey()` seeded by a passphrase). Once both sides share the key, all message integrity is handled automatically.
664
+
571
665
  ## 🔧 Advanced Configuration
572
666
 
573
667
  ### **Security Options**
@@ -1,7 +1,8 @@
1
1
  import * as i0 from '@angular/core';
2
- import { inject, DestroyRef, Injectable, PLATFORM_ID, InjectionToken, signal, computed, makeEnvironmentProviders } from '@angular/core';
3
- import { isPlatformBrowser } from '@angular/common';
4
- import { Observable } from 'rxjs';
2
+ import { inject, DestroyRef, Injectable, PLATFORM_ID, InjectionToken, signal, computed, NgZone, Injector, makeEnvironmentProviders } from '@angular/core';
3
+ import { isPlatformBrowser, DOCUMENT } from '@angular/common';
4
+ import { Observable, Subject, fromEvent, merge } from 'rxjs';
5
+ import { throttleTime } from 'rxjs/operators';
5
6
 
6
7
  /**
7
8
  * Security service for regular expressions that prevents ReDoS
@@ -285,10 +286,10 @@ class RegexSecurityService {
285
286
  });
286
287
  this.workers.clear();
287
288
  }
288
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.4", ngImport: i0, type: RegexSecurityService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
289
- static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.4", ngImport: i0, type: RegexSecurityService });
289
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.13", ngImport: i0, type: RegexSecurityService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
290
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.13", ngImport: i0, type: RegexSecurityService });
290
291
  }
291
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.4", ngImport: i0, type: RegexSecurityService, decorators: [{
292
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.13", ngImport: i0, type: RegexSecurityService, decorators: [{
292
293
  type: Injectable
293
294
  }] });
294
295
  /**
@@ -528,10 +529,10 @@ class WebCryptoService {
528
529
  hmacHashName(algorithm) {
529
530
  return algorithm.replace('HMAC-', '');
530
531
  }
531
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.4", ngImport: i0, type: WebCryptoService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
532
- static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.4", ngImport: i0, type: WebCryptoService });
532
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.13", ngImport: i0, type: WebCryptoService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
533
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.13", ngImport: i0, type: WebCryptoService });
533
534
  }
534
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.4", ngImport: i0, type: WebCryptoService, decorators: [{
535
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.13", ngImport: i0, type: WebCryptoService, decorators: [{
535
536
  type: Injectable
536
537
  }] });
537
538
 
@@ -719,10 +720,10 @@ class SecureStorageService {
719
720
  base64ToBytes(base64) {
720
721
  return Uint8Array.from(atob(base64), (c) => c.charCodeAt(0));
721
722
  }
722
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.4", ngImport: i0, type: SecureStorageService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
723
- static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.4", ngImport: i0, type: SecureStorageService });
723
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.13", ngImport: i0, type: SecureStorageService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
724
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.13", ngImport: i0, type: SecureStorageService });
724
725
  }
725
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.4", ngImport: i0, type: SecureStorageService, decorators: [{
726
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.13", ngImport: i0, type: SecureStorageService, decorators: [{
726
727
  type: Injectable
727
728
  }], ctorParameters: () => [] });
728
729
 
@@ -1067,10 +1068,10 @@ class InputSanitizerService {
1067
1068
  return null;
1068
1069
  }
1069
1070
  }
1070
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.4", ngImport: i0, type: InputSanitizerService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
1071
- static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.4", ngImport: i0, type: InputSanitizerService });
1071
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.13", ngImport: i0, type: InputSanitizerService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
1072
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.13", ngImport: i0, type: InputSanitizerService });
1072
1073
  }
1073
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.4", ngImport: i0, type: InputSanitizerService, decorators: [{
1074
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.13", ngImport: i0, type: InputSanitizerService, decorators: [{
1074
1075
  type: Injectable
1075
1076
  }], ctorParameters: () => [] });
1076
1077
 
@@ -1102,10 +1103,10 @@ class PasswordStrengthService {
1102
1103
  assess(password) {
1103
1104
  return assessPasswordStrength(password);
1104
1105
  }
1105
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.4", ngImport: i0, type: PasswordStrengthService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
1106
- static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.4", ngImport: i0, type: PasswordStrengthService });
1106
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.13", ngImport: i0, type: PasswordStrengthService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
1107
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.13", ngImport: i0, type: PasswordStrengthService });
1107
1108
  }
1108
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.4", ngImport: i0, type: PasswordStrengthService, decorators: [{
1109
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.13", ngImport: i0, type: PasswordStrengthService, decorators: [{
1109
1110
  type: Injectable
1110
1111
  }] });
1111
1112
 
@@ -1217,10 +1218,10 @@ class JwtService {
1217
1218
  }
1218
1219
  return payload[name] ?? null;
1219
1220
  }
1220
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.4", ngImport: i0, type: JwtService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
1221
- static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.4", ngImport: i0, type: JwtService });
1221
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.13", ngImport: i0, type: JwtService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
1222
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.13", ngImport: i0, type: JwtService });
1222
1223
  }
1223
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.4", ngImport: i0, type: JwtService, decorators: [{
1224
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.13", ngImport: i0, type: JwtService, decorators: [{
1224
1225
  type: Injectable
1225
1226
  }] });
1226
1227
  function base64UrlDecode(input) {
@@ -1349,10 +1350,24 @@ class SensitiveClipboardService {
1349
1350
  return 'read-denied';
1350
1351
  }
1351
1352
  }
1352
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.4", ngImport: i0, type: SensitiveClipboardService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
1353
- static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.4", ngImport: i0, type: SensitiveClipboardService });
1353
+ /**
1354
+ * Forcefully clears the clipboard unconditionally.
1355
+ */
1356
+ async clear() {
1357
+ if (!this.isSupported())
1358
+ return;
1359
+ this.cancelPendingClear();
1360
+ try {
1361
+ await navigator.clipboard.writeText('');
1362
+ }
1363
+ catch {
1364
+ // ignore
1365
+ }
1366
+ }
1367
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.13", ngImport: i0, type: SensitiveClipboardService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
1368
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.13", ngImport: i0, type: SensitiveClipboardService });
1354
1369
  }
1355
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.4", ngImport: i0, type: SensitiveClipboardService, decorators: [{
1370
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.13", ngImport: i0, type: SensitiveClipboardService, decorators: [{
1356
1371
  type: Injectable
1357
1372
  }], ctorParameters: () => [] });
1358
1373
 
@@ -1425,11 +1440,14 @@ class HibpService {
1425
1440
  const match = findSuffixMatch(body, suffix);
1426
1441
  return match > 0 ? { leaked: true, count: match } : { leaked: false, count: 0 };
1427
1442
  }
1428
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.4", ngImport: i0, type: HibpService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
1429
- static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.4", ngImport: i0, type: HibpService });
1443
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.13", ngImport: i0, type: HibpService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
1444
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.13", ngImport: i0, type: HibpService, providedIn: 'root' });
1430
1445
  }
1431
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.4", ngImport: i0, type: HibpService, decorators: [{
1432
- type: Injectable
1446
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.13", ngImport: i0, type: HibpService, decorators: [{
1447
+ type: Injectable,
1448
+ args: [{
1449
+ providedIn: 'root',
1450
+ }]
1433
1451
  }], ctorParameters: () => [] });
1434
1452
  function findSuffixMatch(body, suffix) {
1435
1453
  const lines = body.split(/\r?\n/);
@@ -1597,10 +1615,10 @@ class RateLimiterService {
1597
1615
  bucket.lastRefillAt = now;
1598
1616
  bucket.remaining.set(Math.floor(bucket.tokens));
1599
1617
  }
1600
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.4", ngImport: i0, type: RateLimiterService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
1601
- static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.4", ngImport: i0, type: RateLimiterService });
1618
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.13", ngImport: i0, type: RateLimiterService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
1619
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.13", ngImport: i0, type: RateLimiterService });
1602
1620
  }
1603
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.4", ngImport: i0, type: RateLimiterService, decorators: [{
1621
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.13", ngImport: i0, type: RateLimiterService, decorators: [{
1604
1622
  type: Injectable
1605
1623
  }], ctorParameters: () => [] });
1606
1624
  function validatePolicy(policy) {
@@ -1679,10 +1697,10 @@ class CsrfService {
1679
1697
  get nativeStorage() {
1680
1698
  return this.storageTarget === 'session' ? sessionStorage : localStorage;
1681
1699
  }
1682
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.4", ngImport: i0, type: CsrfService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
1683
- static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.4", ngImport: i0, type: CsrfService });
1700
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.13", ngImport: i0, type: CsrfService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
1701
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.13", ngImport: i0, type: CsrfService });
1684
1702
  }
1685
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.4", ngImport: i0, type: CsrfService, decorators: [{
1703
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.13", ngImport: i0, type: CsrfService, decorators: [{
1686
1704
  type: Injectable
1687
1705
  }], ctorParameters: () => [] });
1688
1706
  const DEFAULT_HEADER_NAME = 'X-CSRF-Token';
@@ -1716,6 +1734,221 @@ function withCsrfHeader(options = {}) {
1716
1734
  };
1717
1735
  }
1718
1736
 
1737
+ const DEFAULT_EVENTS = ['mousemove', 'keydown', 'mousedown', 'touchstart', 'scroll'];
1738
+ class SessionIdleService {
1739
+ ngZone = inject(NgZone);
1740
+ document = inject(DOCUMENT);
1741
+ injector = inject(Injector);
1742
+ destroyRef = inject(DestroyRef);
1743
+ _isIdle = signal(false, ...(ngDevMode ? [{ debugName: "_isIdle" }] : /* istanbul ignore next */ []));
1744
+ _isWarning = signal(false, ...(ngDevMode ? [{ debugName: "_isWarning" }] : /* istanbul ignore next */ []));
1745
+ _timeRemaining = signal(null, ...(ngDevMode ? [{ debugName: "_timeRemaining" }] : /* istanbul ignore next */ []));
1746
+ _timeoutSubject = new Subject();
1747
+ isIdle = this._isIdle.asReadonly();
1748
+ isWarning = this._isWarning.asReadonly();
1749
+ timeRemaining = this._timeRemaining.asReadonly();
1750
+ onTimeout = this._timeoutSubject.asObservable();
1751
+ config = null;
1752
+ lastActivityTime = 0;
1753
+ timerInterval = null;
1754
+ eventSubscription;
1755
+ constructor() {
1756
+ this.destroyRef.onDestroy(() => this.stop());
1757
+ }
1758
+ start(config) {
1759
+ this.stop();
1760
+ this.config = config;
1761
+ this._isIdle.set(false);
1762
+ this._isWarning.set(false);
1763
+ this._timeRemaining.set(config.timeoutMs);
1764
+ this.lastActivityTime = Date.now();
1765
+ const eventsToTrack = config.events || DEFAULT_EVENTS;
1766
+ this.ngZone.runOutsideAngular(() => {
1767
+ const observables = eventsToTrack.map((ev) => fromEvent(this.document, ev));
1768
+ this.eventSubscription = merge(...observables)
1769
+ .pipe(throttleTime(500))
1770
+ .subscribe(() => {
1771
+ this.lastActivityTime = Date.now();
1772
+ });
1773
+ this.timerInterval = setInterval(() => this.checkIdle(), 1000);
1774
+ });
1775
+ }
1776
+ stop() {
1777
+ if (this.timerInterval) {
1778
+ clearInterval(this.timerInterval);
1779
+ this.timerInterval = null;
1780
+ }
1781
+ if (this.eventSubscription) {
1782
+ this.eventSubscription.unsubscribe();
1783
+ this.eventSubscription = undefined;
1784
+ }
1785
+ this.config = null;
1786
+ this._timeRemaining.set(null);
1787
+ this._isIdle.set(false);
1788
+ this._isWarning.set(false);
1789
+ }
1790
+ reset() {
1791
+ if (!this.config)
1792
+ return;
1793
+ this.lastActivityTime = Date.now();
1794
+ this._timeRemaining.set(this.config.timeoutMs);
1795
+ this._isIdle.set(false);
1796
+ this._isWarning.set(false);
1797
+ }
1798
+ checkIdle() {
1799
+ if (!this.config || this._isIdle())
1800
+ return;
1801
+ const elapsed = Date.now() - this.lastActivityTime;
1802
+ const remaining = Math.max(0, this.config.timeoutMs - elapsed);
1803
+ if (remaining === 0) {
1804
+ this.triggerTimeout();
1805
+ }
1806
+ else {
1807
+ const warningThreshold = this.config.warningThresholdMs || 0;
1808
+ const shouldBeWarning = remaining <= warningThreshold;
1809
+ this.ngZone.run(() => {
1810
+ this._timeRemaining.set(remaining);
1811
+ if (this._isWarning() !== shouldBeWarning) {
1812
+ this._isWarning.set(shouldBeWarning);
1813
+ }
1814
+ });
1815
+ }
1816
+ }
1817
+ triggerTimeout() {
1818
+ const config = this.config;
1819
+ this.stop();
1820
+ // Restore config temporarily to check for autoClear flags
1821
+ this.config = config;
1822
+ this.ngZone.run(() => {
1823
+ this._timeRemaining.set(0);
1824
+ this._isIdle.set(true);
1825
+ this._timeoutSubject.next();
1826
+ if (this.config?.autoClearStorage) {
1827
+ const storage = this.injector.get(SecureStorageService, null);
1828
+ if (storage)
1829
+ storage.clear();
1830
+ }
1831
+ if (this.config?.autoClearClipboard) {
1832
+ const clipboard = this.injector.get(SensitiveClipboardService, null);
1833
+ if (clipboard)
1834
+ clipboard.clear();
1835
+ }
1836
+ this.config = null;
1837
+ });
1838
+ }
1839
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.13", ngImport: i0, type: SessionIdleService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
1840
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.13", ngImport: i0, type: SessionIdleService, providedIn: 'root' });
1841
+ }
1842
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.13", ngImport: i0, type: SessionIdleService, decorators: [{
1843
+ type: Injectable,
1844
+ args: [{
1845
+ providedIn: 'root',
1846
+ }]
1847
+ }], ctorParameters: () => [] });
1848
+
1849
+ const REPLAY_WINDOW_MS = 30_000;
1850
+ class SecureMessageService {
1851
+ ngZone = inject(NgZone);
1852
+ platformId = inject(PLATFORM_ID);
1853
+ document = inject(DOCUMENT);
1854
+ crypto = inject(WebCryptoService);
1855
+ config = null;
1856
+ _lastMessage = signal(null, ...(ngDevMode ? [{ debugName: "_lastMessage" }] : /* istanbul ignore next */ []));
1857
+ _messages$ = new Subject();
1858
+ messageHandler = null;
1859
+ get targetWindow() {
1860
+ return this.document.defaultView;
1861
+ }
1862
+ /** Returns a new HMAC-SHA-256 CryptoKey ready to use in `configure()`. */
1863
+ generateChannelKey() {
1864
+ return this.crypto.generateHmacKey('HMAC-SHA-256');
1865
+ }
1866
+ /**
1867
+ * Configures the service with a signing key and allowed origins.
1868
+ * Starts listening for incoming messages.
1869
+ */
1870
+ configure(config) {
1871
+ this.destroy();
1872
+ this.config = config;
1873
+ if (!isPlatformBrowser(this.platformId))
1874
+ return;
1875
+ this.ngZone.runOutsideAngular(() => {
1876
+ this.messageHandler = (event) => this.handleMessage(event);
1877
+ this.targetWindow.addEventListener('message', this.messageHandler);
1878
+ });
1879
+ }
1880
+ /**
1881
+ * Signs and sends a payload to a target window.
1882
+ * @throws if `targetOrigin` is `'*'`
1883
+ */
1884
+ async send(target, payload, targetOrigin) {
1885
+ if (targetOrigin === '*') {
1886
+ throw new Error('SecureMessageService: targetOrigin must be an explicit origin, not "*".');
1887
+ }
1888
+ if (!this.config) {
1889
+ throw new Error('SecureMessageService: call configure() before send().');
1890
+ }
1891
+ const timestamp = Date.now();
1892
+ const nonce = this.targetWindow.crypto.randomUUID();
1893
+ const body = { payload, timestamp, nonce };
1894
+ const signature = await this.crypto.sign(this.config.signingKey, JSON.stringify(body));
1895
+ const envelope = { __signed: true, ...body, signature };
1896
+ target.postMessage(envelope, targetOrigin);
1897
+ }
1898
+ /** Observable of verified incoming messages. */
1899
+ messages$() {
1900
+ return this._messages$.asObservable();
1901
+ }
1902
+ /** Signal with the last verified incoming message (null before first message). */
1903
+ lastMessage() {
1904
+ return this._lastMessage;
1905
+ }
1906
+ /** Removes the window listener and completes the internal Subject. */
1907
+ destroy() {
1908
+ if (this.messageHandler && isPlatformBrowser(this.platformId)) {
1909
+ this.targetWindow.removeEventListener('message', this.messageHandler);
1910
+ this.messageHandler = null;
1911
+ }
1912
+ this.config = null;
1913
+ }
1914
+ async handleMessage(event) {
1915
+ if (!this.config)
1916
+ return;
1917
+ // 1. Origin whitelist
1918
+ if (!this.config.allowedOrigins.includes(event.origin))
1919
+ return;
1920
+ // 2. Envelope shape
1921
+ const env = event.data;
1922
+ if (env?.__signed !== true)
1923
+ return;
1924
+ const { payload, timestamp, nonce, signature } = env;
1925
+ if (!payload || !timestamp || !nonce || !signature)
1926
+ return;
1927
+ // 3. Replay window
1928
+ if (Date.now() - timestamp > REPLAY_WINDOW_MS)
1929
+ return;
1930
+ // 4. Signature verification
1931
+ const body = { payload, timestamp, nonce };
1932
+ const valid = await this.crypto.verify(this.config.signingKey, JSON.stringify(body), signature);
1933
+ if (!valid)
1934
+ return;
1935
+ // 5. Emit inside NgZone so signals trigger CD
1936
+ this.ngZone.run(() => {
1937
+ const message = { data: payload, origin: event.origin, timestamp };
1938
+ this._lastMessage.set(message);
1939
+ this._messages$.next(message);
1940
+ });
1941
+ }
1942
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.13", ngImport: i0, type: SecureMessageService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
1943
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.13", ngImport: i0, type: SecureMessageService, providedIn: 'root' });
1944
+ }
1945
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.13", ngImport: i0, type: SecureMessageService, decorators: [{
1946
+ type: Injectable,
1947
+ args: [{
1948
+ providedIn: 'root',
1949
+ }]
1950
+ }] });
1951
+
1719
1952
  const defaultSecurityConfig = {
1720
1953
  enableRegexSecurity: true,
1721
1954
  enableWebCrypto: true,
@@ -1727,6 +1960,8 @@ const defaultSecurityConfig = {
1727
1960
  enableHibp: false,
1728
1961
  enableRateLimiter: false,
1729
1962
  enableCsrf: false,
1963
+ enableSessionIdle: false,
1964
+ enableSecureMessage: false,
1730
1965
  defaultTimeout: 5000,
1731
1966
  safeMode: false,
1732
1967
  };
@@ -1755,6 +1990,10 @@ function provideSecurity(config = {}) {
1755
1990
  if (mergedConfig.enableCsrf) {
1756
1991
  providers.push(WebCryptoService, CsrfService);
1757
1992
  }
1993
+ if (mergedConfig.enableSessionIdle)
1994
+ providers.push(SessionIdleService);
1995
+ if (mergedConfig.enableSecureMessage)
1996
+ providers.push(SecureMessageService);
1758
1997
  return makeEnvironmentProviders(providers);
1759
1998
  }
1760
1999
  function provideRegexSecurity() {
@@ -1804,9 +2043,15 @@ function provideCsrf(config) {
1804
2043
  ...(config ? [{ provide: CSRF_CONFIG, useValue: config }] : []),
1805
2044
  ]);
1806
2045
  }
2046
+ function provideSessionIdle() {
2047
+ return makeEnvironmentProviders([SessionIdleService]);
2048
+ }
2049
+ function provideSecureMessage() {
2050
+ return makeEnvironmentProviders([WebCryptoService, SecureMessageService]);
2051
+ }
1807
2052
 
1808
2053
  /**
1809
2054
  * Generated bundle index. Do not edit.
1810
2055
  */
1811
2056
 
1812
- export { CSRF_CONFIG, ClipboardUnsupportedError, CsrfService, DEFAULT_ALLOWED_ATTRIBUTES, DEFAULT_ALLOWED_TAGS, HIBP_CONFIG, HibpService, InputSanitizerService, InvalidJwtError, JwtService, PasswordStrengthService, RATE_LIMITER_CONFIG, RateLimitExceededError, RateLimiterService, RegexSecurityBuilder, RegexSecurityService, SANITIZER_CONFIG, SECURE_STORAGE_CONFIG, SecureStorageService, SensitiveClipboardService, WebCryptoService, assessPasswordStrength, containsScriptInjection, containsSqlInjectionHints, defaultSecurityConfig, isHtmlSafe, isUrlSafe, provideCsrf, provideHibp, provideInputSanitizer, provideJwt, providePasswordStrength, provideRateLimiter, provideRegexSecurity, provideSecureStorage, provideSecurity, provideSensitiveClipboard, provideWebCrypto, sanitizeHtmlString, sanitizeUrlString, withCsrfHeader };
2057
+ export { CSRF_CONFIG, ClipboardUnsupportedError, CsrfService, DEFAULT_ALLOWED_ATTRIBUTES, DEFAULT_ALLOWED_TAGS, HIBP_CONFIG, HibpService, InputSanitizerService, InvalidJwtError, JwtService, PasswordStrengthService, RATE_LIMITER_CONFIG, RateLimitExceededError, RateLimiterService, RegexSecurityBuilder, RegexSecurityService, SANITIZER_CONFIG, SECURE_STORAGE_CONFIG, SecureMessageService, SecureStorageService, SensitiveClipboardService, SessionIdleService, WebCryptoService, assessPasswordStrength, containsScriptInjection, containsSqlInjectionHints, defaultSecurityConfig, isHtmlSafe, isUrlSafe, provideCsrf, provideHibp, provideInputSanitizer, provideJwt, providePasswordStrength, provideRateLimiter, provideRegexSecurity, provideSecureMessage, provideSecureStorage, provideSecurity, provideSensitiveClipboard, provideSessionIdle, provideWebCrypto, sanitizeHtmlString, sanitizeUrlString, withCsrfHeader };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@angular-helpers/security",
3
- "version": "21.3.0",
3
+ "version": "21.4.2",
4
4
  "description": "Angular security helpers for preventing ReDoS and other security vulnerabilities",
5
5
  "keywords": [
6
6
  "angular",
@@ -16,7 +16,12 @@
16
16
  "xss",
17
17
  "sanitization",
18
18
  "password-strength",
19
- "secure-storage"
19
+ "secure-storage",
20
+ "session-idle",
21
+ "inactivity",
22
+ "postmessage",
23
+ "iframe",
24
+ "secure-channel"
20
25
  ],
21
26
  "author": "Angular Helpers Team",
22
27
  "license": "MIT",
@@ -64,5 +69,6 @@
64
69
  "default": "./fesm2022/angular-helpers-security-signal-forms.mjs"
65
70
  }
66
71
  },
67
- "sideEffects": false
68
- }
72
+ "sideEffects": false,
73
+ "type": "module"
74
+ }
@@ -537,6 +537,10 @@ declare class SensitiveClipboardService {
537
537
  */
538
538
  cancelPendingClear(): void;
539
539
  private safeClear;
540
+ /**
541
+ * Forcefully clears the clipboard unconditionally.
542
+ */
543
+ clear(): Promise<void>;
540
544
  static ɵfac: i0.ɵɵFactoryDeclaration<SensitiveClipboardService, never>;
541
545
  static ɵprov: i0.ɵɵInjectableDeclaration<SensitiveClipboardService>;
542
546
  }
@@ -745,6 +749,82 @@ interface CsrfHeaderOptions {
745
749
  */
746
750
  declare function withCsrfHeader(options?: CsrfHeaderOptions): HttpInterceptorFn;
747
751
 
752
+ interface SessionIdleConfig {
753
+ timeoutMs: number;
754
+ warningThresholdMs?: number;
755
+ autoClearStorage?: boolean;
756
+ autoClearClipboard?: boolean;
757
+ events?: string[];
758
+ }
759
+ declare class SessionIdleService {
760
+ private ngZone;
761
+ private document;
762
+ private injector;
763
+ private destroyRef;
764
+ private _isIdle;
765
+ private _isWarning;
766
+ private _timeRemaining;
767
+ private _timeoutSubject;
768
+ readonly isIdle: Signal<boolean>;
769
+ readonly isWarning: Signal<boolean>;
770
+ readonly timeRemaining: Signal<number | null>;
771
+ readonly onTimeout: Observable<void>;
772
+ private config;
773
+ private lastActivityTime;
774
+ private timerInterval;
775
+ private eventSubscription?;
776
+ constructor();
777
+ start(config: SessionIdleConfig): void;
778
+ stop(): void;
779
+ reset(): void;
780
+ private checkIdle;
781
+ private triggerTimeout;
782
+ static ɵfac: i0.ɵɵFactoryDeclaration<SessionIdleService, never>;
783
+ static ɵprov: i0.ɵɵInjectableDeclaration<SessionIdleService>;
784
+ }
785
+
786
+ interface SecureMessageConfig {
787
+ allowedOrigins: string[];
788
+ signingKey: CryptoKey;
789
+ }
790
+ interface SecureMessage<T = unknown> {
791
+ data: T;
792
+ origin: string;
793
+ timestamp: number;
794
+ }
795
+ declare class SecureMessageService {
796
+ private readonly ngZone;
797
+ private readonly platformId;
798
+ private readonly document;
799
+ private readonly crypto;
800
+ private config;
801
+ private _lastMessage;
802
+ private _messages$;
803
+ private messageHandler;
804
+ private get targetWindow();
805
+ /** Returns a new HMAC-SHA-256 CryptoKey ready to use in `configure()`. */
806
+ generateChannelKey(): Promise<CryptoKey>;
807
+ /**
808
+ * Configures the service with a signing key and allowed origins.
809
+ * Starts listening for incoming messages.
810
+ */
811
+ configure(config: SecureMessageConfig): void;
812
+ /**
813
+ * Signs and sends a payload to a target window.
814
+ * @throws if `targetOrigin` is `'*'`
815
+ */
816
+ send<T>(target: Window, payload: T, targetOrigin: string): Promise<void>;
817
+ /** Observable of verified incoming messages. */
818
+ messages$<T = unknown>(): Observable<SecureMessage<T>>;
819
+ /** Signal with the last verified incoming message (null before first message). */
820
+ lastMessage<T = unknown>(): Signal<SecureMessage<T> | null>;
821
+ /** Removes the window listener and completes the internal Subject. */
822
+ destroy(): void;
823
+ private handleMessage;
824
+ static ɵfac: i0.ɵɵFactoryDeclaration<SecureMessageService, never>;
825
+ static ɵprov: i0.ɵɵInjectableDeclaration<SecureMessageService>;
826
+ }
827
+
748
828
  interface SecurityConfig {
749
829
  enableRegexSecurity?: boolean;
750
830
  enableWebCrypto?: boolean;
@@ -756,6 +836,8 @@ interface SecurityConfig {
756
836
  enableHibp?: boolean;
757
837
  enableRateLimiter?: boolean;
758
838
  enableCsrf?: boolean;
839
+ enableSessionIdle?: boolean;
840
+ enableSecureMessage?: boolean;
759
841
  defaultTimeout?: number;
760
842
  safeMode?: boolean;
761
843
  }
@@ -771,6 +853,8 @@ declare function provideSensitiveClipboard(): EnvironmentProviders;
771
853
  declare function provideHibp(config?: HibpConfig): EnvironmentProviders;
772
854
  declare function provideRateLimiter(config?: RateLimiterConfig): EnvironmentProviders;
773
855
  declare function provideCsrf(config?: CsrfConfig): EnvironmentProviders;
856
+ declare function provideSessionIdle(): EnvironmentProviders;
857
+ declare function provideSecureMessage(): EnvironmentProviders;
774
858
 
775
- export { CSRF_CONFIG, ClipboardUnsupportedError, CsrfService, DEFAULT_ALLOWED_ATTRIBUTES, DEFAULT_ALLOWED_TAGS, HIBP_CONFIG, HibpService, InputSanitizerService, InvalidJwtError, JwtService, PasswordStrengthService, RATE_LIMITER_CONFIG, RateLimitExceededError, RateLimiterService, RegexSecurityBuilder, RegexSecurityService, SANITIZER_CONFIG, SECURE_STORAGE_CONFIG, SecureStorageService, SensitiveClipboardService, WebCryptoService, assessPasswordStrength, containsScriptInjection, containsSqlInjectionHints, defaultSecurityConfig, isHtmlSafe, isUrlSafe, provideCsrf, provideHibp, provideInputSanitizer, provideJwt, providePasswordStrength, provideRateLimiter, provideRegexSecurity, provideSecureStorage, provideSecurity, provideSensitiveClipboard, provideWebCrypto, sanitizeHtmlString, sanitizeUrlString, withCsrfHeader };
776
- export type { AesEncryptResult, AesKeyLength, CopyStatus, CsrfConfig, CsrfHeaderOptions, CsrfStorageTarget, HashAlgorithm, HibpConfig, HibpResult, HmacAlgorithm, HtmlSanitizerOptions, HttpMethod, JwtStandardClaims, PasswordAssessment, PasswordLabel, PasswordScore, PasswordStrengthResult, RateLimitPolicy, RateLimiterConfig, RegexBuilderOptions, RegexSecurityConfig, RegexSecurityResult, RegexTestResult, SanitizerConfig, SecureStorageConfig, SecurityConfig, SensitiveCopyOptions, StorageTarget };
859
+ export { CSRF_CONFIG, ClipboardUnsupportedError, CsrfService, DEFAULT_ALLOWED_ATTRIBUTES, DEFAULT_ALLOWED_TAGS, HIBP_CONFIG, HibpService, InputSanitizerService, InvalidJwtError, JwtService, PasswordStrengthService, RATE_LIMITER_CONFIG, RateLimitExceededError, RateLimiterService, RegexSecurityBuilder, RegexSecurityService, SANITIZER_CONFIG, SECURE_STORAGE_CONFIG, SecureMessageService, SecureStorageService, SensitiveClipboardService, SessionIdleService, WebCryptoService, assessPasswordStrength, containsScriptInjection, containsSqlInjectionHints, defaultSecurityConfig, isHtmlSafe, isUrlSafe, provideCsrf, provideHibp, provideInputSanitizer, provideJwt, providePasswordStrength, provideRateLimiter, provideRegexSecurity, provideSecureMessage, provideSecureStorage, provideSecurity, provideSensitiveClipboard, provideSessionIdle, provideWebCrypto, sanitizeHtmlString, sanitizeUrlString, withCsrfHeader };
860
+ export type { AesEncryptResult, AesKeyLength, CopyStatus, CsrfConfig, CsrfHeaderOptions, CsrfStorageTarget, HashAlgorithm, HibpConfig, HibpResult, HmacAlgorithm, HtmlSanitizerOptions, HttpMethod, JwtStandardClaims, PasswordAssessment, PasswordLabel, PasswordScore, PasswordStrengthResult, RateLimitPolicy, RateLimiterConfig, RegexBuilderOptions, RegexSecurityConfig, RegexSecurityResult, RegexTestResult, SanitizerConfig, SecureMessage, SecureMessageConfig, SecureStorageConfig, SecurityConfig, SensitiveCopyOptions, SessionIdleConfig, StorageTarget };