@chdlabs/my-shield 0.2.0 → 0.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.
@@ -0,0 +1,225 @@
1
+ /**
2
+ * Enterprise Client IP Resolution & Gateway Protection
3
+ *
4
+ * Provides safe IP extraction across multi-hop reverse proxies, Cloudflare,
5
+ * Nginx, HAProxy, and containerized Docker/Kubernetes environments while
6
+ * defending against header spoofing and accidental gateway blacklisting.
7
+ *
8
+ * @author ChdLabs
9
+ * @license MIT
10
+ */
11
+ import net from 'node:net';
12
+
13
+ export class IPResolver {
14
+ /**
15
+ * @param {Object} [options={}]
16
+ * @param {boolean|string|string[]} [options.trustProxy='loopback,linklocal,uniquelocal']
17
+ * @param {boolean} [options.protectGateways=true]
18
+ * @param {string[]} [options.protectedSubnets] Additional subnets/IPs to protect from bans
19
+ */
20
+ constructor(options = {}) {
21
+ this.trustProxy = options.trustProxy !== undefined ? options.trustProxy : 'loopback,linklocal,uniquelocal';
22
+ this.gatewayProtection = options.protectGateways !== false;
23
+ this.protectedSubnets = options.protectedSubnets || [];
24
+ }
25
+
26
+ /**
27
+ * Clean and normalize raw IP string
28
+ * - Strips surrounding whitespace
29
+ * - Strips IPv6 zone index (e.g. %eth0)
30
+ * - Strips IPv6 port brackets (e.g. [2001:db8::1]:8080 -> 2001:db8::1)
31
+ * - Strips IPv4 port (e.g. 198.51.100.42:54321 -> 198.51.100.42)
32
+ * - Normalizes IPv6-mapped IPv4 (e.g. ::ffff:192.0.2.1 -> 192.0.2.1)
33
+ * @param {string} rawIp
34
+ * @returns {string}
35
+ */
36
+ normalize(rawIp) {
37
+ if (!rawIp || typeof rawIp !== 'string') return '';
38
+ let ip = rawIp.trim();
39
+
40
+ // 1. Strip IPv6 bracketed port: [2001:db8::1]:8080
41
+ if (ip.startsWith('[') && ip.includes(']')) {
42
+ const closingIdx = ip.indexOf(']');
43
+ ip = ip.substring(1, closingIdx);
44
+ } else if (ip.includes(':')) {
45
+ // Check if it's an IPv4 with port: 1.2.3.4:5678 (contains exactly one ':')
46
+ const colonCount = (ip.match(/:/g) || []).length;
47
+ if (colonCount === 1) {
48
+ const parts = ip.split(':');
49
+ if (net.isIPv4(parts[0])) {
50
+ ip = parts[0];
51
+ }
52
+ }
53
+ }
54
+
55
+ // 2. Strip IPv6 zone index: fe80::1%eth0 -> fe80::1
56
+ if (ip.includes('%')) {
57
+ ip = ip.split('%')[0];
58
+ }
59
+
60
+ // 3. Normalize IPv6-mapped IPv4: ::ffff:192.168.1.1 -> 192.168.1.1
61
+ const lower = ip.toLowerCase();
62
+ if (lower.startsWith('::ffff:') && lower.indexOf('.', 7) !== -1) {
63
+ const candidateV4 = ip.substring(7);
64
+ if (net.isIPv4(candidateV4)) {
65
+ return candidateV4;
66
+ }
67
+ }
68
+
69
+ return ip;
70
+ }
71
+
72
+ /**
73
+ * Check if string is a valid IPv4 or IPv6 address
74
+ * @param {string} ip
75
+ * @returns {boolean}
76
+ */
77
+ isValid(ip) {
78
+ if (!ip || typeof ip !== 'string') return false;
79
+ return net.isIP(ip) !== 0;
80
+ }
81
+
82
+ /**
83
+ * Check if an IPv4 or IPv6 address is private, loopback, or link-local
84
+ * @param {string} ip
85
+ * @returns {boolean}
86
+ */
87
+ isPrivateOrLoopback(ip) {
88
+ const clean = this.normalize(ip);
89
+ if (!this.isValid(clean)) return false;
90
+
91
+ if (net.isIPv4(clean)) {
92
+ const octets = clean.split('.').map(Number);
93
+ const [o1, o2] = octets;
94
+
95
+ // 127.0.0.0/8 (Loopback)
96
+ if (o1 === 127) return true;
97
+ // 10.0.0.0/8 (Private)
98
+ if (o1 === 10) return true;
99
+ // 172.16.0.0/12 (Private including Docker 172.17.x.x)
100
+ if (o1 === 172 && o2 >= 16 && o2 <= 31) return true;
101
+ // 192.168.0.0/16 (Private)
102
+ if (o1 === 192 && o2 === 168) return true;
103
+ // 169.254.0.0/16 (Link-local)
104
+ if (o1 === 169 && o2 === 254) return true;
105
+ // 0.0.0.0
106
+ if (clean === '0.0.0.0') return true;
107
+
108
+ return false;
109
+ }
110
+
111
+ if (net.isIPv6(clean)) {
112
+ const lower = clean.toLowerCase();
113
+ // ::1 (Loopback)
114
+ if (lower === '::1' || lower === '0:0:0:0:0:0:0:1') return true;
115
+ // fe80::/10 (Link-local)
116
+ if (lower.startsWith('fe8') || lower.startsWith('fe9') || lower.startsWith('fea') || lower.startsWith('feb')) return true;
117
+ // fc00::/7 (Unique local / private: fc00::/8 and fd00::/8)
118
+ if (lower.startsWith('fc') || lower.startsWith('fd')) return true;
119
+
120
+ return false;
121
+ }
122
+
123
+ return false;
124
+ }
125
+
126
+ /**
127
+ * Check if an IP is a protected container gateway or loopback that should NEVER be banned
128
+ * (e.g. 172.17.0.1 Docker bridge, 127.0.0.1, ::1)
129
+ * @param {string} ip
130
+ * @returns {boolean}
131
+ */
132
+ isProtectedGateway(ip) {
133
+ if (!this.gatewayProtection) return false;
134
+ const clean = this.normalize(ip);
135
+ if (!this.isValid(clean)) return false;
136
+
137
+ // Loopback is always protected
138
+ if (clean === '127.0.0.1' || clean === '::1' || clean.startsWith('127.')) {
139
+ return true;
140
+ }
141
+
142
+ // Docker default bridge gateway and bridge subnet: 172.17.0.0/16
143
+ if (net.isIPv4(clean)) {
144
+ const octets = clean.split('.').map(Number);
145
+ // Default docker bridge network gateway: 172.17.0.1
146
+ if (octets[0] === 172 && octets[1] === 17) {
147
+ // If it ends with .1 (standard docker/podman host bridge gateway)
148
+ if (octets[3] === 1) return true;
149
+ }
150
+ // Standard docker compose default gateway: 172.18.0.1, 172.19.0.1, 172.20.0.1
151
+ if (octets[0] === 172 && octets[1] >= 17 && octets[1] <= 31 && octets[3] === 1) {
152
+ return true;
153
+ }
154
+ }
155
+
156
+ // Check any user-defined protected subnets/IPs
157
+ for (const subnet of this.protectedSubnets) {
158
+ if (clean === subnet) return true;
159
+ }
160
+
161
+ return false;
162
+ }
163
+
164
+ /**
165
+ * Extract client IP safely from Express request
166
+ * @param {Object} req Express request
167
+ * @returns {string} Normalized client IP
168
+ */
169
+ resolve(req) {
170
+ if (!req) return '127.0.0.1';
171
+
172
+ // 1. Direct exposure mode (if trustProxy: false, ignore all proxy headers)
173
+ if (this.trustProxy === false) {
174
+ const direct = req.socket?.remoteAddress || req.connection?.remoteAddress || req.ip || '127.0.0.1';
175
+ const clean = this.normalize(direct);
176
+ return this.isValid(clean) ? clean : '127.0.0.1';
177
+ }
178
+
179
+ const headers = req.headers || {};
180
+
181
+ // 2. Priority check for single-value reverse proxy headers
182
+ const singleHeaders = [
183
+ 'cf-connecting-ip', // Cloudflare
184
+ 'true-client-ip', // Akamai & Cloudflare Enterprise
185
+ 'x-real-ip', // Nginx proxy_set_header X-Real-IP $remote_addr
186
+ 'x-client-ip' // Apache & other reverse proxies
187
+ ];
188
+
189
+ for (const h of singleHeaders) {
190
+ const val = headers[h];
191
+ if (val && typeof val === 'string') {
192
+ const clean = this.normalize(val);
193
+ if (this.isValid(clean)) {
194
+ return clean;
195
+ }
196
+ }
197
+ }
198
+
199
+ // 3. Multi-hop X-Forwarded-For chain parsing
200
+ const xff = headers['x-forwarded-for'];
201
+ if (xff && typeof xff === 'string') {
202
+ const ips = xff
203
+ .split(',')
204
+ .map(s => this.normalize(s))
205
+ .filter(s => this.isValid(s));
206
+
207
+ if (ips.length > 0) {
208
+ // Find first public IP (skipping private reverse proxy intermediate hops)
209
+ const publicIp = ips.find(ip => !this.isPrivateOrLoopback(ip));
210
+ if (publicIp) {
211
+ return publicIp;
212
+ }
213
+ // If all IPs in chain are private, return the outermost client (first in XFF)
214
+ return ips[0];
215
+ }
216
+ }
217
+
218
+ // 4. Fallback to Express req.ip or socket remote address
219
+ const fallback = req.ip || req.socket?.remoteAddress || req.connection?.remoteAddress || '127.0.0.1';
220
+ const cleanFallback = this.normalize(fallback);
221
+ return this.isValid(cleanFallback) ? cleanFallback : '127.0.0.1';
222
+ }
223
+ }
224
+
225
+ export default IPResolver;
package/src/index.d.ts CHANGED
@@ -47,13 +47,25 @@ export interface AdminOptions {
47
47
  enabled?: boolean;
48
48
  }
49
49
 
50
+ export interface BehavioralVerdict {
51
+ isHuman: boolean;
52
+ probability: number;
53
+ score: number;
54
+ latencyMs: number;
55
+ ip?: string;
56
+ mode?: 'block' | 'audit' | 'header-only';
57
+ reason?: string;
58
+ }
59
+
50
60
  export interface BehavioralGuardOptions {
51
61
  threshold?: number;
62
+ mode?: 'block' | 'audit' | 'header-only';
52
63
  headerName?: string;
53
64
  replayWindowMs?: number;
54
65
  allowFallbackOnError?: boolean;
55
66
  autoBlockIP?: boolean;
56
67
  onBotDetected?: (req: any, res: any, meta: any) => void;
68
+ onVerdict?: (verdict: BehavioralVerdict, req: any, res: any) => void;
57
69
  }
58
70
 
59
71
  export interface BiometricsOptions {
@@ -73,8 +85,44 @@ export interface BehavioralVerificationResult {
73
85
  latencyMs: number;
74
86
  }
75
87
 
88
+ export interface FileStorageOptions {
89
+ type?: 'file';
90
+ directory?: string;
91
+ filename?: string;
92
+ debounceMs?: number;
93
+ }
94
+
95
+ export interface CallbackStorageOptions {
96
+ onLoad?: () => Promise<Array<{ ip: string; reason: string; metadata?: any; expiresAt?: number | null }>>;
97
+ onBlock?: (ip: string, reason: string, metadata?: any, expiresAt?: number | null) => Promise<void>;
98
+ onUnblock?: (ip: string) => Promise<void>;
99
+ }
100
+
101
+ export type StorageOption = 'memory' | 'file' | FileStorageOptions | CallbackStorageOptions | BaseStorageAdapter;
102
+
103
+ export interface IPResolverOptions {
104
+ trustProxy?: boolean | string | string[];
105
+ protectGateways?: boolean;
106
+ protectedSubnets?: string[];
107
+ }
108
+
109
+ export interface OtpGateOptions {
110
+ adminEmail?: string;
111
+ jwtSecret?: string;
112
+ secret?: string;
113
+ cookieName?: string;
114
+ codeTtlMs?: number;
115
+ sessionTtlMs?: number;
116
+ maxAttempts?: number;
117
+ sendOtp?: (email: string, code: string) => Promise<void>;
118
+ }
119
+
76
120
  export interface ShieldOptions {
77
121
  whitelist?: string[];
122
+ trustProxy?: boolean | string | string[];
123
+ protectGateways?: boolean;
124
+ protectedSubnets?: string[];
125
+ storage?: StorageOption;
78
126
  bloom?: BloomOptions;
79
127
  lru?: LRUOptions;
80
128
  honeypot?: HoneypotOptions;
@@ -100,9 +148,90 @@ export interface AdminRouterOptions {
100
148
  authMiddleware?: RequestHandler | RequestHandler[];
101
149
  }
102
150
 
151
+ export class BaseStorageAdapter {
152
+ shield: any;
153
+ init(shield: any): Promise<void>;
154
+ loadBlockedIPs(): Promise<Array<{ ip: string; reason: string; metadata?: any; expiresAt?: number | null }>>;
155
+ saveBlockedIP(ip: string, reason: string, metadata?: any, expiresAt?: number | null): Promise<void>;
156
+ removeBlockedIP(ip: string): Promise<void>;
157
+ close(): Promise<void>;
158
+ }
159
+
160
+ export class MemoryAdapter extends BaseStorageAdapter {}
161
+ export class FileStorageAdapter extends BaseStorageAdapter {
162
+ constructor(options?: FileStorageOptions & { logger?: any });
163
+ flushSync(): void;
164
+ }
165
+ export class CallbackStorageAdapter extends BaseStorageAdapter {
166
+ constructor(options?: CallbackStorageOptions & { logger?: any });
167
+ }
168
+
169
+ export function createStorageAdapter(storageOption?: StorageOption, logger?: any): BaseStorageAdapter;
170
+
171
+ export class IPResolver {
172
+ constructor(options?: IPResolverOptions);
173
+ normalize(rawIp: string): string;
174
+ isValid(ip: string): boolean;
175
+ isPrivateOrLoopback(ip: string): boolean;
176
+ isProtectedGateway(ip: string): boolean;
177
+ resolve(req: any): string;
178
+ }
179
+
180
+ export class OTPGate {
181
+ constructor(options?: OtpGateOptions & { logger?: any });
182
+ generateCode(): string;
183
+ createSessionToken(email: string): string;
184
+ verifySessionToken(token: string): { valid: boolean; email?: string };
185
+ middleware(): RequestHandler;
186
+ }
187
+
188
+ export function createOtpGate(options?: OtpGateOptions & { logger?: any }): RequestHandler;
189
+
190
+ export interface MouseTelemetryCollectorOptions {
191
+ windowLength?: number;
192
+ sampleIntervalMs?: number;
193
+ maxBufferSize?: number;
194
+ maxIdleMs?: number;
195
+ minPoints?: number;
196
+ flushOnConsumption?: boolean;
197
+ }
198
+
199
+ export interface ShieldClientOptions extends MouseTelemetryCollectorOptions {
200
+ autoInjectTripwire?: boolean;
201
+ }
202
+
203
+ export interface InstrumentFetchOptions {
204
+ headerName?: string;
205
+ endpoints?: string[];
206
+ }
207
+
208
+ export class MouseTelemetryCollector {
209
+ windowLength: number;
210
+ sampleIntervalMs: number;
211
+ maxBufferSize: number;
212
+ constructor(options?: MouseTelemetryCollectorOptions);
213
+ start(): void;
214
+ stop(): void;
215
+ getPayload(): { velocity: number[]; isTrusted: boolean; timestamp: number } | null;
216
+ }
217
+
218
+ export class ShieldClient {
219
+ collector: MouseTelemetryCollector;
220
+ constructor(options?: ShieldClientOptions);
221
+ start(): void;
222
+ stop(): void;
223
+ getPayload(): { velocity: number[]; isTrusted: boolean; timestamp: number } | null;
224
+ instrumentFetch(options?: InstrumentFetchOptions): void;
225
+ instrumentAxios(axiosInstance: any, options?: InstrumentFetchOptions): void;
226
+ }
227
+
103
228
  export class Shield extends EventEmitter {
229
+ ipResolver: IPResolver;
230
+ storageAdapter: BaseStorageAdapter;
231
+
104
232
  constructor(options?: ShieldOptions);
105
233
 
234
+ init(): Promise<this>;
106
235
  middleware(): RequestHandler;
107
236
  honeypot(): RequestHandler;
108
237
  notFoundCollector(): RequestHandler;
@@ -110,6 +239,8 @@ export class Shield extends EventEmitter {
110
239
  costThrottle(options?: CostThrottleOptions): RequestHandler;
111
240
  keyLeakDetector(): RequestHandler;
112
241
  behavioralGuard(options?: BehavioralGuardOptions): RequestHandler;
242
+ strictBotGuard(options?: BehavioralGuardOptions): RequestHandler;
243
+ otpGate(options?: OtpGateOptions): RequestHandler;
113
244
  verifyBiometrics(velocity: number[] | Float64Array): BehavioralVerificationResult;
114
245
 
115
246
  blockIP(ip: string, reason?: string, metadata?: Record<string, any>, customTtlMs?: number | null): void;
@@ -148,16 +279,6 @@ export class DualRidgeClassifier {
148
279
  predict(features: Float32Array | number[]): { score: number; probability: number; isHuman: boolean };
149
280
  }
150
281
 
151
- export class MouseTelemetryCollector {
152
- windowLength: number;
153
- sampleIntervalMs: number;
154
- maxBufferSize: number;
155
- constructor(options?: { windowLength?: number; sampleIntervalMs?: number; maxBufferSize?: number });
156
- start(): void;
157
- stop(): void;
158
- getPayload(): { velocity: number[]; isTrusted: boolean; timestamp: number } | null;
159
- }
160
-
161
282
  export function injectTripwire(): void;
162
283
  export function createShield(options?: ShieldOptions): Shield;
163
284
  export default createShield;
package/src/index.js CHANGED
@@ -21,6 +21,9 @@ import { BehavioralShieldService } from './middleware/behavioralGuard.js';
21
21
  import { MiniRocket } from './biometrics/minirocket.js';
22
22
  import { DualRidgeClassifier } from './biometrics/ridge.js';
23
23
  import { createAdminRouter } from './admin/adminRouter.js';
24
+ import { IPResolver } from './core/ipResolver.js';
25
+ import { createStorageAdapter } from './storage/index.js';
26
+ import { createOtpGate, OTPGate } from './admin/otpGate.js';
24
27
 
25
28
  export class Shield extends EventEmitter {
26
29
  /**
@@ -136,6 +139,47 @@ export class Shield extends EventEmitter {
136
139
  // 9. Admin Dashboard Configuration (Default: false)
137
140
  const adminOpts = options.admin || {};
138
141
  this.adminEnabled = Boolean(adminOpts.enabled);
142
+
143
+ // 10. Enterprise Client IP Resolution & Gateway Protection
144
+ this.ipResolver = new IPResolver({
145
+ trustProxy: options.trustProxy,
146
+ protectGateways: options.protectGateways !== false,
147
+ protectedSubnets: options.protectedSubnets || []
148
+ });
149
+
150
+ // 11. Pluggable Storage Adapter (Memory, File, or Database Callbacks)
151
+ this.storageAdapter = createStorageAdapter(options.storage, this.logger);
152
+ }
153
+
154
+ /**
155
+ * Asynchronous initialization: hydrates unexpired threats from persistent storage
156
+ * into Bloom filter and LRU cache.
157
+ * @returns {Promise<this>}
158
+ */
159
+ async init() {
160
+ await this.storageAdapter.init(this);
161
+ try {
162
+ const blockedList = await this.storageAdapter.loadBlockedIPs();
163
+ if (Array.isArray(blockedList)) {
164
+ const now = Date.now();
165
+ for (const item of blockedList) {
166
+ if (item && item.ip) {
167
+ const cleanIp = this.ipResolver ? this.ipResolver.normalize(item.ip) : item.ip.trim();
168
+ if (!this.whitelist.has(cleanIp) && !this.ipResolver.isProtectedGateway(cleanIp)) {
169
+ this.bloom.add(cleanIp);
170
+ const ttl = item.expiresAt ? Math.max(1, item.expiresAt - now) : null;
171
+ this.lru.set(cleanIp, { reason: item.reason || 'restored_threat', metadata: item.metadata || {} }, ttl);
172
+ }
173
+ }
174
+ }
175
+ if (blockedList.length > 0) {
176
+ this.logger.info?.(`[SHIELD] Hydrated ${blockedList.length} persistent threats into Bloom & LRU.`);
177
+ }
178
+ }
179
+ } catch (err) {
180
+ this.logger.error?.('[SHIELD:STORAGE] Error during startup threat hydration:', err.message);
181
+ }
182
+ return this;
139
183
  }
140
184
 
141
185
  /**
@@ -191,6 +235,32 @@ export class Shield extends EventEmitter {
191
235
  return this.behavioralShieldService.middleware(opts);
192
236
  }
193
237
 
238
+ /**
239
+ * Route-level strict behavioral bot defense decorator
240
+ * Designed specifically for mission-critical routes like /login, /checkout, /register
241
+ * Automatically rejects bots with 403 and blacklists their IP in Shield L1.
242
+ * @param {Object} [opts]
243
+ */
244
+ strictBotGuard(opts = {}) {
245
+ return this.behavioralGuard({
246
+ mode: 'block',
247
+ autoBlockIP: true,
248
+ threshold: 0.50,
249
+ ...opts
250
+ });
251
+ }
252
+
253
+ /**
254
+ * Shield Standalone OTP Authentication Gate Middleware
255
+ * @param {Object} [options]
256
+ */
257
+ otpGate(options = {}) {
258
+ return createOtpGate({
259
+ logger: this.logger,
260
+ ...options
261
+ });
262
+ }
263
+
194
264
  /**
195
265
  * Verify kinematics velocity series against MiniRocket model
196
266
  * @param {number[]|Float64Array} velocityArray
@@ -208,16 +278,28 @@ export class Shield extends EventEmitter {
208
278
  */
209
279
  blockIP(ip, reason = 'manual_block', metadata = {}, customTtlMs = null) {
210
280
  if (!ip || typeof ip !== 'string') return;
211
- const cleanIp = ip.trim();
281
+ const cleanIp = this.ipResolver ? this.ipResolver.normalize(ip) : ip.trim();
212
282
 
213
- // Whitelisted IPs can never be blocked
283
+ // Whitelisted IPs and protected gateways can never be blocked
214
284
  if (this.whitelist.has(cleanIp)) return;
285
+ if (this.ipResolver && this.ipResolver.isProtectedGateway(cleanIp)) {
286
+ this.logger.warn?.(`[SHIELD] Block attempted on protected gateway/loopback (${cleanIp}). Block prevented.`);
287
+ return;
288
+ }
215
289
 
216
290
  // 1. Add to Bloom filter (for sub-0.1ms pass-through check)
217
291
  this.bloom.add(cleanIp);
218
292
 
219
293
  // 2. Add to LRU confirmed threat cache (with TTL)
220
294
  this.lru.set(cleanIp, { reason, metadata }, customTtlMs);
295
+
296
+ // 3. Persist to storage adapter
297
+ const expiresAt = customTtlMs ? Date.now() + customTtlMs : (this.lru.ttlMs ? Date.now() + this.lru.ttlMs : null);
298
+ try {
299
+ this.storageAdapter?.saveBlockedIP(cleanIp, reason, metadata, expiresAt);
300
+ } catch (err) {
301
+ this.logger.error?.('[SHIELD:STORAGE] Error saving blocked IP to storage:', err.message);
302
+ }
221
303
  }
222
304
 
223
305
  /**
@@ -227,8 +309,14 @@ export class Shield extends EventEmitter {
227
309
  */
228
310
  unblockIP(ip) {
229
311
  if (!ip || typeof ip !== 'string') return;
230
- const cleanIp = ip.trim();
312
+ const cleanIp = this.ipResolver ? this.ipResolver.normalize(ip) : ip.trim();
231
313
  this.lru.delete(cleanIp);
314
+
315
+ try {
316
+ this.storageAdapter?.removeBlockedIP(cleanIp);
317
+ } catch (err) {
318
+ this.logger.error?.('[SHIELD:STORAGE] Error removing blocked IP from storage:', err.message);
319
+ }
232
320
  }
233
321
 
234
322
  /**
@@ -328,8 +416,9 @@ export class Shield extends EventEmitter {
328
416
  * Graceful shutdown: flush in-memory state to 404_intelligence.json
329
417
  */
330
418
  async shutdown() {
331
- this.logger.info?.('[SHIELD] Graceful shutdown initiated. Flushing intelligence to disk...');
419
+ this.logger.info?.('[SHIELD] Graceful shutdown initiated. Flushing state to disk...');
332
420
  await this.filePersistence.stop();
421
+ await this.storageAdapter?.close?.();
333
422
  this.notFoundHandler.destroy();
334
423
  this.costThrottleInstance.destroy();
335
424
  this.keyLeakDetectorInstance.destroy();
@@ -349,5 +438,17 @@ export function createShield(options = {}) {
349
438
  export { MiniRocket } from './biometrics/minirocket.js';
350
439
  export { DualRidgeClassifier } from './biometrics/ridge.js';
351
440
  export { BehavioralShieldService } from './middleware/behavioralGuard.js';
441
+ export { IPResolver } from './core/ipResolver.js';
442
+ export { OTPGate, createOtpGate } from './admin/otpGate.js';
443
+ export {
444
+ BaseStorageAdapter,
445
+ MemoryAdapter,
446
+ FileStorageAdapter,
447
+ CallbackStorageAdapter,
448
+ createStorageAdapter
449
+ } from './storage/index.js';
450
+ export { ShieldClient } from './client/shieldClient.js';
451
+ export { MouseTelemetryCollector } from './client/collector.js';
452
+ export { injectTripwire } from './client/index.js';
352
453
 
353
454
  export default createShield;