@digitaldefiance/node-express-suite 3.12.11 → 3.12.13

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.
Files changed (41) hide show
  1. package/package.json +6 -3
  2. package/src/interfaces/index.d.ts +1 -0
  3. package/src/interfaces/index.d.ts.map +1 -1
  4. package/src/interfaces/index.js +1 -0
  5. package/src/interfaces/index.js.map +1 -1
  6. package/src/interfaces/network/index.d.ts +3 -0
  7. package/src/interfaces/network/index.d.ts.map +1 -0
  8. package/src/interfaces/network/index.js +6 -0
  9. package/src/interfaces/network/index.js.map +1 -0
  10. package/src/interfaces/network/upnpService.d.ts +86 -0
  11. package/src/interfaces/network/upnpService.d.ts.map +1 -0
  12. package/src/interfaces/network/upnpService.js +3 -0
  13. package/src/interfaces/network/upnpService.js.map +1 -0
  14. package/src/interfaces/network/upnpTypes.d.ts +120 -0
  15. package/src/interfaces/network/upnpTypes.d.ts.map +1 -0
  16. package/src/interfaces/network/upnpTypes.js +57 -0
  17. package/src/interfaces/network/upnpTypes.js.map +1 -0
  18. package/src/plugins/index.d.ts +1 -0
  19. package/src/plugins/index.d.ts.map +1 -1
  20. package/src/plugins/index.js +1 -0
  21. package/src/plugins/index.js.map +1 -1
  22. package/src/plugins/upnp.d.ts +129 -0
  23. package/src/plugins/upnp.d.ts.map +1 -0
  24. package/src/plugins/upnp.js +158 -0
  25. package/src/plugins/upnp.js.map +1 -0
  26. package/src/services/index.d.ts +3 -0
  27. package/src/services/index.d.ts.map +1 -1
  28. package/src/services/index.js +3 -0
  29. package/src/services/index.js.map +1 -1
  30. package/src/services/upnp-config.d.ts +131 -0
  31. package/src/services/upnp-config.d.ts.map +1 -0
  32. package/src/services/upnp-config.js +225 -0
  33. package/src/services/upnp-config.js.map +1 -0
  34. package/src/services/upnp-manager.d.ts +211 -0
  35. package/src/services/upnp-manager.d.ts.map +1 -0
  36. package/src/services/upnp-manager.js +447 -0
  37. package/src/services/upnp-manager.js.map +1 -0
  38. package/src/services/upnp.d.ts +241 -0
  39. package/src/services/upnp.d.ts.map +1 -0
  40. package/src/services/upnp.js +415 -0
  41. package/src/services/upnp.js.map +1 -0
@@ -0,0 +1,241 @@
1
+ /**
2
+ * UPnP Port Mapping Service.
3
+ *
4
+ * Provides automatic port forwarding via UPnP/NAT-PMP protocols,
5
+ * enabling servers behind NAT routers to be reachable from the internet.
6
+ * Uses nat-upnp as the primary client with retry logic and exponential
7
+ * backoff for resilience against slow or unreliable routers.
8
+ *
9
+ * Requirements: 2.1, 2.2, 2.3, 2.4, 2.5, 2.6, 2.7, 2.8, 2.9, 2.10
10
+ */
11
+ import { IUpnpService } from '../interfaces/network/upnpService';
12
+ import { IUpnpConfig, IUpnpMapping, PortMappingProtocol } from '../interfaces/network/upnpTypes';
13
+ /**
14
+ * Error thrown when a port number is outside the valid range (1-65535).
15
+ *
16
+ * @example
17
+ * ```typescript
18
+ * try {
19
+ * UpnpService.validatePort(70000);
20
+ * } catch (err) {
21
+ * if (err instanceof PortRangeError) {
22
+ * console.error(err.message); // "Port 70000 is outside the valid range (1-65535)"
23
+ * }
24
+ * }
25
+ * ```
26
+ */
27
+ export declare class PortRangeError extends Error {
28
+ constructor(port: number);
29
+ }
30
+ /**
31
+ * Error thrown when UPnP operations fail after all retry attempts.
32
+ *
33
+ * @example
34
+ * ```typescript
35
+ * try {
36
+ * await service.getExternalIp();
37
+ * } catch (err) {
38
+ * if (err instanceof UpnpOperationError) {
39
+ * console.error(`Operation failed: ${err.message}`);
40
+ * }
41
+ * }
42
+ * ```
43
+ */
44
+ export declare class UpnpOperationError extends Error {
45
+ constructor(operation: string, cause?: string);
46
+ }
47
+ /**
48
+ * Error thrown when the UPnP service has been closed and an operation is attempted.
49
+ *
50
+ * @example
51
+ * ```typescript
52
+ * const service = new UpnpService();
53
+ * await service.close();
54
+ *
55
+ * try {
56
+ * await service.getExternalIp(); // throws UpnpServiceClosedError
57
+ * } catch (err) {
58
+ * if (err instanceof UpnpServiceClosedError) {
59
+ * console.error('Service already closed');
60
+ * }
61
+ * }
62
+ * ```
63
+ */
64
+ export declare class UpnpServiceClosedError extends Error {
65
+ constructor();
66
+ }
67
+ /**
68
+ * UPnP port mapping service implementation.
69
+ *
70
+ * Manages port mappings on a NAT router via the UPnP protocol, with
71
+ * support for external IP discovery, mapping lifecycle management,
72
+ * and retry logic with exponential backoff.
73
+ *
74
+ * **Validates: Requirements 2.1, 2.2, 2.3, 2.4, 2.5, 2.6, 2.7, 2.8, 2.9, 2.10**
75
+ *
76
+ * @example
77
+ * ```typescript
78
+ * const service = new UpnpService();
79
+ *
80
+ * const externalIp = await service.getExternalIp();
81
+ * await service.createPortMapping({
82
+ * public: 3000,
83
+ * private: 3000,
84
+ * protocol: 'tcp',
85
+ * description: 'Express App HTTP',
86
+ * ttl: 3600,
87
+ * });
88
+ *
89
+ * // On shutdown
90
+ * await service.close();
91
+ * ```
92
+ */
93
+ export declare class UpnpService implements IUpnpService {
94
+ /** The nat-upnp client instance */
95
+ private client;
96
+ /** Service configuration */
97
+ private readonly config;
98
+ /** Active port mappings tracked in memory (keyed by "port:protocol") */
99
+ private readonly activeMappings;
100
+ /** Cached external IP address */
101
+ private cachedExternalIp;
102
+ /** Timestamp when the external IP was last fetched */
103
+ private ipCacheTimestamp;
104
+ /** TTL for the external IP cache in milliseconds */
105
+ private readonly ipCacheTtlMs;
106
+ /** Whether the service has been closed */
107
+ private closed;
108
+ /**
109
+ * Create a new UpnpService.
110
+ *
111
+ * **Validates: Requirement 2.1** — Accepts partial config and merges with defaults
112
+ *
113
+ * @param config - UPnP configuration (uses defaults for omitted fields)
114
+ * @param ipCacheTtlMs - External IP cache TTL in milliseconds (default 5 minutes)
115
+ */
116
+ constructor(config?: Partial<IUpnpConfig>, ipCacheTtlMs?: number);
117
+ /**
118
+ * Query the router for the external (public) IP address.
119
+ * Results are cached for the configured TTL to reduce router queries.
120
+ *
121
+ * **Validates: Requirement 2.8** — Return cached IP or query router and cache result
122
+ *
123
+ * @returns The external IP address as a string
124
+ * @throws {UpnpServiceClosedError} If the service has been closed
125
+ * @throws {UpnpOperationError} If the IP cannot be retrieved after retries
126
+ */
127
+ getExternalIp(): Promise<string>;
128
+ /**
129
+ * Create a port mapping on the router.
130
+ *
131
+ * **Validates: Requirements 2.2, 2.3** — Validate ports and create mapping with tracking
132
+ *
133
+ * @param mapping - The port mapping configuration to create
134
+ * @throws {PortRangeError} If any port is outside 1-65535
135
+ * @throws {UpnpServiceClosedError} If the service has been closed
136
+ * @throws {UpnpOperationError} If the mapping cannot be created after retries
137
+ */
138
+ createPortMapping(mapping: IUpnpMapping): Promise<void>;
139
+ /**
140
+ * Remove a specific port mapping from the router.
141
+ *
142
+ * **Validates: Requirement 2.4** — Remove mapping from router and in-memory tracking
143
+ *
144
+ * @param publicPort - The external port number to unmap
145
+ * @param protocol - The transport protocol of the mapping to remove
146
+ * @throws {PortRangeError} If the port is outside 1-65535
147
+ * @throws {UpnpServiceClosedError} If the service has been closed
148
+ * @throws {UpnpOperationError} If the mapping cannot be removed after retries
149
+ */
150
+ removePortMapping(publicPort: number, protocol: PortMappingProtocol): Promise<void>;
151
+ /**
152
+ * Remove all port mappings created by this service.
153
+ *
154
+ * **Validates: Requirement 2.9** — Remove all active mappings on close
155
+ *
156
+ * Attempts to remove each mapping individually. Failures on individual
157
+ * mappings are logged but do not prevent removal of remaining mappings.
158
+ */
159
+ removeAllMappings(): Promise<void>;
160
+ /**
161
+ * Get all active port mappings managed by this service.
162
+ *
163
+ * **Validates: Requirement 2.5** — Return all mappings tracked in memory
164
+ *
165
+ * @returns Array of active port mappings
166
+ */
167
+ getMappings(): Promise<IUpnpMapping[]>;
168
+ /**
169
+ * Close the UPnP client and release resources.
170
+ * Removes all active mappings before closing.
171
+ *
172
+ * **Validates: Requirement 2.9** — Remove all mappings, close client, reject subsequent ops
173
+ *
174
+ * @throws {UpnpServiceClosedError} If the service has already been closed
175
+ */
176
+ close(): Promise<void>;
177
+ /**
178
+ * Validate that a port number is within the valid range (1-65535).
179
+ *
180
+ * @param port - The port number to validate
181
+ * @throws {PortRangeError} If the port is outside the valid range
182
+ */
183
+ static validatePort(port: number): void;
184
+ /**
185
+ * Generate a unique key for a mapping based on port and protocol.
186
+ *
187
+ * @param port - The public port number
188
+ * @param protocol - The transport protocol
189
+ * @returns A string key in the format "port:protocol"
190
+ */
191
+ static mappingKey(port: number, protocol: PortMappingProtocol): string;
192
+ /**
193
+ * Ensure the service has not been closed.
194
+ *
195
+ * @throws {UpnpServiceClosedError} If the service has been closed
196
+ */
197
+ private ensureNotClosed;
198
+ /**
199
+ * Check whether the cached external IP has expired.
200
+ *
201
+ * @returns true if the cache has expired or was never set
202
+ */
203
+ private isIpCacheExpired;
204
+ /**
205
+ * Execute an operation with retry logic and exponential backoff.
206
+ *
207
+ * **Validates: Requirements 2.6, 2.7** — Retry with exponential backoff, throw UpnpOperationError on exhaustion
208
+ *
209
+ * @param operationName - Name of the operation (for error messages)
210
+ * @param operation - The async operation to execute
211
+ * @returns The result of the operation
212
+ * @throws {UpnpOperationError} If all retry attempts are exhausted
213
+ */
214
+ private withRetry;
215
+ /**
216
+ * Promisified wrapper around the nat-upnp externalIp callback API.
217
+ *
218
+ * @returns The external IP address
219
+ */
220
+ private promisifiedExternalIp;
221
+ /**
222
+ * Promisified wrapper around the nat-upnp portMapping callback API.
223
+ *
224
+ * @param options - Port mapping options
225
+ */
226
+ private promisifiedPortMapping;
227
+ /**
228
+ * Promisified wrapper around the nat-upnp portUnmapping callback API.
229
+ *
230
+ * @param options - Port unmapping options
231
+ */
232
+ private promisifiedPortUnmapping;
233
+ /**
234
+ * Sleep for the specified duration.
235
+ *
236
+ * @param ms - Duration in milliseconds
237
+ * @returns A promise that resolves after the specified duration
238
+ */
239
+ static sleep(ms: number): Promise<void>;
240
+ }
241
+ //# sourceMappingURL=upnp.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"upnp.d.ts","sourceRoot":"","sources":["../../../../../packages/digitaldefiance-node-express-suite/src/services/upnp.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAIH,OAAO,EAAE,YAAY,EAAE,MAAM,mCAAmC,CAAC;AACjE,OAAO,EACL,WAAW,EACX,YAAY,EACZ,mBAAmB,EAEpB,MAAM,iCAAiC,CAAC;AAezC;;;;;;;;;;;;;GAaG;AACH,qBAAa,cAAe,SAAQ,KAAK;gBAC3B,IAAI,EAAE,MAAM;CAIzB;AAED;;;;;;;;;;;;;GAaG;AACH,qBAAa,kBAAmB,SAAQ,KAAK;gBAC/B,SAAS,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,MAAM;CAO9C;AAED;;;;;;;;;;;;;;;;GAgBG;AACH,qBAAa,sBAAuB,SAAQ,KAAK;;CAKhD;AAID;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AACH,qBAAa,WAAY,YAAW,YAAY;IAC9C,mCAAmC;IACnC,OAAO,CAAC,MAAM,CAAiB;IAE/B,4BAA4B;IAC5B,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAwB;IAE/C,wEAAwE;IACxE,OAAO,CAAC,QAAQ,CAAC,cAAc,CAAwC;IAEvE,iCAAiC;IACjC,OAAO,CAAC,gBAAgB,CAAuB;IAE/C,sDAAsD;IACtD,OAAO,CAAC,gBAAgB,CAAK;IAE7B,oDAAoD;IACpD,OAAO,CAAC,QAAQ,CAAC,YAAY,CAAS;IAEtC,0CAA0C;IAC1C,OAAO,CAAC,MAAM,CAAS;IAEvB;;;;;;;OAOG;gBAED,MAAM,GAAE,OAAO,CAAC,WAAW,CAAM,EACjC,YAAY,GAAE,MAAgC;IAShD;;;;;;;;;OASG;IACG,aAAa,IAAI,OAAO,CAAC,MAAM,CAAC;IAkBtC;;;;;;;;;OASG;IACG,iBAAiB,CAAC,OAAO,EAAE,YAAY,GAAG,OAAO,CAAC,IAAI,CAAC;IAoB7D;;;;;;;;;;OAUG;IACG,iBAAiB,CACrB,UAAU,EAAE,MAAM,EAClB,QAAQ,EAAE,mBAAmB,GAC5B,OAAO,CAAC,IAAI,CAAC;IAgBhB;;;;;;;OAOG;IACG,iBAAiB,IAAI,OAAO,CAAC,IAAI,CAAC;IA+BxC;;;;;;OAMG;IACG,WAAW,IAAI,OAAO,CAAC,YAAY,EAAE,CAAC;IAK5C;;;;;;;OAOG;IACG,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;IAiB5B;;;;;OAKG;IACH,MAAM,CAAC,YAAY,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI;IAMvC;;;;;;OAMG;IACH,MAAM,CAAC,UAAU,CAAC,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,mBAAmB,GAAG,MAAM;IAMtE;;;;OAIG;IACH,OAAO,CAAC,eAAe;IAMvB;;;;OAIG;IACH,OAAO,CAAC,gBAAgB;IAIxB;;;;;;;;;OASG;YACW,SAAS;IA0BvB;;;;OAIG;IACH,OAAO,CAAC,qBAAqB;IAc7B;;;;OAIG;IACH,OAAO,CAAC,sBAAsB;IAc9B;;;;OAIG;IACH,OAAO,CAAC,wBAAwB;IAchC;;;;;OAKG;IACH,MAAM,CAAC,KAAK,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;CAGxC"}
@@ -0,0 +1,415 @@
1
+ "use strict";
2
+ /**
3
+ * UPnP Port Mapping Service.
4
+ *
5
+ * Provides automatic port forwarding via UPnP/NAT-PMP protocols,
6
+ * enabling servers behind NAT routers to be reachable from the internet.
7
+ * Uses nat-upnp as the primary client with retry logic and exponential
8
+ * backoff for resilience against slow or unreliable routers.
9
+ *
10
+ * Requirements: 2.1, 2.2, 2.3, 2.4, 2.5, 2.6, 2.7, 2.8, 2.9, 2.10
11
+ */
12
+ Object.defineProperty(exports, "__esModule", { value: true });
13
+ exports.UpnpService = exports.UpnpServiceClosedError = exports.UpnpOperationError = exports.PortRangeError = void 0;
14
+ const tslib_1 = require("tslib");
15
+ const natUpnp = tslib_1.__importStar(require("nat-upnp"));
16
+ const upnpTypes_1 = require("../interfaces/network/upnpTypes");
17
+ // ─── Constants ──────────────────────────────────────────────────────────────
18
+ /** Minimum valid port number */
19
+ const MIN_PORT = 1;
20
+ /** Maximum valid port number */
21
+ const MAX_PORT = 65535;
22
+ /** Default external IP cache TTL in milliseconds (5 minutes) */
23
+ const DEFAULT_IP_CACHE_TTL_MS = 5 * 60 * 1000;
24
+ // ─── Error classes ──────────────────────────────────────────────────────────
25
+ /**
26
+ * Error thrown when a port number is outside the valid range (1-65535).
27
+ *
28
+ * @example
29
+ * ```typescript
30
+ * try {
31
+ * UpnpService.validatePort(70000);
32
+ * } catch (err) {
33
+ * if (err instanceof PortRangeError) {
34
+ * console.error(err.message); // "Port 70000 is outside the valid range (1-65535)"
35
+ * }
36
+ * }
37
+ * ```
38
+ */
39
+ class PortRangeError extends Error {
40
+ constructor(port) {
41
+ super(`Port ${port} is outside the valid range (${MIN_PORT}-${MAX_PORT})`);
42
+ this.name = 'PortRangeError';
43
+ }
44
+ }
45
+ exports.PortRangeError = PortRangeError;
46
+ /**
47
+ * Error thrown when UPnP operations fail after all retry attempts.
48
+ *
49
+ * @example
50
+ * ```typescript
51
+ * try {
52
+ * await service.getExternalIp();
53
+ * } catch (err) {
54
+ * if (err instanceof UpnpOperationError) {
55
+ * console.error(`Operation failed: ${err.message}`);
56
+ * }
57
+ * }
58
+ * ```
59
+ */
60
+ class UpnpOperationError extends Error {
61
+ constructor(operation, cause) {
62
+ const message = cause
63
+ ? `UPnP ${operation} failed: ${cause}`
64
+ : `UPnP ${operation} failed`;
65
+ super(message);
66
+ this.name = 'UpnpOperationError';
67
+ }
68
+ }
69
+ exports.UpnpOperationError = UpnpOperationError;
70
+ /**
71
+ * Error thrown when the UPnP service has been closed and an operation is attempted.
72
+ *
73
+ * @example
74
+ * ```typescript
75
+ * const service = new UpnpService();
76
+ * await service.close();
77
+ *
78
+ * try {
79
+ * await service.getExternalIp(); // throws UpnpServiceClosedError
80
+ * } catch (err) {
81
+ * if (err instanceof UpnpServiceClosedError) {
82
+ * console.error('Service already closed');
83
+ * }
84
+ * }
85
+ * ```
86
+ */
87
+ class UpnpServiceClosedError extends Error {
88
+ constructor() {
89
+ super('UPnP service has been closed');
90
+ this.name = 'UpnpServiceClosedError';
91
+ }
92
+ }
93
+ exports.UpnpServiceClosedError = UpnpServiceClosedError;
94
+ // ─── Service ────────────────────────────────────────────────────────────────
95
+ /**
96
+ * UPnP port mapping service implementation.
97
+ *
98
+ * Manages port mappings on a NAT router via the UPnP protocol, with
99
+ * support for external IP discovery, mapping lifecycle management,
100
+ * and retry logic with exponential backoff.
101
+ *
102
+ * **Validates: Requirements 2.1, 2.2, 2.3, 2.4, 2.5, 2.6, 2.7, 2.8, 2.9, 2.10**
103
+ *
104
+ * @example
105
+ * ```typescript
106
+ * const service = new UpnpService();
107
+ *
108
+ * const externalIp = await service.getExternalIp();
109
+ * await service.createPortMapping({
110
+ * public: 3000,
111
+ * private: 3000,
112
+ * protocol: 'tcp',
113
+ * description: 'Express App HTTP',
114
+ * ttl: 3600,
115
+ * });
116
+ *
117
+ * // On shutdown
118
+ * await service.close();
119
+ * ```
120
+ */
121
+ class UpnpService {
122
+ /** The nat-upnp client instance */
123
+ client;
124
+ /** Service configuration */
125
+ config;
126
+ /** Active port mappings tracked in memory (keyed by "port:protocol") */
127
+ activeMappings = new Map();
128
+ /** Cached external IP address */
129
+ cachedExternalIp = null;
130
+ /** Timestamp when the external IP was last fetched */
131
+ ipCacheTimestamp = 0;
132
+ /** TTL for the external IP cache in milliseconds */
133
+ ipCacheTtlMs;
134
+ /** Whether the service has been closed */
135
+ closed = false;
136
+ /**
137
+ * Create a new UpnpService.
138
+ *
139
+ * **Validates: Requirement 2.1** — Accepts partial config and merges with defaults
140
+ *
141
+ * @param config - UPnP configuration (uses defaults for omitted fields)
142
+ * @param ipCacheTtlMs - External IP cache TTL in milliseconds (default 5 minutes)
143
+ */
144
+ constructor(config = {}, ipCacheTtlMs = DEFAULT_IP_CACHE_TTL_MS) {
145
+ this.config = { ...upnpTypes_1.UPNP_CONFIG_DEFAULTS, ...config };
146
+ this.ipCacheTtlMs = ipCacheTtlMs;
147
+ this.client = natUpnp.createClient();
148
+ }
149
+ // ─── Public API ─────────────────────────────────────────────────────────
150
+ /**
151
+ * Query the router for the external (public) IP address.
152
+ * Results are cached for the configured TTL to reduce router queries.
153
+ *
154
+ * **Validates: Requirement 2.8** — Return cached IP or query router and cache result
155
+ *
156
+ * @returns The external IP address as a string
157
+ * @throws {UpnpServiceClosedError} If the service has been closed
158
+ * @throws {UpnpOperationError} If the IP cannot be retrieved after retries
159
+ */
160
+ async getExternalIp() {
161
+ this.ensureNotClosed();
162
+ // Return cached IP if still valid
163
+ if (this.cachedExternalIp && !this.isIpCacheExpired()) {
164
+ return this.cachedExternalIp;
165
+ }
166
+ const ip = await this.withRetry('getExternalIp', () => this.promisifiedExternalIp());
167
+ this.cachedExternalIp = ip;
168
+ this.ipCacheTimestamp = Date.now();
169
+ return ip;
170
+ }
171
+ /**
172
+ * Create a port mapping on the router.
173
+ *
174
+ * **Validates: Requirements 2.2, 2.3** — Validate ports and create mapping with tracking
175
+ *
176
+ * @param mapping - The port mapping configuration to create
177
+ * @throws {PortRangeError} If any port is outside 1-65535
178
+ * @throws {UpnpServiceClosedError} If the service has been closed
179
+ * @throws {UpnpOperationError} If the mapping cannot be created after retries
180
+ */
181
+ async createPortMapping(mapping) {
182
+ this.ensureNotClosed();
183
+ UpnpService.validatePort(mapping.public);
184
+ UpnpService.validatePort(mapping.private);
185
+ await this.withRetry('createPortMapping', () => this.promisifiedPortMapping({
186
+ public: mapping.public,
187
+ private: mapping.private,
188
+ protocol: mapping.protocol,
189
+ description: mapping.description,
190
+ ttl: mapping.ttl,
191
+ }));
192
+ // Track the mapping in memory
193
+ const key = UpnpService.mappingKey(mapping.public, mapping.protocol);
194
+ this.activeMappings.set(key, { ...mapping });
195
+ }
196
+ /**
197
+ * Remove a specific port mapping from the router.
198
+ *
199
+ * **Validates: Requirement 2.4** — Remove mapping from router and in-memory tracking
200
+ *
201
+ * @param publicPort - The external port number to unmap
202
+ * @param protocol - The transport protocol of the mapping to remove
203
+ * @throws {PortRangeError} If the port is outside 1-65535
204
+ * @throws {UpnpServiceClosedError} If the service has been closed
205
+ * @throws {UpnpOperationError} If the mapping cannot be removed after retries
206
+ */
207
+ async removePortMapping(publicPort, protocol) {
208
+ this.ensureNotClosed();
209
+ UpnpService.validatePort(publicPort);
210
+ await this.withRetry('removePortMapping', () => this.promisifiedPortUnmapping({
211
+ public: publicPort,
212
+ protocol,
213
+ }));
214
+ // Remove from tracked mappings
215
+ const key = UpnpService.mappingKey(publicPort, protocol);
216
+ this.activeMappings.delete(key);
217
+ }
218
+ /**
219
+ * Remove all port mappings created by this service.
220
+ *
221
+ * **Validates: Requirement 2.9** — Remove all active mappings on close
222
+ *
223
+ * Attempts to remove each mapping individually. Failures on individual
224
+ * mappings are logged but do not prevent removal of remaining mappings.
225
+ */
226
+ async removeAllMappings() {
227
+ this.ensureNotClosed();
228
+ const mappings = Array.from(this.activeMappings.values());
229
+ const errors = [];
230
+ for (const mapping of mappings) {
231
+ try {
232
+ await this.removePortMapping(mapping.public, mapping.protocol);
233
+ }
234
+ catch (error) {
235
+ errors.push({
236
+ mapping,
237
+ error: error instanceof Error ? error : new Error(String(error)),
238
+ });
239
+ }
240
+ }
241
+ // Clear all tracked mappings even if some removals failed
242
+ this.activeMappings.clear();
243
+ if (errors.length > 0) {
244
+ const details = errors
245
+ .map((e) => `${e.mapping.public}/${e.mapping.protocol}: ${e.error.message}`)
246
+ .join('; ');
247
+ throw new UpnpOperationError('removeAllMappings', details);
248
+ }
249
+ }
250
+ /**
251
+ * Get all active port mappings managed by this service.
252
+ *
253
+ * **Validates: Requirement 2.5** — Return all mappings tracked in memory
254
+ *
255
+ * @returns Array of active port mappings
256
+ */
257
+ async getMappings() {
258
+ this.ensureNotClosed();
259
+ return Array.from(this.activeMappings.values());
260
+ }
261
+ /**
262
+ * Close the UPnP client and release resources.
263
+ * Removes all active mappings before closing.
264
+ *
265
+ * **Validates: Requirement 2.9** — Remove all mappings, close client, reject subsequent ops
266
+ *
267
+ * @throws {UpnpServiceClosedError} If the service has already been closed
268
+ */
269
+ async close() {
270
+ this.ensureNotClosed();
271
+ // Best-effort removal of all mappings before closing
272
+ try {
273
+ await this.removeAllMappings();
274
+ }
275
+ catch {
276
+ // Swallow errors during cleanup — we're shutting down
277
+ }
278
+ this.client.close();
279
+ this.closed = true;
280
+ this.cachedExternalIp = null;
281
+ }
282
+ // ─── Static helpers ─────────────────────────────────────────────────────
283
+ /**
284
+ * Validate that a port number is within the valid range (1-65535).
285
+ *
286
+ * @param port - The port number to validate
287
+ * @throws {PortRangeError} If the port is outside the valid range
288
+ */
289
+ static validatePort(port) {
290
+ if (!Number.isInteger(port) || port < MIN_PORT || port > MAX_PORT) {
291
+ throw new PortRangeError(port);
292
+ }
293
+ }
294
+ /**
295
+ * Generate a unique key for a mapping based on port and protocol.
296
+ *
297
+ * @param port - The public port number
298
+ * @param protocol - The transport protocol
299
+ * @returns A string key in the format "port:protocol"
300
+ */
301
+ static mappingKey(port, protocol) {
302
+ return `${port}:${protocol}`;
303
+ }
304
+ // ─── Private helpers ────────────────────────────────────────────────────
305
+ /**
306
+ * Ensure the service has not been closed.
307
+ *
308
+ * @throws {UpnpServiceClosedError} If the service has been closed
309
+ */
310
+ ensureNotClosed() {
311
+ if (this.closed) {
312
+ throw new UpnpServiceClosedError();
313
+ }
314
+ }
315
+ /**
316
+ * Check whether the cached external IP has expired.
317
+ *
318
+ * @returns true if the cache has expired or was never set
319
+ */
320
+ isIpCacheExpired() {
321
+ return Date.now() - this.ipCacheTimestamp > this.ipCacheTtlMs;
322
+ }
323
+ /**
324
+ * Execute an operation with retry logic and exponential backoff.
325
+ *
326
+ * **Validates: Requirements 2.6, 2.7** — Retry with exponential backoff, throw UpnpOperationError on exhaustion
327
+ *
328
+ * @param operationName - Name of the operation (for error messages)
329
+ * @param operation - The async operation to execute
330
+ * @returns The result of the operation
331
+ * @throws {UpnpOperationError} If all retry attempts are exhausted
332
+ */
333
+ async withRetry(operationName, operation) {
334
+ let lastError;
335
+ for (let attempt = 0; attempt <= this.config.retryAttempts; attempt++) {
336
+ try {
337
+ return await operation();
338
+ }
339
+ catch (error) {
340
+ lastError = error instanceof Error ? error : new Error(String(error));
341
+ // Don't delay after the last attempt
342
+ if (attempt < this.config.retryAttempts) {
343
+ const delay = this.config.retryDelay * Math.pow(2, attempt);
344
+ await UpnpService.sleep(delay);
345
+ }
346
+ }
347
+ }
348
+ throw new UpnpOperationError(operationName, lastError?.message ?? 'unknown error');
349
+ }
350
+ /**
351
+ * Promisified wrapper around the nat-upnp externalIp callback API.
352
+ *
353
+ * @returns The external IP address
354
+ */
355
+ promisifiedExternalIp() {
356
+ return new Promise((resolve, reject) => {
357
+ this.client.externalIp((err, ip) => {
358
+ if (err) {
359
+ reject(err);
360
+ }
361
+ else if (ip) {
362
+ resolve(ip);
363
+ }
364
+ else {
365
+ reject(new Error('No external IP returned'));
366
+ }
367
+ });
368
+ });
369
+ }
370
+ /**
371
+ * Promisified wrapper around the nat-upnp portMapping callback API.
372
+ *
373
+ * @param options - Port mapping options
374
+ */
375
+ promisifiedPortMapping(options) {
376
+ return new Promise((resolve, reject) => {
377
+ this.client.portMapping(options, (err) => {
378
+ if (err) {
379
+ reject(err);
380
+ }
381
+ else {
382
+ resolve();
383
+ }
384
+ });
385
+ });
386
+ }
387
+ /**
388
+ * Promisified wrapper around the nat-upnp portUnmapping callback API.
389
+ *
390
+ * @param options - Port unmapping options
391
+ */
392
+ promisifiedPortUnmapping(options) {
393
+ return new Promise((resolve, reject) => {
394
+ this.client.portUnmapping(options, (err) => {
395
+ if (err) {
396
+ reject(err);
397
+ }
398
+ else {
399
+ resolve();
400
+ }
401
+ });
402
+ });
403
+ }
404
+ /**
405
+ * Sleep for the specified duration.
406
+ *
407
+ * @param ms - Duration in milliseconds
408
+ * @returns A promise that resolves after the specified duration
409
+ */
410
+ static sleep(ms) {
411
+ return new Promise((resolve) => setTimeout(resolve, ms));
412
+ }
413
+ }
414
+ exports.UpnpService = UpnpService;
415
+ //# sourceMappingURL=upnp.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"upnp.js","sourceRoot":"","sources":["../../../../../packages/digitaldefiance-node-express-suite/src/services/upnp.ts"],"names":[],"mappings":"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"}