Compare commits

..

12 Commits

Author SHA1 Message Date
20eda1ab3e v11.10.6
Some checks failed
Docker (tags) / security (push) Failing after 2s
Docker (tags) / test (push) Has been skipped
Docker (tags) / release (push) Has been skipped
Docker (tags) / metadata (push) Has been skipped
2026-03-26 07:40:56 +00:00
44f2a7f0a9 fix(typescript): tighten TypeScript null safety and error handling across backend and ops UI 2026-03-26 07:40:56 +00:00
0195a21f30 v11.10.5
Some checks failed
Docker (tags) / security (push) Failing after 4s
Docker (tags) / test (push) Has been skipped
Docker (tags) / release (push) Has been skipped
Docker (tags) / metadata (push) Has been skipped
2026-03-26 07:10:59 +00:00
4dca747386 fix(build): rename smart tooling config to .smartconfig.json and update package references 2026-03-26 07:10:59 +00:00
7663f502fa v11.10.4
Some checks failed
Docker (tags) / security (push) Failing after 3s
Docker (tags) / test (push) Has been skipped
Docker (tags) / release (push) Has been skipped
Docker (tags) / metadata (push) Has been skipped
2026-03-24 13:40:28 +00:00
104cd417d8 fix(monitoring): handle multiple protocol cache entries per backend in metrics output 2026-03-24 13:40:28 +00:00
93254d5d3d v11.10.3
Some checks failed
Docker (tags) / security (push) Failing after 3s
Docker (tags) / test (push) Has been skipped
Docker (tags) / release (push) Has been skipped
Docker (tags) / metadata (push) Has been skipped
2026-03-23 21:18:20 +00:00
9a3f121a9c fix(deps): bump tstest, smartmetrics, and taskbuffer to latest patch releases 2026-03-23 21:18:20 +00:00
bef74eb1aa v11.10.2
Some checks failed
Docker (tags) / security (push) Failing after 3s
Docker (tags) / test (push) Has been skipped
Docker (tags) / release (push) Has been skipped
Docker (tags) / metadata (push) Has been skipped
2026-03-23 14:22:24 +00:00
308d8e4851 fix(deps): bump @api.global/typedserver to ^8.4.6 and @push.rocks/smartproxy to ^26.2.1 2026-03-23 14:22:24 +00:00
dc010dc3ae v11.10.1
Some checks failed
Docker (tags) / security (push) Failing after 3s
Docker (tags) / test (push) Has been skipped
Docker (tags) / release (push) Has been skipped
Docker (tags) / metadata (push) Has been skipped
2026-03-23 10:29:16 +00:00
61d5d3b1ad fix(deps): bump @push.rocks/smartproxy to ^26.2.0 2026-03-23 10:29:16 +00:00
44 changed files with 1018 additions and 915 deletions

View File

@@ -1,7 +1,7 @@
{
"json.schemas": [
{
"fileMatch": ["/npmextra.json"],
"fileMatch": ["/.smartconfig.json"],
"schema": {
"type": "object",
"properties": {

View File

@@ -1,5 +1,45 @@
# Changelog
## 2026-03-26 - 11.10.6 - fix(typescript)
tighten TypeScript null safety and error handling across backend and ops UI
- add explicit unknown error typing and safe message access in logging and handler code
- mark deferred-initialized class properties with definite assignment assertions to satisfy stricter TypeScript checks
- harden ops web state access and action return types with non-null assertions and explicit Promise state typing
- update storage reads to allow missing values and align license file references with the lowercase license filename
## 2026-03-26 - 11.10.5 - fix(build)
rename smart tooling config to .smartconfig.json and update package references
- Moves the shared tool configuration from npmextra.json to .smartconfig.json.
- Updates package.json published files and documentation to reference the new config file.
- Refreshes several development and runtime dependency versions alongside the config migration.
## 2026-03-24 - 11.10.4 - fix(monitoring)
handle multiple protocol cache entries per backend in metrics output
- Group detected protocol cache entries by backend host and port so multiple domain-specific records are preserved.
- Emit one backend metrics row per cached domain and avoid dropping unmatched protocol cache entries by tracking seen entries with a composite host:port:domain key.
- Use cached protocol values when available while keeping backend-only rows for metrics without protocol cache data.
## 2026-03-23 - 11.10.3 - fix(deps)
bump tstest, smartmetrics, and taskbuffer to latest patch releases
- update @git.zone/tstest from ^3.5.0 to ^3.5.1
- update @push.rocks/smartmetrics from ^3.0.2 to ^3.0.3
- update @push.rocks/taskbuffer from ^8.0.0 to ^8.0.2
## 2026-03-23 - 11.10.2 - fix(deps)
bump @api.global/typedserver to ^8.4.6 and @push.rocks/smartproxy to ^26.2.1
- Updates @api.global/typedserver from ^8.4.2 to ^8.4.6
- Updates @push.rocks/smartproxy from ^26.2.0 to ^26.2.1
## 2026-03-23 - 11.10.1 - fix(deps)
bump @push.rocks/smartproxy to ^26.2.0
- Updates the @push.rocks/smartproxy dependency from ^26.1.0 to ^26.2.0 in package.json.
## 2026-03-23 - 11.10.0 - feat(monitoring)
add backend protocol metrics to network stats and ops dashboard

21
license Normal file
View File

@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2024 Task Venture Capital GmbH
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@@ -1,7 +1,7 @@
{
"name": "@serve.zone/dcrouter",
"private": false,
"version": "11.10.0",
"version": "11.10.6",
"description": "A multifaceted routing service handling mail and SMS delivery functions.",
"type": "module",
"exports": {
@@ -22,48 +22,48 @@
"watch": "tswatch"
},
"devDependencies": {
"@git.zone/tsbuild": "^4.3.0",
"@git.zone/tsbundle": "^2.9.1",
"@git.zone/tsrun": "^2.0.1",
"@git.zone/tstest": "^3.5.0",
"@git.zone/tswatch": "^3.3.0",
"@git.zone/tsbuild": "^4.4.0",
"@git.zone/tsbundle": "^2.10.0",
"@git.zone/tsrun": "^2.0.2",
"@git.zone/tstest": "^3.6.0",
"@git.zone/tswatch": "^3.3.2",
"@types/node": "^25.5.0"
},
"dependencies": {
"@api.global/typedrequest": "^3.3.0",
"@api.global/typedrequest-interfaces": "^3.0.19",
"@api.global/typedserver": "^8.4.2",
"@api.global/typedserver": "^8.4.6",
"@api.global/typedsocket": "^4.1.2",
"@apiclient.xyz/cloudflare": "^7.1.0",
"@design.estate/dees-catalog": "^3.48.5",
"@design.estate/dees-catalog": "^3.49.0",
"@design.estate/dees-element": "^2.2.3",
"@push.rocks/lik": "^6.4.0",
"@push.rocks/projectinfo": "^5.0.2",
"@push.rocks/qenv": "^6.1.3",
"@push.rocks/smartacme": "^9.3.0",
"@push.rocks/smartdata": "^7.1.0",
"@push.rocks/smartdata": "^7.1.2",
"@push.rocks/smartdns": "^7.9.0",
"@push.rocks/smartfile": "^13.1.2",
"@push.rocks/smartguard": "^3.1.0",
"@push.rocks/smartjwt": "^2.2.1",
"@push.rocks/smartlog": "^3.2.1",
"@push.rocks/smartmetrics": "^3.0.2",
"@push.rocks/smartmetrics": "^3.0.3",
"@push.rocks/smartmongo": "^5.1.0",
"@push.rocks/smartmta": "^5.3.1",
"@push.rocks/smartnetwork": "^4.4.0",
"@push.rocks/smartpath": "^6.0.0",
"@push.rocks/smartpromise": "^4.2.3",
"@push.rocks/smartproxy": "^26.1.0",
"@push.rocks/smartproxy": "^26.2.4",
"@push.rocks/smartradius": "^1.1.1",
"@push.rocks/smartrequest": "^5.0.1",
"@push.rocks/smartrx": "^3.0.10",
"@push.rocks/smartstate": "^2.2.0",
"@push.rocks/smartstate": "^2.2.1",
"@push.rocks/smartunique": "^3.0.9",
"@push.rocks/taskbuffer": "^8.0.0",
"@push.rocks/taskbuffer": "^8.0.2",
"@serve.zone/catalog": "^2.9.0",
"@serve.zone/interfaces": "^5.3.0",
"@serve.zone/remoteingress": "^4.14.1",
"@tsclass/tsclass": "^9.4.0",
"@serve.zone/remoteingress": "^4.14.2",
"@tsclass/tsclass": "^9.5.0",
"lru-cache": "^11.2.7",
"uuid": "^13.0.0"
},
@@ -112,7 +112,7 @@
"dist_ts_apiclient/**/*",
"assets/**/*",
"cli.js",
"npmextra.json",
".smartconfig.json",
"readme.md"
]
}

981
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@@ -133,7 +133,7 @@ The project now uses tswatch for development:
```bash
pnpm run watch
```
Configuration in `npmextra.json`:
Configuration in `.smartconfig.json`:
```json
{
"@git.zone/tswatch": {

View File

@@ -1449,7 +1449,7 @@ The Docker build supports multi-platform (`linux/amd64`, `linux/arm64`) via [tsd
## License and Legal Information
This repository contains open-source code licensed under the MIT License. A copy of the license can be found in the [LICENSE](./LICENSE) file.
This repository contains open-source code licensed under the MIT License. A copy of the license can be found in the [license](./license) file.
**Please note:** The MIT License does not grant permission to use the trade names, trademarks, service marks, or product names of the project, except as required for reasonable and customary use in describing the origin of the work and reproducing the content of the NOTICE file.

View File

@@ -3,6 +3,6 @@
*/
export const commitinfo = {
name: '@serve.zone/dcrouter',
version: '11.10.0',
version: '11.10.6',
description: 'A multifaceted routing service handling mail and SMS delivery functions.'
}

View File

@@ -48,14 +48,14 @@ export class CacheCleaner {
this.isRunning = true;
// Run cleanup immediately on start
this.runCleanup().catch((error) => {
logger.log('error', `Initial cache cleanup failed: ${error.message}`);
this.runCleanup().catch((error: unknown) => {
logger.log('error', `Initial cache cleanup failed: ${(error as Error).message}`);
});
// Schedule periodic cleanup
this.cleanupInterval = setInterval(() => {
this.runCleanup().catch((error) => {
logger.log('error', `Cache cleanup failed: ${error.message}`);
this.runCleanup().catch((error: unknown) => {
logger.log('error', `Cache cleanup failed: ${(error as Error).message}`);
});
}, this.options.intervalMs);
@@ -113,8 +113,8 @@ export class CacheCleaner {
`Cache cleanup completed. Deleted ${totalDeleted} expired documents. ${summary || 'No deletions.'}`
);
}
} catch (error) {
logger.log('error', `Cache cleanup error: ${error.message}`);
} catch (error: unknown) {
logger.log('error', `Cache cleanup error: ${(error as Error).message}`);
throw error;
}
}
@@ -138,14 +138,14 @@ export class CacheCleaner {
try {
await doc.delete();
deletedCount++;
} catch (deleteError) {
logger.log('warn', `Failed to delete expired document: ${deleteError.message}`);
} catch (deleteError: unknown) {
logger.log('warn', `Failed to delete expired document: ${(deleteError as Error).message}`);
}
}
return deletedCount;
} catch (error) {
logger.log('error', `Error cleaning collection: ${error.message}`);
} catch (error: unknown) {
logger.log('error', `Error cleaning collection: ${(error as Error).message}`);
return 0;
}
}

View File

@@ -22,7 +22,7 @@ export abstract class CachedDocument<T extends CachedDocument<T>> extends plugin
* Timestamp when the document expires and should be cleaned up
* NOTE: Subclasses must add @svDb() decorator
*/
public expiresAt: Date;
public expiresAt!: Date;
/**
* Timestamp of last access (for LRU-style eviction if needed)

View File

@@ -23,8 +23,8 @@ export interface ICacheDbOptions {
export class CacheDb {
private static instance: CacheDb | null = null;
private localTsmDb: plugins.smartmongo.LocalTsmDb;
private smartdataDb: plugins.smartdata.SmartdataDb;
private localTsmDb!: plugins.smartmongo.LocalTsmDb;
private smartdataDb!: plugins.smartdata.SmartdataDb;
private options: Required<ICacheDbOptions>;
private isStarted: boolean = false;
@@ -89,8 +89,8 @@ export class CacheDb {
this.isStarted = true;
logger.log('info', `CacheDb started at ${this.options.storagePath}`);
} catch (error) {
logger.log('error', `Failed to start CacheDb: ${error.message}`);
} catch (error: unknown) {
logger.log('error', `Failed to start CacheDb: ${(error as Error).message}`);
throw error;
}
}
@@ -116,8 +116,8 @@ export class CacheDb {
this.isStarted = false;
logger.log('info', 'CacheDb stopped');
} catch (error) {
logger.log('error', `Error stopping CacheDb: ${error.message}`);
} catch (error: unknown) {
logger.log('error', `Error stopping CacheDb: ${(error as Error).message}`);
throw error;
}
}

View File

@@ -35,55 +35,55 @@ export class CachedEmail extends CachedDocument<CachedEmail> {
*/
@plugins.smartdata.unI()
@plugins.smartdata.svDb()
public id: string;
public id!: string;
/**
* Email message ID (RFC 822 Message-ID header)
*/
@plugins.smartdata.svDb()
public messageId: string;
public messageId!: string;
/**
* Sender email address (envelope from)
*/
@plugins.smartdata.svDb()
public from: string;
public from!: string;
/**
* Recipient email addresses
*/
@plugins.smartdata.svDb()
public to: string[];
public to!: string[];
/**
* CC recipients
*/
@plugins.smartdata.svDb()
public cc: string[];
public cc!: string[];
/**
* BCC recipients
*/
@plugins.smartdata.svDb()
public bcc: string[];
public bcc!: string[];
/**
* Email subject
*/
@plugins.smartdata.svDb()
public subject: string;
public subject!: string;
/**
* Raw RFC822 email content
*/
@plugins.smartdata.svDb()
public rawContent: string;
public rawContent!: string;
/**
* Current status of the email
*/
@plugins.smartdata.svDb()
public status: TCachedEmailStatus;
public status!: TCachedEmailStatus;
/**
* Number of delivery attempts
@@ -101,25 +101,25 @@ export class CachedEmail extends CachedDocument<CachedEmail> {
* Timestamp for next delivery attempt
*/
@plugins.smartdata.svDb()
public nextAttempt: Date;
public nextAttempt!: Date;
/**
* Last error message if delivery failed
*/
@plugins.smartdata.svDb()
public lastError: string;
public lastError!: string;
/**
* Timestamp when the email was successfully delivered
*/
@plugins.smartdata.svDb()
public deliveredAt: Date;
public deliveredAt!: Date;
/**
* Sender domain (for querying/filtering)
*/
@plugins.smartdata.svDb()
public senderDomain: string;
public senderDomain!: string;
/**
* Priority level (higher = more important)
@@ -131,7 +131,7 @@ export class CachedEmail extends CachedDocument<CachedEmail> {
* JSON-serialized route data
*/
@plugins.smartdata.svDb()
public routeData: string;
public routeData!: string;
/**
* DKIM signature status

View File

@@ -45,61 +45,61 @@ export class CachedIPReputation extends CachedDocument<CachedIPReputation> {
*/
@plugins.smartdata.unI()
@plugins.smartdata.svDb()
public ipAddress: string;
public ipAddress!: string;
/**
* Reputation score (0-100, higher = better)
*/
@plugins.smartdata.svDb()
public score: number;
public score!: number;
/**
* Whether the IP is flagged as spam source
*/
@plugins.smartdata.svDb()
public isSpam: boolean;
public isSpam!: boolean;
/**
* Whether the IP is a known proxy
*/
@plugins.smartdata.svDb()
public isProxy: boolean;
public isProxy!: boolean;
/**
* Whether the IP is a Tor exit node
*/
@plugins.smartdata.svDb()
public isTor: boolean;
public isTor!: boolean;
/**
* Whether the IP is a VPN endpoint
*/
@plugins.smartdata.svDb()
public isVPN: boolean;
public isVPN!: boolean;
/**
* Country code (ISO 3166-1 alpha-2)
*/
@plugins.smartdata.svDb()
public country: string;
public country!: string;
/**
* Autonomous System Number
*/
@plugins.smartdata.svDb()
public asn: string;
public asn!: string;
/**
* Organization name
*/
@plugins.smartdata.svDb()
public org: string;
public org!: string;
/**
* List of blacklists the IP appears on
*/
@plugins.smartdata.svDb()
public blacklists: string[];
public blacklists!: string[];
/**
* Number of times this IP has been checked

View File

@@ -215,7 +215,7 @@ export class DcRouter {
public emailServer?: UnifiedEmailServer;
public radiusServer?: RadiusServer;
public storageManager: StorageManager;
public opsServer: OpsServer;
public opsServer!: OpsServer;
public metricsManager?: MetricsManager;
// Cache system (smartdata + LocalTsmDb)
@@ -448,7 +448,7 @@ export class DcRouter {
}
// DNS Server: optional, depends on SmartProxy
if (this.options.dnsNsDomains?.length > 0 && this.options.dnsScopes?.length > 0) {
if (this.options.dnsNsDomains && this.options.dnsNsDomains.length > 0 && this.options.dnsScopes && this.options.dnsScopes.length > 0) {
this.serviceManager.addService(
new plugins.taskbuffer.Service('DnsServer')
.optional()
@@ -787,7 +787,7 @@ export class DcRouter {
eventComms.log(`Attempting DNS-01 via SmartAcme for ${domain}`);
eventComms.setSource('smartacme-dns-01');
const isWildcardDomain = domain.startsWith('*.');
const cert = await this.smartAcme.getCertificateForDomain(domain, {
const cert = await this.smartAcme!.getCertificateForDomain(domain, {
includeWildcard: !isWildcardDomain,
});
if (cert.validUntil) {
@@ -806,10 +806,10 @@ export class DcRouter {
// Success — clear any backoff
await scheduler.clearBackoff(domain);
return result;
} catch (err) {
} catch (err: unknown) {
// Record failure for backoff tracking
await scheduler.recordFailure(domain, err.message);
eventComms.warn(`SmartAcme DNS-01 failed for ${domain}: ${err.message}, falling back to http-01`);
await scheduler.recordFailure(domain, (err as Error).message);
eventComms.warn(`SmartAcme DNS-01 failed for ${domain}: ${(err as Error).message}, falling back to http-01`);
return 'http01';
}
};
@@ -1248,21 +1248,21 @@ export class DcRouter {
// Wire delivery events to MetricsManager and logger
if (this.metricsManager && this.emailServer.deliverySystem) {
this.emailServer.deliverySystem.on('deliveryStart', (item: any) => {
this.metricsManager.trackEmailReceived(item?.from);
this.metricsManager!.trackEmailReceived(item?.from);
logger.log('info', `Email delivery started: ${item?.from}${item?.to}`, { zone: 'email' });
});
this.emailServer.deliverySystem.on('deliverySuccess', (item: any) => {
this.metricsManager.trackEmailSent(item?.to);
this.metricsManager!.trackEmailSent(item?.to);
logger.log('info', `Email delivered to ${item?.to}`, { zone: 'email' });
});
this.emailServer.deliverySystem.on('deliveryFailed', (item: any, error: any) => {
this.metricsManager.trackEmailFailed(item?.to, error?.message);
this.metricsManager!.trackEmailFailed(item?.to, error?.message);
logger.log('warn', `Email delivery failed to ${item?.to}: ${error?.message}`, { zone: 'email' });
});
}
if (this.metricsManager && this.emailServer) {
this.emailServer.on('bounceProcessed', () => {
this.metricsManager.trackEmailBounced();
this.metricsManager!.trackEmailBounced();
logger.log('warn', 'Email bounce processed', { zone: 'email' });
});
}
@@ -1305,12 +1305,12 @@ export class DcRouter {
}
logger.log('info', 'All unified email components stopped');
} catch (error) {
logger.log('error', `Error stopping unified email components: ${error.message}`);
} catch (error: unknown) {
logger.log('error', `Error stopping unified email components: ${(error as Error).message}`);
throw error;
}
}
/**
* Update domain rules for email routing
* @param rules New domain rules to apply
@@ -1468,7 +1468,7 @@ export class DcRouter {
this.dnsServer.on('query', (event: plugins.smartdns.dnsServerMod.IDnsQueryCompletedEvent) => {
// Metrics tracking
for (const question of event.questions) {
this.metricsManager.trackDnsQuery(
this.metricsManager?.trackDnsQuery(
question.type,
question.name,
false,
@@ -1553,8 +1553,8 @@ export class DcRouter {
// Use the built-in socket handler from smartdns
// This handles HTTP/2, DoH protocol, etc.
await (this.dnsServer as any).handleHttpsSocket(socket);
} catch (error) {
logger.log('error', `DNS socket handler error: ${error.message}`);
} catch (error: unknown) {
logger.log('error', `DNS socket handler error: ${(error as Error).message}`);
if (!socket.destroyed) {
socket.destroy();
}
@@ -1695,14 +1695,14 @@ export class DcRouter {
} else {
logger.log('warn', `Invalid DKIM record structure in ${file}`);
}
} catch (error) {
logger.log('error', `Failed to load DKIM record from ${file}: ${error.message}`);
} catch (error: unknown) {
logger.log('error', `Failed to load DKIM record from ${file}: ${(error as Error).message}`);
}
}
} catch (error) {
logger.log('error', `Failed to load DKIM records: ${error.message}`);
} catch (error: unknown) {
logger.log('error', `Failed to load DKIM records: ${(error as Error).message}`);
}
return records;
}
@@ -1734,11 +1734,11 @@ export class DcRouter {
// This ensures keys are ready even if DNS mode changes later
await dkimCreator.handleDKIMKeysForDomain(domainConfig.domain);
logger.log('info', `DKIM keys initialized for ${domainConfig.domain}`);
} catch (error) {
logger.log('error', `Failed to initialize DKIM for ${domainConfig.domain}: ${error.message}`);
} catch (error: unknown) {
logger.log('error', `Failed to initialize DKIM for ${domainConfig.domain}: ${(error as Error).message}`);
}
}
logger.log('info', 'DKIM initialization complete');
}
@@ -1779,10 +1779,10 @@ export class DcRouter {
} else {
logger.log('warn', 'Could not auto-discover public IPv4 address');
}
} catch (error) {
logger.log('error', `Failed to auto-discover public IP: ${error.message}`);
} catch (error: unknown) {
logger.log('error', `Failed to auto-discover public IP: ${(error as Error).message}`);
}
if (!publicIp) {
logger.log('warn', 'No public IP available. Nameserver A records require either proxyIps, publicIp, or successful auto-discovery.');
}
@@ -1876,8 +1876,8 @@ export class DcRouter {
}
return null;
} catch (error) {
logger.log('warn', `Failed to detect public IP: ${error.message}`);
} catch (error: unknown) {
logger.log('warn', `Failed to detect public IP: ${(error as Error).message}`);
return null;
}
}
@@ -1911,8 +1911,8 @@ export class DcRouter {
const keyPem = plugins.fs.readFileSync(riCfg.tls.keyPath, 'utf8');
tlsConfig = { certPem, keyPem };
logger.log('info', 'Using explicit TLS cert/key for RemoteIngress tunnel');
} catch (err) {
logger.log('warn', `Failed to read RemoteIngress TLS cert/key files: ${err.message}`);
} catch (err: unknown) {
logger.log('warn', `Failed to read RemoteIngress TLS cert/key files: ${(err as Error).message}`);
}
}

View File

@@ -170,7 +170,7 @@ export class ConfigValidator {
} else if (rules.items.schema && itemType === 'object') {
const itemResult = this.validate(value[i], rules.items.schema);
if (!itemResult.valid) {
errors.push(...itemResult.errors.map(err => `${key}[${i}].${err}`));
errors.push(...itemResult.errors!.map(err => `${key}[${i}].${err}`));
}
}
}
@@ -181,7 +181,7 @@ export class ConfigValidator {
if (rules.schema) {
const nestedResult = this.validate(value, rules.schema);
if (!nestedResult.valid) {
errors.push(...nestedResult.errors.map(err => `${key}.${err}`));
errors.push(...nestedResult.errors!.map(err => `${key}.${err}`));
}
validatedConfig[key] = nestedResult.config;
}
@@ -233,8 +233,8 @@ export class ConfigValidator {
// Apply defaults to array items
if (result[key] && rules.type === 'array' && rules.items && rules.items.schema) {
result[key] = result[key].map(item =>
typeof item === 'object' ? this.applyDefaults(item, rules.items.schema) : item
result[key] = result[key].map(item =>
typeof item === 'object' ? this.applyDefaults(item, rules.items!.schema!) : item
);
}
}
@@ -255,7 +255,7 @@ export class ConfigValidator {
if (!result.valid) {
throw new ValidationError(
`Configuration validation failed: ${result.errors.join(', ')}`,
`Configuration validation failed: ${result.errors!.join(', ')}`,
'CONFIG_VALIDATION_ERROR',
{ data: { errors: result.errors } }
);

View File

@@ -227,7 +227,7 @@ export class PlatformError extends Error {
const { retry } = this.context;
if (!retry) return false;
return retry.currentRetry < retry.maxRetries;
return (retry.currentRetry ?? 0) < (retry.maxRetries ?? 0);
}
/**

View File

@@ -296,11 +296,11 @@ export class MetricsManager {
const proxyMetrics = this.dcRouter.smartProxy ? this.dcRouter.smartProxy.getMetrics() : null;
if (!proxyMetrics) {
return [];
return [] as Array<{ type: string; count: number; source: string; lastActivity: Date }>;
}
const connectionsByRoute = proxyMetrics.connections.byRoute();
const connectionInfo = [];
const connectionInfo: Array<{ type: string; count: number; source: string; lastActivity: Date }> = [];
for (const [routeName, count] of connectionsByRoute) {
connectionInfo.push({
@@ -595,47 +595,84 @@ export class MetricsManager {
const backendMetrics = proxyMetrics.backends.byBackend();
const protocolCache = proxyMetrics.backends.detectedProtocols();
// Index protocol cache by "host:port"
const cacheByKey = new Map<string, (typeof protocolCache)[number]>();
// Group protocol cache entries by host:port so we can match them to backend metrics.
// The protocol cache is keyed by (host, port, domain) in Rust, so the same host:port
// can have multiple entries for different domains.
const cacheByBackend = new Map<string, (typeof protocolCache)[number][]>();
for (const entry of protocolCache) {
cacheByKey.set(`${entry.host}:${entry.port}`, entry);
const backendKey = `${entry.host}:${entry.port}`;
let entries = cacheByBackend.get(backendKey);
if (!entries) {
entries = [];
cacheByBackend.set(backendKey, entries);
}
entries.push(entry);
}
const backends: Array<any> = [];
const seen = new Set<string>();
const seenCacheKeys = new Set<string>();
for (const [key, bm] of backendMetrics) {
seen.add(key);
const cache = cacheByKey.get(key);
backends.push({
backend: key,
domain: cache?.domain ?? null,
protocol: bm.protocol,
activeConnections: bm.activeConnections,
totalConnections: bm.totalConnections,
connectErrors: bm.connectErrors,
handshakeErrors: bm.handshakeErrors,
requestErrors: bm.requestErrors,
avgConnectTimeMs: Math.round(bm.avgConnectTimeMs * 10) / 10,
poolHitRate: Math.round(bm.poolHitRate * 1000) / 1000,
h2Failures: bm.h2Failures,
h2Suppressed: cache?.h2Suppressed ?? false,
h3Suppressed: cache?.h3Suppressed ?? false,
h2CooldownRemainingSecs: cache?.h2CooldownRemainingSecs ?? null,
h3CooldownRemainingSecs: cache?.h3CooldownRemainingSecs ?? null,
h2ConsecutiveFailures: cache?.h2ConsecutiveFailures ?? null,
h3ConsecutiveFailures: cache?.h3ConsecutiveFailures ?? null,
h3Port: cache?.h3Port ?? null,
cacheAgeSecs: cache?.ageSecs ?? null,
});
const cacheEntries = cacheByBackend.get(key);
if (!cacheEntries || cacheEntries.length === 0) {
// No protocol cache entry — emit one row with backend metrics only
backends.push({
backend: key,
domain: null,
protocol: bm.protocol,
activeConnections: bm.activeConnections,
totalConnections: bm.totalConnections,
connectErrors: bm.connectErrors,
handshakeErrors: bm.handshakeErrors,
requestErrors: bm.requestErrors,
avgConnectTimeMs: Math.round(bm.avgConnectTimeMs * 10) / 10,
poolHitRate: Math.round(bm.poolHitRate * 1000) / 1000,
h2Failures: bm.h2Failures,
h2Suppressed: false,
h3Suppressed: false,
h2CooldownRemainingSecs: null,
h3CooldownRemainingSecs: null,
h2ConsecutiveFailures: null,
h3ConsecutiveFailures: null,
h3Port: null,
cacheAgeSecs: null,
});
} else {
// One row per domain, each enriched with the shared backend metrics
for (const cache of cacheEntries) {
const compositeKey = `${cache.host}:${cache.port}:${cache.domain ?? ''}`;
seenCacheKeys.add(compositeKey);
backends.push({
backend: key,
domain: cache.domain ?? null,
protocol: cache.protocol ?? bm.protocol,
activeConnections: bm.activeConnections,
totalConnections: bm.totalConnections,
connectErrors: bm.connectErrors,
handshakeErrors: bm.handshakeErrors,
requestErrors: bm.requestErrors,
avgConnectTimeMs: Math.round(bm.avgConnectTimeMs * 10) / 10,
poolHitRate: Math.round(bm.poolHitRate * 1000) / 1000,
h2Failures: bm.h2Failures,
h2Suppressed: cache.h2Suppressed,
h3Suppressed: cache.h3Suppressed,
h2CooldownRemainingSecs: cache.h2CooldownRemainingSecs,
h3CooldownRemainingSecs: cache.h3CooldownRemainingSecs,
h2ConsecutiveFailures: cache.h2ConsecutiveFailures,
h3ConsecutiveFailures: cache.h3ConsecutiveFailures,
h3Port: cache.h3Port,
cacheAgeSecs: cache.ageSecs,
});
}
}
}
// Include protocol cache entries with no matching backend metric
for (const entry of protocolCache) {
const key = `${entry.host}:${entry.port}`;
if (!seen.has(key)) {
const compositeKey = `${entry.host}:${entry.port}:${entry.domain ?? ''}`;
if (!seenCacheKeys.has(compositeKey)) {
backends.push({
backend: key,
backend: `${entry.host}:${entry.port}`,
domain: entry.domain,
protocol: entry.protocol,
activeConnections: 0,

View File

@@ -7,7 +7,7 @@ import { requireValidIdentity, requireAdminIdentity } from './helpers/guards.js'
export class OpsServer {
public dcRouterRef: DcRouter;
public server: plugins.typedserver.utilityservers.UtilityWebsiteServer;
public server!: plugins.typedserver.utilityservers.UtilityWebsiteServer;
// Main TypedRouter — unauthenticated endpoints (login/logout/verify) and own-auth handlers
public typedrouter = new plugins.typedrequest.TypedRouter();
@@ -17,17 +17,17 @@ export class OpsServer {
public adminRouter = new plugins.typedrequest.TypedRouter<{ request: { identity: interfaces.data.IIdentity } }>();
// Handler instances
public adminHandler: handlers.AdminHandler;
private configHandler: handlers.ConfigHandler;
private logsHandler: handlers.LogsHandler;
private securityHandler: handlers.SecurityHandler;
private statsHandler: handlers.StatsHandler;
private radiusHandler: handlers.RadiusHandler;
private emailOpsHandler: handlers.EmailOpsHandler;
private certificateHandler: handlers.CertificateHandler;
private remoteIngressHandler: handlers.RemoteIngressHandler;
private routeManagementHandler: handlers.RouteManagementHandler;
private apiTokenHandler: handlers.ApiTokenHandler;
public adminHandler!: handlers.AdminHandler;
private configHandler!: handlers.ConfigHandler;
private logsHandler!: handlers.LogsHandler;
private securityHandler!: handlers.SecurityHandler;
private statsHandler!: handlers.StatsHandler;
private radiusHandler!: handlers.RadiusHandler;
private emailOpsHandler!: handlers.EmailOpsHandler;
private certificateHandler!: handlers.CertificateHandler;
private remoteIngressHandler!: handlers.RemoteIngressHandler;
private routeManagementHandler!: handlers.RouteManagementHandler;
private apiTokenHandler!: handlers.ApiTokenHandler;
constructor(dcRouterRefArg: DcRouter) {
this.dcRouterRef = dcRouterRefArg;
@@ -39,7 +39,7 @@ export class OpsServer {
public async start() {
this.server = new plugins.typedserver.utilityservers.UtilityWebsiteServer({
domain: 'localhost',
feedMetadata: null,
feedMetadata: undefined,
serveDir: paths.distServe,
});

View File

@@ -12,7 +12,7 @@ export class AdminHandler {
public typedrouter = new plugins.typedrequest.TypedRouter();
// JWT instance
public smartjwtInstance: plugins.smartjwt.SmartJwt<IJwtData>;
public smartjwtInstance!: plugins.smartjwt.SmartJwt<IJwtData>;
// Simple in-memory user storage (in production, use proper database)
private users = new Map<string, {

View File

@@ -311,8 +311,8 @@ export class CertificateHandler {
}
}
return { success: true, message: `Certificate reprovisioning triggered for route '${routeName}'` };
} catch (err) {
return { success: false, message: err.message || 'Failed to reprovision certificate' };
} catch (err: unknown) {
return { success: false, message: (err as Error).message || 'Failed to reprovision certificate' };
}
}
@@ -340,8 +340,8 @@ export class CertificateHandler {
try {
await dcRouter.smartAcme.getCertificateForDomain(domain);
return { success: true, message: `Certificate reprovisioning triggered for domain '${domain}'` };
} catch (err) {
return { success: false, message: err.message || `Failed to reprovision certificate for ${domain}` };
} catch (err: unknown) {
return { success: false, message: (err as Error).message || `Failed to reprovision certificate for ${domain}` };
}
}
@@ -351,8 +351,8 @@ export class CertificateHandler {
try {
await smartProxy.provisionCertificate(routeNames[0]);
return { success: true, message: `Certificate reprovisioning triggered for domain '${domain}' via route '${routeNames[0]}'` };
} catch (err) {
return { success: false, message: err.message || `Failed to reprovision certificate for ${domain}` };
} catch (err: unknown) {
return { success: false, message: (err as Error).message || `Failed to reprovision certificate for ${domain}` };
}
}

View File

@@ -52,8 +52,8 @@ export class RadiusHandler {
try {
await radiusServer.addClient(dataArg.client);
return { success: true };
} catch (error) {
return { success: false, message: error.message };
} catch (error: unknown) {
return { success: false, message: (error as Error).message };
}
}
)
@@ -144,8 +144,8 @@ export class RadiusHandler {
updatedAt: mapping.updatedAt,
},
};
} catch (error) {
return { success: false, message: error.message };
} catch (error: unknown) {
return { success: false, message: (error as Error).message };
}
}
)

View File

@@ -279,7 +279,7 @@ export class StatsHandler {
if (sections.network && this.opsServerRef.dcRouterRef.metricsManager) {
promises.push(
(async () => {
const stats = await this.opsServerRef.dcRouterRef.metricsManager.getNetworkStats();
const stats = await this.opsServerRef.dcRouterRef.metricsManager!.getNetworkStats();
const serverStats = await this.collectServerStats();
// Build per-IP bandwidth lookup from throughputByIP

View File

@@ -518,8 +518,8 @@ export class AccountingManager {
if (deletedCount > 0) {
logger.log('info', `Cleaned up ${deletedCount} old accounting sessions`);
}
} catch (error) {
logger.log('error', `Failed to cleanup old sessions: ${error.message}`);
} catch (error: unknown) {
logger.log('error', `Failed to cleanup old sessions: ${(error as Error).message}`);
}
return deletedCount;
@@ -582,8 +582,8 @@ export class AccountingManager {
// Ignore individual errors
}
}
} catch (error) {
logger.log('warn', `Failed to load active sessions: ${error.message}`);
} catch (error: unknown) {
logger.log('warn', `Failed to load active sessions: ${(error as Error).message}`);
}
}
@@ -598,8 +598,8 @@ export class AccountingManager {
const key = `${this.config.storagePrefix}/active/${session.sessionId}.json`;
try {
await this.storageManager.setJSON(key, session);
} catch (error) {
logger.log('error', `Failed to persist session ${session.sessionId}: ${error.message}`);
} catch (error: unknown) {
logger.log('error', `Failed to persist session ${session.sessionId}: ${(error as Error).message}`);
}
}
@@ -620,8 +620,8 @@ export class AccountingManager {
const date = new Date(session.endTime);
const archiveKey = `${this.config.storagePrefix}/archive/${date.getFullYear()}/${String(date.getMonth() + 1).padStart(2, '0')}/${String(date.getDate()).padStart(2, '0')}/${session.sessionId}.json`;
await this.storageManager.setJSON(archiveKey, session);
} catch (error) {
logger.log('error', `Failed to archive session ${session.sessionId}: ${error.message}`);
} catch (error: unknown) {
logger.log('error', `Failed to archive session ${session.sessionId}: ${(error as Error).message}`);
}
}
@@ -653,8 +653,8 @@ export class AccountingManager {
// Ignore individual errors
}
}
} catch (error) {
logger.log('warn', `Failed to get archived sessions: ${error.message}`);
} catch (error: unknown) {
logger.log('warn', `Failed to get archived sessions: ${(error as Error).message}`);
}
return sessions;

View File

@@ -310,8 +310,8 @@ export class RadiusServer {
default:
logger.log('debug', `RADIUS Acct Unknown status type: ${statusType}`);
}
} catch (error) {
logger.log('error', `RADIUS accounting error: ${error.message}`);
} catch (error: unknown) {
logger.log('error', `RADIUS accounting error: ${(error as Error).message}`);
}
return { code: plugins.smartradius.ERadiusCode.AccountingResponse };

View File

@@ -104,7 +104,7 @@ export class VlanManager {
if (this.normalizedMacCache.size > 10000) {
const iterator = this.normalizedMacCache.keys();
for (let i = 0; i < 1000; i++) {
this.normalizedMacCache.delete(iterator.next().value);
this.normalizedMacCache.delete(iterator.next().value!);
}
}
@@ -348,8 +348,8 @@ export class VlanManager {
}
logger.log('info', `Loaded ${data.length} VLAN mappings from storage`);
}
} catch (error) {
logger.log('warn', `Failed to load VLAN mappings from storage: ${error.message}`);
} catch (error: unknown) {
logger.log('warn', `Failed to load VLAN mappings from storage: ${(error as Error).message}`);
}
}
@@ -364,8 +364,8 @@ export class VlanManager {
try {
const mappings = Array.from(this.mappings.values());
await this.storageManager.setJSON(this.config.storagePrefix, mappings);
} catch (error) {
logger.log('error', `Failed to save VLAN mappings to storage: ${error.message}`);
} catch (error: unknown) {
logger.log('error', `Failed to save VLAN mappings to storage: ${(error as Error).message}`);
}
}
}

View File

@@ -136,7 +136,7 @@ Manages the Rust-based RemoteIngressHub lifecycle. Syncs allowed edges, tracks c
## License and Legal Information
This repository contains open-source code licensed under the MIT License. A copy of the license can be found in the [LICENSE](../LICENSE) file.
This repository contains open-source code licensed under the MIT License. A copy of the license can be found in the [license](../license) file.
**Please note:** The MIT License does not grant permission to use the trade names, trademarks, service marks, or product names of the project, except as required for reasonable and customary use in describing the origin of the work and reproducing the content of the NOTICE file.

View File

@@ -60,7 +60,7 @@ export enum ThreatCategory {
* Content Scanner for detecting malicious email content
*/
export class ContentScanner {
private static instance: ContentScanner;
private static instance: ContentScanner | undefined;
private scanCache: LRUCache<string, IScanResult>;
private options: Required<IContentScannerOptions>;
@@ -258,12 +258,12 @@ export class ContentScanner {
}
return result;
} catch (error) {
logger.log('error', `Error scanning email: ${error.message}`, {
} catch (error: unknown) {
logger.log('error', `Error scanning email: ${(error as Error).message}`, {
messageId: email.getMessageId(),
error: error.stack
error: (error as Error).stack
});
// Return a safe default with error indication
return {
isClean: true, // Let it pass if scanner fails (configure as desired)
@@ -271,7 +271,7 @@ export class ContentScanner {
scannedElements: ['error'],
timestamp: Date.now(),
threatType: 'scan_error',
threatDetails: `Scan error: ${error.message}`
threatDetails: `Scan error: ${(error as Error).message}`
};
}
}
@@ -625,8 +625,8 @@ export class ContentScanner {
return sample.toString('utf8')
.replace(/[\x00-\x09\x0B-\x1F\x7F-\x9F]/g, '') // Remove control chars
.replace(/\uFFFD/g, ''); // Remove replacement char
} catch (error) {
logger.log('warn', `Error extracting text from buffer: ${error.message}`);
} catch (error: unknown) {
logger.log('warn', `Error extracting text from buffer: ${(error as Error).message}`);
return '';
}
}
@@ -699,10 +699,10 @@ export class ContentScanner {
subject: email.subject
},
success: false,
domain: email.getFromDomain()
domain: email.getFromDomain() ?? undefined
});
}
/**
* Log a threat finding to the security logger
* @param email The email containing the threat
@@ -722,10 +722,10 @@ export class ContentScanner {
subject: email.subject
},
success: false,
domain: email.getFromDomain()
domain: email.getFromDomain() ?? undefined
});
}
/**
* Get threat level description based on score
* @param score Threat score

View File

@@ -61,7 +61,7 @@ export interface IIPReputationOptions {
* Class for checking IP reputation of inbound email senders
*/
export class IPReputationChecker {
private static instance: IPReputationChecker;
private static instance: IPReputationChecker | undefined;
private reputationCache: LRUCache<string, IReputationResult>;
private options: Required<IIPReputationOptions>;
private storageManager?: any; // StorageManager instance
@@ -127,8 +127,8 @@ export class IPReputationChecker {
// Load cache from disk if enabled
if (this.options.enableLocalCache) {
// Fire and forget the load operation
this.loadCache().catch(error => {
logger.log('error', `Failed to load IP reputation cache during initialization: ${error.message}`);
this.loadCache().catch((error: unknown) => {
logger.log('error', `Failed to load IP reputation cache during initialization: ${(error as Error).message}`);
});
}
}
@@ -237,13 +237,13 @@ export class IPReputationChecker {
this.logReputationCheck(ip, result);
return result;
} catch (error) {
logger.log('error', `Error checking IP reputation for ${ip}: ${error.message}`, {
} catch (error: unknown) {
logger.log('error', `Error checking IP reputation for ${ip}: ${(error as Error).message}`, {
ip,
stack: error.stack
stack: (error as Error).stack
});
return this.createErrorResult(ip, error.message);
return this.createErrorResult(ip, (error as Error).message);
}
}
@@ -266,8 +266,8 @@ export class IPReputationChecker {
const lookupDomain = `${reversedIP}.${server}`;
await plugins.dns.promises.resolve(lookupDomain);
return server; // IP is listed in this DNSBL
} catch (error) {
if (error.code === 'ENOTFOUND') {
} catch (error: unknown) {
if ((error as any).code === 'ENOTFOUND') {
return null; // IP is not listed in this DNSBL
}
throw error; // Other error
@@ -286,8 +286,8 @@ export class IPReputationChecker {
listCount: lists.length,
lists
};
} catch (error) {
logger.log('error', `Error checking DNSBL for ${ip}: ${error.message}`);
} catch (error: unknown) {
logger.log('error', `Error checking DNSBL for ${ip}: ${(error as Error).message}`);
return {
listCount: 0,
lists: []
@@ -349,8 +349,8 @@ export class IPReputationChecker {
org: this.determineOrg(ip), // Simplified, would use real org data
type
};
} catch (error) {
logger.log('error', `Error getting IP info for ${ip}: ${error.message}`);
} catch (error: unknown) {
logger.log('error', `Error getting IP info for ${ip}: ${(error as Error).message}`);
return {
type: IPType.UNKNOWN
};
@@ -468,8 +468,8 @@ export class IPReputationChecker {
}
this.saveCacheTimer = setTimeout(() => {
this.saveCacheTimer = null;
this.saveCache().catch(error => {
logger.log('error', `Failed to save IP reputation cache: ${error.message}`);
this.saveCache().catch((error: unknown) => {
logger.log('error', `Failed to save IP reputation cache: ${(error as Error).message}`);
});
}, IPReputationChecker.SAVE_CACHE_DEBOUNCE_MS);
}
@@ -506,11 +506,11 @@ export class IPReputationChecker {
logger.log('info', `Saved ${entries.length} IP reputation cache entries to disk`);
}
} catch (error) {
logger.log('error', `Failed to save IP reputation cache: ${error.message}`);
} catch (error: unknown) {
logger.log('error', `Failed to save IP reputation cache: ${(error as Error).message}`);
}
}
/**
* Load cache from disk or storage manager
*/
@@ -542,12 +542,12 @@ export class IPReputationChecker {
plugins.fs.unlinkSync(cacheFile);
logger.log('info', 'Old cache file removed after migration');
} catch (deleteError) {
logger.log('warn', `Could not delete old cache file: ${deleteError.message}`);
logger.log('warn', `Could not delete old cache file: ${(deleteError as Error).message}`);
}
}
}
} catch (error) {
logger.log('error', `Error loading from StorageManager: ${error.message}`);
} catch (error: unknown) {
logger.log('error', `Error loading from StorageManager: ${(error as Error).message}`);
}
} else {
// No storage manager, load from filesystem
@@ -578,8 +578,8 @@ export class IPReputationChecker {
const source = fromFilesystem ? 'disk' : 'StorageManager';
logger.log('info', `Loaded ${validEntries.length} IP reputation cache entries from ${source}`);
}
} catch (error) {
logger.log('error', `Failed to load IP reputation cache: ${error.message}`);
} catch (error: unknown) {
logger.log('error', `Failed to load IP reputation cache: ${(error as Error).message}`);
}
}
@@ -611,8 +611,8 @@ export class IPReputationChecker {
// If cache is enabled and we have entries, save them to the new storage manager
if (this.options.enableLocalCache && this.reputationCache.size > 0) {
this.saveCache().catch(error => {
logger.log('error', `Failed to save cache to new storage manager: ${error.message}`);
this.saveCache().catch((error: unknown) => {
logger.log('error', `Failed to save cache to new storage manager: ${(error as Error).message}`);
});
}
}

View File

@@ -58,7 +58,7 @@ export interface ISecurityEvent {
* Security logger for enhanced security monitoring
*/
export class SecurityLogger {
private static instance: SecurityLogger;
private static instance: SecurityLogger | undefined;
private securityEvents: ISecurityEvent[] = [];
private maxEventHistory: number;
private enableNotifications: boolean;
@@ -154,11 +154,13 @@ export class SecurityLogger {
}
if (filter.fromTimestamp) {
filteredEvents = filteredEvents.filter(event => event.timestamp >= filter.fromTimestamp);
const fromTs = filter.fromTimestamp;
filteredEvents = filteredEvents.filter(event => event.timestamp >= fromTs);
}
if (filter.toTimestamp) {
filteredEvents = filteredEvents.filter(event => event.timestamp <= filter.toTimestamp);
const toTs = filter.toTimestamp;
filteredEvents = filteredEvents.filter(event => event.timestamp <= toTs);
}
}

View File

@@ -7,7 +7,7 @@ import { smsConfigSchema } from './config/sms.schema.js';
import { ConfigValidator } from '../config/validator.js';
export class SmsService {
public projectinfo: plugins.projectinfo.ProjectInfo;
public projectinfo!: plugins.projectinfo.ProjectInfo;
public typedrouter = new plugins.typedrequest.TypedRouter();
public config: ISmsConfig;
@@ -16,7 +16,7 @@ export class SmsService {
const validationResult = ConfigValidator.validate(options, smsConfigSchema);
if (!validationResult.valid) {
logger.warn(`SMS service configuration has validation errors: ${validationResult.errors.join(', ')}`);
logger.warn(`SMS service configuration has validation errors: ${validationResult.errors!.join(', ')}`);
}
// Set configuration with defaults

View File

@@ -15,7 +15,7 @@ export interface IStorageConfig {
/** Filesystem path for storage */
fsPath?: string;
/** Custom read function */
readFunction?: (key: string) => Promise<string>;
readFunction?: (key: string) => Promise<string | null>;
/** Custom write function */
writeFunction?: (key: string, value: string) => Promise<void>;
}
@@ -57,9 +57,7 @@ export class StorageManager {
this.ensureDirectory(this.fsBasePath);
// Set up internal filesystem read/write functions
this.config.readFunction = async (key: string) => {
return this.fsRead(key);
};
this.config.readFunction = (key: string): Promise<string | null> => this.fsRead(key);
this.config.writeFunction = async (key: string, value: string) => {
await this.fsWrite(key, value);
};
@@ -88,8 +86,8 @@ export class StorageManager {
private async ensureDirectory(dirPath: string): Promise<void> {
try {
await plugins.fsUtils.ensureDir(dirPath);
} catch (error) {
logger.log('error', `Failed to create storage directory: ${error.message}`);
} catch (error: unknown) {
logger.log('error', `Failed to create storage directory: ${(error as Error).message}`);
throw error;
}
}
@@ -129,19 +127,19 @@ export class StorageManager {
/**
* Internal filesystem read function
*/
private async fsRead(key: string): Promise<string> {
private async fsRead(key: string): Promise<string | null> {
const filePath = this.keyToPath(key);
try {
const content = await readFile(filePath, 'utf8');
return content;
} catch (error) {
if (error.code === 'ENOENT') {
} catch (error: unknown) {
if ((error as any).code === 'ENOENT') {
return null;
}
throw error;
}
}
/**
* Internal filesystem write function
*/
@@ -186,8 +184,8 @@ export class StorageManager {
default:
throw new Error(`Unknown backend: ${this.backend}`);
}
} catch (error) {
logger.log('error', `Storage get error for key ${key}: ${error.message}`);
} catch (error: unknown) {
logger.log('error', `Storage get error for key ${key}: ${(error as Error).message}`);
throw error;
}
}
@@ -230,7 +228,7 @@ export class StorageManager {
this.memoryStore.set(key, value);
// Evict oldest entries if memory store exceeds limit
while (this.memoryStore.size > StorageManager.MAX_MEMORY_ENTRIES) {
const firstKey = this.memoryStore.keys().next().value;
const firstKey = this.memoryStore.keys().next().value!;
this.memoryStore.delete(firstKey);
}
break;
@@ -239,8 +237,8 @@ export class StorageManager {
default:
throw new Error(`Unknown backend: ${this.backend}`);
}
} catch (error) {
logger.log('error', `Storage set error for key ${key}: ${error.message}`);
} catch (error: unknown) {
logger.log('error', `Storage set error for key ${key}: ${(error as Error).message}`);
throw error;
}
}
@@ -257,8 +255,8 @@ export class StorageManager {
const filePath = this.keyToPath(key);
try {
await unlink(filePath);
} catch (error) {
if (error.code !== 'ENOENT') {
} catch (error: unknown) {
if ((error as any).code !== 'ENOENT') {
throw error;
}
}
@@ -281,8 +279,8 @@ export class StorageManager {
default:
throw new Error(`Unknown backend: ${this.backend}`);
}
} catch (error) {
logger.log('error', `Storage delete error for key ${key}: ${error.message}`);
} catch (error: unknown) {
logger.log('error', `Storage delete error for key ${key}: ${(error as Error).message}`);
throw error;
}
}
@@ -319,8 +317,8 @@ export class StorageManager {
}
}
}
} catch (error) {
if (error.code !== 'ENOENT') {
} catch (error: unknown) {
if ((error as any).code !== 'ENOENT') {
throw error;
}
}
@@ -348,8 +346,8 @@ export class StorageManager {
default:
throw new Error(`Unknown backend: ${this.backend}`);
}
} catch (error) {
logger.log('error', `Storage list error for prefix ${prefix}: ${error.message}`);
} catch (error: unknown) {
logger.log('error', `Storage list error for prefix ${prefix}: ${(error as Error).message}`);
throw error;
}
}
@@ -390,8 +388,8 @@ export class StorageManager {
try {
return JSON.parse(value) as T;
} catch (error) {
logger.log('error', `Failed to parse JSON for key ${key}: ${error.message}`);
} catch (error: unknown) {
logger.log('error', `Failed to parse JSON for key ${key}: ${(error as Error).message}`);
throw error;
}
}

View File

@@ -259,7 +259,7 @@ Resource classes (`Route`, `Certificate`, `ApiToken`, `RemoteIngress`, `Email`)
## License and Legal Information
This repository contains open-source code licensed under the MIT License. A copy of the license can be found in the [LICENSE](../LICENSE) file.
This repository contains open-source code licensed under the MIT License. A copy of the license can be found in the [license](../license) file.
**Please note:** The MIT License does not grant permission to use the trade names, trademarks, service marks, or product names of the project, except as required for reasonable and customary use in describing the origin of the work and reproducing the content of the NOTICE file.

View File

@@ -280,7 +280,7 @@ console.log('Connection token:', tokenResponse.token);
## License and Legal Information
This repository contains open-source code licensed under the MIT License. A copy of the license can be found in the [LICENSE](../LICENSE) file.
This repository contains open-source code licensed under the MIT License. A copy of the license can be found in the [license](../license) file.
**Please note:** The MIT License does not grant permission to use the trade names, trademarks, service marks, or product names of the project, except as required for reasonable and customary use in describing the origin of the work and reproducing the content of the NOTICE file.

View File

@@ -3,6 +3,6 @@
*/
export const commitinfo = {
name: '@serve.zone/dcrouter',
version: '11.10.0',
version: '11.10.6',
description: 'A multifaceted routing service handling mail and SMS delivery functions.'
}

View File

@@ -240,7 +240,7 @@ interface IActionContext {
}
const getActionContext = (): IActionContext => {
const identity = loginStatePart.getState().identity;
const identity = loginStatePart.getState()!.identity;
// Treat expired JWTs as no identity — prevents stale persisted sessions from firing requests
if (identity && identity.expiresAt && identity.expiresAt < Date.now()) {
return { identity: null };
@@ -252,7 +252,7 @@ const getActionContext = (): IActionContext => {
export const loginAction = loginStatePart.createAction<{
username: string;
password: string;
}>(async (statePartArg, dataArg) => {
}>(async (statePartArg, dataArg): Promise<ILoginState> => {
const typedRequest = new plugins.domtools.plugins.typedrequest.TypedRequest<
interfaces.requests.IReq_AdminLoginWithUsernameAndPassword
>('/typedrequest', 'adminLoginWithUsernameAndPassword');
@@ -269,10 +269,10 @@ export const loginAction = loginStatePart.createAction<{
isLoggedIn: true,
};
}
return statePartArg.getState();
} catch (error) {
return statePartArg.getState()!;
} catch (error: unknown) {
console.error('Login failed:', error);
return statePartArg.getState();
return statePartArg.getState()!;
}
});
@@ -300,9 +300,9 @@ export const logoutAction = loginStatePart.createAction(async (statePartArg) =>
});
// Fetch All Stats Action - Using combined endpoint for efficiency
export const fetchAllStatsAction = statsStatePart.createAction(async (statePartArg) => {
export const fetchAllStatsAction = statsStatePart.createAction(async (statePartArg): Promise<IStatsState> => {
const context = getActionContext();
const currentState = statePartArg.getState();
const currentState = statePartArg.getState()!;
if (!context.identity) return currentState;
try {
@@ -310,7 +310,7 @@ export const fetchAllStatsAction = statsStatePart.createAction(async (statePartA
const combinedRequest = new plugins.domtools.plugins.typedrequest.TypedRequest<
interfaces.requests.IReq_GetCombinedMetrics
>('/typedrequest', 'getCombinedMetrics');
const combinedResponse = await combinedRequest.fire({
identity: context.identity,
sections: {
@@ -332,19 +332,19 @@ export const fetchAllStatsAction = statsStatePart.createAction(async (statePartA
isLoading: false,
error: null,
};
} catch (error) {
} catch (error: unknown) {
return {
...currentState,
isLoading: false,
error: error.message || 'Failed to fetch statistics',
error: (error as Error).message || 'Failed to fetch statistics',
};
}
});
// Fetch Configuration Action (read-only)
export const fetchConfigurationAction = configStatePart.createAction(async (statePartArg) => {
export const fetchConfigurationAction = configStatePart.createAction(async (statePartArg): Promise<IConfigState> => {
const context = getActionContext();
const currentState = statePartArg.getState();
const currentState = statePartArg.getState()!;
if (!context.identity) return currentState;
try {
@@ -361,11 +361,11 @@ export const fetchConfigurationAction = configStatePart.createAction(async (stat
isLoading: false,
error: null,
};
} catch (error) {
} catch (error: unknown) {
return {
...currentState,
isLoading: false,
error: error.message || 'Failed to fetch configuration',
error: (error as Error).message || 'Failed to fetch configuration',
};
}
});
@@ -375,9 +375,9 @@ export const fetchRecentLogsAction = logStatePart.createAction<{
limit?: number;
level?: 'debug' | 'info' | 'warn' | 'error';
category?: 'smtp' | 'dns' | 'security' | 'system' | 'email';
}>(async (statePartArg, dataArg) => {
}>(async (statePartArg, dataArg): Promise<ILogState> => {
const context = getActionContext();
if (!context.identity) return statePartArg.getState();
if (!context.identity) return statePartArg.getState()!;
const logsRequest = new plugins.domtools.plugins.typedrequest.TypedRequest<
interfaces.requests.IReq_GetRecentLogs
@@ -391,14 +391,14 @@ export const fetchRecentLogsAction = logStatePart.createAction<{
});
return {
...statePartArg.getState(),
...statePartArg.getState()!,
recentLogs: response.logs,
};
});
// Toggle Auto Refresh Action
export const toggleAutoRefreshAction = uiStatePart.createAction(async (statePartArg) => {
const currentState = statePartArg.getState();
export const toggleAutoRefreshAction = uiStatePart.createAction(async (statePartArg): Promise<IUiState> => {
const currentState = statePartArg.getState()!;
return {
...currentState,
autoRefresh: !currentState.autoRefresh,
@@ -406,9 +406,9 @@ export const toggleAutoRefreshAction = uiStatePart.createAction(async (statePart
});
// Set Active View Action
export const setActiveViewAction = uiStatePart.createAction<string>(async (statePartArg, viewName) => {
const currentState = statePartArg.getState();
export const setActiveViewAction = uiStatePart.createAction<string>(async (statePartArg, viewName): Promise<IUiState> => {
const currentState = statePartArg.getState()!;
// If switching to network view, ensure we fetch network data
if (viewName === 'network' && currentState.activeView !== 'network') {
setTimeout(() => {
@@ -451,9 +451,9 @@ export const setActiveViewAction = uiStatePart.createAction<string>(async (state
});
// Fetch Network Stats Action
export const fetchNetworkStatsAction = networkStatePart.createAction(async (statePartArg) => {
export const fetchNetworkStatsAction = networkStatePart.createAction(async (statePartArg): Promise<INetworkState> => {
const context = getActionContext();
const currentState = statePartArg.getState();
const currentState = statePartArg.getState()!;
if (!context.identity) return currentState;
try {
@@ -525,9 +525,9 @@ export const fetchNetworkStatsAction = networkStatePart.createAction(async (stat
// ============================================================================
// Fetch All Emails Action
export const fetchAllEmailsAction = emailOpsStatePart.createAction(async (statePartArg) => {
export const fetchAllEmailsAction = emailOpsStatePart.createAction(async (statePartArg): Promise<IEmailOpsState> => {
const context = getActionContext();
const currentState = statePartArg.getState();
const currentState = statePartArg.getState()!;
if (!context.identity) return currentState;
try {
@@ -558,9 +558,9 @@ export const fetchAllEmailsAction = emailOpsStatePart.createAction(async (stateP
// Certificate Actions
// ============================================================================
export const fetchCertificateOverviewAction = certificateStatePart.createAction(async (statePartArg) => {
export const fetchCertificateOverviewAction = certificateStatePart.createAction(async (statePartArg): Promise<ICertificateState> => {
const context = getActionContext();
const currentState = statePartArg.getState();
const currentState = statePartArg.getState()!;
if (!context.identity) return currentState;
try {
@@ -589,9 +589,9 @@ export const fetchCertificateOverviewAction = certificateStatePart.createAction(
});
export const reprovisionCertificateAction = certificateStatePart.createAction<string>(
async (statePartArg, domain, actionContext) => {
async (statePartArg, domain, actionContext): Promise<ICertificateState> => {
const context = getActionContext();
const currentState = statePartArg.getState();
const currentState = statePartArg.getState()!;
try {
const request = new plugins.domtools.plugins.typedrequest.TypedRequest<
@@ -599,13 +599,13 @@ export const reprovisionCertificateAction = certificateStatePart.createAction<st
>('/typedrequest', 'reprovisionCertificateDomain');
await request.fire({
identity: context.identity,
identity: context.identity!,
domain,
});
// Re-fetch overview after reprovisioning
return await actionContext.dispatch(fetchCertificateOverviewAction, null);
} catch (error) {
return await actionContext!.dispatch(fetchCertificateOverviewAction, null);
} catch (error: unknown) {
return {
...currentState,
error: error instanceof Error ? error.message : 'Failed to reprovision certificate',
@@ -615,9 +615,9 @@ export const reprovisionCertificateAction = certificateStatePart.createAction<st
);
export const deleteCertificateAction = certificateStatePart.createAction<string>(
async (statePartArg, domain, actionContext) => {
async (statePartArg, domain, actionContext): Promise<ICertificateState> => {
const context = getActionContext();
const currentState = statePartArg.getState();
const currentState = statePartArg.getState()!;
try {
const request = new plugins.domtools.plugins.typedrequest.TypedRequest<
@@ -625,13 +625,13 @@ export const deleteCertificateAction = certificateStatePart.createAction<string>
>('/typedrequest', 'deleteCertificate');
await request.fire({
identity: context.identity,
identity: context.identity!,
domain,
});
// Re-fetch overview after deletion
return await actionContext.dispatch(fetchCertificateOverviewAction, null);
} catch (error) {
return await actionContext!.dispatch(fetchCertificateOverviewAction, null);
} catch (error: unknown) {
return {
...currentState,
error: error instanceof Error ? error.message : 'Failed to delete certificate',
@@ -649,9 +649,9 @@ export const importCertificateAction = certificateStatePart.createAction<{
publicKey: string;
csr: string;
}>(
async (statePartArg, cert, actionContext) => {
async (statePartArg, cert, actionContext): Promise<ICertificateState> => {
const context = getActionContext();
const currentState = statePartArg.getState();
const currentState = statePartArg.getState()!;
try {
const request = new plugins.domtools.plugins.typedrequest.TypedRequest<
@@ -659,13 +659,13 @@ export const importCertificateAction = certificateStatePart.createAction<{
>('/typedrequest', 'importCertificate');
await request.fire({
identity: context.identity,
identity: context.identity!,
cert,
});
// Re-fetch overview after import
return await actionContext.dispatch(fetchCertificateOverviewAction, null);
} catch (error) {
return await actionContext!.dispatch(fetchCertificateOverviewAction, null);
} catch (error: unknown) {
return {
...currentState,
error: error instanceof Error ? error.message : 'Failed to import certificate',
@@ -681,7 +681,7 @@ export async function fetchCertificateExport(domain: string) {
>('/typedrequest', 'exportCertificate');
return request.fire({
identity: context.identity,
identity: context.identity!,
domain,
});
}
@@ -695,16 +695,16 @@ export async function fetchConnectionToken(edgeId: string) {
const request = new plugins.domtools.plugins.typedrequest.TypedRequest<
interfaces.requests.IReq_GetRemoteIngressConnectionToken
>('/typedrequest', 'getRemoteIngressConnectionToken');
return request.fire({ identity: context.identity, edgeId });
return request.fire({ identity: context.identity!, edgeId });
}
// ============================================================================
// Remote Ingress Actions
// ============================================================================
export const fetchRemoteIngressAction = remoteIngressStatePart.createAction(async (statePartArg) => {
export const fetchRemoteIngressAction = remoteIngressStatePart.createAction(async (statePartArg): Promise<IRemoteIngressState> => {
const context = getActionContext();
const currentState = statePartArg.getState();
const currentState = statePartArg.getState()!;
if (!context.identity) return currentState;
try {
@@ -743,9 +743,9 @@ export const createRemoteIngressAction = remoteIngressStatePart.createAction<{
listenPorts?: number[];
autoDerivePorts?: boolean;
tags?: string[];
}>(async (statePartArg, dataArg, actionContext) => {
}>(async (statePartArg, dataArg, actionContext): Promise<IRemoteIngressState> => {
const context = getActionContext();
const currentState = statePartArg.getState();
const currentState = statePartArg.getState()!;
try {
const request = new plugins.domtools.plugins.typedrequest.TypedRequest<
@@ -753,7 +753,7 @@ export const createRemoteIngressAction = remoteIngressStatePart.createAction<{
>('/typedrequest', 'createRemoteIngress');
const response = await request.fire({
identity: context.identity,
identity: context.identity!,
name: dataArg.name,
listenPorts: dataArg.listenPorts,
autoDerivePorts: dataArg.autoDerivePorts,
@@ -762,16 +762,16 @@ export const createRemoteIngressAction = remoteIngressStatePart.createAction<{
if (response.success) {
// Refresh the list
await actionContext.dispatch(fetchRemoteIngressAction, null);
await actionContext!.dispatch(fetchRemoteIngressAction, null);
return {
...statePartArg.getState(),
...statePartArg.getState()!,
newEdgeId: response.edge.id,
};
}
return currentState;
} catch (error) {
} catch (error: unknown) {
return {
...currentState,
error: error instanceof Error ? error.message : 'Failed to create edge',
@@ -780,9 +780,9 @@ export const createRemoteIngressAction = remoteIngressStatePart.createAction<{
});
export const deleteRemoteIngressAction = remoteIngressStatePart.createAction<string>(
async (statePartArg, edgeId, actionContext) => {
async (statePartArg, edgeId, actionContext): Promise<IRemoteIngressState> => {
const context = getActionContext();
const currentState = statePartArg.getState();
const currentState = statePartArg.getState()!;
try {
const request = new plugins.domtools.plugins.typedrequest.TypedRequest<
@@ -790,12 +790,12 @@ export const deleteRemoteIngressAction = remoteIngressStatePart.createAction<str
>('/typedrequest', 'deleteRemoteIngress');
await request.fire({
identity: context.identity,
identity: context.identity!,
id: edgeId,
});
return await actionContext.dispatch(fetchRemoteIngressAction, null);
} catch (error) {
return await actionContext!.dispatch(fetchRemoteIngressAction, null);
} catch (error: unknown) {
return {
...currentState,
error: error instanceof Error ? error.message : 'Failed to delete edge',
@@ -810,9 +810,9 @@ export const updateRemoteIngressAction = remoteIngressStatePart.createAction<{
listenPorts?: number[];
autoDerivePorts?: boolean;
tags?: string[];
}>(async (statePartArg, dataArg, actionContext) => {
}>(async (statePartArg, dataArg, actionContext): Promise<IRemoteIngressState> => {
const context = getActionContext();
const currentState = statePartArg.getState();
const currentState = statePartArg.getState()!;
try {
const request = new plugins.domtools.plugins.typedrequest.TypedRequest<
@@ -820,7 +820,7 @@ export const updateRemoteIngressAction = remoteIngressStatePart.createAction<{
>('/typedrequest', 'updateRemoteIngress');
await request.fire({
identity: context.identity,
identity: context.identity!,
id: dataArg.id,
name: dataArg.name,
listenPorts: dataArg.listenPorts,
@@ -828,8 +828,8 @@ export const updateRemoteIngressAction = remoteIngressStatePart.createAction<{
tags: dataArg.tags,
});
return await actionContext.dispatch(fetchRemoteIngressAction, null);
} catch (error) {
return await actionContext!.dispatch(fetchRemoteIngressAction, null);
} catch (error: unknown) {
return {
...currentState,
error: error instanceof Error ? error.message : 'Failed to update edge',
@@ -838,9 +838,9 @@ export const updateRemoteIngressAction = remoteIngressStatePart.createAction<{
});
export const regenerateRemoteIngressSecretAction = remoteIngressStatePart.createAction<string>(
async (statePartArg, edgeId) => {
async (statePartArg, edgeId): Promise<IRemoteIngressState> => {
const context = getActionContext();
const currentState = statePartArg.getState();
const currentState = statePartArg.getState()!;
try {
const request = new plugins.domtools.plugins.typedrequest.TypedRequest<
@@ -848,7 +848,7 @@ export const regenerateRemoteIngressSecretAction = remoteIngressStatePart.create
>('/typedrequest', 'regenerateRemoteIngressSecret');
const response = await request.fire({
identity: context.identity,
identity: context.identity!,
id: edgeId,
});
@@ -870,9 +870,9 @@ export const regenerateRemoteIngressSecretAction = remoteIngressStatePart.create
);
export const clearNewEdgeIdAction = remoteIngressStatePart.createAction(
async (statePartArg) => {
async (statePartArg): Promise<IRemoteIngressState> => {
return {
...statePartArg.getState(),
...statePartArg.getState()!,
newEdgeId: null,
};
}
@@ -881,9 +881,9 @@ export const clearNewEdgeIdAction = remoteIngressStatePart.createAction(
export const toggleRemoteIngressAction = remoteIngressStatePart.createAction<{
id: string;
enabled: boolean;
}>(async (statePartArg, dataArg, actionContext) => {
}>(async (statePartArg, dataArg, actionContext): Promise<IRemoteIngressState> => {
const context = getActionContext();
const currentState = statePartArg.getState();
const currentState = statePartArg.getState()!;
try {
const request = new plugins.domtools.plugins.typedrequest.TypedRequest<
@@ -891,13 +891,13 @@ export const toggleRemoteIngressAction = remoteIngressStatePart.createAction<{
>('/typedrequest', 'updateRemoteIngress');
await request.fire({
identity: context.identity,
identity: context.identity!,
id: dataArg.id,
enabled: dataArg.enabled,
});
return await actionContext.dispatch(fetchRemoteIngressAction, null);
} catch (error) {
return await actionContext!.dispatch(fetchRemoteIngressAction, null);
} catch (error: unknown) {
return {
...currentState,
error: error instanceof Error ? error.message : 'Failed to toggle edge',
@@ -909,9 +909,9 @@ export const toggleRemoteIngressAction = remoteIngressStatePart.createAction<{
// Route Management Actions
// ============================================================================
export const fetchMergedRoutesAction = routeManagementStatePart.createAction(async (statePartArg) => {
export const fetchMergedRoutesAction = routeManagementStatePart.createAction(async (statePartArg): Promise<IRouteManagementState> => {
const context = getActionContext();
const currentState = statePartArg.getState();
const currentState = statePartArg.getState()!;
if (!context.identity) return currentState;
try {
@@ -943,9 +943,9 @@ export const fetchMergedRoutesAction = routeManagementStatePart.createAction(asy
export const createRouteAction = routeManagementStatePart.createAction<{
route: any;
enabled?: boolean;
}>(async (statePartArg, dataArg, actionContext) => {
}>(async (statePartArg, dataArg, actionContext): Promise<IRouteManagementState> => {
const context = getActionContext();
const currentState = statePartArg.getState();
const currentState = statePartArg.getState()!;
try {
const request = new plugins.domtools.plugins.typedrequest.TypedRequest<
@@ -953,13 +953,13 @@ export const createRouteAction = routeManagementStatePart.createAction<{
>('/typedrequest', 'createRoute');
await request.fire({
identity: context.identity,
identity: context.identity!,
route: dataArg.route,
enabled: dataArg.enabled,
});
return await actionContext.dispatch(fetchMergedRoutesAction, null);
} catch (error) {
return await actionContext!.dispatch(fetchMergedRoutesAction, null);
} catch (error: unknown) {
return {
...currentState,
error: error instanceof Error ? error.message : 'Failed to create route',
@@ -968,9 +968,9 @@ export const createRouteAction = routeManagementStatePart.createAction<{
});
export const deleteRouteAction = routeManagementStatePart.createAction<string>(
async (statePartArg, routeId, actionContext) => {
async (statePartArg, routeId, actionContext): Promise<IRouteManagementState> => {
const context = getActionContext();
const currentState = statePartArg.getState();
const currentState = statePartArg.getState()!;
try {
const request = new plugins.domtools.plugins.typedrequest.TypedRequest<
@@ -978,12 +978,12 @@ export const deleteRouteAction = routeManagementStatePart.createAction<string>(
>('/typedrequest', 'deleteRoute');
await request.fire({
identity: context.identity,
identity: context.identity!,
id: routeId,
});
return await actionContext.dispatch(fetchMergedRoutesAction, null);
} catch (error) {
return await actionContext!.dispatch(fetchMergedRoutesAction, null);
} catch (error: unknown) {
return {
...currentState,
error: error instanceof Error ? error.message : 'Failed to delete route',
@@ -995,9 +995,9 @@ export const deleteRouteAction = routeManagementStatePart.createAction<string>(
export const toggleRouteAction = routeManagementStatePart.createAction<{
id: string;
enabled: boolean;
}>(async (statePartArg, dataArg, actionContext) => {
}>(async (statePartArg, dataArg, actionContext): Promise<IRouteManagementState> => {
const context = getActionContext();
const currentState = statePartArg.getState();
const currentState = statePartArg.getState()!;
try {
const request = new plugins.domtools.plugins.typedrequest.TypedRequest<
@@ -1005,13 +1005,13 @@ export const toggleRouteAction = routeManagementStatePart.createAction<{
>('/typedrequest', 'toggleRoute');
await request.fire({
identity: context.identity,
identity: context.identity!,
id: dataArg.id,
enabled: dataArg.enabled,
});
return await actionContext.dispatch(fetchMergedRoutesAction, null);
} catch (error) {
return await actionContext!.dispatch(fetchMergedRoutesAction, null);
} catch (error: unknown) {
return {
...currentState,
error: error instanceof Error ? error.message : 'Failed to toggle route',
@@ -1022,9 +1022,9 @@ export const toggleRouteAction = routeManagementStatePart.createAction<{
export const setRouteOverrideAction = routeManagementStatePart.createAction<{
routeName: string;
enabled: boolean;
}>(async (statePartArg, dataArg, actionContext) => {
}>(async (statePartArg, dataArg, actionContext): Promise<IRouteManagementState> => {
const context = getActionContext();
const currentState = statePartArg.getState();
const currentState = statePartArg.getState()!;
try {
const request = new plugins.domtools.plugins.typedrequest.TypedRequest<
@@ -1032,13 +1032,13 @@ export const setRouteOverrideAction = routeManagementStatePart.createAction<{
>('/typedrequest', 'setRouteOverride');
await request.fire({
identity: context.identity,
identity: context.identity!,
routeName: dataArg.routeName,
enabled: dataArg.enabled,
});
return await actionContext.dispatch(fetchMergedRoutesAction, null);
} catch (error) {
return await actionContext!.dispatch(fetchMergedRoutesAction, null);
} catch (error: unknown) {
return {
...currentState,
error: error instanceof Error ? error.message : 'Failed to set override',
@@ -1047,9 +1047,9 @@ export const setRouteOverrideAction = routeManagementStatePart.createAction<{
});
export const removeRouteOverrideAction = routeManagementStatePart.createAction<string>(
async (statePartArg, routeName, actionContext) => {
async (statePartArg, routeName, actionContext): Promise<IRouteManagementState> => {
const context = getActionContext();
const currentState = statePartArg.getState();
const currentState = statePartArg.getState()!;
try {
const request = new plugins.domtools.plugins.typedrequest.TypedRequest<
@@ -1057,12 +1057,12 @@ export const removeRouteOverrideAction = routeManagementStatePart.createAction<s
>('/typedrequest', 'removeRouteOverride');
await request.fire({
identity: context.identity,
identity: context.identity!,
routeName,
});
return await actionContext.dispatch(fetchMergedRoutesAction, null);
} catch (error) {
return await actionContext!.dispatch(fetchMergedRoutesAction, null);
} catch (error: unknown) {
return {
...currentState,
error: error instanceof Error ? error.message : 'Failed to remove override',
@@ -1075,9 +1075,9 @@ export const removeRouteOverrideAction = routeManagementStatePart.createAction<s
// API Token Actions
// ============================================================================
export const fetchApiTokensAction = routeManagementStatePart.createAction(async (statePartArg) => {
export const fetchApiTokensAction = routeManagementStatePart.createAction(async (statePartArg): Promise<IRouteManagementState> => {
const context = getActionContext();
const currentState = statePartArg.getState();
const currentState = statePartArg.getState()!;
if (!context.identity) return currentState;
try {
@@ -1108,7 +1108,7 @@ export async function createApiToken(name: string, scopes: interfaces.data.TApiT
>('/typedrequest', 'createApiToken');
return request.fire({
identity: context.identity,
identity: context.identity!,
name,
scopes,
expiresInDays,
@@ -1122,15 +1122,15 @@ export async function rollApiToken(id: string) {
>('/typedrequest', 'rollApiToken');
return request.fire({
identity: context.identity,
identity: context.identity!,
id,
});
}
export const revokeApiTokenAction = routeManagementStatePart.createAction<string>(
async (statePartArg, tokenId, actionContext) => {
async (statePartArg, tokenId, actionContext): Promise<IRouteManagementState> => {
const context = getActionContext();
const currentState = statePartArg.getState();
const currentState = statePartArg.getState()!;
try {
const request = new plugins.domtools.plugins.typedrequest.TypedRequest<
@@ -1138,12 +1138,12 @@ export const revokeApiTokenAction = routeManagementStatePart.createAction<string
>('/typedrequest', 'revokeApiToken');
await request.fire({
identity: context.identity,
identity: context.identity!,
id: tokenId,
});
return await actionContext.dispatch(fetchApiTokensAction, null);
} catch (error) {
return await actionContext!.dispatch(fetchApiTokensAction, null);
} catch (error: unknown) {
return {
...currentState,
error: error instanceof Error ? error.message : 'Failed to revoke token',
@@ -1155,9 +1155,9 @@ export const revokeApiTokenAction = routeManagementStatePart.createAction<string
export const toggleApiTokenAction = routeManagementStatePart.createAction<{
id: string;
enabled: boolean;
}>(async (statePartArg, dataArg, actionContext) => {
}>(async (statePartArg, dataArg, actionContext): Promise<IRouteManagementState> => {
const context = getActionContext();
const currentState = statePartArg.getState();
const currentState = statePartArg.getState()!;
try {
const request = new plugins.domtools.plugins.typedrequest.TypedRequest<
@@ -1165,13 +1165,13 @@ export const toggleApiTokenAction = routeManagementStatePart.createAction<{
>('/typedrequest', 'toggleApiToken');
await request.fire({
identity: context.identity,
identity: context.identity!,
id: dataArg.id,
enabled: dataArg.enabled,
});
return await actionContext.dispatch(fetchApiTokensAction, null);
} catch (error) {
return await actionContext!.dispatch(fetchApiTokensAction, null);
} catch (error: unknown) {
return {
...currentState,
error: error instanceof Error ? error.message : 'Failed to toggle token',
@@ -1191,13 +1191,13 @@ socketRouter.addTypedHandler(
new plugins.domtools.plugins.typedrequest.TypedHandler<interfaces.requests.IReq_PushLogEntry>(
'pushLogEntry',
async (dataArg) => {
const current = logStatePart.getState();
const current = logStatePart.getState()!;
const updated = [...current.recentLogs, dataArg.entry];
// Cap at 2000 entries
if (updated.length > 2000) {
updated.splice(0, updated.length - 2000);
}
logStatePart.setState({ ...current, recentLogs: updated });
logStatePart.setState({ ...current, recentLogs: updated } as ILogState);
return {};
}
)
@@ -1232,14 +1232,14 @@ async function disconnectSocket() {
async function dispatchCombinedRefreshAction() {
const context = getActionContext();
if (!context.identity) return;
const currentView = uiStatePart.getState().activeView;
const currentView = uiStatePart.getState()!.activeView;
try {
// Always fetch basic stats for dashboard widgets
const combinedRequest = new plugins.domtools.plugins.typedrequest.TypedRequest<
interfaces.requests.IReq_GetCombinedMetrics
>('/typedrequest', 'getCombinedMetrics');
const combinedResponse = await combinedRequest.fire({
identity: context.identity,
sections: {
@@ -1252,12 +1252,13 @@ async function dispatchCombinedRefreshAction() {
});
// Update all stats from combined response
const currentStatsState = statsStatePart.getState()!;
statsStatePart.setState({
...statsStatePart.getState(),
serverStats: combinedResponse.metrics.server || statsStatePart.getState().serverStats,
emailStats: combinedResponse.metrics.email || statsStatePart.getState().emailStats,
dnsStats: combinedResponse.metrics.dns || statsStatePart.getState().dnsStats,
securityMetrics: combinedResponse.metrics.security || statsStatePart.getState().securityMetrics,
...currentStatsState,
serverStats: combinedResponse.metrics.server || currentStatsState.serverStats,
emailStats: combinedResponse.metrics.email || currentStatsState.emailStats,
dnsStats: combinedResponse.metrics.dns || currentStatsState.dnsStats,
securityMetrics: combinedResponse.metrics.security || currentStatsState.securityMetrics,
lastUpdated: Date.now(),
isLoading: false,
error: null,
@@ -1284,7 +1285,7 @@ async function dispatchCombinedRefreshAction() {
});
networkStatePart.setState({
...networkStatePart.getState(),
...networkStatePart.getState()!,
connections: connectionsResponse.connections,
connectionsByIP,
throughputRate: {
@@ -1297,14 +1298,15 @@ async function dispatchCombinedRefreshAction() {
throughputHistory: network.throughputHistory || [],
requestsPerSecond: network.requestsPerSecond || 0,
requestsTotal: network.requestsTotal || 0,
backends: network.backends || [],
lastUpdated: Date.now(),
isLoading: false,
error: null,
});
} catch (error) {
} catch (error: unknown) {
console.error('Failed to fetch connections:', error);
networkStatePart.setState({
...networkStatePart.getState(),
...networkStatePart.getState()!,
connections: [],
connectionsByIP,
throughputRate: {
@@ -1317,6 +1319,7 @@ async function dispatchCombinedRefreshAction() {
throughputHistory: network.throughputHistory || [],
requestsPerSecond: network.requestsPerSecond || 0,
requestsTotal: network.requestsTotal || 0,
backends: network.backends || [],
lastUpdated: Date.now(),
isLoading: false,
error: null,
@@ -1359,9 +1362,9 @@ let currentRefreshRate = 1000; // Track current refresh rate to avoid unnecessar
// Initialize auto-refresh when UI state is ready
(() => {
const startAutoRefresh = () => {
const uiState = uiStatePart.getState();
const loginState = loginStatePart.getState();
const uiState = uiStatePart.getState()!;
const loginState = loginStatePart.getState()!;
// Only start if conditions are met and not already running at the same rate
if (uiState.autoRefresh && loginState.isLoggedIn) {
// Check if we need to restart the interval (rate changed or not running)
@@ -1387,9 +1390,9 @@ let currentRefreshRate = 1000; // Track current refresh rate to avoid unnecessar
};
// Watch for relevant changes only
let previousAutoRefresh = uiStatePart.getState().autoRefresh;
let previousRefreshInterval = uiStatePart.getState().refreshInterval;
let previousIsLoggedIn = loginStatePart.getState().isLoggedIn;
let previousAutoRefresh = uiStatePart.getState()!.autoRefresh;
let previousRefreshInterval = uiStatePart.getState()!.refreshInterval;
let previousIsLoggedIn = loginStatePart.getState()!.isLoggedIn;
uiStatePart.state.subscribe((state) => {
// Only restart if relevant values changed
@@ -1420,7 +1423,7 @@ let currentRefreshRate = 1000; // Track current refresh rate to avoid unnecessar
startAutoRefresh();
// Connect TypedSocket if already logged in (e.g., persistent session)
if (loginStatePart.getState().isLoggedIn) {
if (loginStatePart.getState()!.isLoggedIn) {
connectSocket();
}
})();

View File

@@ -195,17 +195,18 @@ export class OpsDashboard extends DeesElement {
}
public async firstUpdated() {
const simpleLogin = this.shadowRoot.querySelector('dees-simple-login');
simpleLogin.addEventListener('login', (e: CustomEvent) => {
const simpleLogin = this.shadowRoot!.querySelector('dees-simple-login') as any;
simpleLogin.addEventListener('login', (e: Event) => {
// Handle logout event
this.login(e.detail.data.username, e.detail.data.password);
const detail = (e as CustomEvent).detail;
this.login(detail.data.username, detail.data.password);
});
// Handle view changes
const appDash = this.shadowRoot.querySelector('dees-simple-appdash');
const appDash = this.shadowRoot!.querySelector('dees-simple-appdash');
if (appDash) {
appDash.addEventListener('view-select', (e: CustomEvent) => {
const viewName = e.detail.view.name.toLowerCase();
appDash.addEventListener('view-select', (e: Event) => {
const viewName = (e as CustomEvent).detail.view.name.toLowerCase();
// Use router for navigation instead of direct state update
appRouter.navigateToView(viewName);
});
@@ -217,7 +218,7 @@ export class OpsDashboard extends DeesElement {
}
// Handle initial state - check if we have a stored session that's still valid
const loginState = appstate.loginStatePart.getState();
const loginState = appstate.loginStatePart.getState()!;
if (loginState.identity?.jwt) {
if (loginState.identity.expiresAt > Date.now()) {
// Client-side expiry looks valid — verify with server (keypair may have changed)
@@ -229,7 +230,7 @@ export class OpsDashboard extends DeesElement {
if (response.valid) {
// JWT confirmed valid by server
this.loginState = loginState;
await simpleLogin.switchToSlottedContent();
await (simpleLogin as any).switchToSlottedContent();
await appstate.statsStatePart.dispatchAction(appstate.fetchAllStatsAction, null);
await appstate.configStatePart.dispatchAction(appstate.fetchConfigurationAction, null);
} else {
@@ -250,8 +251,8 @@ export class OpsDashboard extends DeesElement {
private async login(username: string, password: string) {
const domtools = await this.domtoolsPromise;
console.log(`Attempting to login...`);
const simpleLogin = this.shadowRoot.querySelector('dees-simple-login');
const form = simpleLogin.shadowRoot.querySelector('dees-form');
const simpleLogin = this.shadowRoot!.querySelector('dees-simple-login') as any;
const form = simpleLogin.shadowRoot!.querySelector('dees-form') as any;
form.setStatus('pending', 'Logging in...');
const state = await appstate.loginStatePart.dispatchAction(appstate.loginAction, {
@@ -262,14 +263,14 @@ export class OpsDashboard extends DeesElement {
if (state.identity) {
console.log('Login successful');
this.loginState = state;
form.setStatus('success', 'Logged in!');
await simpleLogin.switchToSlottedContent();
form!.setStatus('success', 'Logged in!');
await simpleLogin!.switchToSlottedContent();
await appstate.statsStatePart.dispatchAction(appstate.fetchAllStatsAction, null);
await appstate.configStatePart.dispatchAction(appstate.fetchConfigurationAction, null);
} else {
form.setStatus('error', 'Login failed!');
form!.setStatus('error', 'Login failed!');
await domtools.convenience.smartdelay.delayFor(2000);
form.reset();
form!.reset();
}
}
}

View File

@@ -21,7 +21,7 @@ declare global {
@customElement('ops-view-certificates')
export class OpsViewCertificates extends DeesElement {
@state()
accessor certState: appstate.ICertificateState = appstate.certificateStatePart.getState();
accessor certState: appstate.ICertificateState = appstate.certificateStatePart.getState()!;
constructor() {
super();
@@ -264,10 +264,10 @@ export class OpsViewCertificates extends DeesElement {
{
name: 'Import',
iconName: 'lucide:upload',
action: async (modal) => {
action: async (modal: any) => {
const { DeesToast } = await import('@design.estate/dees-catalog');
try {
const form = modal.shadowRoot.querySelector('dees-form') as any;
const form = modal.shadowRoot!.querySelector('dees-form') as any;
const formData = await form.collectFormData();
const files = formData.certJsonFile;
if (!files || files.length === 0) {
@@ -287,8 +287,8 @@ export class OpsViewCertificates extends DeesElement {
);
DeesToast.show({ message: `Certificate imported for ${cert.domainName}`, type: 'success', duration: 3000 });
modal.destroy();
} catch (err) {
DeesToast.show({ message: `Import failed: ${err.message}`, type: 'error', duration: 4000 });
} catch (err: unknown) {
DeesToast.show({ message: `Import failed: ${(err as Error).message}`, type: 'error', duration: 4000 });
}
},
},
@@ -339,8 +339,8 @@ export class OpsViewCertificates extends DeesElement {
} else {
DeesToast.show({ message: response.message || 'Export failed', type: 'error', duration: 4000 });
}
} catch (err) {
DeesToast.show({ message: `Export failed: ${err.message}`, type: 'error', duration: 4000 });
} catch (err: unknown) {
DeesToast.show({ message: `Export failed: ${(err as Error).message}`, type: 'error', duration: 4000 });
}
},
},
@@ -363,7 +363,7 @@ export class OpsViewCertificates extends DeesElement {
{
name: 'Delete',
iconName: 'lucide:trash-2',
action: async (modal) => {
action: async (modal: any) => {
try {
await appstate.certificateStatePart.dispatchAction(
appstate.deleteCertificateAction,
@@ -371,8 +371,8 @@ export class OpsViewCertificates extends DeesElement {
);
DeesToast.show({ message: `Certificate deleted for ${cert.domain}`, type: 'success', duration: 3000 });
modal.destroy();
} catch (err) {
DeesToast.show({ message: `Delete failed: ${err.message}`, type: 'error', duration: 4000 });
} catch (err: unknown) {
DeesToast.show({ message: `Delete failed: ${(err as Error).message}`, type: 'error', duration: 4000 });
}
},
},

View File

@@ -102,7 +102,7 @@ export class OpsViewConfig extends DeesElement {
`;
}
private renderSystemSection(sys: appstate.IConfigState['config']['system']): TemplateResult {
private renderSystemSection(sys: NonNullable<appstate.IConfigState['config']>['system']): TemplateResult {
// Annotate proxy IPs with source hint when Remote Ingress is active
const ri = this.configState.config?.remoteIngress;
let proxyIpValues: string[] | null = sys.proxyIps.length > 0 ? [...sys.proxyIps] : null;
@@ -133,7 +133,7 @@ export class OpsViewConfig extends DeesElement {
`;
}
private renderSmartProxySection(proxy: appstate.IConfigState['config']['smartProxy']): TemplateResult {
private renderSmartProxySection(proxy: NonNullable<appstate.IConfigState['config']>['smartProxy']): TemplateResult {
const fields: IConfigField[] = [
{ key: 'Route Count', value: proxy.routeCount },
];
@@ -164,7 +164,7 @@ export class OpsViewConfig extends DeesElement {
`;
}
private renderEmailSection(email: appstate.IConfigState['config']['email']): TemplateResult {
private renderEmailSection(email: NonNullable<appstate.IConfigState['config']>['email']): TemplateResult {
const fields: IConfigField[] = [
{ key: 'Ports', value: email.ports.length > 0 ? email.ports.map(String) : null, type: 'pills' },
{ key: 'Hostname', value: email.hostname },
@@ -196,7 +196,7 @@ export class OpsViewConfig extends DeesElement {
`;
}
private renderDnsSection(dns: appstate.IConfigState['config']['dns']): TemplateResult {
private renderDnsSection(dns: NonNullable<appstate.IConfigState['config']>['dns']): TemplateResult {
const fields: IConfigField[] = [
{ key: 'Port', value: dns.port },
{ key: 'NS Domains', value: dns.nsDomains.length > 0 ? dns.nsDomains : null, type: 'pills' },
@@ -216,7 +216,7 @@ export class OpsViewConfig extends DeesElement {
`;
}
private renderTlsSection(tls: appstate.IConfigState['config']['tls']): TemplateResult {
private renderTlsSection(tls: NonNullable<appstate.IConfigState['config']>['tls']): TemplateResult {
const fields: IConfigField[] = [
{ key: 'Contact Email', value: tls.contactEmail },
{ key: 'Domain', value: tls.domain },
@@ -242,7 +242,7 @@ export class OpsViewConfig extends DeesElement {
`;
}
private renderCacheSection(cache: appstate.IConfigState['config']['cache']): TemplateResult {
private renderCacheSection(cache: NonNullable<appstate.IConfigState['config']>['cache']): TemplateResult {
const fields: IConfigField[] = [
{ key: 'Storage Path', value: cache.storagePath },
{ key: 'DB Name', value: cache.dbName },
@@ -267,7 +267,7 @@ export class OpsViewConfig extends DeesElement {
`;
}
private renderRadiusSection(radius: appstate.IConfigState['config']['radius']): TemplateResult {
private renderRadiusSection(radius: NonNullable<appstate.IConfigState['config']>['radius']): TemplateResult {
const fields: IConfigField[] = [
{ key: 'Auth Port', value: radius.authPort },
{ key: 'Accounting Port', value: radius.acctPort },
@@ -296,7 +296,7 @@ export class OpsViewConfig extends DeesElement {
`;
}
private renderRemoteIngressSection(ri: appstate.IConfigState['config']['remoteIngress']): TemplateResult {
private renderRemoteIngressSection(ri: NonNullable<appstate.IConfigState['config']>['remoteIngress']): TemplateResult {
const fields: IConfigField[] = [
{ key: 'Tunnel Port', value: ri.tunnelPort },
{ key: 'Hub Domain', value: ri.hubDomain },

View File

@@ -83,13 +83,13 @@ export class OpsViewEmails extends DeesElement {
private async handleEmailClick(e: CustomEvent<interfaces.requests.IEmail>) {
const emailSummary = e.detail;
try {
const context = appstate.loginStatePart.getState();
const context = appstate.loginStatePart.getState()!;
const request = new plugins.domtools.plugins.typedrequest.TypedRequest<
interfaces.requests.IReq_GetEmailDetail
>('/typedrequest', 'getEmailDetail');
const response = await request.fire({
identity: context.identity,
identity: context.identity!,
emailId: emailSummary.id,
});

View File

@@ -29,10 +29,10 @@ interface INetworkRequest {
@customElement('ops-view-network')
export class OpsViewNetwork extends DeesElement {
@state()
accessor statsState = appstate.statsStatePart.getState();
accessor statsState = appstate.statsStatePart.getState()!;
@state()
accessor networkState = appstate.networkStatePart.getState();
accessor networkState = appstate.networkStatePart.getState()!;
@state()

View File

@@ -21,7 +21,7 @@ declare global {
@customElement('ops-view-remoteingress')
export class OpsViewRemoteIngress extends DeesElement {
@state()
accessor riState: appstate.IRemoteIngressState = appstate.remoteIngressStatePart.getState();
accessor riState: appstate.IRemoteIngressState = appstate.remoteIngressStatePart.getState()!;
constructor() {
super();
@@ -184,7 +184,7 @@ export class OpsViewRemoteIngress extends DeesElement {
@click=${async () => {
const { DeesToast } = await import('@design.estate/dees-catalog');
try {
const response = await appstate.fetchConnectionToken(this.riState.newEdgeId);
const response = await appstate.fetchConnectionToken(this.riState.newEdgeId!);
if (response.success && response.token) {
if (navigator.clipboard && typeof navigator.clipboard.writeText === 'function') {
await navigator.clipboard.writeText(response.token);
@@ -202,8 +202,8 @@ export class OpsViewRemoteIngress extends DeesElement {
} else {
DeesToast.show({ message: response.message || 'Failed to get token', type: 'error', duration: 4000 });
}
} catch (err) {
DeesToast.show({ message: `Failed: ${err.message}`, type: 'error', duration: 4000 });
} catch (err: unknown) {
DeesToast.show({ message: `Failed: ${(err as Error).message}`, type: 'error', duration: 4000 });
}
}}
>Copy Connection Token</dees-button>
@@ -399,8 +399,8 @@ export class OpsViewRemoteIngress extends DeesElement {
} else {
DeesToast.show({ message: response.message || 'Failed to get token', type: 'error', duration: 4000 });
}
} catch (err) {
DeesToast.show({ message: `Failed: ${err.message}`, type: 'error', duration: 4000 });
} catch (err: unknown) {
DeesToast.show({ message: `Failed: ${(err as Error).message}`, type: 'error', duration: 4000 });
}
},
},

View File

@@ -237,7 +237,7 @@ export class OpsViewMyView extends DeesElement {
## License and Legal Information
This repository contains open-source code licensed under the MIT License. A copy of the license can be found in the [LICENSE](../LICENSE) file.
This repository contains open-source code licensed under the MIT License. A copy of the license can be found in the [license](../license) file.
**Please note:** The MIT License does not grant permission to use the trade names, trademarks, service marks, or product names of the project, except as required for reasonable and customary use in describing the origin of the work and reproducing the content of the NOTICE file.

View File

@@ -71,12 +71,12 @@ class AppRouter {
private updateViewState(view: string): void {
this.suppressStateUpdate = true;
const currentState = appstate.uiStatePart.getState();
const currentState = appstate.uiStatePart.getState()!;
if (currentState.activeView !== view) {
appstate.uiStatePart.setState({
...currentState,
activeView: view,
});
} as appstate.IUiState);
}
this.suppressStateUpdate = false;
}
@@ -94,7 +94,7 @@ class AppRouter {
}
public getCurrentView(): string {
return appstate.uiStatePart.getState().activeView;
return appstate.uiStatePart.getState()!.activeView;
}
public destroy(): void {