Compare commits
41 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 21157477b4 | |||
| fcf36e5cd5 | |||
| f5740fa565 | |||
| 4a9fba53a9 | |||
| da61adc9a2 | |||
| 616066ffd0 | |||
| bd5cccb405 | |||
| fbade85cda | |||
| 9060d26f3a | |||
| c889141ec3 | |||
| fb472f353c | |||
| 090bd747e1 | |||
| 4d77a94bbb | |||
| 7f5284b10f | |||
| 9cd5db2d81 | |||
| de0b7d1fe0 | |||
| 4e32745a8f | |||
| 121573de2f | |||
| cd957526e2 | |||
| 7aa5f07731 | |||
| 5b6f7b30c3 | |||
| 18cc21a49e | |||
| 46fa2f6ade | |||
| 0a6315f177 | |||
| 841f99e19d | |||
| 8e9de46cd2 | |||
| 2d44528345 | |||
| 28a38252da | |||
| dfb268bbfc | |||
| 6532c7ff22 | |||
| d2c63cf170 | |||
| 09d66e4528 | |||
| 3078fa9d7b | |||
| 57fbb128e6 | |||
| d73266eeb8 | |||
| 2dbdf2d2b1 | |||
| 383e0adc23 | |||
| d7789f5a44 | |||
| 2638990667 | |||
| c33ecdc26f | |||
| b033d80927 |
152
changelog.md
152
changelog.md
@@ -1,5 +1,157 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## 2026-02-16 - 6.4.4 - fix(deps)
|
||||||
|
bump @push.rocks/smartproxy to ^25.7.3
|
||||||
|
|
||||||
|
- Updated @push.rocks/smartproxy from ^25.7.2 to ^25.7.3 in package.json
|
||||||
|
|
||||||
|
## 2026-02-16 - 6.4.3 - fix(deps)
|
||||||
|
bump @push.rocks/smartproxy to ^25.7.2
|
||||||
|
|
||||||
|
- Updated package.json: @push.rocks/smartproxy ^25.7.1 -> ^25.7.2 (patch dependency update)
|
||||||
|
|
||||||
|
## 2026-02-16 - 6.4.2 - fix(smartproxy)
|
||||||
|
bump @push.rocks/smartproxy to ^25.7.1
|
||||||
|
|
||||||
|
- Updated dependency @push.rocks/smartproxy from ^25.7.0 to ^25.7.1 in package.json
|
||||||
|
- No other source changes; dependency patch bump only
|
||||||
|
|
||||||
|
## 2026-02-16 - 6.4.1 - fix(deps)
|
||||||
|
bump dependencies: @push.rocks/smartproxy to ^25.7.0 and @serve.zone/remoteingress to ^3.0.2
|
||||||
|
|
||||||
|
- Bumped @push.rocks/smartproxy from ^25.5.0 to ^25.7.0
|
||||||
|
- Bumped @serve.zone/remoteingress from ^3.0.1 to ^3.0.2
|
||||||
|
- Package current version is 6.4.0 — recommended patch release
|
||||||
|
|
||||||
|
## 2026-02-16 - 6.4.0 - feat(remoteingress)
|
||||||
|
add Remote Ingress hub and management for edge tunnel nodes, including backend managers, tunnel hub integration, opsserver handlers, typedrequest APIs, and web UI
|
||||||
|
|
||||||
|
- Introduce RemoteIngressManager for CRUD and persistent storage of edge registrations
|
||||||
|
- Introduce TunnelManager to run the RemoteIngressHub, track connected edge statuses, and sync allowed edges to the hub
|
||||||
|
- Integrate remote ingress into DcRouter (options.remoteIngressConfig, setupRemoteIngress, startup/shutdown handling, and startup summary)
|
||||||
|
- Add OpsServer RemoteIngressHandler exposing typedrequest APIs (create/update/delete/regenerate/get/status)
|
||||||
|
- Add web UI: Remote Ingress view, app state parts, actions and components to manage edges and display runtime statuses
|
||||||
|
- Add typedrequest and data interfaces for remoteingress and export the remoteingress module; add @serve.zone/remoteingress dependency in package.json
|
||||||
|
|
||||||
|
## 2026-02-16 - 6.3.0 - feat(dcrouter)
|
||||||
|
add configurable baseDir and centralized path resolution; use resolved data paths for storage, cache and DNS
|
||||||
|
|
||||||
|
- Introduce IDcRouterOptions.baseDir to allow configuring base directory for dcrouter data (defaults to ~/.serve.zone/dcrouter).
|
||||||
|
- Add DcRouter.resolvedPaths and resolvePaths(baseDir) in ts/paths.ts to centralize computation of dcrouterHomeDir, dataDir, defaultTsmDbPath, defaultStoragePath and dnsRecordsDir.
|
||||||
|
- Use resolvedPaths throughout DcRouter: default filesystem storage fsPath, CacheDb storagePath, and DNS records loading now reference resolved paths.
|
||||||
|
- Replace ensureDirectories() behavior with ensureDataDirectories(resolvedPaths) to only create data-related directories; keep legacy ensureDirectories wrapper delegating to the new function.
|
||||||
|
- Simplify paths module by removing unused legacy path constants and adding a focused API for path resolution and directory creation.
|
||||||
|
- Remove an unused import (paths) in contentscanner, cleaning up imports.
|
||||||
|
|
||||||
|
## 2026-02-16 - 6.2.4 - fix(deps)
|
||||||
|
bump @push.rocks/smartproxy to ^25.5.0
|
||||||
|
|
||||||
|
- Updated @push.rocks/smartproxy from ^25.4.0 to ^25.5.0 in package.json
|
||||||
|
|
||||||
|
## 2026-02-16 - 6.2.3 - fix(dcrouter)
|
||||||
|
persist proxy certificate validity dates and improve certificate status initialization
|
||||||
|
|
||||||
|
- Bump @push.rocks/smartacme dependency from ^9.0.0 to ^9.1.3
|
||||||
|
- Store validFrom and validUntil alongside proxy cert entries (/proxy-certs) when saving, extracting values by parsing PEM where possible
|
||||||
|
- Use stored cert entries (domain, publicKey, validUntil, validFrom) to populate certificateStatusMap at startup
|
||||||
|
- Fallback to SmartAcme /certs/ metadata and finally to parsing X.509 from stored PEM to determine expiry/issuedAt when initializing status
|
||||||
|
- Update opsserver certificate handler to parse publicKey PEM from cert-store and set expiry/issuedAt and issuer accordingly
|
||||||
|
- Adjust variable names and logging to reflect stored cert entry usage
|
||||||
|
|
||||||
|
## 2026-02-16 - 6.2.2 - fix(certs)
|
||||||
|
Populate certificate status for cert-store-loaded certificates after SmartProxy startup and check proxy-certs in opsserver certificate handler
|
||||||
|
|
||||||
|
- Track domains loaded from storageManager '/proxy-certs/' and populate certificateStatusMap with status, routeNames, expiryDate and issuedAt (when available) after SmartProxy starts
|
||||||
|
- Opsserver certificate handler now falls back to '/proxy-certs/{domain}' if '/certs/{cleanDomain}' is missing and marks cert-store-only entries as valid with issuer 'cert-store'
|
||||||
|
- Bump @push.rocks/smartproxy dependency from ^25.3.1 to ^25.4.0
|
||||||
|
|
||||||
|
## 2026-02-16 - 6.2.1 - fix(smartacme,storage)
|
||||||
|
Respect wildcard domain requests when retrieving certificates and treat empty/whitespace storage values as null in getJSON
|
||||||
|
|
||||||
|
- Pass includeWildcard flag to smartAcme.getCertificateForDomain to avoid incorrectly including/excluding wildcard certificates based on whether the requested domain itself is a wildcard
|
||||||
|
- Detect wildcard domains via domain.startsWith('*.') and set includeWildcard to false for wildcard requests
|
||||||
|
- Treat empty or whitespace-only stored values as null in StorageManager.getJSON to avoid parsing empty strings as JSON and potential errors
|
||||||
|
|
||||||
|
## 2026-02-16 - 6.2.0 - feat(ts_web)
|
||||||
|
add Certificate Management documentation and ops-view-certificates reference
|
||||||
|
|
||||||
|
- Adds a new 'Certificate Management' section to ts_web/readme.md describing domain-centric overview, certificate sources (ACME/provision/static), expiry monitoring, per-domain backoff, and one-click reprovisioning
|
||||||
|
- Adds ops-view-certificates.ts entry to the ops UI file list
|
||||||
|
- Documents new route mapping '/certificates' in the readme navigation
|
||||||
|
|
||||||
|
## 2026-02-16 - 6.1.0 - feat(certs)
|
||||||
|
integrate smartacme v9 for ACME certificate provisioning and add certificate management features, docs, dashboard views, API endpoints, and per-domain backoff scheduler
|
||||||
|
|
||||||
|
- Bump dependency: @push.rocks/smartacme -> ^9.0.0
|
||||||
|
- Add Certificate Management documentation, examples, and a new Certificates view in the OpsServer dashboard (status, source, expiry, backoff, one‑click reprovision)
|
||||||
|
- Integrate smartacme v9 features: per-domain deduplication, global concurrency control, account rate limiting, structured errors, and clean shutdown behavior
|
||||||
|
- Introduce per-domain exponential backoff persisted via StorageManager (CertProvisionScheduler) and remove the older serial stagger queue (smartacme v9 handles concurrency/deduping)
|
||||||
|
- Expose new typedrequest API methods: getCertificateOverview, reprovisionCertificate (legacy), reprovisionCertificateDomain (preferred)
|
||||||
|
- DcRouter now surfaces smartAcme, certProvisionScheduler, and certificateStatusMap; cert provisioning paths call smartAcme directly and clear backoff on success
|
||||||
|
- Docs updated to note parallel shutdown/cleanup of HTTP agents and DNS clients
|
||||||
|
|
||||||
|
## 2026-02-15 - 6.0.0 - BREAKING CHANGE(certs)
|
||||||
|
Introduce domain-centric certificate provisioning with per-domain exponential backoff and a staggered serial scheduler; add domain-based reprovision API and UI backoff display; change certificate overview API to be domain-first and include backoff info; bump related deps.
|
||||||
|
|
||||||
|
- Add CertProvisionScheduler: persistent per-domain exponential backoff, retry calculation, and an in-memory serial stagger queue.
|
||||||
|
- Integrate scheduler with SmartAcme certProvisionFunction: enqueue provisions, clear backoff on success, record failures to drive backoff.
|
||||||
|
- Switch certificate event tracking to be keyed by domain (certificateStatusMap now keyed by domain) and add findRouteNamesForDomain helper.
|
||||||
|
- BREAKING: ICertificateInfo shape changed — replaced routeName/domains with domain and routeNames; added optional backoffInfo (failures, retryAfter, lastError).
|
||||||
|
- Add domain-based reprovision endpoint (reprovisionCertificateDomain) while retaining legacy route-based reprovision for backward compatibility (internal rename to reprovisionCertificateByRoute).
|
||||||
|
- Web UI updated to domain-centric certificate overview, displays route pills, backoff indicator and retry timing, and uses domain-based reprovision action.
|
||||||
|
- Dependency bumps: @push.rocks/smartlog -> ^3.1.11, @push.rocks/smartproxy -> ^25.3.1.
|
||||||
|
|
||||||
|
## 2026-02-14 - 5.5.0 - feat(certs)
|
||||||
|
persist ACME certificates in StorageManager, add storage-backed cert manager, default storage to filesystem, and improve certificate status reporting
|
||||||
|
|
||||||
|
- Add StorageBackedCertManager to persist SmartAcme certificates under /certs/ via StorageManager
|
||||||
|
- Default storage to filesystem path (dcrouterHomeDir/storage) when options.storage is not provided
|
||||||
|
- Wire SmartAcme to use StorageBackedCertManager and provide SmartProxy certStore handlers that load/save/remove certs under /proxy-certs/
|
||||||
|
- Ops server certificate handler reads persisted cert data to report expiry/issued dates and treats acme/provision-function routes with no cert data as provisioning
|
||||||
|
- Bump @push.rocks/smartproxy dependency to ^25.3.0
|
||||||
|
|
||||||
|
## 2026-02-14 - 5.4.6 - fix(deps)
|
||||||
|
bump @push.rocks/smartproxy dependency to ^25.2.2
|
||||||
|
|
||||||
|
- Updated dependency @push.rocks/smartproxy: ^25.2.0 → ^25.2.2
|
||||||
|
- Change is a dependency-only patch update, no source code modifications
|
||||||
|
- Current package version is 5.4.5; recommend a patch release
|
||||||
|
|
||||||
|
## 2026-02-14 - 5.4.5 - fix(dcrouter)
|
||||||
|
bump patch for release pipeline consistency - no code changes
|
||||||
|
|
||||||
|
- current version: 5.4.4 (from package.json)
|
||||||
|
- git diff: no changes detected
|
||||||
|
- recommend patch bump to trigger release artifacts if required
|
||||||
|
|
||||||
|
## 2026-02-14 - 5.4.4 - fix(deps)
|
||||||
|
bump @push.rocks/smartproxy to ^25.2.0
|
||||||
|
|
||||||
|
- Updated @push.rocks/smartproxy from ^25.1.0 to ^25.2.0 (patch, non-breaking).
|
||||||
|
- Current package version is 5.4.3; recommend a patch release to 5.4.4.
|
||||||
|
|
||||||
|
## 2026-02-14 - 5.4.3 - fix(dependencies)
|
||||||
|
bump @push.rocks/smartproxy to ^25.1.0
|
||||||
|
|
||||||
|
- Updated @push.rocks/smartproxy from ^25.0.0 to ^25.1.0 in package.json
|
||||||
|
|
||||||
|
## 2026-02-13 - 5.4.2 - fix(dcrouter)
|
||||||
|
improve domain pattern matching to support routing-glob and wildcard patterns and use matching logic when resolving routes
|
||||||
|
|
||||||
|
- Support routing-glob patterns beginning with '*' (e.g. *example.com) to match base domain, wildcard form, and subdomains
|
||||||
|
- Treat standard wildcard patterns ('*.example.com') as matching both the base domain (example.com) and its subdomains
|
||||||
|
- Use isDomainMatch when resolving routes instead of exact array includes to allow pattern matching
|
||||||
|
- Normalize domain and pattern to lowercase and simplify equality checks
|
||||||
|
|
||||||
|
## 2026-02-13 - 5.4.1 - fix(network,dcrouter)
|
||||||
|
Always register SmartProxy certificate event handlers and include total bytes + improved connection metrics in network stats/UI
|
||||||
|
|
||||||
|
- Always register SmartProxy 'certificate-issued', 'certificate-renewed', and 'certificate-failed' handlers (previously only registered when acmeConfig was present) so certificate events are processed regardless of provisioning path.
|
||||||
|
- Add totalBytes (in/out) to network stats and propagate it through ts_interfaces and app state so total data transferred is available to the UI.
|
||||||
|
- Combine metricsManager.getNetworkStats with collectServerStats to compute activeConnections and adjust connectionDetails/TopEndpoints handling.
|
||||||
|
- Update ops UI to display totalBytes in throughput cards and remove a redundant network-specific auto-refresh fetch.
|
||||||
|
- Type and state updates: ts_interfaces/data/stats.ts and ts_web/appstate.ts updated with totalBytes and initialization/default mapping adjusted.
|
||||||
|
|
||||||
## 2026-02-13 - 5.4.0 - feat(certificates)
|
## 2026-02-13 - 5.4.0 - feat(certificates)
|
||||||
include certificate source/issuer and Rust-side status checks; pass eventComms into certProvisionFunction and record expiry information
|
include certificate source/issuer and Rust-side status checks; pass eventComms into certProvisionFunction and record expiry information
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "@serve.zone/dcrouter",
|
"name": "@serve.zone/dcrouter",
|
||||||
"private": false,
|
"private": false,
|
||||||
"version": "5.4.0",
|
"version": "6.4.4",
|
||||||
"description": "A multifaceted routing service handling mail and SMS delivery functions.",
|
"description": "A multifaceted routing service handling mail and SMS delivery functions.",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"exports": {
|
"exports": {
|
||||||
@@ -36,26 +36,27 @@
|
|||||||
"@design.estate/dees-element": "^2.1.6",
|
"@design.estate/dees-element": "^2.1.6",
|
||||||
"@push.rocks/projectinfo": "^5.0.2",
|
"@push.rocks/projectinfo": "^5.0.2",
|
||||||
"@push.rocks/qenv": "^6.1.3",
|
"@push.rocks/qenv": "^6.1.3",
|
||||||
"@push.rocks/smartacme": "^8.0.0",
|
"@push.rocks/smartacme": "^9.1.3",
|
||||||
"@push.rocks/smartdata": "^7.0.15",
|
"@push.rocks/smartdata": "^7.0.15",
|
||||||
"@push.rocks/smartdns": "^7.8.1",
|
"@push.rocks/smartdns": "^7.8.1",
|
||||||
"@push.rocks/smartfile": "^13.1.2",
|
"@push.rocks/smartfile": "^13.1.2",
|
||||||
"@push.rocks/smartguard": "^3.1.0",
|
"@push.rocks/smartguard": "^3.1.0",
|
||||||
"@push.rocks/smartjwt": "^2.2.1",
|
"@push.rocks/smartjwt": "^2.2.1",
|
||||||
"@push.rocks/smartlog": "^3.1.10",
|
"@push.rocks/smartlog": "^3.1.11",
|
||||||
"@push.rocks/smartmetrics": "^2.0.10",
|
"@push.rocks/smartmetrics": "^2.0.10",
|
||||||
"@push.rocks/smartmongo": "^5.1.0",
|
"@push.rocks/smartmongo": "^5.1.0",
|
||||||
"@push.rocks/smartmta": "^5.2.2",
|
"@push.rocks/smartmta": "^5.2.2",
|
||||||
"@push.rocks/smartnetwork": "^4.4.0",
|
"@push.rocks/smartnetwork": "^4.4.0",
|
||||||
"@push.rocks/smartpath": "^6.0.0",
|
"@push.rocks/smartpath": "^6.0.0",
|
||||||
"@push.rocks/smartpromise": "^4.2.3",
|
"@push.rocks/smartpromise": "^4.2.3",
|
||||||
"@push.rocks/smartproxy": "^25.0.0",
|
"@push.rocks/smartproxy": "^25.7.3",
|
||||||
"@push.rocks/smartradius": "^1.1.1",
|
"@push.rocks/smartradius": "^1.1.1",
|
||||||
"@push.rocks/smartrequest": "^5.0.1",
|
"@push.rocks/smartrequest": "^5.0.1",
|
||||||
"@push.rocks/smartrx": "^3.0.10",
|
"@push.rocks/smartrx": "^3.0.10",
|
||||||
"@push.rocks/smartstate": "^2.0.30",
|
"@push.rocks/smartstate": "^2.0.30",
|
||||||
"@push.rocks/smartunique": "^3.0.9",
|
"@push.rocks/smartunique": "^3.0.9",
|
||||||
"@serve.zone/interfaces": "^5.3.0",
|
"@serve.zone/interfaces": "^5.3.0",
|
||||||
|
"@serve.zone/remoteingress": "^3.0.2",
|
||||||
"@tsclass/tsclass": "^9.3.0",
|
"@tsclass/tsclass": "^9.3.0",
|
||||||
"lru-cache": "^11.2.6",
|
"lru-cache": "^11.2.6",
|
||||||
"uuid": "^13.0.0"
|
"uuid": "^13.0.0"
|
||||||
|
|||||||
323
pnpm-lock.yaml
generated
323
pnpm-lock.yaml
generated
@@ -36,8 +36,8 @@ importers:
|
|||||||
specifier: ^6.1.3
|
specifier: ^6.1.3
|
||||||
version: 6.1.3
|
version: 6.1.3
|
||||||
'@push.rocks/smartacme':
|
'@push.rocks/smartacme':
|
||||||
specifier: ^8.0.0
|
specifier: ^9.1.3
|
||||||
version: 8.0.0(@push.rocks/smartserve@2.0.1)(socks@2.8.7)
|
version: 9.1.3(socks@2.8.7)
|
||||||
'@push.rocks/smartdata':
|
'@push.rocks/smartdata':
|
||||||
specifier: ^7.0.15
|
specifier: ^7.0.15
|
||||||
version: 7.0.15(socks@2.8.7)
|
version: 7.0.15(socks@2.8.7)
|
||||||
@@ -54,8 +54,8 @@ importers:
|
|||||||
specifier: ^2.2.1
|
specifier: ^2.2.1
|
||||||
version: 2.2.1
|
version: 2.2.1
|
||||||
'@push.rocks/smartlog':
|
'@push.rocks/smartlog':
|
||||||
specifier: ^3.1.10
|
specifier: ^3.1.11
|
||||||
version: 3.1.10
|
version: 3.1.11
|
||||||
'@push.rocks/smartmetrics':
|
'@push.rocks/smartmetrics':
|
||||||
specifier: ^2.0.10
|
specifier: ^2.0.10
|
||||||
version: 2.0.10
|
version: 2.0.10
|
||||||
@@ -75,8 +75,8 @@ importers:
|
|||||||
specifier: ^4.2.3
|
specifier: ^4.2.3
|
||||||
version: 4.2.3
|
version: 4.2.3
|
||||||
'@push.rocks/smartproxy':
|
'@push.rocks/smartproxy':
|
||||||
specifier: ^25.0.0
|
specifier: ^25.7.3
|
||||||
version: 25.0.0(@push.rocks/smartserve@2.0.1)(socks@2.8.7)
|
version: 25.7.3
|
||||||
'@push.rocks/smartradius':
|
'@push.rocks/smartradius':
|
||||||
specifier: ^1.1.1
|
specifier: ^1.1.1
|
||||||
version: 1.1.1
|
version: 1.1.1
|
||||||
@@ -95,6 +95,9 @@ importers:
|
|||||||
'@serve.zone/interfaces':
|
'@serve.zone/interfaces':
|
||||||
specifier: ^5.3.0
|
specifier: ^5.3.0
|
||||||
version: 5.3.0
|
version: 5.3.0
|
||||||
|
'@serve.zone/remoteingress':
|
||||||
|
specifier: ^3.0.2
|
||||||
|
version: 3.0.2
|
||||||
'@tsclass/tsclass':
|
'@tsclass/tsclass':
|
||||||
specifier: ^9.3.0
|
specifier: ^9.3.0
|
||||||
version: 9.3.0
|
version: 9.3.0
|
||||||
@@ -116,7 +119,7 @@ importers:
|
|||||||
version: 2.0.1
|
version: 2.0.1
|
||||||
'@git.zone/tstest':
|
'@git.zone/tstest':
|
||||||
specifier: ^3.1.8
|
specifier: ^3.1.8
|
||||||
version: 3.1.8(socks@2.8.7)(typescript@5.9.3)
|
version: 3.1.8(@push.rocks/smartserve@2.0.1)(socks@2.8.7)(typescript@5.9.3)
|
||||||
'@git.zone/tswatch':
|
'@git.zone/tswatch':
|
||||||
specifier: ^3.1.0
|
specifier: ^3.1.0
|
||||||
version: 3.1.0(@tiptap/pm@2.27.2)
|
version: 3.1.0(@tiptap/pm@2.27.2)
|
||||||
@@ -154,9 +157,6 @@ packages:
|
|||||||
peerDependencies:
|
peerDependencies:
|
||||||
'@push.rocks/smartserve': '>=1.1.0'
|
'@push.rocks/smartserve': '>=1.1.0'
|
||||||
|
|
||||||
'@apiclient.xyz/cloudflare@6.4.3':
|
|
||||||
resolution: {integrity: sha512-ztegUdUO3Zd4mUoTSylKlCEKPBMHEcggrLelR+7CiblM4beHMwopMVlryBmiCY7bOVbUSPoK0xsVTF7VIy3p/A==}
|
|
||||||
|
|
||||||
'@apiclient.xyz/cloudflare@7.1.0':
|
'@apiclient.xyz/cloudflare@7.1.0':
|
||||||
resolution: {integrity: sha512-qb+PWcE5OjOCPO0+4rexOgtEf9Q1VOIHfrGmav/gXAtkdNL5omifSxPbUseyFKsZrxnRv4rLzvjckUCj0hkvFw==}
|
resolution: {integrity: sha512-qb+PWcE5OjOCPO0+4rexOgtEf9Q1VOIHfrGmav/gXAtkdNL5omifSxPbUseyFKsZrxnRv4rLzvjckUCj0hkvFw==}
|
||||||
|
|
||||||
@@ -651,9 +651,6 @@ packages:
|
|||||||
resolution: {integrity: sha512-AokJm4tuBHillT+FpMtxQ60n8ObyXBatq7jD2/JA9dxbDDokKQm8KMht5ibGzLVU9IJDIKK4TPKgMHEYMn3lMg==}
|
resolution: {integrity: sha512-AokJm4tuBHillT+FpMtxQ60n8ObyXBatq7jD2/JA9dxbDDokKQm8KMht5ibGzLVU9IJDIKK4TPKgMHEYMn3lMg==}
|
||||||
engines: {node: '>=18'}
|
engines: {node: '>=18'}
|
||||||
|
|
||||||
'@leichtgewicht/ip-codec@2.0.5':
|
|
||||||
resolution: {integrity: sha512-Vo+PSpZG2/fmgmiNzYK9qWRh8h/CHrwD0mo1h1DzL4yzHNSfWYujGTYsWGreD000gcgmZ7K4Ys6Tx9TxtsKdDw==}
|
|
||||||
|
|
||||||
'@lit-labs/ssr-dom-shim@1.5.1':
|
'@lit-labs/ssr-dom-shim@1.5.1':
|
||||||
resolution: {integrity: sha512-Aou5UdlSpr5whQe8AA/bZG0jMj96CoJIWbGfZ91qieWu5AWUMKw8VR/pAkQkJYvBNhmCcWnZlyyk5oze8JIqYA==}
|
resolution: {integrity: sha512-Aou5UdlSpr5whQe8AA/bZG0jMj96CoJIWbGfZ91qieWu5AWUMKw8VR/pAkQkJYvBNhmCcWnZlyyk5oze8JIqYA==}
|
||||||
|
|
||||||
@@ -858,8 +855,8 @@ packages:
|
|||||||
'@push.rocks/qenv@6.1.3':
|
'@push.rocks/qenv@6.1.3':
|
||||||
resolution: {integrity: sha512-+z2hsAU/7CIgpYLFqvda8cn9rUBMHqLdQLjsFfRn5jPoD7dJ5rFlpkbhfM4Ws8mHMniwWaxGKo+q/YBhtzRBLg==}
|
resolution: {integrity: sha512-+z2hsAU/7CIgpYLFqvda8cn9rUBMHqLdQLjsFfRn5jPoD7dJ5rFlpkbhfM4Ws8mHMniwWaxGKo+q/YBhtzRBLg==}
|
||||||
|
|
||||||
'@push.rocks/smartacme@8.0.0':
|
'@push.rocks/smartacme@9.1.3':
|
||||||
resolution: {integrity: sha512-Oq+m+LX4IG0p4qCGZLEwa6UlMo5Hfq7paRjpREwQNsaGSKl23xsjsEJLxjxkePwaXnaIkHEwU/5MtrEkg2uKEQ==}
|
resolution: {integrity: sha512-rxb4zGZQvcR7l8cb8SvLy+zkCgXKg8rO7b12zaE9ZBe5Q+khoInxscC0eKjmNZ7BOUFFDOxDKoQhgeqwHGOqZQ==}
|
||||||
|
|
||||||
'@push.rocks/smartarchive@4.2.4':
|
'@push.rocks/smartarchive@4.2.4':
|
||||||
resolution: {integrity: sha512-uiqVAXPxmr8G5rv3uZvZFMOCt8l7cZC3nzvsy4YQqKf/VkPhKIEX+b7LkAeNlxPSYUiBQUkNRoawg9+5BaMcHg==}
|
resolution: {integrity: sha512-uiqVAXPxmr8G5rv3uZvZFMOCt8l7cZC3nzvsy4YQqKf/VkPhKIEX+b7LkAeNlxPSYUiBQUkNRoawg9+5BaMcHg==}
|
||||||
@@ -901,9 +898,6 @@ packages:
|
|||||||
'@push.rocks/smartdelay@3.0.5':
|
'@push.rocks/smartdelay@3.0.5':
|
||||||
resolution: {integrity: sha512-mUuI7kj2f7ztjpic96FvRIlf2RsKBa5arw81AHNsndbxO6asRcxuWL8dTVxouEIK8YsBUlj0AsrCkHhMbLQdHw==}
|
resolution: {integrity: sha512-mUuI7kj2f7ztjpic96FvRIlf2RsKBa5arw81AHNsndbxO6asRcxuWL8dTVxouEIK8YsBUlj0AsrCkHhMbLQdHw==}
|
||||||
|
|
||||||
'@push.rocks/smartdns@6.2.2':
|
|
||||||
resolution: {integrity: sha512-MhJcHujbyIuwIIFdnXb2OScGtRjNsliLUS8GoAurFsKtcCOaA0ytfP+PNzkukyBufjb1nMiJF3rjhswXdHakAQ==}
|
|
||||||
|
|
||||||
'@push.rocks/smartdns@7.8.1':
|
'@push.rocks/smartdns@7.8.1':
|
||||||
resolution: {integrity: sha512-qEizM9dFzhq4XGICDC8Im7JLjwdokHdDZ6wLufBInaEOupq+8XOa9bC6EGlBQVsCXFUyrKzsFk6eBa9BSZMKPw==}
|
resolution: {integrity: sha512-qEizM9dFzhq4XGICDC8Im7JLjwdokHdDZ6wLufBInaEOupq+8XOa9bC6EGlBQVsCXFUyrKzsFk6eBa9BSZMKPw==}
|
||||||
|
|
||||||
@@ -970,8 +964,8 @@ packages:
|
|||||||
'@push.rocks/smartlog-interfaces@3.0.2':
|
'@push.rocks/smartlog-interfaces@3.0.2':
|
||||||
resolution: {integrity: sha512-8hGRTJehbsFSJxLhCQkA018mZtXVPxPTblbg9VaE/EqISRzUw+eosJ2EJV7M4Qu0eiTJZjnWnNLn8CkD77ziWw==}
|
resolution: {integrity: sha512-8hGRTJehbsFSJxLhCQkA018mZtXVPxPTblbg9VaE/EqISRzUw+eosJ2EJV7M4Qu0eiTJZjnWnNLn8CkD77ziWw==}
|
||||||
|
|
||||||
'@push.rocks/smartlog@3.1.10':
|
'@push.rocks/smartlog@3.1.11':
|
||||||
resolution: {integrity: sha512-5pf5JyzOE2WTCUislNIW4EHePo1a7hiXB+jbil38+N5hW71AEwcPFe6oGxbp5w9ALlz66hV2+E+25R0SsxN+fQ==}
|
resolution: {integrity: sha512-zyLH8pQD2UD7l76wJBESEWXU1FSTBLOuRI0/DN139EYyMkwMq1+pdQKptTkJhhVL/OIj56oMg9SpJb4bJB7uKg==}
|
||||||
|
|
||||||
'@push.rocks/smartmail@2.2.0':
|
'@push.rocks/smartmail@2.2.0':
|
||||||
resolution: {integrity: sha512-28K4HAcda7ODUUpFCgbS/uA+eqwVRcmLJERIdM9AvLHXaHAPLHH97HmwPPcAu9Sp3z05Um0inmDF51X6yVVkcw==}
|
resolution: {integrity: sha512-28K4HAcda7ODUUpFCgbS/uA+eqwVRcmLJERIdM9AvLHXaHAPLHH97HmwPPcAu9Sp3z05Um0inmDF51X6yVVkcw==}
|
||||||
@@ -1040,8 +1034,8 @@ packages:
|
|||||||
'@push.rocks/smartpromise@4.2.3':
|
'@push.rocks/smartpromise@4.2.3':
|
||||||
resolution: {integrity: sha512-Ycg/TJR+tMt+S3wSFurOpEoW6nXv12QBtKXgBcjMZ4RsdO28geN46U09osPn9N9WuwQy1PkmTV5J/V4F9U8qEw==}
|
resolution: {integrity: sha512-Ycg/TJR+tMt+S3wSFurOpEoW6nXv12QBtKXgBcjMZ4RsdO28geN46U09osPn9N9WuwQy1PkmTV5J/V4F9U8qEw==}
|
||||||
|
|
||||||
'@push.rocks/smartproxy@25.0.0':
|
'@push.rocks/smartproxy@25.7.3':
|
||||||
resolution: {integrity: sha512-FuXIyKAlTdUUSFszzYjP/WAMb3Dq//gBdluADvjgAeQn1YplFonMo/afRU+qSI7WsPsB7X7vkFwLba5ASYdiUg==}
|
resolution: {integrity: sha512-9b5dwsLAhuDqnJptGBum4qBHlZwZPqPG3CJKxAwE3uFKjCmcE8qGDwodI0CjrQ7KW2PJ1BMq/Lk4ghs3Da6PWw==}
|
||||||
|
|
||||||
'@push.rocks/smartpuppeteer@2.0.5':
|
'@push.rocks/smartpuppeteer@2.0.5':
|
||||||
resolution: {integrity: sha512-yK/qSeWVHIGWRp3c8S5tfdGP6WCKllZC4DR8d8CQlEjszOSBmHtlTdyyqOMBZ/BA4kd+eU5f3A1r4K2tGYty1g==}
|
resolution: {integrity: sha512-yK/qSeWVHIGWRp3c8S5tfdGP6WCKllZC4DR8d8CQlEjszOSBmHtlTdyyqOMBZ/BA4kd+eU5f3A1r4K2tGYty1g==}
|
||||||
@@ -1100,6 +1094,9 @@ packages:
|
|||||||
'@push.rocks/smarttime@4.1.1':
|
'@push.rocks/smarttime@4.1.1':
|
||||||
resolution: {integrity: sha512-Ha/3J/G+zfTl4ahpZgF6oUOZnUjpLhrBja0OQ2cloFxF9sKT8I1COaSqIfBGDtoK2Nly4UD4aTJ3JcJNOg/kgA==}
|
resolution: {integrity: sha512-Ha/3J/G+zfTl4ahpZgF6oUOZnUjpLhrBja0OQ2cloFxF9sKT8I1COaSqIfBGDtoK2Nly4UD4aTJ3JcJNOg/kgA==}
|
||||||
|
|
||||||
|
'@push.rocks/smarttime@4.2.3':
|
||||||
|
resolution: {integrity: sha512-8gMg8RUkrCG4p9NcEUZV7V6KpL24+jAMK02g7qyhfA6giz/JJWD0+8w8xjSR+G7qe16KVQ2y3RbvAL9TxmO36g==}
|
||||||
|
|
||||||
'@push.rocks/smartunique@3.0.9':
|
'@push.rocks/smartunique@3.0.9':
|
||||||
resolution: {integrity: sha512-q6DYQgT7/dqdWi9HusvtWCjdsFzLFXY9LTtaZV6IYNJt6teZOonoygxTdNt9XLn6niBSbLYrHSKvJNTRH/uK+g==}
|
resolution: {integrity: sha512-q6DYQgT7/dqdWi9HusvtWCjdsFzLFXY9LTtaZV6IYNJt6teZOonoygxTdNt9XLn6niBSbLYrHSKvJNTRH/uK+g==}
|
||||||
|
|
||||||
@@ -1128,9 +1125,15 @@ packages:
|
|||||||
'@push.rocks/taskbuffer@4.2.0':
|
'@push.rocks/taskbuffer@4.2.0':
|
||||||
resolution: {integrity: sha512-ttoBe5y/WXkAo5/wSMcC/Y4Zbyw4XG8kwAsEaqnAPCxa3M9MI1oV/yM1e9gU1IH97HVPidzbTxRU5/PcHDdUsg==}
|
resolution: {integrity: sha512-ttoBe5y/WXkAo5/wSMcC/Y4Zbyw4XG8kwAsEaqnAPCxa3M9MI1oV/yM1e9gU1IH97HVPidzbTxRU5/PcHDdUsg==}
|
||||||
|
|
||||||
|
'@push.rocks/taskbuffer@6.1.2':
|
||||||
|
resolution: {integrity: sha512-sdqKd8N/GidztQ1k3r8A86rLvD8Afyir5FjYCNJXDD9837JLoqzHaOKGltUSBsCGh2gjsZn6GydsY6HhXQgvZQ==}
|
||||||
|
|
||||||
'@push.rocks/webrequest@3.0.37':
|
'@push.rocks/webrequest@3.0.37':
|
||||||
resolution: {integrity: sha512-fLN7kP6GeHFxE4UH4r9C9pjcQb0QkJxHeAMwXvbOqB9hh0MFNKhtGU7GoaTn8SVRGRMPc9UqZVNwo6u5l8Wn0A==}
|
resolution: {integrity: sha512-fLN7kP6GeHFxE4UH4r9C9pjcQb0QkJxHeAMwXvbOqB9hh0MFNKhtGU7GoaTn8SVRGRMPc9UqZVNwo6u5l8Wn0A==}
|
||||||
|
|
||||||
|
'@push.rocks/webrequest@4.0.1':
|
||||||
|
resolution: {integrity: sha512-I60XZZLVf8W5I7YdmUVVu4G92teE3rg3/aKaV00BRg8vJ3VXx3wc59Qj4em7zxQ5o0HvL8m1Aezw3RFMDPyVgA==}
|
||||||
|
|
||||||
'@push.rocks/webrequest@4.0.2':
|
'@push.rocks/webrequest@4.0.2':
|
||||||
resolution: {integrity: sha512-rowzty+Q2papFBcnNYPcy+8CQJukSn/FGfQG8ap0bUgQUsx882u8kEyLM0Q+GlGHS5OiZ+Z0z5TZqLKlk3XHxA==}
|
resolution: {integrity: sha512-rowzty+Q2papFBcnNYPcy+8CQJukSn/FGfQG8ap0bUgQUsx882u8kEyLM0Q+GlGHS5OiZ+Z0z5TZqLKlk3XHxA==}
|
||||||
|
|
||||||
@@ -1337,6 +1340,9 @@ packages:
|
|||||||
'@serve.zone/interfaces@5.3.0':
|
'@serve.zone/interfaces@5.3.0':
|
||||||
resolution: {integrity: sha512-venO7wtDR9ixzD9NhdERBGjNKbFA5LL0yHw4eqGh0UpmvtXVc3SFG0uuHDilOKMZqZ8bttV88qVsFy1aSTJrtA==}
|
resolution: {integrity: sha512-venO7wtDR9ixzD9NhdERBGjNKbFA5LL0yHw4eqGh0UpmvtXVc3SFG0uuHDilOKMZqZ8bttV88qVsFy1aSTJrtA==}
|
||||||
|
|
||||||
|
'@serve.zone/remoteingress@3.0.2':
|
||||||
|
resolution: {integrity: sha512-FnwNVO0Dn9xuNv0t81u6pjCizSeCyMjkRKm6wN5qycCdGFoJmFbBamHqV01KtK1KcgDTd7LX+PowSqKReNrBGw==}
|
||||||
|
|
||||||
'@sindresorhus/is@5.6.0':
|
'@sindresorhus/is@5.6.0':
|
||||||
resolution: {integrity: sha512-TV7t8GKYaJWsn00tFDqBw8+Uqmr8A0fRU1tvTQhyZzGv0sJCGRQL3JGMI3ucuKo3XIZdUP+Lx7/gh2t3lewy7g==}
|
resolution: {integrity: sha512-TV7t8GKYaJWsn00tFDqBw8+Uqmr8A0fRU1tvTQhyZzGv0sJCGRQL3JGMI3ucuKo3XIZdUP+Lx7/gh2t3lewy7g==}
|
||||||
engines: {node: '>=14.16'}
|
engines: {node: '>=14.16'}
|
||||||
@@ -1754,18 +1760,12 @@ packages:
|
|||||||
'@tsclass/tsclass@4.4.4':
|
'@tsclass/tsclass@4.4.4':
|
||||||
resolution: {integrity: sha512-YZOAF+u+r4u5rCev2uUd1KBTBdfyFdtDmcv4wuN+864lMccbdfRICR3SlJwCfYS1lbeV3QNLYGD30wjRXgvCJA==}
|
resolution: {integrity: sha512-YZOAF+u+r4u5rCev2uUd1KBTBdfyFdtDmcv4wuN+864lMccbdfRICR3SlJwCfYS1lbeV3QNLYGD30wjRXgvCJA==}
|
||||||
|
|
||||||
'@tsclass/tsclass@5.0.0':
|
|
||||||
resolution: {integrity: sha512-2X66VCk0Oe1L01j6GQHC6F9Gj7lpZPPSUTDNax7e29lm4OqBTyAzTR3ePR8coSbWBwsmRV8awLRSrSI+swlqWA==}
|
|
||||||
|
|
||||||
'@tsclass/tsclass@9.3.0':
|
'@tsclass/tsclass@9.3.0':
|
||||||
resolution: {integrity: sha512-KD3oTUN3RGu67tgjNHgWWZGsdYipr1RUDxQ9MMKSgIJ6oNZ4q5m2rg0ibrgyHWkAjTPlHVa6kHP3uVOY+8bnHw==}
|
resolution: {integrity: sha512-KD3oTUN3RGu67tgjNHgWWZGsdYipr1RUDxQ9MMKSgIJ6oNZ4q5m2rg0ibrgyHWkAjTPlHVa6kHP3uVOY+8bnHw==}
|
||||||
|
|
||||||
'@tybys/wasm-util@0.10.1':
|
'@tybys/wasm-util@0.10.1':
|
||||||
resolution: {integrity: sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==}
|
resolution: {integrity: sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==}
|
||||||
|
|
||||||
'@types/bn.js@5.2.0':
|
|
||||||
resolution: {integrity: sha512-DLbJ1BPqxvQhIGbeu8VbUC1DiAiahHtAYvA0ZEAa4P31F7IaArc8z3C3BRQdWX4mtLQuABG4yzp76ZrS02Ui1Q==}
|
|
||||||
|
|
||||||
'@types/body-parser@1.19.6':
|
'@types/body-parser@1.19.6':
|
||||||
resolution: {integrity: sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g==}
|
resolution: {integrity: sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g==}
|
||||||
|
|
||||||
@@ -1784,12 +1784,6 @@ packages:
|
|||||||
'@types/debug@4.1.12':
|
'@types/debug@4.1.12':
|
||||||
resolution: {integrity: sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ==}
|
resolution: {integrity: sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ==}
|
||||||
|
|
||||||
'@types/dns-packet@5.6.5':
|
|
||||||
resolution: {integrity: sha512-qXOC7XLOEe43ehtWJCMnQXvgcIpv6rPmQ1jXT98Ad8A3TB1Ue50jsCbSSSyuazScEuZ/Q026vHbrOTVkmwA+7Q==}
|
|
||||||
|
|
||||||
'@types/elliptic@6.4.18':
|
|
||||||
resolution: {integrity: sha512-UseG6H5vjRiNpQvrhy4VF/JXdA3V/Fp5amvveaL+fs28BZ6xIKJBPnUPRlEaZpysD9MbpfaLi8lbl7PGUAkpWw==}
|
|
||||||
|
|
||||||
'@types/express-serve-static-core@5.1.1':
|
'@types/express-serve-static-core@5.1.1':
|
||||||
resolution: {integrity: sha512-v4zIMr/cX7/d2BpAEX3KNKL/JrT1s43s96lLvvdTmza1oEvDudCqK9aF/djc/SWgy8Yh0h30TZx5VpzqFCxk5A==}
|
resolution: {integrity: sha512-v4zIMr/cX7/d2BpAEX3KNKL/JrT1s43s96lLvvdTmza1oEvDudCqK9aF/djc/SWgy8Yh0h30TZx5VpzqFCxk5A==}
|
||||||
|
|
||||||
@@ -1847,10 +1841,6 @@ packages:
|
|||||||
'@types/minimatch@5.1.2':
|
'@types/minimatch@5.1.2':
|
||||||
resolution: {integrity: sha512-K0VQKziLUWkVKiRVrx4a40iPaxTUefQmjtkQofBkYRcoaaL/8rhwDWww9qWbrgicNOgnpIsMxyNIUM4+n6dUIA==}
|
resolution: {integrity: sha512-K0VQKziLUWkVKiRVrx4a40iPaxTUefQmjtkQofBkYRcoaaL/8rhwDWww9qWbrgicNOgnpIsMxyNIUM4+n6dUIA==}
|
||||||
|
|
||||||
'@types/minimatch@6.0.0':
|
|
||||||
resolution: {integrity: sha512-zmPitbQ8+6zNutpwgcQuLcsEpn/Cj54Kbn7L5pX0Os5kdWplB7xPgEh/g+SWOB/qmows2gpuCaPyduq8ZZRnxA==}
|
|
||||||
deprecated: This is a stub types definition. minimatch provides its own type definitions, so you do not need this installed.
|
|
||||||
|
|
||||||
'@types/ms@2.1.0':
|
'@types/ms@2.1.0':
|
||||||
resolution: {integrity: sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==}
|
resolution: {integrity: sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==}
|
||||||
|
|
||||||
@@ -2097,9 +2087,6 @@ packages:
|
|||||||
bintrees@1.0.2:
|
bintrees@1.0.2:
|
||||||
resolution: {integrity: sha1-SfiW1uhYpKSZ34XDj7OZua/4QPg=}
|
resolution: {integrity: sha1-SfiW1uhYpKSZ34XDj7OZua/4QPg=}
|
||||||
|
|
||||||
bn.js@4.12.2:
|
|
||||||
resolution: {integrity: sha512-n4DSx829VRTRByMRGdjQ9iqsN0Bh4OolPsFnaZBLcbi8iXcB+kJ9s7EnRt4wILZNV3kPLHkRVfOc/HvhC3ovDw==}
|
|
||||||
|
|
||||||
body-parser@2.2.2:
|
body-parser@2.2.2:
|
||||||
resolution: {integrity: sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA==}
|
resolution: {integrity: sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA==}
|
||||||
engines: {node: '>=18'}
|
engines: {node: '>=18'}
|
||||||
@@ -2120,9 +2107,6 @@ packages:
|
|||||||
broadcast-channel@7.3.0:
|
broadcast-channel@7.3.0:
|
||||||
resolution: {integrity: sha512-UHPhLBQKfQ8OmMFMpmPfO5dRakyA1vsfiDGWTYNvChYol65tbuhivPEGgZZiuetorvExdvxaWiBy/ym1Ty08yA==}
|
resolution: {integrity: sha512-UHPhLBQKfQ8OmMFMpmPfO5dRakyA1vsfiDGWTYNvChYol65tbuhivPEGgZZiuetorvExdvxaWiBy/ym1Ty08yA==}
|
||||||
|
|
||||||
brorand@1.1.0:
|
|
||||||
resolution: {integrity: sha1-EsJe/kCkXjwyPrhnWgoM5XsiNx8=}
|
|
||||||
|
|
||||||
bson@6.10.4:
|
bson@6.10.4:
|
||||||
resolution: {integrity: sha512-WIsKqkSC0ABoBJuT1LEX+2HEvNmNKKgnTAyd0fL8qzK4SH2i9NXg+t08YtdZp/V9IZ33cxe3iV4yM0qg8lMQng==}
|
resolution: {integrity: sha512-WIsKqkSC0ABoBJuT1LEX+2HEvNmNKKgnTAyd0fL8qzK4SH2i9NXg+t08YtdZp/V9IZ33cxe3iV4yM0qg8lMQng==}
|
||||||
engines: {node: '>=16.20.1'}
|
engines: {node: '>=16.20.1'}
|
||||||
@@ -2282,6 +2266,10 @@ packages:
|
|||||||
crelt@1.0.6:
|
crelt@1.0.6:
|
||||||
resolution: {integrity: sha512-VQ2MBenTq1fWZUH9DJNGti7kKv6EeAuYr3cLwxUWhIu1baTaXh4Ib5W2CqHVqib4/MqbYGJqiL3Zb8GJZr3l4g==}
|
resolution: {integrity: sha512-VQ2MBenTq1fWZUH9DJNGti7kKv6EeAuYr3cLwxUWhIu1baTaXh4Ib5W2CqHVqib4/MqbYGJqiL3Zb8GJZr3l4g==}
|
||||||
|
|
||||||
|
croner@10.0.1:
|
||||||
|
resolution: {integrity: sha512-ixNtAJndqh173VQ4KodSdJEI6nuioBWI0V1ITNKhZZsO0pEMoDxz539T4FTTbSZ/xIOSuDnzxLVRqBVSvPNE2g==}
|
||||||
|
engines: {node: '>=18.0'}
|
||||||
|
|
||||||
croner@9.1.0:
|
croner@9.1.0:
|
||||||
resolution: {integrity: sha512-p9nwwR4qyT5W996vBZhdvBCnMhicY5ytZkR4D1Xj0wuTDEiMnjwR57Q3RXYY/s0EpX6Ay3vgIcfaR+ewGHsi+g==}
|
resolution: {integrity: sha512-p9nwwR4qyT5W996vBZhdvBCnMhicY5ytZkR4D1Xj0wuTDEiMnjwR57Q3RXYY/s0EpX6Ay3vgIcfaR+ewGHsi+g==}
|
||||||
engines: {node: '>=18.0'}
|
engines: {node: '>=18.0'}
|
||||||
@@ -2375,10 +2363,6 @@ packages:
|
|||||||
devtools-protocol@0.0.1566079:
|
devtools-protocol@0.0.1566079:
|
||||||
resolution: {integrity: sha512-MJfAEA1UfVhSs7fbSQOG4czavUp1ajfg6prlAN0+cmfa2zNjaIbvq8VneP7do1WAQQIvgNJWSMeP6UyI90gIlQ==}
|
resolution: {integrity: sha512-MJfAEA1UfVhSs7fbSQOG4czavUp1ajfg6prlAN0+cmfa2zNjaIbvq8VneP7do1WAQQIvgNJWSMeP6UyI90gIlQ==}
|
||||||
|
|
||||||
dns-packet@5.6.1:
|
|
||||||
resolution: {integrity: sha512-l4gcSouhcgIKRvyy99RNVOgxXiicE+2jZoNmaNmZ6JXiGajBOJAesk1OBlJuM5k2c+eudGdLxDqXuPCKIj6kpw==}
|
|
||||||
engines: {node: '>=6'}
|
|
||||||
|
|
||||||
dom-serializer@2.0.0:
|
dom-serializer@2.0.0:
|
||||||
resolution: {integrity: sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==}
|
resolution: {integrity: sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==}
|
||||||
|
|
||||||
@@ -2408,9 +2392,6 @@ packages:
|
|||||||
ee-first@1.1.1:
|
ee-first@1.1.1:
|
||||||
resolution: {integrity: sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0=}
|
resolution: {integrity: sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0=}
|
||||||
|
|
||||||
elliptic@6.6.1:
|
|
||||||
resolution: {integrity: sha512-RaddvvMatK2LJHqFJ+YA4WysVN5Ita9E35botqIYspQ4TkRAlCicdzKOjlyv/1Za5RyTNn7di//eEV0uTAfe3g==}
|
|
||||||
|
|
||||||
emoji-regex@8.0.0:
|
emoji-regex@8.0.0:
|
||||||
resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==}
|
resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==}
|
||||||
|
|
||||||
@@ -2743,9 +2724,6 @@ packages:
|
|||||||
resolution: {integrity: sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==}
|
resolution: {integrity: sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==}
|
||||||
engines: {node: '>= 0.4'}
|
engines: {node: '>= 0.4'}
|
||||||
|
|
||||||
hash.js@1.1.7:
|
|
||||||
resolution: {integrity: sha512-taOaskGt4z4SOANNseOviYDvjEJinIkRgmp7LbKP2YTTmVxWBl87s/uzK9r+44BclBSp2X7K1hqeNfz9JbBeXA==}
|
|
||||||
|
|
||||||
hasown@2.0.2:
|
hasown@2.0.2:
|
||||||
resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==}
|
resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==}
|
||||||
engines: {node: '>= 0.4'}
|
engines: {node: '>= 0.4'}
|
||||||
@@ -2767,9 +2745,6 @@ packages:
|
|||||||
resolution: {integrity: sha512-Xwwo44whKBVCYoliBQwaPvtd/2tYFkRQtXDWj1nackaV2JPXx3L0+Jvd8/qCJ2p+ML0/XVkJ2q+Mr+UVdpJK5w==}
|
resolution: {integrity: sha512-Xwwo44whKBVCYoliBQwaPvtd/2tYFkRQtXDWj1nackaV2JPXx3L0+Jvd8/qCJ2p+ML0/XVkJ2q+Mr+UVdpJK5w==}
|
||||||
engines: {node: '>=12.0.0'}
|
engines: {node: '>=12.0.0'}
|
||||||
|
|
||||||
hmac-drbg@1.0.1:
|
|
||||||
resolution: {integrity: sha1-0nRXAQJabHdabFRXk+1QL8DGSaE=}
|
|
||||||
|
|
||||||
html-minifier@4.0.0:
|
html-minifier@4.0.0:
|
||||||
resolution: {integrity: sha512-aoGxanpFPLg7MkIl/DDFYtb0iWz7jMFGqFhvEDZga6/4QTjneiD8I/NXL1x5aaoCp7FSIT6h/OhykDdPsbtMig==}
|
resolution: {integrity: sha512-aoGxanpFPLg7MkIl/DDFYtb0iWz7jMFGqFhvEDZga6/4QTjneiD8I/NXL1x5aaoCp7FSIT6h/OhykDdPsbtMig==}
|
||||||
engines: {node: '>=6'}
|
engines: {node: '>=6'}
|
||||||
@@ -3280,12 +3255,6 @@ packages:
|
|||||||
mingo@7.2.0:
|
mingo@7.2.0:
|
||||||
resolution: {integrity: sha512-UeX942qZpofn5L97h295SkS7j/ADf7Qac8gdRCMBPxi0/1m70aeB2owLFvWbyuMj1dowonlivlVRQVDx+6h+7Q==}
|
resolution: {integrity: sha512-UeX942qZpofn5L97h295SkS7j/ADf7Qac8gdRCMBPxi0/1m70aeB2owLFvWbyuMj1dowonlivlVRQVDx+6h+7Q==}
|
||||||
|
|
||||||
minimalistic-assert@1.0.1:
|
|
||||||
resolution: {integrity: sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==}
|
|
||||||
|
|
||||||
minimalistic-crypto-utils@1.0.1:
|
|
||||||
resolution: {integrity: sha1-9sAMHAsIIkblxNmd+4x8CDsrWCo=}
|
|
||||||
|
|
||||||
minimatch@10.1.2:
|
minimatch@10.1.2:
|
||||||
resolution: {integrity: sha512-fu656aJ0n2kcXwsnwnv9g24tkU5uSmOlTjd6WyyaKm2Z+h1qmY6bAjrcaIxF/BslFqbZ8UBtbJi7KgQOZD2PTw==}
|
resolution: {integrity: sha512-fu656aJ0n2kcXwsnwnv9g24tkU5uSmOlTjd6WyyaKm2Z+h1qmY6bAjrcaIxF/BslFqbZ8UBtbJi7KgQOZD2PTw==}
|
||||||
engines: {node: 20 || >=22}
|
engines: {node: 20 || >=22}
|
||||||
@@ -4379,7 +4348,7 @@ snapshots:
|
|||||||
'@push.rocks/smartfeed': 1.4.0
|
'@push.rocks/smartfeed': 1.4.0
|
||||||
'@push.rocks/smartfile': 11.2.7
|
'@push.rocks/smartfile': 11.2.7
|
||||||
'@push.rocks/smartjson': 5.2.0
|
'@push.rocks/smartjson': 5.2.0
|
||||||
'@push.rocks/smartlog': 3.1.10
|
'@push.rocks/smartlog': 3.1.11
|
||||||
'@push.rocks/smartlog-destination-devtools': 1.0.12
|
'@push.rocks/smartlog-destination-devtools': 1.0.12
|
||||||
'@push.rocks/smartlog-interfaces': 3.0.2
|
'@push.rocks/smartlog-interfaces': 3.0.2
|
||||||
'@push.rocks/smartmanifest': 2.0.2
|
'@push.rocks/smartmanifest': 2.0.2
|
||||||
@@ -4428,7 +4397,7 @@ snapshots:
|
|||||||
'@push.rocks/smartfile': 13.1.2
|
'@push.rocks/smartfile': 13.1.2
|
||||||
'@push.rocks/smartfs': 1.3.1
|
'@push.rocks/smartfs': 1.3.1
|
||||||
'@push.rocks/smartjson': 5.2.0
|
'@push.rocks/smartjson': 5.2.0
|
||||||
'@push.rocks/smartlog': 3.1.10
|
'@push.rocks/smartlog': 3.1.11
|
||||||
'@push.rocks/smartlog-destination-devtools': 1.0.12
|
'@push.rocks/smartlog-destination-devtools': 1.0.12
|
||||||
'@push.rocks/smartlog-interfaces': 3.0.2
|
'@push.rocks/smartlog-interfaces': 3.0.2
|
||||||
'@push.rocks/smartmanifest': 2.0.2
|
'@push.rocks/smartmanifest': 2.0.2
|
||||||
@@ -4492,22 +4461,10 @@ snapshots:
|
|||||||
'@push.rocks/smartstring': 4.1.0
|
'@push.rocks/smartstring': 4.1.0
|
||||||
'@push.rocks/smarturl': 3.1.0
|
'@push.rocks/smarturl': 3.1.0
|
||||||
|
|
||||||
'@apiclient.xyz/cloudflare@6.4.3':
|
|
||||||
dependencies:
|
|
||||||
'@push.rocks/smartdelay': 3.0.5
|
|
||||||
'@push.rocks/smartlog': 3.1.10
|
|
||||||
'@push.rocks/smartpromise': 4.2.3
|
|
||||||
'@push.rocks/smartrequest': 5.0.1
|
|
||||||
'@push.rocks/smartstring': 4.1.0
|
|
||||||
'@tsclass/tsclass': 9.3.0
|
|
||||||
cloudflare: 5.2.0
|
|
||||||
transitivePeerDependencies:
|
|
||||||
- encoding
|
|
||||||
|
|
||||||
'@apiclient.xyz/cloudflare@7.1.0':
|
'@apiclient.xyz/cloudflare@7.1.0':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@push.rocks/smartdelay': 3.0.5
|
'@push.rocks/smartdelay': 3.0.5
|
||||||
'@push.rocks/smartlog': 3.1.10
|
'@push.rocks/smartlog': 3.1.11
|
||||||
'@push.rocks/smartpromise': 4.2.3
|
'@push.rocks/smartpromise': 4.2.3
|
||||||
'@push.rocks/smartrequest': 5.0.1
|
'@push.rocks/smartrequest': 5.0.1
|
||||||
'@push.rocks/smartstring': 4.1.0
|
'@push.rocks/smartstring': 4.1.0
|
||||||
@@ -5241,7 +5198,7 @@ snapshots:
|
|||||||
'@push.rocks/smartdelay': 3.0.5
|
'@push.rocks/smartdelay': 3.0.5
|
||||||
'@push.rocks/smartfile': 13.1.2
|
'@push.rocks/smartfile': 13.1.2
|
||||||
'@push.rocks/smartfs': 1.3.1
|
'@push.rocks/smartfs': 1.3.1
|
||||||
'@push.rocks/smartlog': 3.1.10
|
'@push.rocks/smartlog': 3.1.11
|
||||||
'@push.rocks/smartpath': 6.0.0
|
'@push.rocks/smartpath': 6.0.0
|
||||||
'@push.rocks/smartpromise': 4.2.3
|
'@push.rocks/smartpromise': 4.2.3
|
||||||
typescript: 5.9.3
|
typescript: 5.9.3
|
||||||
@@ -5262,7 +5219,7 @@ snapshots:
|
|||||||
'@push.rocks/smartdelay': 3.0.5
|
'@push.rocks/smartdelay': 3.0.5
|
||||||
'@push.rocks/smartfs': 1.3.1
|
'@push.rocks/smartfs': 1.3.1
|
||||||
'@push.rocks/smartinteract': 2.0.16
|
'@push.rocks/smartinteract': 2.0.16
|
||||||
'@push.rocks/smartlog': 3.1.10
|
'@push.rocks/smartlog': 3.1.11
|
||||||
'@push.rocks/smartlog-destination-local': 9.0.2
|
'@push.rocks/smartlog-destination-local': 9.0.2
|
||||||
'@push.rocks/smartpath': 6.0.0
|
'@push.rocks/smartpath': 6.0.0
|
||||||
'@push.rocks/smartpromise': 4.2.3
|
'@push.rocks/smartpromise': 4.2.3
|
||||||
@@ -5288,7 +5245,7 @@ snapshots:
|
|||||||
'@push.rocks/smartdelay': 3.0.5
|
'@push.rocks/smartdelay': 3.0.5
|
||||||
'@push.rocks/smartfile': 13.1.2
|
'@push.rocks/smartfile': 13.1.2
|
||||||
'@push.rocks/smartfs': 1.3.1
|
'@push.rocks/smartfs': 1.3.1
|
||||||
'@push.rocks/smartlog': 3.1.10
|
'@push.rocks/smartlog': 3.1.11
|
||||||
'@push.rocks/smartnpm': 2.0.6
|
'@push.rocks/smartnpm': 2.0.6
|
||||||
'@push.rocks/smartpath': 6.0.0
|
'@push.rocks/smartpath': 6.0.0
|
||||||
'@push.rocks/smartrequest': 5.0.1
|
'@push.rocks/smartrequest': 5.0.1
|
||||||
@@ -5308,7 +5265,7 @@ snapshots:
|
|||||||
'@push.rocks/smartshell': 3.3.0
|
'@push.rocks/smartshell': 3.3.0
|
||||||
tsx: 4.21.0
|
tsx: 4.21.0
|
||||||
|
|
||||||
'@git.zone/tstest@3.1.8(socks@2.8.7)(typescript@5.9.3)':
|
'@git.zone/tstest@3.1.8(@push.rocks/smartserve@2.0.1)(socks@2.8.7)(typescript@5.9.3)':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@api.global/typedserver': 3.0.80(@push.rocks/smartserve@2.0.1)
|
'@api.global/typedserver': 3.0.80(@push.rocks/smartserve@2.0.1)
|
||||||
'@git.zone/tsbundle': 2.8.3
|
'@git.zone/tsbundle': 2.8.3
|
||||||
@@ -5323,7 +5280,7 @@ snapshots:
|
|||||||
'@push.rocks/smartexpect': 2.5.0
|
'@push.rocks/smartexpect': 2.5.0
|
||||||
'@push.rocks/smartfile': 11.2.7
|
'@push.rocks/smartfile': 11.2.7
|
||||||
'@push.rocks/smartjson': 5.2.0
|
'@push.rocks/smartjson': 5.2.0
|
||||||
'@push.rocks/smartlog': 3.1.10
|
'@push.rocks/smartlog': 3.1.11
|
||||||
'@push.rocks/smartmongo': 2.2.0(socks@2.8.7)
|
'@push.rocks/smartmongo': 2.2.0(socks@2.8.7)
|
||||||
'@push.rocks/smartnetwork': 4.4.0
|
'@push.rocks/smartnetwork': 4.4.0
|
||||||
'@push.rocks/smartpath': 6.0.0
|
'@push.rocks/smartpath': 6.0.0
|
||||||
@@ -5339,6 +5296,7 @@ snapshots:
|
|||||||
- '@aws-sdk/credential-providers'
|
- '@aws-sdk/credential-providers'
|
||||||
- '@mongodb-js/zstd'
|
- '@mongodb-js/zstd'
|
||||||
- '@nuxt/kit'
|
- '@nuxt/kit'
|
||||||
|
- '@push.rocks/smartserve'
|
||||||
- '@swc/helpers'
|
- '@swc/helpers'
|
||||||
- aws-crt
|
- aws-crt
|
||||||
- bare-abort-controller
|
- bare-abort-controller
|
||||||
@@ -5368,7 +5326,7 @@ snapshots:
|
|||||||
'@push.rocks/smartdelay': 3.0.5
|
'@push.rocks/smartdelay': 3.0.5
|
||||||
'@push.rocks/smartfs': 1.3.1
|
'@push.rocks/smartfs': 1.3.1
|
||||||
'@push.rocks/smartinteract': 2.0.16
|
'@push.rocks/smartinteract': 2.0.16
|
||||||
'@push.rocks/smartlog': 3.1.10
|
'@push.rocks/smartlog': 3.1.11
|
||||||
'@push.rocks/smartlog-destination-local': 9.0.2
|
'@push.rocks/smartlog-destination-local': 9.0.2
|
||||||
'@push.rocks/smartshell': 3.3.0
|
'@push.rocks/smartshell': 3.3.0
|
||||||
'@push.rocks/smartwatch': 6.3.0
|
'@push.rocks/smartwatch': 6.3.0
|
||||||
@@ -5500,8 +5458,6 @@ snapshots:
|
|||||||
|
|
||||||
'@isaacs/cliui@9.0.0': {}
|
'@isaacs/cliui@9.0.0': {}
|
||||||
|
|
||||||
'@leichtgewicht/ip-codec@2.0.5': {}
|
|
||||||
|
|
||||||
'@lit-labs/ssr-dom-shim@1.5.1': {}
|
'@lit-labs/ssr-dom-shim@1.5.1': {}
|
||||||
|
|
||||||
'@lit/reactive-element@2.1.2':
|
'@lit/reactive-element@2.1.2':
|
||||||
@@ -5805,7 +5761,7 @@ snapshots:
|
|||||||
'@push.rocks/qenv': 6.1.3
|
'@push.rocks/qenv': 6.1.3
|
||||||
'@push.rocks/smartfile': 11.2.7
|
'@push.rocks/smartfile': 11.2.7
|
||||||
'@push.rocks/smartjson': 5.2.0
|
'@push.rocks/smartjson': 5.2.0
|
||||||
'@push.rocks/smartlog': 3.1.10
|
'@push.rocks/smartlog': 3.1.11
|
||||||
'@push.rocks/smartpath': 6.0.0
|
'@push.rocks/smartpath': 6.0.0
|
||||||
'@push.rocks/smartpromise': 4.2.3
|
'@push.rocks/smartpromise': 4.2.3
|
||||||
'@push.rocks/smartrx': 3.0.10
|
'@push.rocks/smartrx': 3.0.10
|
||||||
@@ -5829,34 +5785,29 @@ snapshots:
|
|||||||
'@api.global/typedrequest': 3.2.6
|
'@api.global/typedrequest': 3.2.6
|
||||||
'@configvault.io/interfaces': 1.0.17
|
'@configvault.io/interfaces': 1.0.17
|
||||||
'@push.rocks/smartfile': 11.2.7
|
'@push.rocks/smartfile': 11.2.7
|
||||||
'@push.rocks/smartlog': 3.1.10
|
'@push.rocks/smartlog': 3.1.11
|
||||||
'@push.rocks/smartpath': 6.0.0
|
'@push.rocks/smartpath': 6.0.0
|
||||||
|
|
||||||
'@push.rocks/smartacme@8.0.0(@push.rocks/smartserve@2.0.1)(socks@2.8.7)':
|
'@push.rocks/smartacme@9.1.3(socks@2.8.7)':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@api.global/typedserver': 3.0.80(@push.rocks/smartserve@2.0.1)
|
'@apiclient.xyz/cloudflare': 7.1.0
|
||||||
'@apiclient.xyz/cloudflare': 6.4.3
|
'@peculiar/x509': 1.14.3
|
||||||
'@push.rocks/lik': 6.2.2
|
'@push.rocks/lik': 6.2.2
|
||||||
'@push.rocks/smartdata': 5.16.7(socks@2.8.7)
|
'@push.rocks/smartdata': 7.0.15(socks@2.8.7)
|
||||||
'@push.rocks/smartdelay': 3.0.5
|
'@push.rocks/smartdelay': 3.0.5
|
||||||
'@push.rocks/smartdns': 6.2.2
|
'@push.rocks/smartdns': 7.8.1
|
||||||
'@push.rocks/smartfile': 11.2.7
|
'@push.rocks/smartlog': 3.1.11
|
||||||
'@push.rocks/smartlog': 3.1.10
|
|
||||||
'@push.rocks/smartnetwork': 4.4.0
|
'@push.rocks/smartnetwork': 4.4.0
|
||||||
'@push.rocks/smartpromise': 4.2.3
|
|
||||||
'@push.rocks/smartrequest': 2.1.0
|
|
||||||
'@push.rocks/smartstring': 4.1.0
|
'@push.rocks/smartstring': 4.1.0
|
||||||
'@push.rocks/smarttime': 4.1.1
|
'@push.rocks/smarttime': 4.2.3
|
||||||
'@push.rocks/smartunique': 3.0.9
|
'@push.rocks/smartunique': 3.0.9
|
||||||
|
'@push.rocks/taskbuffer': 6.1.2
|
||||||
'@tsclass/tsclass': 9.3.0
|
'@tsclass/tsclass': 9.3.0
|
||||||
acme-client: 5.4.0
|
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- '@aws-sdk/credential-providers'
|
- '@aws-sdk/credential-providers'
|
||||||
- '@mongodb-js/zstd'
|
- '@mongodb-js/zstd'
|
||||||
- '@nuxt/kit'
|
- '@nuxt/kit'
|
||||||
- '@push.rocks/smartserve'
|
|
||||||
- bare-abort-controller
|
- bare-abort-controller
|
||||||
- bufferutil
|
|
||||||
- encoding
|
- encoding
|
||||||
- gcp-metadata
|
- gcp-metadata
|
||||||
- kerberos
|
- kerberos
|
||||||
@@ -5866,7 +5817,6 @@ snapshots:
|
|||||||
- snappy
|
- snappy
|
||||||
- socks
|
- socks
|
||||||
- supports-color
|
- supports-color
|
||||||
- utf-8-validate
|
|
||||||
- vue
|
- vue
|
||||||
|
|
||||||
'@push.rocks/smartarchive@4.2.4':
|
'@push.rocks/smartarchive@4.2.4':
|
||||||
@@ -5956,7 +5906,7 @@ snapshots:
|
|||||||
'@push.rocks/smartcli@4.0.20':
|
'@push.rocks/smartcli@4.0.20':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@push.rocks/lik': 6.2.2
|
'@push.rocks/lik': 6.2.2
|
||||||
'@push.rocks/smartlog': 3.1.10
|
'@push.rocks/smartlog': 3.1.11
|
||||||
'@push.rocks/smartobject': 1.0.12
|
'@push.rocks/smartobject': 1.0.12
|
||||||
'@push.rocks/smartpromise': 4.2.3
|
'@push.rocks/smartpromise': 4.2.3
|
||||||
'@push.rocks/smartrx': 3.0.10
|
'@push.rocks/smartrx': 3.0.10
|
||||||
@@ -5981,7 +5931,7 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
'@push.rocks/lik': 6.2.2
|
'@push.rocks/lik': 6.2.2
|
||||||
'@push.rocks/smartdelay': 3.0.5
|
'@push.rocks/smartdelay': 3.0.5
|
||||||
'@push.rocks/smartlog': 3.1.10
|
'@push.rocks/smartlog': 3.1.11
|
||||||
'@push.rocks/smartmongo': 2.2.0(socks@2.8.7)
|
'@push.rocks/smartmongo': 2.2.0(socks@2.8.7)
|
||||||
'@push.rocks/smartpromise': 4.2.3
|
'@push.rocks/smartpromise': 4.2.3
|
||||||
'@push.rocks/smartrx': 3.0.10
|
'@push.rocks/smartrx': 3.0.10
|
||||||
@@ -6010,7 +5960,7 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
'@push.rocks/lik': 6.2.2
|
'@push.rocks/lik': 6.2.2
|
||||||
'@push.rocks/smartdelay': 3.0.5
|
'@push.rocks/smartdelay': 3.0.5
|
||||||
'@push.rocks/smartlog': 3.1.10
|
'@push.rocks/smartlog': 3.1.11
|
||||||
'@push.rocks/smartmongo': 2.2.0(socks@2.8.7)
|
'@push.rocks/smartmongo': 2.2.0(socks@2.8.7)
|
||||||
'@push.rocks/smartpromise': 4.2.3
|
'@push.rocks/smartpromise': 4.2.3
|
||||||
'@push.rocks/smartrx': 3.0.10
|
'@push.rocks/smartrx': 3.0.10
|
||||||
@@ -6039,22 +5989,6 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
'@push.rocks/smartpromise': 4.2.3
|
'@push.rocks/smartpromise': 4.2.3
|
||||||
|
|
||||||
'@push.rocks/smartdns@6.2.2':
|
|
||||||
dependencies:
|
|
||||||
'@push.rocks/smartdelay': 3.0.5
|
|
||||||
'@push.rocks/smartenv': 5.0.13
|
|
||||||
'@push.rocks/smartpromise': 4.2.3
|
|
||||||
'@push.rocks/smartrequest': 2.1.0
|
|
||||||
'@tsclass/tsclass': 5.0.0
|
|
||||||
'@types/dns-packet': 5.6.5
|
|
||||||
'@types/elliptic': 6.4.18
|
|
||||||
acme-client: 5.4.0
|
|
||||||
dns-packet: 5.6.1
|
|
||||||
elliptic: 6.6.1
|
|
||||||
minimatch: 10.1.2
|
|
||||||
transitivePeerDependencies:
|
|
||||||
- supports-color
|
|
||||||
|
|
||||||
'@push.rocks/smartdns@7.8.1':
|
'@push.rocks/smartdns@7.8.1':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@push.rocks/smartdelay': 3.0.5
|
'@push.rocks/smartdelay': 3.0.5
|
||||||
@@ -6218,7 +6152,7 @@ snapshots:
|
|||||||
'@api.global/typedrequest-interfaces': 2.0.2
|
'@api.global/typedrequest-interfaces': 2.0.2
|
||||||
'@tsclass/tsclass': 4.4.4
|
'@tsclass/tsclass': 4.4.4
|
||||||
|
|
||||||
'@push.rocks/smartlog@3.1.10':
|
'@push.rocks/smartlog@3.1.11':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@api.global/typedrequest-interfaces': 3.0.19
|
'@api.global/typedrequest-interfaces': 3.0.19
|
||||||
'@push.rocks/consolecolor': 2.0.3
|
'@push.rocks/consolecolor': 2.0.3
|
||||||
@@ -6228,7 +6162,7 @@ snapshots:
|
|||||||
'@push.rocks/smarthash': 3.2.6
|
'@push.rocks/smarthash': 3.2.6
|
||||||
'@push.rocks/smartpromise': 4.2.3
|
'@push.rocks/smartpromise': 4.2.3
|
||||||
'@push.rocks/smarttime': 4.1.1
|
'@push.rocks/smarttime': 4.1.1
|
||||||
'@push.rocks/webrequest': 3.0.37
|
'@push.rocks/webrequest': 4.0.1
|
||||||
'@tsclass/tsclass': 9.3.0
|
'@tsclass/tsclass': 9.3.0
|
||||||
|
|
||||||
'@push.rocks/smartmail@2.2.0':
|
'@push.rocks/smartmail@2.2.0':
|
||||||
@@ -6265,7 +6199,7 @@ snapshots:
|
|||||||
'@push.rocks/smartmetrics@2.0.10':
|
'@push.rocks/smartmetrics@2.0.10':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@push.rocks/smartdelay': 3.0.5
|
'@push.rocks/smartdelay': 3.0.5
|
||||||
'@push.rocks/smartlog': 3.1.10
|
'@push.rocks/smartlog': 3.1.11
|
||||||
'@types/pidusage': 2.0.5
|
'@types/pidusage': 2.0.5
|
||||||
pidtree: 0.6.0
|
pidtree: 0.6.0
|
||||||
pidusage: 4.0.1
|
pidusage: 4.0.1
|
||||||
@@ -6338,7 +6272,7 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
'@push.rocks/smartfile': 13.1.2
|
'@push.rocks/smartfile': 13.1.2
|
||||||
'@push.rocks/smartfs': 1.3.1
|
'@push.rocks/smartfs': 1.3.1
|
||||||
'@push.rocks/smartlog': 3.1.10
|
'@push.rocks/smartlog': 3.1.11
|
||||||
'@push.rocks/smartmail': 2.2.0
|
'@push.rocks/smartmail': 2.2.0
|
||||||
'@push.rocks/smartpath': 6.0.0
|
'@push.rocks/smartpath': 6.0.0
|
||||||
'@push.rocks/smartrust': 1.2.1
|
'@push.rocks/smartrust': 1.2.1
|
||||||
@@ -6441,45 +6375,13 @@ snapshots:
|
|||||||
|
|
||||||
'@push.rocks/smartpromise@4.2.3': {}
|
'@push.rocks/smartpromise@4.2.3': {}
|
||||||
|
|
||||||
'@push.rocks/smartproxy@25.0.0(@push.rocks/smartserve@2.0.1)(socks@2.8.7)':
|
'@push.rocks/smartproxy@25.7.3':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@push.rocks/lik': 6.2.2
|
|
||||||
'@push.rocks/smartacme': 8.0.0(@push.rocks/smartserve@2.0.1)(socks@2.8.7)
|
|
||||||
'@push.rocks/smartcrypto': 2.0.4
|
'@push.rocks/smartcrypto': 2.0.4
|
||||||
'@push.rocks/smartdelay': 3.0.5
|
'@push.rocks/smartlog': 3.1.11
|
||||||
'@push.rocks/smartfile': 13.1.2
|
|
||||||
'@push.rocks/smartlog': 3.1.10
|
|
||||||
'@push.rocks/smartnetwork': 4.4.0
|
|
||||||
'@push.rocks/smartpromise': 4.2.3
|
|
||||||
'@push.rocks/smartrequest': 5.0.1
|
|
||||||
'@push.rocks/smartrust': 1.2.1
|
'@push.rocks/smartrust': 1.2.1
|
||||||
'@push.rocks/smartrx': 3.0.10
|
|
||||||
'@push.rocks/smartstring': 4.1.0
|
|
||||||
'@push.rocks/taskbuffer': 4.2.0
|
|
||||||
'@tsclass/tsclass': 9.3.0
|
'@tsclass/tsclass': 9.3.0
|
||||||
'@types/minimatch': 6.0.0
|
|
||||||
'@types/ws': 8.18.1
|
|
||||||
minimatch: 10.2.0
|
minimatch: 10.2.0
|
||||||
pretty-ms: 9.3.0
|
|
||||||
ws: 8.19.0
|
|
||||||
transitivePeerDependencies:
|
|
||||||
- '@aws-sdk/credential-providers'
|
|
||||||
- '@mongodb-js/zstd'
|
|
||||||
- '@nuxt/kit'
|
|
||||||
- '@push.rocks/smartserve'
|
|
||||||
- bare-abort-controller
|
|
||||||
- bufferutil
|
|
||||||
- encoding
|
|
||||||
- gcp-metadata
|
|
||||||
- kerberos
|
|
||||||
- mongodb-client-encryption
|
|
||||||
- react
|
|
||||||
- react-native-b4a
|
|
||||||
- snappy
|
|
||||||
- socks
|
|
||||||
- supports-color
|
|
||||||
- utf-8-validate
|
|
||||||
- vue
|
|
||||||
|
|
||||||
'@push.rocks/smartpuppeteer@2.0.5(typescript@5.9.3)':
|
'@push.rocks/smartpuppeteer@2.0.5(typescript@5.9.3)':
|
||||||
dependencies:
|
dependencies:
|
||||||
@@ -6557,7 +6459,7 @@ snapshots:
|
|||||||
'@cfworker/json-schema': 4.1.1
|
'@cfworker/json-schema': 4.1.1
|
||||||
'@push.rocks/lik': 6.2.2
|
'@push.rocks/lik': 6.2.2
|
||||||
'@push.rocks/smartenv': 6.0.0
|
'@push.rocks/smartenv': 6.0.0
|
||||||
'@push.rocks/smartlog': 3.1.10
|
'@push.rocks/smartlog': 3.1.11
|
||||||
'@push.rocks/smartpath': 6.0.0
|
'@push.rocks/smartpath': 6.0.0
|
||||||
ws: 8.19.0
|
ws: 8.19.0
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
@@ -6592,7 +6494,7 @@ snapshots:
|
|||||||
'@push.rocks/smartdelay': 3.0.5
|
'@push.rocks/smartdelay': 3.0.5
|
||||||
'@push.rocks/smartenv': 5.0.13
|
'@push.rocks/smartenv': 5.0.13
|
||||||
'@push.rocks/smartjson': 5.2.0
|
'@push.rocks/smartjson': 5.2.0
|
||||||
'@push.rocks/smartlog': 3.1.10
|
'@push.rocks/smartlog': 3.1.11
|
||||||
'@push.rocks/smartpromise': 4.2.3
|
'@push.rocks/smartpromise': 4.2.3
|
||||||
'@push.rocks/smartrx': 3.0.10
|
'@push.rocks/smartrx': 3.0.10
|
||||||
'@push.rocks/smarttime': 4.1.1
|
'@push.rocks/smarttime': 4.1.1
|
||||||
@@ -6656,6 +6558,17 @@ snapshots:
|
|||||||
is-nan: 1.3.2
|
is-nan: 1.3.2
|
||||||
pretty-ms: 9.3.0
|
pretty-ms: 9.3.0
|
||||||
|
|
||||||
|
'@push.rocks/smarttime@4.2.3':
|
||||||
|
dependencies:
|
||||||
|
'@push.rocks/lik': 6.2.2
|
||||||
|
'@push.rocks/smartdelay': 3.0.5
|
||||||
|
'@push.rocks/smartpromise': 4.2.3
|
||||||
|
croner: 10.0.1
|
||||||
|
date-fns: 4.1.0
|
||||||
|
dayjs: 1.11.19
|
||||||
|
is-nan: 1.3.2
|
||||||
|
pretty-ms: 9.3.0
|
||||||
|
|
||||||
'@push.rocks/smartunique@3.0.9':
|
'@push.rocks/smartunique@3.0.9':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@types/uuid': 9.0.8
|
'@types/uuid': 9.0.8
|
||||||
@@ -6696,7 +6609,7 @@ snapshots:
|
|||||||
'@design.estate/dees-element': 2.1.6
|
'@design.estate/dees-element': 2.1.6
|
||||||
'@push.rocks/lik': 6.2.2
|
'@push.rocks/lik': 6.2.2
|
||||||
'@push.rocks/smartdelay': 3.0.5
|
'@push.rocks/smartdelay': 3.0.5
|
||||||
'@push.rocks/smartlog': 3.1.10
|
'@push.rocks/smartlog': 3.1.11
|
||||||
'@push.rocks/smartpromise': 4.2.3
|
'@push.rocks/smartpromise': 4.2.3
|
||||||
'@push.rocks/smartrx': 3.0.10
|
'@push.rocks/smartrx': 3.0.10
|
||||||
'@push.rocks/smarttime': 4.1.1
|
'@push.rocks/smarttime': 4.1.1
|
||||||
@@ -6712,7 +6625,7 @@ snapshots:
|
|||||||
'@design.estate/dees-element': 2.1.6
|
'@design.estate/dees-element': 2.1.6
|
||||||
'@push.rocks/lik': 6.2.2
|
'@push.rocks/lik': 6.2.2
|
||||||
'@push.rocks/smartdelay': 3.0.5
|
'@push.rocks/smartdelay': 3.0.5
|
||||||
'@push.rocks/smartlog': 3.1.10
|
'@push.rocks/smartlog': 3.1.11
|
||||||
'@push.rocks/smartpromise': 4.2.3
|
'@push.rocks/smartpromise': 4.2.3
|
||||||
'@push.rocks/smartrx': 3.0.10
|
'@push.rocks/smartrx': 3.0.10
|
||||||
'@push.rocks/smarttime': 4.1.1
|
'@push.rocks/smarttime': 4.1.1
|
||||||
@@ -6723,6 +6636,22 @@ snapshots:
|
|||||||
- supports-color
|
- supports-color
|
||||||
- vue
|
- vue
|
||||||
|
|
||||||
|
'@push.rocks/taskbuffer@6.1.2':
|
||||||
|
dependencies:
|
||||||
|
'@design.estate/dees-element': 2.1.6
|
||||||
|
'@push.rocks/lik': 6.2.2
|
||||||
|
'@push.rocks/smartdelay': 3.0.5
|
||||||
|
'@push.rocks/smartlog': 3.1.11
|
||||||
|
'@push.rocks/smartpromise': 4.2.3
|
||||||
|
'@push.rocks/smartrx': 3.0.10
|
||||||
|
'@push.rocks/smarttime': 4.2.3
|
||||||
|
'@push.rocks/smartunique': 3.0.9
|
||||||
|
transitivePeerDependencies:
|
||||||
|
- '@nuxt/kit'
|
||||||
|
- react
|
||||||
|
- supports-color
|
||||||
|
- vue
|
||||||
|
|
||||||
'@push.rocks/webrequest@3.0.37':
|
'@push.rocks/webrequest@3.0.37':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@push.rocks/smartdelay': 3.0.5
|
'@push.rocks/smartdelay': 3.0.5
|
||||||
@@ -6731,6 +6660,14 @@ snapshots:
|
|||||||
'@push.rocks/smartpromise': 4.2.3
|
'@push.rocks/smartpromise': 4.2.3
|
||||||
'@push.rocks/webstore': 2.0.20
|
'@push.rocks/webstore': 2.0.20
|
||||||
|
|
||||||
|
'@push.rocks/webrequest@4.0.1':
|
||||||
|
dependencies:
|
||||||
|
'@push.rocks/smartdelay': 3.0.5
|
||||||
|
'@push.rocks/smartenv': 5.0.13
|
||||||
|
'@push.rocks/smartjson': 5.2.0
|
||||||
|
'@push.rocks/smartpromise': 4.2.3
|
||||||
|
'@push.rocks/webstore': 2.0.20
|
||||||
|
|
||||||
'@push.rocks/webrequest@4.0.2':
|
'@push.rocks/webrequest@4.0.2':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@push.rocks/smartdelay': 3.0.5
|
'@push.rocks/smartdelay': 3.0.5
|
||||||
@@ -6916,6 +6853,11 @@ snapshots:
|
|||||||
'@push.rocks/smartlog-interfaces': 3.0.2
|
'@push.rocks/smartlog-interfaces': 3.0.2
|
||||||
'@tsclass/tsclass': 9.3.0
|
'@tsclass/tsclass': 9.3.0
|
||||||
|
|
||||||
|
'@serve.zone/remoteingress@3.0.2':
|
||||||
|
dependencies:
|
||||||
|
'@push.rocks/qenv': 6.1.3
|
||||||
|
'@push.rocks/smartrust': 1.2.1
|
||||||
|
|
||||||
'@sindresorhus/is@5.6.0': {}
|
'@sindresorhus/is@5.6.0': {}
|
||||||
|
|
||||||
'@smithy/abort-controller@4.2.8':
|
'@smithy/abort-controller@4.2.8':
|
||||||
@@ -7450,10 +7392,6 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
type-fest: 4.41.0
|
type-fest: 4.41.0
|
||||||
|
|
||||||
'@tsclass/tsclass@5.0.0':
|
|
||||||
dependencies:
|
|
||||||
type-fest: 4.41.0
|
|
||||||
|
|
||||||
'@tsclass/tsclass@9.3.0':
|
'@tsclass/tsclass@9.3.0':
|
||||||
dependencies:
|
dependencies:
|
||||||
type-fest: 4.41.0
|
type-fest: 4.41.0
|
||||||
@@ -7463,10 +7401,6 @@ snapshots:
|
|||||||
tslib: 2.8.1
|
tslib: 2.8.1
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
'@types/bn.js@5.2.0':
|
|
||||||
dependencies:
|
|
||||||
'@types/node': 25.2.3
|
|
||||||
|
|
||||||
'@types/body-parser@1.19.6':
|
'@types/body-parser@1.19.6':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@types/connect': 3.4.38
|
'@types/connect': 3.4.38
|
||||||
@@ -7491,14 +7425,6 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
'@types/ms': 2.1.0
|
'@types/ms': 2.1.0
|
||||||
|
|
||||||
'@types/dns-packet@5.6.5':
|
|
||||||
dependencies:
|
|
||||||
'@types/node': 25.2.3
|
|
||||||
|
|
||||||
'@types/elliptic@6.4.18':
|
|
||||||
dependencies:
|
|
||||||
'@types/bn.js': 5.2.0
|
|
||||||
|
|
||||||
'@types/express-serve-static-core@5.1.1':
|
'@types/express-serve-static-core@5.1.1':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@types/node': 25.2.3
|
'@types/node': 25.2.3
|
||||||
@@ -7570,10 +7496,6 @@ snapshots:
|
|||||||
|
|
||||||
'@types/minimatch@5.1.2': {}
|
'@types/minimatch@5.1.2': {}
|
||||||
|
|
||||||
'@types/minimatch@6.0.0':
|
|
||||||
dependencies:
|
|
||||||
minimatch: 10.2.0
|
|
||||||
|
|
||||||
'@types/ms@2.1.0': {}
|
'@types/ms@2.1.0': {}
|
||||||
|
|
||||||
'@types/mute-stream@0.0.4':
|
'@types/mute-stream@0.0.4':
|
||||||
@@ -7819,8 +7741,6 @@ snapshots:
|
|||||||
|
|
||||||
bintrees@1.0.2: {}
|
bintrees@1.0.2: {}
|
||||||
|
|
||||||
bn.js@4.12.2: {}
|
|
||||||
|
|
||||||
body-parser@2.2.2:
|
body-parser@2.2.2:
|
||||||
dependencies:
|
dependencies:
|
||||||
bytes: 3.1.2
|
bytes: 3.1.2
|
||||||
@@ -7857,8 +7777,6 @@ snapshots:
|
|||||||
p-queue: 6.6.2
|
p-queue: 6.6.2
|
||||||
unload: 2.4.1
|
unload: 2.4.1
|
||||||
|
|
||||||
brorand@1.1.0: {}
|
|
||||||
|
|
||||||
bson@6.10.4: {}
|
bson@6.10.4: {}
|
||||||
|
|
||||||
bson@7.2.0: {}
|
bson@7.2.0: {}
|
||||||
@@ -8009,6 +7927,8 @@ snapshots:
|
|||||||
|
|
||||||
crelt@1.0.6: {}
|
crelt@1.0.6: {}
|
||||||
|
|
||||||
|
croner@10.0.1: {}
|
||||||
|
|
||||||
croner@9.1.0: {}
|
croner@9.1.0: {}
|
||||||
|
|
||||||
cross-spawn@7.0.6:
|
cross-spawn@7.0.6:
|
||||||
@@ -8081,10 +8001,6 @@ snapshots:
|
|||||||
|
|
||||||
devtools-protocol@0.0.1566079: {}
|
devtools-protocol@0.0.1566079: {}
|
||||||
|
|
||||||
dns-packet@5.6.1:
|
|
||||||
dependencies:
|
|
||||||
'@leichtgewicht/ip-codec': 2.0.5
|
|
||||||
|
|
||||||
dom-serializer@2.0.0:
|
dom-serializer@2.0.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
domelementtype: 2.3.0
|
domelementtype: 2.3.0
|
||||||
@@ -8121,16 +8037,6 @@ snapshots:
|
|||||||
|
|
||||||
ee-first@1.1.1: {}
|
ee-first@1.1.1: {}
|
||||||
|
|
||||||
elliptic@6.6.1:
|
|
||||||
dependencies:
|
|
||||||
bn.js: 4.12.2
|
|
||||||
brorand: 1.1.0
|
|
||||||
hash.js: 1.1.7
|
|
||||||
hmac-drbg: 1.0.1
|
|
||||||
inherits: 2.0.4
|
|
||||||
minimalistic-assert: 1.0.1
|
|
||||||
minimalistic-crypto-utils: 1.0.1
|
|
||||||
|
|
||||||
emoji-regex@8.0.0: {}
|
emoji-regex@8.0.0: {}
|
||||||
|
|
||||||
emoji-regex@9.2.2: {}
|
emoji-regex@9.2.2: {}
|
||||||
@@ -8560,11 +8466,6 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
has-symbols: 1.1.0
|
has-symbols: 1.1.0
|
||||||
|
|
||||||
hash.js@1.1.7:
|
|
||||||
dependencies:
|
|
||||||
inherits: 2.0.4
|
|
||||||
minimalistic-assert: 1.0.1
|
|
||||||
|
|
||||||
hasown@2.0.2:
|
hasown@2.0.2:
|
||||||
dependencies:
|
dependencies:
|
||||||
function-bind: 1.1.2
|
function-bind: 1.1.2
|
||||||
@@ -8597,12 +8498,6 @@ snapshots:
|
|||||||
|
|
||||||
highlight.js@11.11.1: {}
|
highlight.js@11.11.1: {}
|
||||||
|
|
||||||
hmac-drbg@1.0.1:
|
|
||||||
dependencies:
|
|
||||||
hash.js: 1.1.7
|
|
||||||
minimalistic-assert: 1.0.1
|
|
||||||
minimalistic-crypto-utils: 1.0.1
|
|
||||||
|
|
||||||
html-minifier@4.0.0:
|
html-minifier@4.0.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
camel-case: 3.0.0
|
camel-case: 3.0.0
|
||||||
@@ -9311,10 +9206,6 @@ snapshots:
|
|||||||
|
|
||||||
mingo@7.2.0: {}
|
mingo@7.2.0: {}
|
||||||
|
|
||||||
minimalistic-assert@1.0.1: {}
|
|
||||||
|
|
||||||
minimalistic-crypto-utils@1.0.1: {}
|
|
||||||
|
|
||||||
minimatch@10.1.2:
|
minimatch@10.1.2:
|
||||||
dependencies:
|
dependencies:
|
||||||
'@isaacs/brace-expansion': 5.0.1
|
'@isaacs/brace-expansion': 5.0.1
|
||||||
|
|||||||
102
readme.md
102
readme.md
@@ -21,6 +21,7 @@ For reporting bugs, issues, or security vulnerabilities, please visit [community
|
|||||||
- [Email System](#email-system)
|
- [Email System](#email-system)
|
||||||
- [DNS Server](#dns-server)
|
- [DNS Server](#dns-server)
|
||||||
- [RADIUS Server](#radius-server)
|
- [RADIUS Server](#radius-server)
|
||||||
|
- [Certificate Management](#certificate-management)
|
||||||
- [Storage & Caching](#storage--caching)
|
- [Storage & Caching](#storage--caching)
|
||||||
- [Security Features](#security-features)
|
- [Security Features](#security-features)
|
||||||
- [OpsServer Dashboard](#opsserver-dashboard)
|
- [OpsServer Dashboard](#opsserver-dashboard)
|
||||||
@@ -46,7 +47,9 @@ For reporting bugs, issues, or security vulnerabilities, please visit [community
|
|||||||
- **Hierarchical rate limiting** — global, per-domain, per-sender
|
- **Hierarchical rate limiting** — global, per-domain, per-sender
|
||||||
|
|
||||||
### 🔒 Enterprise Security
|
### 🔒 Enterprise Security
|
||||||
- **Automatic TLS certificates** via ACME with Cloudflare DNS-01 challenges
|
- **Automatic TLS certificates** via ACME (smartacme v9) with Cloudflare DNS-01 challenges
|
||||||
|
- **Smart certificate scheduling** — per-domain deduplication, controlled parallelism, and account rate limiting handled automatically
|
||||||
|
- **Per-domain exponential backoff** — failed provisioning attempts are tracked and backed off to avoid hammering ACME servers
|
||||||
- **IP reputation checking** with caching and configurable thresholds
|
- **IP reputation checking** with caching and configurable thresholds
|
||||||
- **Content scanning** for spam, viruses, and malicious attachments
|
- **Content scanning** for spam, viruses, and malicious attachments
|
||||||
- **Security event logging** with structured audit trails
|
- **Security event logging** with structured audit trails
|
||||||
@@ -73,7 +76,8 @@ For reporting bugs, issues, or security vulnerabilities, please visit [community
|
|||||||
### 🖥️ OpsServer Dashboard
|
### 🖥️ OpsServer Dashboard
|
||||||
- **Web-based management interface** with real-time monitoring
|
- **Web-based management interface** with real-time monitoring
|
||||||
- **JWT authentication** with session persistence
|
- **JWT authentication** with session persistence
|
||||||
- **Live views** for connections, email queues, DNS queries, RADIUS sessions, and security events
|
- **Live views** for connections, email queues, DNS queries, RADIUS sessions, certificates, and security events
|
||||||
|
- **Domain-centric certificate overview** with backoff status and one-click reprovisioning
|
||||||
- **Read-only configuration display** — DcRouter is configured through code
|
- **Read-only configuration display** — DcRouter is configured through code
|
||||||
|
|
||||||
## Installation
|
## Installation
|
||||||
@@ -250,7 +254,7 @@ graph TB
|
|||||||
ES[smartmta Email Server<br/><i>TypeScript + Rust</i>]
|
ES[smartmta Email Server<br/><i>TypeScript + Rust</i>]
|
||||||
DS[SmartDNS Server<br/><i>Rust-powered</i>]
|
DS[SmartDNS Server<br/><i>Rust-powered</i>]
|
||||||
RS[SmartRadius Server]
|
RS[SmartRadius Server]
|
||||||
CM[Certificate Manager]
|
CM[Certificate Manager<br/><i>smartacme v9</i>]
|
||||||
OS[OpsServer Dashboard]
|
OS[OpsServer Dashboard]
|
||||||
MM[Metrics Manager]
|
MM[Metrics Manager]
|
||||||
SM[Storage Manager]
|
SM[Storage Manager]
|
||||||
@@ -297,6 +301,7 @@ graph TB
|
|||||||
| **SmartProxy** | `@push.rocks/smartproxy` | High-performance HTTP/HTTPS and TCP/SNI proxy with route-based config (Rust engine) |
|
| **SmartProxy** | `@push.rocks/smartproxy` | High-performance HTTP/HTTPS and TCP/SNI proxy with route-based config (Rust engine) |
|
||||||
| **UnifiedEmailServer** | `@push.rocks/smartmta` | Full SMTP server with pattern-based routing, DKIM, queue management (TypeScript + Rust) |
|
| **UnifiedEmailServer** | `@push.rocks/smartmta` | Full SMTP server with pattern-based routing, DKIM, queue management (TypeScript + Rust) |
|
||||||
| **DNS Server** | `@push.rocks/smartdns` | Authoritative DNS with dynamic records and DKIM TXT auto-generation (Rust engine) |
|
| **DNS Server** | `@push.rocks/smartdns` | Authoritative DNS with dynamic records and DKIM TXT auto-generation (Rust engine) |
|
||||||
|
| **SmartAcme** | `@push.rocks/smartacme` | ACME certificate management with per-domain dedup, concurrency control, and rate limiting |
|
||||||
| **RADIUS Server** | `@push.rocks/smartradius` | Network authentication with MAB, VLAN assignment, and accounting |
|
| **RADIUS Server** | `@push.rocks/smartradius` | Network authentication with MAB, VLAN assignment, and accounting |
|
||||||
| **OpsServer** | `@api.global/typedserver` | Web dashboard + TypedRequest API for monitoring and management |
|
| **OpsServer** | `@api.global/typedserver` | Web dashboard + TypedRequest API for monitoring and management |
|
||||||
| **MetricsManager** | `@push.rocks/smartmetrics` | Real-time metrics collection (CPU, memory, email, DNS, security) |
|
| **MetricsManager** | `@push.rocks/smartmetrics` | Real-time metrics collection (CPU, memory, email, DNS, security) |
|
||||||
@@ -308,8 +313,8 @@ graph TB
|
|||||||
DcRouter acts purely as an **orchestrator** — it doesn't implement protocols itself. Instead, it wires together best-in-class packages for each protocol:
|
DcRouter acts purely as an **orchestrator** — it doesn't implement protocols itself. Instead, it wires together best-in-class packages for each protocol:
|
||||||
|
|
||||||
1. **On `start()`**: DcRouter initializes OpsServer (port 3000), then spins up SmartProxy, smartmta, SmartDNS, and SmartRadius based on which configs are provided.
|
1. **On `start()`**: DcRouter initializes OpsServer (port 3000), then spins up SmartProxy, smartmta, SmartDNS, and SmartRadius based on which configs are provided.
|
||||||
2. **During operation**: Each service handles its own protocol independently. SmartProxy uses a Rust-powered engine for maximum throughput. smartmta uses a hybrid TypeScript + Rust architecture for reliable email delivery.
|
2. **During operation**: Each service handles its own protocol independently. SmartProxy uses a Rust-powered engine for maximum throughput. smartmta uses a hybrid TypeScript + Rust architecture for reliable email delivery. SmartAcme v9 handles all certificate operations with built-in concurrency control and rate limiting.
|
||||||
3. **On `stop()`**: All services are gracefully shut down in reverse order.
|
3. **On `stop()`**: All services are gracefully shut down in parallel, including cleanup of HTTP agents and DNS clients.
|
||||||
|
|
||||||
### Rust-Powered Architecture
|
### Rust-Powered Architecture
|
||||||
|
|
||||||
@@ -584,15 +589,6 @@ match: { sizeRange: { min: 1000, max: 5000000 }, hasAttachments: true }
|
|||||||
match: { subject: /invoice|receipt/i }
|
match: { subject: /invoice|receipt/i }
|
||||||
```
|
```
|
||||||
|
|
||||||
### Socket-Handler Mode 🔌
|
|
||||||
|
|
||||||
When `useSocketHandler: true` is set, SmartProxy passes sockets directly to the email server — no internal port binding, lower latency, and fewer open ports:
|
|
||||||
|
|
||||||
```
|
|
||||||
Traditional: External Port → SmartProxy → Internal Port → Email Server
|
|
||||||
Socket Mode: External Port → SmartProxy → (direct socket) → Email Server
|
|
||||||
```
|
|
||||||
|
|
||||||
### Email Security Stack
|
### Email Security Stack
|
||||||
|
|
||||||
- **DKIM** — Automatic key generation, signing, and rotation for all domains
|
- **DKIM** — Automatic key generation, signing, and rotation for all domains
|
||||||
@@ -705,6 +701,73 @@ RADIUS is fully manageable at runtime via the OpsServer API:
|
|||||||
- Session monitoring and forced disconnects
|
- Session monitoring and forced disconnects
|
||||||
- Accounting summaries and statistics
|
- Accounting summaries and statistics
|
||||||
|
|
||||||
|
## Certificate Management
|
||||||
|
|
||||||
|
DcRouter uses [`@push.rocks/smartacme`](https://code.foss.global/push.rocks/smartacme) v9 for ACME certificate provisioning. smartacme v9 brings significant improvements over previous versions:
|
||||||
|
|
||||||
|
### How It Works
|
||||||
|
|
||||||
|
When a `dnsChallenge` is configured (e.g. with a Cloudflare API key), DcRouter creates a SmartAcme instance that handles DNS-01 challenges for automatic certificate provisioning. SmartProxy calls the `certProvisionFunction` whenever a route needs a TLS certificate, and SmartAcme takes care of the rest.
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const router = new DcRouter({
|
||||||
|
smartProxyConfig: {
|
||||||
|
routes: [
|
||||||
|
{
|
||||||
|
name: 'secure-app',
|
||||||
|
match: { domains: ['app.example.com'], ports: [443] },
|
||||||
|
action: {
|
||||||
|
type: 'forward',
|
||||||
|
targets: [{ host: '192.168.1.10', port: 8080 }],
|
||||||
|
tls: { mode: 'terminate', certificate: 'auto' } // ← triggers ACME provisioning
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
acme: { email: 'admin@example.com', enabled: true, useProduction: true }
|
||||||
|
},
|
||||||
|
tls: { contactEmail: 'admin@example.com' },
|
||||||
|
dnsChallenge: { cloudflareApiKey: process.env.CLOUDFLARE_API_KEY }
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
### smartacme v9 Features
|
||||||
|
|
||||||
|
| Feature | Description |
|
||||||
|
|---------|-------------|
|
||||||
|
| **Per-domain deduplication** | Concurrent requests for the same domain share a single ACME operation |
|
||||||
|
| **Global concurrency cap** | Default 5 parallel ACME operations to prevent overload |
|
||||||
|
| **Account rate limiting** | Sliding window (250 orders / 3 hours) to stay within ACME provider limits |
|
||||||
|
| **Structured errors** | `AcmeError` with `isRetryable`, `isRateLimited`, `retryAfter` fields |
|
||||||
|
| **Clean shutdown** | `stop()` properly destroys HTTP agents and DNS clients |
|
||||||
|
|
||||||
|
### Per-Domain Backoff
|
||||||
|
|
||||||
|
DcRouter's `CertProvisionScheduler` adds **per-domain exponential backoff** on top of smartacme's built-in protections. If a DNS-01 challenge fails for a domain:
|
||||||
|
|
||||||
|
1. The failure is recorded (persisted to storage)
|
||||||
|
2. The domain enters backoff: `min(failures² × 1 hour, 24 hours)`
|
||||||
|
3. Subsequent requests for that domain are rejected until the backoff expires
|
||||||
|
4. On success, the backoff is cleared
|
||||||
|
|
||||||
|
This prevents hammering ACME servers for domains with persistent issues (e.g. missing DNS delegation).
|
||||||
|
|
||||||
|
### Fallback to HTTP-01
|
||||||
|
|
||||||
|
If DNS-01 fails, the `certProvisionFunction` returns `'http01'` to tell SmartProxy to fall back to HTTP-01 challenge validation. This provides a safety net for domains where DNS-01 isn't viable.
|
||||||
|
|
||||||
|
### Certificate Storage
|
||||||
|
|
||||||
|
Certificates are persisted via the `StorageBackedCertManager` which uses DcRouter's `StorageManager`. This means certs survive restarts and don't need to be re-provisioned unless they expire.
|
||||||
|
|
||||||
|
### Dashboard
|
||||||
|
|
||||||
|
The OpsServer includes a **Certificates** view showing:
|
||||||
|
- All domains with their certificate status (valid, expiring, expired, failed)
|
||||||
|
- Certificate source (ACME, provision function, static)
|
||||||
|
- Expiry dates and issuer information
|
||||||
|
- Backoff status for failed domains
|
||||||
|
- One-click reprovisioning per domain
|
||||||
|
|
||||||
## Storage & Caching
|
## Storage & Caching
|
||||||
|
|
||||||
### StorageManager
|
### StorageManager
|
||||||
@@ -725,7 +788,7 @@ storage: {
|
|||||||
// Simply omit the storage config
|
// Simply omit the storage config
|
||||||
```
|
```
|
||||||
|
|
||||||
Used for: DKIM keys, email routes, bounce/suppression lists, IP reputation data, domain configs.
|
Used for: TLS certificates, DKIM keys, email routes, bounce/suppression lists, IP reputation data, domain configs, cert backoff state.
|
||||||
|
|
||||||
### Cache Database
|
### Cache Database
|
||||||
|
|
||||||
@@ -811,6 +874,7 @@ The OpsServer provides a web-based management interface served on port 3000. It'
|
|||||||
| 📊 **Overview** | Real-time server stats, CPU/memory, connection counts, email throughput |
|
| 📊 **Overview** | Real-time server stats, CPU/memory, connection counts, email throughput |
|
||||||
| 🌐 **Network** | Active connections, top IPs, throughput rates, SmartProxy metrics |
|
| 🌐 **Network** | Active connections, top IPs, throughput rates, SmartProxy metrics |
|
||||||
| 📧 **Email** | Queue monitoring (queued/sent/failed), bounce records, security incidents |
|
| 📧 **Email** | Queue monitoring (queued/sent/failed), bounce records, security incidents |
|
||||||
|
| 🔐 **Certificates** | Domain-centric certificate overview, status, backoff info, reprovisioning |
|
||||||
| 📜 **Logs** | Real-time log viewer with level filtering and search |
|
| 📜 **Logs** | Real-time log viewer with level filtering and search |
|
||||||
| ⚙️ **Configuration** | Read-only view of current system configuration |
|
| ⚙️ **Configuration** | Read-only view of current system configuration |
|
||||||
| 🛡️ **Security** | IP reputation, rate limit status, blocked connections |
|
| 🛡️ **Security** | IP reputation, rate limit status, blocked connections |
|
||||||
@@ -838,6 +902,11 @@ All management is done via TypedRequest over HTTP POST to `/typedrequest`:
|
|||||||
'getBounceRecords' // Bounce records
|
'getBounceRecords' // Bounce records
|
||||||
'removeFromSuppressionList' // Unsuppress an address
|
'removeFromSuppressionList' // Unsuppress an address
|
||||||
|
|
||||||
|
// Certificates
|
||||||
|
'getCertificateOverview' // Domain-centric certificate status
|
||||||
|
'reprovisionCertificate' // Reprovision by route name (legacy)
|
||||||
|
'reprovisionCertificateDomain' // Reprovision by domain (preferred)
|
||||||
|
|
||||||
// Configuration (read-only)
|
// Configuration (read-only)
|
||||||
'getConfiguration' // Current system config
|
'getConfiguration' // Current system config
|
||||||
|
|
||||||
@@ -884,6 +953,7 @@ const router = new DcRouter(options: IDcRouterOptions);
|
|||||||
|----------|------|-------------|
|
|----------|------|-------------|
|
||||||
| `options` | `IDcRouterOptions` | Current configuration |
|
| `options` | `IDcRouterOptions` | Current configuration |
|
||||||
| `smartProxy` | `SmartProxy` | SmartProxy instance |
|
| `smartProxy` | `SmartProxy` | SmartProxy instance |
|
||||||
|
| `smartAcme` | `SmartAcme` | SmartAcme v9 certificate manager instance |
|
||||||
| `emailServer` | `UnifiedEmailServer` | Email server instance (from smartmta) |
|
| `emailServer` | `UnifiedEmailServer` | Email server instance (from smartmta) |
|
||||||
| `dnsServer` | `DnsServer` | DNS server instance |
|
| `dnsServer` | `DnsServer` | DNS server instance |
|
||||||
| `radiusServer` | `RadiusServer` | RADIUS server instance |
|
| `radiusServer` | `RadiusServer` | RADIUS server instance |
|
||||||
@@ -891,6 +961,8 @@ const router = new DcRouter(options: IDcRouterOptions);
|
|||||||
| `opsServer` | `OpsServer` | OpsServer/dashboard instance |
|
| `opsServer` | `OpsServer` | OpsServer/dashboard instance |
|
||||||
| `metricsManager` | `MetricsManager` | Metrics collector |
|
| `metricsManager` | `MetricsManager` | Metrics collector |
|
||||||
| `cacheDb` | `CacheDb` | Cache database instance |
|
| `cacheDb` | `CacheDb` | Cache database instance |
|
||||||
|
| `certProvisionScheduler` | `CertProvisionScheduler` | Per-domain backoff scheduler for cert provisioning |
|
||||||
|
| `certificateStatusMap` | `Map<string, ...>` | Domain-keyed certificate status from SmartProxy events |
|
||||||
|
|
||||||
### Re-exported Types
|
### Re-exported Types
|
||||||
|
|
||||||
|
|||||||
@@ -3,6 +3,6 @@
|
|||||||
*/
|
*/
|
||||||
export const commitinfo = {
|
export const commitinfo = {
|
||||||
name: '@serve.zone/dcrouter',
|
name: '@serve.zone/dcrouter',
|
||||||
version: '5.4.0',
|
version: '6.4.4',
|
||||||
description: 'A multifaceted routing service handling mail and SMS delivery functions.'
|
description: 'A multifaceted routing service handling mail and SMS delivery functions.'
|
||||||
}
|
}
|
||||||
|
|||||||
130
ts/classes.cert-provision-scheduler.ts
Normal file
130
ts/classes.cert-provision-scheduler.ts
Normal file
@@ -0,0 +1,130 @@
|
|||||||
|
import { logger } from './logger.js';
|
||||||
|
import type { StorageManager } from './storage/index.js';
|
||||||
|
|
||||||
|
interface IBackoffEntry {
|
||||||
|
failures: number;
|
||||||
|
lastFailure: string; // ISO string
|
||||||
|
retryAfter: string; // ISO string
|
||||||
|
lastError?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Manages certificate provisioning scheduling with:
|
||||||
|
* - Per-domain exponential backoff persisted in StorageManager
|
||||||
|
*
|
||||||
|
* Note: Serial stagger queue was removed — smartacme v9 handles
|
||||||
|
* concurrency, per-domain dedup, and rate limiting internally.
|
||||||
|
*/
|
||||||
|
export class CertProvisionScheduler {
|
||||||
|
private storageManager: StorageManager;
|
||||||
|
private maxBackoffHours: number;
|
||||||
|
|
||||||
|
// In-memory backoff cache (mirrors storage for fast lookups)
|
||||||
|
private backoffCache = new Map<string, IBackoffEntry>();
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
storageManager: StorageManager,
|
||||||
|
options?: { maxBackoffHours?: number }
|
||||||
|
) {
|
||||||
|
this.storageManager = storageManager;
|
||||||
|
this.maxBackoffHours = options?.maxBackoffHours ?? 24;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Storage key for a domain's backoff entry
|
||||||
|
*/
|
||||||
|
private backoffKey(domain: string): string {
|
||||||
|
const clean = domain.replace(/\*/g, '_wildcard_').replace(/[^a-zA-Z0-9._-]/g, '_');
|
||||||
|
return `/cert-backoff/${clean}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Load backoff entry from storage (with in-memory cache)
|
||||||
|
*/
|
||||||
|
private async loadBackoff(domain: string): Promise<IBackoffEntry | null> {
|
||||||
|
const cached = this.backoffCache.get(domain);
|
||||||
|
if (cached) return cached;
|
||||||
|
|
||||||
|
const entry = await this.storageManager.getJSON<IBackoffEntry>(this.backoffKey(domain));
|
||||||
|
if (entry) {
|
||||||
|
this.backoffCache.set(domain, entry);
|
||||||
|
}
|
||||||
|
return entry;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Save backoff entry to both cache and storage
|
||||||
|
*/
|
||||||
|
private async saveBackoff(domain: string, entry: IBackoffEntry): Promise<void> {
|
||||||
|
this.backoffCache.set(domain, entry);
|
||||||
|
await this.storageManager.setJSON(this.backoffKey(domain), entry);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if a domain is currently in backoff
|
||||||
|
*/
|
||||||
|
async isInBackoff(domain: string): Promise<boolean> {
|
||||||
|
const entry = await this.loadBackoff(domain);
|
||||||
|
if (!entry) return false;
|
||||||
|
|
||||||
|
const retryAfter = new Date(entry.retryAfter);
|
||||||
|
return retryAfter.getTime() > Date.now();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Record a provisioning failure for a domain.
|
||||||
|
* Sets exponential backoff: min(failures^2 * 1h, maxBackoffHours)
|
||||||
|
*/
|
||||||
|
async recordFailure(domain: string, error?: string): Promise<void> {
|
||||||
|
const existing = await this.loadBackoff(domain);
|
||||||
|
const failures = (existing?.failures ?? 0) + 1;
|
||||||
|
|
||||||
|
// Exponential backoff: failures^2 hours, capped
|
||||||
|
const backoffHours = Math.min(failures * failures, this.maxBackoffHours);
|
||||||
|
const retryAfter = new Date(Date.now() + backoffHours * 60 * 60 * 1000);
|
||||||
|
|
||||||
|
const entry: IBackoffEntry = {
|
||||||
|
failures,
|
||||||
|
lastFailure: new Date().toISOString(),
|
||||||
|
retryAfter: retryAfter.toISOString(),
|
||||||
|
lastError: error,
|
||||||
|
};
|
||||||
|
|
||||||
|
await this.saveBackoff(domain, entry);
|
||||||
|
logger.log('warn', `Cert backoff for ${domain}: ${failures} failures, retry after ${retryAfter.toISOString()}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clear backoff for a domain (on success or manual override)
|
||||||
|
*/
|
||||||
|
async clearBackoff(domain: string): Promise<void> {
|
||||||
|
this.backoffCache.delete(domain);
|
||||||
|
try {
|
||||||
|
await this.storageManager.delete(this.backoffKey(domain));
|
||||||
|
} catch {
|
||||||
|
// Ignore delete errors (key may not exist)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get backoff info for UI display
|
||||||
|
*/
|
||||||
|
async getBackoffInfo(domain: string): Promise<{
|
||||||
|
failures: number;
|
||||||
|
retryAfter?: string;
|
||||||
|
lastError?: string;
|
||||||
|
} | null> {
|
||||||
|
const entry = await this.loadBackoff(domain);
|
||||||
|
if (!entry) return null;
|
||||||
|
|
||||||
|
// Only return if still in backoff
|
||||||
|
const retryAfter = new Date(entry.retryAfter);
|
||||||
|
if (retryAfter.getTime() <= Date.now()) return null;
|
||||||
|
|
||||||
|
return {
|
||||||
|
failures: entry.failures,
|
||||||
|
retryAfter: entry.retryAfter,
|
||||||
|
lastError: entry.lastError,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -13,14 +13,20 @@ import {
|
|||||||
import { logger } from './logger.js';
|
import { logger } from './logger.js';
|
||||||
// Import storage manager
|
// Import storage manager
|
||||||
import { StorageManager, type IStorageConfig } from './storage/index.js';
|
import { StorageManager, type IStorageConfig } from './storage/index.js';
|
||||||
|
import { StorageBackedCertManager } from './classes.storage-cert-manager.js';
|
||||||
|
import { CertProvisionScheduler } from './classes.cert-provision-scheduler.js';
|
||||||
// Import cache system
|
// Import cache system
|
||||||
import { CacheDb, CacheCleaner, type ICacheDbOptions } from './cache/index.js';
|
import { CacheDb, CacheCleaner, type ICacheDbOptions } from './cache/index.js';
|
||||||
|
|
||||||
import { OpsServer } from './opsserver/index.js';
|
import { OpsServer } from './opsserver/index.js';
|
||||||
import { MetricsManager } from './monitoring/index.js';
|
import { MetricsManager } from './monitoring/index.js';
|
||||||
import { RadiusServer, type IRadiusServerConfig } from './radius/index.js';
|
import { RadiusServer, type IRadiusServerConfig } from './radius/index.js';
|
||||||
|
import { RemoteIngressManager, TunnelManager } from './remoteingress/index.js';
|
||||||
|
|
||||||
export interface IDcRouterOptions {
|
export interface IDcRouterOptions {
|
||||||
|
/** Base directory for all dcrouter data. Defaults to ~/.serve.zone/dcrouter */
|
||||||
|
baseDir?: string;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Direct SmartProxy configuration - gives full control over HTTP/HTTPS and TCP/SNI traffic
|
* Direct SmartProxy configuration - gives full control over HTTP/HTTPS and TCP/SNI traffic
|
||||||
* This is the preferred way to configure HTTP/HTTPS and general TCP/SNI traffic
|
* This is the preferred way to configure HTTP/HTTPS and general TCP/SNI traffic
|
||||||
@@ -150,6 +156,22 @@ export interface IDcRouterOptions {
|
|||||||
* Enables MAC Authentication Bypass (MAB) and VLAN assignment
|
* Enables MAC Authentication Bypass (MAB) and VLAN assignment
|
||||||
*/
|
*/
|
||||||
radiusConfig?: IRadiusServerConfig;
|
radiusConfig?: IRadiusServerConfig;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remote Ingress configuration for edge tunnel nodes
|
||||||
|
* Enables edge nodes to accept incoming connections and tunnel them to this DcRouter
|
||||||
|
*/
|
||||||
|
remoteIngressConfig?: {
|
||||||
|
/** Enable remote ingress hub (default: false) */
|
||||||
|
enabled?: boolean;
|
||||||
|
/** Port for tunnel connections from edge nodes (default: 8443) */
|
||||||
|
tunnelPort?: number;
|
||||||
|
/** TLS configuration for the tunnel server */
|
||||||
|
tls?: {
|
||||||
|
certPath?: string;
|
||||||
|
keyPath?: string;
|
||||||
|
};
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -168,6 +190,7 @@ export interface PortProxyRuleContext {
|
|||||||
|
|
||||||
export class DcRouter {
|
export class DcRouter {
|
||||||
public options: IDcRouterOptions;
|
public options: IDcRouterOptions;
|
||||||
|
public resolvedPaths: ReturnType<typeof paths.resolvePaths>;
|
||||||
|
|
||||||
// Core services
|
// Core services
|
||||||
public smartProxy?: plugins.smartproxy.SmartProxy;
|
public smartProxy?: plugins.smartproxy.SmartProxy;
|
||||||
@@ -183,16 +206,23 @@ export class DcRouter {
|
|||||||
public cacheDb?: CacheDb;
|
public cacheDb?: CacheDb;
|
||||||
public cacheCleaner?: CacheCleaner;
|
public cacheCleaner?: CacheCleaner;
|
||||||
|
|
||||||
// Certificate status tracking from SmartProxy events
|
// Remote Ingress
|
||||||
|
public remoteIngressManager?: RemoteIngressManager;
|
||||||
|
public tunnelManager?: TunnelManager;
|
||||||
|
|
||||||
|
// Certificate status tracking from SmartProxy events (keyed by domain)
|
||||||
public certificateStatusMap = new Map<string, {
|
public certificateStatusMap = new Map<string, {
|
||||||
status: 'valid' | 'failed';
|
status: 'valid' | 'failed';
|
||||||
domain: string;
|
routeNames: string[];
|
||||||
expiryDate?: string;
|
expiryDate?: string;
|
||||||
issuedAt?: string;
|
issuedAt?: string;
|
||||||
source?: string;
|
source?: string;
|
||||||
error?: string;
|
error?: string;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
|
// Certificate provisioning scheduler with per-domain backoff
|
||||||
|
public certProvisionScheduler?: CertProvisionScheduler;
|
||||||
|
|
||||||
// TypedRouter for API endpoints
|
// TypedRouter for API endpoints
|
||||||
public typedrouter = new plugins.typedrequest.TypedRouter();
|
public typedrouter = new plugins.typedrequest.TypedRouter();
|
||||||
|
|
||||||
@@ -205,6 +235,16 @@ export class DcRouter {
|
|||||||
...optionsArg
|
...optionsArg
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Resolve all data paths from baseDir
|
||||||
|
this.resolvedPaths = paths.resolvePaths(this.options.baseDir);
|
||||||
|
|
||||||
|
// Default storage to filesystem if not configured
|
||||||
|
if (!this.options.storage) {
|
||||||
|
this.options.storage = {
|
||||||
|
fsPath: this.resolvedPaths.defaultStoragePath,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
// Initialize storage manager
|
// Initialize storage manager
|
||||||
this.storageManager = new StorageManager(this.options.storage);
|
this.storageManager = new StorageManager(this.options.storage);
|
||||||
}
|
}
|
||||||
@@ -247,6 +287,11 @@ export class DcRouter {
|
|||||||
await this.setupRadiusServer();
|
await this.setupRadiusServer();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Set up Remote Ingress hub if configured
|
||||||
|
if (this.options.remoteIngressConfig?.enabled) {
|
||||||
|
await this.setupRemoteIngress();
|
||||||
|
}
|
||||||
|
|
||||||
this.logStartupSummary();
|
this.logStartupSummary();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('❌ Error starting DcRouter:', error);
|
console.error('❌ Error starting DcRouter:', error);
|
||||||
@@ -333,6 +378,16 @@ export class DcRouter {
|
|||||||
console.log(` └─ Accounting: ${this.options.radiusConfig.accounting?.enabled ? 'Enabled' : 'Disabled'}`);
|
console.log(` └─ Accounting: ${this.options.radiusConfig.accounting?.enabled ? 'Enabled' : 'Disabled'}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Remote Ingress summary
|
||||||
|
if (this.tunnelManager && this.options.remoteIngressConfig?.enabled) {
|
||||||
|
console.log('\n🌐 Remote Ingress:');
|
||||||
|
console.log(` ├─ Tunnel Port: ${this.options.remoteIngressConfig.tunnelPort || 8443}`);
|
||||||
|
const edgeCount = this.remoteIngressManager?.getAllEdges().length || 0;
|
||||||
|
const connectedCount = this.tunnelManager.getConnectedCount();
|
||||||
|
console.log(` ├─ Registered Edges: ${edgeCount}`);
|
||||||
|
console.log(` └─ Connected Edges: ${connectedCount}`);
|
||||||
|
}
|
||||||
|
|
||||||
// Storage summary
|
// Storage summary
|
||||||
if (this.storageManager && this.options.storage) {
|
if (this.storageManager && this.options.storage) {
|
||||||
console.log('\n💾 Storage:');
|
console.log('\n💾 Storage:');
|
||||||
@@ -360,7 +415,7 @@ export class DcRouter {
|
|||||||
|
|
||||||
// Initialize CacheDb singleton
|
// Initialize CacheDb singleton
|
||||||
this.cacheDb = CacheDb.getInstance({
|
this.cacheDb = CacheDb.getInstance({
|
||||||
storagePath: cacheConfig.storagePath || paths.defaultTsmDbPath,
|
storagePath: cacheConfig.storagePath || this.resolvedPaths.defaultTsmDbPath,
|
||||||
dbName: cacheConfig.dbName || 'dcrouter',
|
dbName: cacheConfig.dbName || 'dcrouter',
|
||||||
debug: false,
|
debug: false,
|
||||||
});
|
});
|
||||||
@@ -433,33 +488,81 @@ export class DcRouter {
|
|||||||
if (routes.length > 0 || this.options.smartProxyConfig) {
|
if (routes.length > 0 || this.options.smartProxyConfig) {
|
||||||
console.log('Setting up SmartProxy with combined configuration');
|
console.log('Setting up SmartProxy with combined configuration');
|
||||||
|
|
||||||
|
// Track cert entries loaded from cert store so we can populate certificateStatusMap after start
|
||||||
|
const loadedCertEntries: Array<{domain: string; publicKey: string; validUntil?: number; validFrom?: number}> = [];
|
||||||
|
|
||||||
// Create SmartProxy configuration
|
// Create SmartProxy configuration
|
||||||
const smartProxyConfig: plugins.smartproxy.ISmartProxyOptions = {
|
const smartProxyConfig: plugins.smartproxy.ISmartProxyOptions = {
|
||||||
...this.options.smartProxyConfig,
|
...this.options.smartProxyConfig,
|
||||||
routes,
|
routes,
|
||||||
acme: acmeConfig
|
acme: acmeConfig,
|
||||||
|
certStore: {
|
||||||
|
loadAll: async () => {
|
||||||
|
const keys = await this.storageManager.list('/proxy-certs/');
|
||||||
|
const certs: Array<{ domain: string; publicKey: string; privateKey: string; ca?: string }> = [];
|
||||||
|
for (const key of keys) {
|
||||||
|
const data = await this.storageManager.getJSON(key);
|
||||||
|
if (data) {
|
||||||
|
certs.push(data);
|
||||||
|
loadedCertEntries.push({ domain: data.domain, publicKey: data.publicKey, validUntil: data.validUntil, validFrom: data.validFrom });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return certs;
|
||||||
|
},
|
||||||
|
save: async (domain: string, publicKey: string, privateKey: string, ca?: string) => {
|
||||||
|
let validUntil: number | undefined;
|
||||||
|
let validFrom: number | undefined;
|
||||||
|
try {
|
||||||
|
const x509 = new plugins.crypto.X509Certificate(publicKey);
|
||||||
|
validUntil = new Date(x509.validTo).getTime();
|
||||||
|
validFrom = new Date(x509.validFrom).getTime();
|
||||||
|
} catch { /* PEM parsing failed */ }
|
||||||
|
await this.storageManager.setJSON(`/proxy-certs/${domain}`, {
|
||||||
|
domain, publicKey, privateKey, ca, validUntil, validFrom,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
remove: async (domain: string) => {
|
||||||
|
await this.storageManager.delete(`/proxy-certs/${domain}`);
|
||||||
|
},
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Initialize cert provision scheduler
|
||||||
|
this.certProvisionScheduler = new CertProvisionScheduler(this.storageManager);
|
||||||
|
|
||||||
// If we have DNS challenge handlers, create SmartAcme and wire to certProvisionFunction
|
// If we have DNS challenge handlers, create SmartAcme and wire to certProvisionFunction
|
||||||
if (challengeHandlers.length > 0) {
|
if (challengeHandlers.length > 0) {
|
||||||
this.smartAcme = new plugins.smartacme.SmartAcme({
|
this.smartAcme = new plugins.smartacme.SmartAcme({
|
||||||
accountEmail: acmeConfig?.accountEmail || this.options.tls?.contactEmail || 'admin@example.com',
|
accountEmail: acmeConfig?.accountEmail || this.options.tls?.contactEmail || 'admin@example.com',
|
||||||
certManager: new plugins.smartacme.certmanagers.MemoryCertManager(),
|
certManager: new StorageBackedCertManager(this.storageManager),
|
||||||
environment: 'production',
|
environment: 'production',
|
||||||
challengeHandlers: challengeHandlers,
|
challengeHandlers: challengeHandlers,
|
||||||
challengePriority: ['dns-01'],
|
challengePriority: ['dns-01'],
|
||||||
});
|
});
|
||||||
await this.smartAcme.start();
|
await this.smartAcme.start();
|
||||||
|
|
||||||
|
const scheduler = this.certProvisionScheduler;
|
||||||
smartProxyConfig.certProvisionFunction = async (domain, eventComms) => {
|
smartProxyConfig.certProvisionFunction = async (domain, eventComms) => {
|
||||||
|
// Check backoff before attempting provision
|
||||||
|
if (await scheduler.isInBackoff(domain)) {
|
||||||
|
const info = await scheduler.getBackoffInfo(domain);
|
||||||
|
const msg = `Domain ${domain} is in backoff (${info?.failures} failures), retry after ${info?.retryAfter}`;
|
||||||
|
eventComms.warn(msg);
|
||||||
|
throw new Error(msg);
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
// smartacme v9 handles concurrency, per-domain dedup, and rate limiting internally
|
||||||
eventComms.log(`Attempting DNS-01 via SmartAcme for ${domain}`);
|
eventComms.log(`Attempting DNS-01 via SmartAcme for ${domain}`);
|
||||||
eventComms.setSource('smartacme-dns-01');
|
eventComms.setSource('smartacme-dns-01');
|
||||||
const cert = await this.smartAcme.getCertificateForDomain(domain);
|
const isWildcardDomain = domain.startsWith('*.');
|
||||||
|
const cert = await this.smartAcme.getCertificateForDomain(domain, {
|
||||||
|
includeWildcard: !isWildcardDomain,
|
||||||
|
});
|
||||||
if (cert.validUntil) {
|
if (cert.validUntil) {
|
||||||
eventComms.setExpiryDate(new Date(cert.validUntil));
|
eventComms.setExpiryDate(new Date(cert.validUntil));
|
||||||
}
|
}
|
||||||
return {
|
const result = {
|
||||||
id: cert.id,
|
id: cert.id,
|
||||||
domainName: cert.domainName,
|
domainName: cert.domainName,
|
||||||
created: cert.created,
|
created: cert.created,
|
||||||
@@ -468,7 +571,13 @@ export class DcRouter {
|
|||||||
publicKey: cert.publicKey,
|
publicKey: cert.publicKey,
|
||||||
csr: cert.csr,
|
csr: cert.csr,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Success — clear any backoff
|
||||||
|
await scheduler.clearBackoff(domain);
|
||||||
|
return result;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
// 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`);
|
eventComms.warn(`SmartAcme DNS-01 failed for ${domain}: ${err.message}, falling back to http-01`);
|
||||||
return 'http01';
|
return 'http01';
|
||||||
}
|
}
|
||||||
@@ -491,48 +600,95 @@ export class DcRouter {
|
|||||||
console.error('[DcRouter] Error stack:', err.stack);
|
console.error('[DcRouter] Error stack:', err.stack);
|
||||||
});
|
});
|
||||||
|
|
||||||
if (acmeConfig) {
|
// Always listen for certificate events — emitted by both ACME and certProvisionFunction paths
|
||||||
|
// Events are keyed by domain for domain-centric certificate tracking
|
||||||
this.smartProxy.on('certificate-issued', (event: plugins.smartproxy.ICertificateIssuedEvent) => {
|
this.smartProxy.on('certificate-issued', (event: plugins.smartproxy.ICertificateIssuedEvent) => {
|
||||||
console.log(`[DcRouter] Certificate issued for ${event.domain} via ${event.source}, expires ${event.expiryDate}`);
|
console.log(`[DcRouter] Certificate issued for ${event.domain} via ${event.source}, expires ${event.expiryDate}`);
|
||||||
const routeName = this.findRouteNameForDomain(event.domain);
|
const routeNames = this.findRouteNamesForDomain(event.domain);
|
||||||
if (routeName) {
|
this.certificateStatusMap.set(event.domain, {
|
||||||
this.certificateStatusMap.set(routeName, {
|
status: 'valid', routeNames,
|
||||||
status: 'valid', domain: event.domain,
|
|
||||||
expiryDate: event.expiryDate, issuedAt: new Date().toISOString(),
|
expiryDate: event.expiryDate, issuedAt: new Date().toISOString(),
|
||||||
source: event.source,
|
source: event.source,
|
||||||
});
|
});
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
this.smartProxy.on('certificate-renewed', (event: plugins.smartproxy.ICertificateIssuedEvent) => {
|
this.smartProxy.on('certificate-renewed', (event: plugins.smartproxy.ICertificateIssuedEvent) => {
|
||||||
console.log(`[DcRouter] Certificate renewed for ${event.domain} via ${event.source}, expires ${event.expiryDate}`);
|
console.log(`[DcRouter] Certificate renewed for ${event.domain} via ${event.source}, expires ${event.expiryDate}`);
|
||||||
const routeName = this.findRouteNameForDomain(event.domain);
|
const routeNames = this.findRouteNamesForDomain(event.domain);
|
||||||
if (routeName) {
|
this.certificateStatusMap.set(event.domain, {
|
||||||
this.certificateStatusMap.set(routeName, {
|
status: 'valid', routeNames,
|
||||||
status: 'valid', domain: event.domain,
|
|
||||||
expiryDate: event.expiryDate, issuedAt: new Date().toISOString(),
|
expiryDate: event.expiryDate, issuedAt: new Date().toISOString(),
|
||||||
source: event.source,
|
source: event.source,
|
||||||
});
|
});
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
this.smartProxy.on('certificate-failed', (event: plugins.smartproxy.ICertificateFailedEvent) => {
|
this.smartProxy.on('certificate-failed', (event: plugins.smartproxy.ICertificateFailedEvent) => {
|
||||||
console.error(`[DcRouter] Certificate failed for ${event.domain} (${event.source}):`, event.error);
|
console.error(`[DcRouter] Certificate failed for ${event.domain} (${event.source}):`, event.error);
|
||||||
const routeName = this.findRouteNameForDomain(event.domain);
|
const routeNames = this.findRouteNamesForDomain(event.domain);
|
||||||
if (routeName) {
|
this.certificateStatusMap.set(event.domain, {
|
||||||
this.certificateStatusMap.set(routeName, {
|
status: 'failed', routeNames, error: event.error,
|
||||||
status: 'failed', domain: event.domain, error: event.error,
|
|
||||||
source: event.source,
|
source: event.source,
|
||||||
});
|
});
|
||||||
}
|
|
||||||
});
|
});
|
||||||
}
|
|
||||||
|
|
||||||
// Start SmartProxy
|
// Start SmartProxy
|
||||||
console.log('[DcRouter] Starting SmartProxy...');
|
console.log('[DcRouter] Starting SmartProxy...');
|
||||||
await this.smartProxy.start();
|
await this.smartProxy.start();
|
||||||
console.log('[DcRouter] SmartProxy started successfully');
|
console.log('[DcRouter] SmartProxy started successfully');
|
||||||
|
|
||||||
|
// Populate certificateStatusMap for certs loaded from store at startup
|
||||||
|
for (const entry of loadedCertEntries) {
|
||||||
|
if (!this.certificateStatusMap.has(entry.domain)) {
|
||||||
|
const routeNames = this.findRouteNamesForDomain(entry.domain);
|
||||||
|
let expiryDate: string | undefined;
|
||||||
|
let issuedAt: string | undefined;
|
||||||
|
|
||||||
|
// Use validUntil/validFrom from stored proxy-certs data if available
|
||||||
|
if (entry.validUntil) {
|
||||||
|
expiryDate = new Date(entry.validUntil).toISOString();
|
||||||
|
}
|
||||||
|
if (entry.validFrom) {
|
||||||
|
issuedAt = new Date(entry.validFrom).toISOString();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try SmartAcme /certs/ metadata as secondary source
|
||||||
|
if (!expiryDate) {
|
||||||
|
try {
|
||||||
|
const cleanDomain = entry.domain.replace(/^\*\.?/, '');
|
||||||
|
const certMeta = await this.storageManager.getJSON(`/certs/${cleanDomain}`);
|
||||||
|
if (certMeta?.validUntil) {
|
||||||
|
expiryDate = new Date(certMeta.validUntil).toISOString();
|
||||||
|
}
|
||||||
|
if (certMeta?.created && !issuedAt) {
|
||||||
|
issuedAt = new Date(certMeta.created).toISOString();
|
||||||
|
}
|
||||||
|
} catch { /* no metadata available */ }
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback: parse X509 from PEM to get expiry
|
||||||
|
if (!expiryDate && entry.publicKey) {
|
||||||
|
try {
|
||||||
|
const x509 = new plugins.crypto.X509Certificate(entry.publicKey);
|
||||||
|
expiryDate = new Date(x509.validTo).toISOString();
|
||||||
|
if (!issuedAt) {
|
||||||
|
issuedAt = new Date(x509.validFrom).toISOString();
|
||||||
|
}
|
||||||
|
} catch { /* PEM parsing failed */ }
|
||||||
|
}
|
||||||
|
|
||||||
|
this.certificateStatusMap.set(entry.domain, {
|
||||||
|
status: 'valid',
|
||||||
|
routeNames,
|
||||||
|
expiryDate,
|
||||||
|
issuedAt,
|
||||||
|
source: 'cert-store',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (loadedCertEntries.length > 0) {
|
||||||
|
console.log(`[DcRouter] Populated certificate status for ${loadedCertEntries.length} store-loaded domain(s)`);
|
||||||
|
}
|
||||||
|
|
||||||
console.log(`SmartProxy started with ${routes.length} routes`);
|
console.log(`SmartProxy started with ${routes.length} routes`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -675,29 +831,30 @@ export class DcRouter {
|
|||||||
* @returns Whether the domain matches the pattern
|
* @returns Whether the domain matches the pattern
|
||||||
*/
|
*/
|
||||||
private isDomainMatch(domain: string, pattern: string): boolean {
|
private isDomainMatch(domain: string, pattern: string): boolean {
|
||||||
// Normalize inputs
|
|
||||||
domain = domain.toLowerCase();
|
domain = domain.toLowerCase();
|
||||||
pattern = pattern.toLowerCase();
|
pattern = pattern.toLowerCase();
|
||||||
|
|
||||||
// Check for exact match
|
if (domain === pattern) return true;
|
||||||
if (domain === pattern) {
|
|
||||||
return true;
|
// Routing-glob: *example.com matches example.com, sub.example.com, *.example.com
|
||||||
|
if (pattern.startsWith('*') && !pattern.startsWith('*.')) {
|
||||||
|
const baseDomain = pattern.slice(1); // *nevermind.cloud → nevermind.cloud
|
||||||
|
if (domain === baseDomain || domain === `*.${baseDomain}`) return true;
|
||||||
|
if (domain.endsWith(baseDomain) && domain.length > baseDomain.length) return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check for wildcard match (*.example.com)
|
// Standard wildcard: *.example.com matches sub.example.com and example.com
|
||||||
if (pattern.startsWith('*.')) {
|
if (pattern.startsWith('*.')) {
|
||||||
const patternSuffix = pattern.slice(2); // Remove the "*." prefix
|
const suffix = pattern.slice(2);
|
||||||
|
if (domain === suffix) return true;
|
||||||
// Check if domain ends with the pattern suffix and has at least one character before it
|
return domain.endsWith(suffix) && domain.length > suffix.length;
|
||||||
return domain.endsWith(patternSuffix) && domain.length > patternSuffix.length;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// No match
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Find the route name that matches a given domain
|
* Find the first route name that matches a given domain
|
||||||
*/
|
*/
|
||||||
private findRouteNameForDomain(domain: string): string | undefined {
|
private findRouteNameForDomain(domain: string): string | undefined {
|
||||||
if (!this.smartProxy) return undefined;
|
if (!this.smartProxy) return undefined;
|
||||||
@@ -706,11 +863,34 @@ export class DcRouter {
|
|||||||
const routeDomains = Array.isArray(route.match.domains)
|
const routeDomains = Array.isArray(route.match.domains)
|
||||||
? route.match.domains
|
? route.match.domains
|
||||||
: [route.match.domains];
|
: [route.match.domains];
|
||||||
if (routeDomains.includes(domain)) return route.name;
|
for (const pattern of routeDomains) {
|
||||||
|
if (this.isDomainMatch(domain, pattern)) return route.name;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Find ALL route names that match a given domain
|
||||||
|
*/
|
||||||
|
public findRouteNamesForDomain(domain: string): string[] {
|
||||||
|
if (!this.smartProxy) return [];
|
||||||
|
const names: string[] = [];
|
||||||
|
for (const route of this.smartProxy.routeManager.getRoutes()) {
|
||||||
|
if (!route.match.domains || !route.name) continue;
|
||||||
|
const routeDomains = Array.isArray(route.match.domains)
|
||||||
|
? route.match.domains
|
||||||
|
: [route.match.domains];
|
||||||
|
for (const pattern of routeDomains) {
|
||||||
|
if (this.isDomainMatch(domain, pattern)) {
|
||||||
|
names.push(route.name);
|
||||||
|
break; // This route already matched, no need to check other patterns
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return names;
|
||||||
|
}
|
||||||
|
|
||||||
public async stop() {
|
public async stop() {
|
||||||
console.log('Stopping DcRouter services...');
|
console.log('Stopping DcRouter services...');
|
||||||
|
|
||||||
@@ -742,6 +922,11 @@ export class DcRouter {
|
|||||||
// Stop RADIUS server if running
|
// Stop RADIUS server if running
|
||||||
this.radiusServer ?
|
this.radiusServer ?
|
||||||
this.radiusServer.stop().catch(err => console.error('Error stopping RADIUS server:', err)) :
|
this.radiusServer.stop().catch(err => console.error('Error stopping RADIUS server:', err)) :
|
||||||
|
Promise.resolve(),
|
||||||
|
|
||||||
|
// Stop Remote Ingress tunnel manager if running
|
||||||
|
this.tunnelManager ?
|
||||||
|
this.tunnelManager.stop().catch(err => console.error('Error stopping TunnelManager:', err)) :
|
||||||
Promise.resolve()
|
Promise.resolve()
|
||||||
]);
|
]);
|
||||||
|
|
||||||
@@ -1169,7 +1354,7 @@ export class DcRouter {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
// Ensure paths are imported
|
// Ensure paths are imported
|
||||||
const dnsDir = paths.dnsRecordsDir;
|
const dnsDir = this.resolvedPaths.dnsRecordsDir;
|
||||||
|
|
||||||
// Check if directory exists
|
// Check if directory exists
|
||||||
if (!plugins.fs.existsSync(dnsDir)) {
|
if (!plugins.fs.existsSync(dnsDir)) {
|
||||||
@@ -1233,7 +1418,7 @@ export class DcRouter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Ensure necessary directories exist
|
// Ensure necessary directories exist
|
||||||
paths.ensureDirectories();
|
paths.ensureDataDirectories(this.resolvedPaths);
|
||||||
|
|
||||||
// Generate DKIM keys for each email domain
|
// Generate DKIM keys for each email domain
|
||||||
for (const domainConfig of this.options.emailConfig.domains) {
|
for (const domainConfig of this.options.emailConfig.domains) {
|
||||||
@@ -1388,6 +1573,31 @@ export class DcRouter {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set up Remote Ingress hub for edge tunnel connections
|
||||||
|
*/
|
||||||
|
private async setupRemoteIngress(): Promise<void> {
|
||||||
|
if (!this.options.remoteIngressConfig?.enabled) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.log('info', 'Setting up Remote Ingress hub...');
|
||||||
|
|
||||||
|
// Initialize the edge registration manager
|
||||||
|
this.remoteIngressManager = new RemoteIngressManager(this.storageManager);
|
||||||
|
await this.remoteIngressManager.initialize();
|
||||||
|
|
||||||
|
// Create and start the tunnel manager
|
||||||
|
this.tunnelManager = new TunnelManager(this.remoteIngressManager, {
|
||||||
|
tunnelPort: this.options.remoteIngressConfig.tunnelPort ?? 8443,
|
||||||
|
targetHost: '127.0.0.1',
|
||||||
|
});
|
||||||
|
await this.tunnelManager.start();
|
||||||
|
|
||||||
|
const edgeCount = this.remoteIngressManager.getAllEdges().length;
|
||||||
|
logger.log('info', `Remote Ingress hub started on port ${this.options.remoteIngressConfig.tunnelPort || 8443} with ${edgeCount} registered edge(s)`);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Set up RADIUS server for network authentication
|
* Set up RADIUS server for network authentication
|
||||||
*/
|
*/
|
||||||
|
|||||||
46
ts/classes.storage-cert-manager.ts
Normal file
46
ts/classes.storage-cert-manager.ts
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
import * as plugins from './plugins.js';
|
||||||
|
import { StorageManager } from './storage/index.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ICertManager implementation backed by StorageManager.
|
||||||
|
* Persists SmartAcme certificates under a /certs/ key prefix so they
|
||||||
|
* survive process restarts without re-hitting ACME.
|
||||||
|
*/
|
||||||
|
export class StorageBackedCertManager implements plugins.smartacme.ICertManager {
|
||||||
|
private keyPrefix = '/certs/';
|
||||||
|
|
||||||
|
constructor(private storageManager: StorageManager) {}
|
||||||
|
|
||||||
|
async init(): Promise<void> {}
|
||||||
|
|
||||||
|
async retrieveCertificate(domainName: string): Promise<plugins.smartacme.Cert | null> {
|
||||||
|
const data = await this.storageManager.getJSON(this.keyPrefix + domainName);
|
||||||
|
if (!data) return null;
|
||||||
|
return new plugins.smartacme.Cert(data);
|
||||||
|
}
|
||||||
|
|
||||||
|
async storeCertificate(cert: plugins.smartacme.Cert): Promise<void> {
|
||||||
|
await this.storageManager.setJSON(this.keyPrefix + cert.domainName, {
|
||||||
|
id: cert.id,
|
||||||
|
domainName: cert.domainName,
|
||||||
|
created: cert.created,
|
||||||
|
privateKey: cert.privateKey,
|
||||||
|
publicKey: cert.publicKey,
|
||||||
|
csr: cert.csr,
|
||||||
|
validUntil: cert.validUntil,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async deleteCertificate(domainName: string): Promise<void> {
|
||||||
|
await this.storageManager.delete(this.keyPrefix + domainName);
|
||||||
|
}
|
||||||
|
|
||||||
|
async close(): Promise<void> {}
|
||||||
|
|
||||||
|
async wipe(): Promise<void> {
|
||||||
|
const keys = await this.storageManager.list(this.keyPrefix);
|
||||||
|
for (const key of keys) {
|
||||||
|
await this.storageManager.delete(key);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -10,4 +10,7 @@ export * from './classes.dcrouter.js';
|
|||||||
// RADIUS module
|
// RADIUS module
|
||||||
export * from './radius/index.js';
|
export * from './radius/index.js';
|
||||||
|
|
||||||
|
// Remote Ingress module
|
||||||
|
export * from './remoteingress/index.js';
|
||||||
|
|
||||||
export const runCli = async () => {};
|
export const runCli = async () => {};
|
||||||
|
|||||||
@@ -489,8 +489,12 @@ export class MetricsManager {
|
|||||||
return {
|
return {
|
||||||
connectionsByIP: new Map<string, number>(),
|
connectionsByIP: new Map<string, number>(),
|
||||||
throughputRate: { bytesInPerSecond: 0, bytesOutPerSecond: 0 },
|
throughputRate: { bytesInPerSecond: 0, bytesOutPerSecond: 0 },
|
||||||
topIPs: [],
|
topIPs: [] as Array<{ ip: string; count: number }>,
|
||||||
totalDataTransferred: { bytesIn: 0, bytesOut: 0 },
|
totalDataTransferred: { bytesIn: 0, bytesOut: 0 },
|
||||||
|
throughputHistory: [] as Array<{ timestamp: number; in: number; out: number }>,
|
||||||
|
throughputByIP: new Map<string, { in: number; out: number }>(),
|
||||||
|
requestsPerSecond: 0,
|
||||||
|
requestsTotal: 0,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -513,11 +517,25 @@ export class MetricsManager {
|
|||||||
bytesOut: proxyMetrics.totals.bytesOut()
|
bytesOut: proxyMetrics.totals.bytesOut()
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Get throughput history from Rust engine (up to 300 seconds)
|
||||||
|
const throughputHistory = proxyMetrics.throughput.history(300);
|
||||||
|
|
||||||
|
// Get per-IP throughput
|
||||||
|
const throughputByIP = proxyMetrics.throughput.byIP();
|
||||||
|
|
||||||
|
// Get HTTP request rates
|
||||||
|
const requestsPerSecond = proxyMetrics.requests.perSecond();
|
||||||
|
const requestsTotal = proxyMetrics.requests.total();
|
||||||
|
|
||||||
return {
|
return {
|
||||||
connectionsByIP,
|
connectionsByIP,
|
||||||
throughputRate,
|
throughputRate,
|
||||||
topIPs,
|
topIPs,
|
||||||
totalDataTransferred,
|
totalDataTransferred,
|
||||||
|
throughputHistory,
|
||||||
|
throughputByIP,
|
||||||
|
requestsPerSecond,
|
||||||
|
requestsTotal,
|
||||||
};
|
};
|
||||||
}, 200); // Use 200ms cache for more frequent updates
|
}, 200); // Use 200ms cache for more frequent updates
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ export class OpsServer {
|
|||||||
private radiusHandler: handlers.RadiusHandler;
|
private radiusHandler: handlers.RadiusHandler;
|
||||||
private emailOpsHandler: handlers.EmailOpsHandler;
|
private emailOpsHandler: handlers.EmailOpsHandler;
|
||||||
private certificateHandler: handlers.CertificateHandler;
|
private certificateHandler: handlers.CertificateHandler;
|
||||||
|
private remoteIngressHandler: handlers.RemoteIngressHandler;
|
||||||
|
|
||||||
constructor(dcRouterRefArg: DcRouter) {
|
constructor(dcRouterRefArg: DcRouter) {
|
||||||
this.dcRouterRef = dcRouterRefArg;
|
this.dcRouterRef = dcRouterRefArg;
|
||||||
@@ -59,6 +60,7 @@ export class OpsServer {
|
|||||||
this.radiusHandler = new handlers.RadiusHandler(this);
|
this.radiusHandler = new handlers.RadiusHandler(this);
|
||||||
this.emailOpsHandler = new handlers.EmailOpsHandler(this);
|
this.emailOpsHandler = new handlers.EmailOpsHandler(this);
|
||||||
this.certificateHandler = new handlers.CertificateHandler(this);
|
this.certificateHandler = new handlers.CertificateHandler(this);
|
||||||
|
this.remoteIngressHandler = new handlers.RemoteIngressHandler(this);
|
||||||
|
|
||||||
console.log('✅ OpsServer TypedRequest handlers initialized');
|
console.log('✅ OpsServer TypedRequest handlers initialized');
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,24 +23,45 @@ export class CertificateHandler {
|
|||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
// Reprovision Certificate
|
// Legacy route-based reprovision (backward compat)
|
||||||
this.typedrouter.addTypedHandler(
|
this.typedrouter.addTypedHandler(
|
||||||
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_ReprovisionCertificate>(
|
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_ReprovisionCertificate>(
|
||||||
'reprovisionCertificate',
|
'reprovisionCertificate',
|
||||||
async (dataArg) => {
|
async (dataArg) => {
|
||||||
return this.reprovisionCertificate(dataArg.routeName);
|
return this.reprovisionCertificateByRoute(dataArg.routeName);
|
||||||
|
}
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
// Domain-based reprovision (preferred)
|
||||||
|
this.typedrouter.addTypedHandler(
|
||||||
|
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_ReprovisionCertificateDomain>(
|
||||||
|
'reprovisionCertificateDomain',
|
||||||
|
async (dataArg) => {
|
||||||
|
return this.reprovisionCertificateDomain(dataArg.domain);
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Build domain-centric certificate overview.
|
||||||
|
* Instead of one row per route, we produce one row per unique domain.
|
||||||
|
*/
|
||||||
private async buildCertificateOverview(): Promise<interfaces.requests.ICertificateInfo[]> {
|
private async buildCertificateOverview(): Promise<interfaces.requests.ICertificateInfo[]> {
|
||||||
const dcRouter = this.opsServerRef.dcRouterRef;
|
const dcRouter = this.opsServerRef.dcRouterRef;
|
||||||
const smartProxy = dcRouter.smartProxy;
|
const smartProxy = dcRouter.smartProxy;
|
||||||
if (!smartProxy) return [];
|
if (!smartProxy) return [];
|
||||||
|
|
||||||
const routes = smartProxy.routeManager.getRoutes();
|
const routes = smartProxy.routeManager.getRoutes();
|
||||||
const certificates: interfaces.requests.ICertificateInfo[] = [];
|
|
||||||
|
// Phase 1: Collect unique domains with their associated route info
|
||||||
|
const domainMap = new Map<string, {
|
||||||
|
routeNames: string[];
|
||||||
|
source: interfaces.requests.TCertificateSource;
|
||||||
|
tlsMode: 'terminate' | 'terminate-and-reencrypt' | 'passthrough';
|
||||||
|
canReprovision: boolean;
|
||||||
|
}>();
|
||||||
|
|
||||||
for (const route of routes) {
|
for (const route of routes) {
|
||||||
if (!route.name) continue;
|
if (!route.name) continue;
|
||||||
@@ -58,7 +79,6 @@ export class CertificateHandler {
|
|||||||
// Determine source
|
// Determine source
|
||||||
let source: interfaces.requests.TCertificateSource = 'none';
|
let source: interfaces.requests.TCertificateSource = 'none';
|
||||||
if (tls.certificate === 'auto') {
|
if (tls.certificate === 'auto') {
|
||||||
// Check if a certProvisionFunction is configured
|
|
||||||
if ((smartProxy.settings as any).certProvisionFunction) {
|
if ((smartProxy.settings as any).certProvisionFunction) {
|
||||||
source = 'provision-function';
|
source = 'provision-function';
|
||||||
} else {
|
} else {
|
||||||
@@ -68,15 +88,44 @@ export class CertificateHandler {
|
|||||||
source = 'static';
|
source = 'static';
|
||||||
}
|
}
|
||||||
|
|
||||||
// Start with unknown status
|
const canReprovision = source === 'acme' || source === 'provision-function';
|
||||||
|
const tlsMode = tls.mode as 'terminate' | 'terminate-and-reencrypt' | 'passthrough';
|
||||||
|
|
||||||
|
for (const domain of routeDomains) {
|
||||||
|
const existing = domainMap.get(domain);
|
||||||
|
if (existing) {
|
||||||
|
// Add this route name to the existing domain entry
|
||||||
|
if (!existing.routeNames.includes(route.name)) {
|
||||||
|
existing.routeNames.push(route.name);
|
||||||
|
}
|
||||||
|
// Upgrade source if more specific
|
||||||
|
if (existing.source === 'none' && source !== 'none') {
|
||||||
|
existing.source = source;
|
||||||
|
existing.canReprovision = canReprovision;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
domainMap.set(domain, {
|
||||||
|
routeNames: [route.name],
|
||||||
|
source,
|
||||||
|
tlsMode,
|
||||||
|
canReprovision,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Phase 2: Resolve status for each unique domain
|
||||||
|
const certificates: interfaces.requests.ICertificateInfo[] = [];
|
||||||
|
|
||||||
|
for (const [domain, info] of domainMap) {
|
||||||
let status: interfaces.requests.TCertificateStatus = 'unknown';
|
let status: interfaces.requests.TCertificateStatus = 'unknown';
|
||||||
let expiryDate: string | undefined;
|
let expiryDate: string | undefined;
|
||||||
let issuedAt: string | undefined;
|
let issuedAt: string | undefined;
|
||||||
let issuer: string | undefined;
|
let issuer: string | undefined;
|
||||||
let error: string | undefined;
|
let error: string | undefined;
|
||||||
|
|
||||||
// Check event-based status from DcRouter's certificateStatusMap
|
// Check event-based status from certificateStatusMap (now keyed by domain)
|
||||||
const eventStatus = dcRouter.certificateStatusMap.get(route.name);
|
const eventStatus = dcRouter.certificateStatusMap.get(domain);
|
||||||
if (eventStatus) {
|
if (eventStatus) {
|
||||||
status = eventStatus.status;
|
status = eventStatus.status;
|
||||||
expiryDate = eventStatus.expiryDate;
|
expiryDate = eventStatus.expiryDate;
|
||||||
@@ -87,10 +136,10 @@ export class CertificateHandler {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Try Rust-side certificate status if no event data
|
// Try SmartProxy certificate status if no event data
|
||||||
if (status === 'unknown') {
|
if (status === 'unknown' && info.routeNames.length > 0) {
|
||||||
try {
|
try {
|
||||||
const rustStatus = await smartProxy.getCertificateStatus(route.name);
|
const rustStatus = await smartProxy.getCertificateStatus(info.routeNames[0]);
|
||||||
if (rustStatus) {
|
if (rustStatus) {
|
||||||
if (rustStatus.expiryDate) expiryDate = rustStatus.expiryDate;
|
if (rustStatus.expiryDate) expiryDate = rustStatus.expiryDate;
|
||||||
if (rustStatus.issuer) issuer = rustStatus.issuer;
|
if (rustStatus.issuer) issuer = rustStatus.issuer;
|
||||||
@@ -104,7 +153,36 @@ export class CertificateHandler {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Compute status from expiry date if we have one and status is still valid/unknown
|
// Check persisted cert data from StorageManager
|
||||||
|
if (status === 'unknown') {
|
||||||
|
const cleanDomain = domain.replace(/^\*\.?/, '');
|
||||||
|
let certData = await dcRouter.storageManager.getJSON(`/certs/${cleanDomain}`);
|
||||||
|
if (!certData) {
|
||||||
|
// Also check certStore path (proxy-certs)
|
||||||
|
certData = await dcRouter.storageManager.getJSON(`/proxy-certs/${domain}`);
|
||||||
|
}
|
||||||
|
if (certData?.validUntil) {
|
||||||
|
expiryDate = new Date(certData.validUntil).toISOString();
|
||||||
|
if (certData.created) {
|
||||||
|
issuedAt = new Date(certData.created).toISOString();
|
||||||
|
}
|
||||||
|
issuer = 'smartacme-dns-01';
|
||||||
|
} else if (certData?.publicKey) {
|
||||||
|
// certStore has the cert — parse PEM for expiry
|
||||||
|
try {
|
||||||
|
const x509 = new plugins.crypto.X509Certificate(certData.publicKey);
|
||||||
|
expiryDate = new Date(x509.validTo).toISOString();
|
||||||
|
issuedAt = new Date(x509.validFrom).toISOString();
|
||||||
|
} catch { /* PEM parsing failed */ }
|
||||||
|
status = 'valid';
|
||||||
|
issuer = 'cert-store';
|
||||||
|
} else if (certData) {
|
||||||
|
status = 'valid';
|
||||||
|
issuer = 'cert-store';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Compute status from expiry date
|
||||||
if (expiryDate && (status === 'valid' || status === 'unknown')) {
|
if (expiryDate && (status === 'valid' || status === 'unknown')) {
|
||||||
const expiry = new Date(expiryDate);
|
const expiry = new Date(expiryDate);
|
||||||
const now = new Date();
|
const now = new Date();
|
||||||
@@ -120,23 +198,36 @@ export class CertificateHandler {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Static certs with no other info default to 'valid'
|
// Static certs with no other info default to 'valid'
|
||||||
if (source === 'static' && status === 'unknown') {
|
if (info.source === 'static' && status === 'unknown') {
|
||||||
status = 'valid';
|
status = 'valid';
|
||||||
}
|
}
|
||||||
|
|
||||||
const canReprovision = source === 'acme' || source === 'provision-function';
|
// ACME/provision-function routes with no cert data are still provisioning
|
||||||
|
if (status === 'unknown' && (info.source === 'acme' || info.source === 'provision-function')) {
|
||||||
|
status = 'provisioning';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Phase 3: Attach backoff info
|
||||||
|
let backoffInfo: interfaces.requests.ICertificateInfo['backoffInfo'];
|
||||||
|
if (dcRouter.certProvisionScheduler) {
|
||||||
|
const bi = await dcRouter.certProvisionScheduler.getBackoffInfo(domain);
|
||||||
|
if (bi) {
|
||||||
|
backoffInfo = bi;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
certificates.push({
|
certificates.push({
|
||||||
routeName: route.name,
|
domain,
|
||||||
domains: routeDomains,
|
routeNames: info.routeNames,
|
||||||
status,
|
status,
|
||||||
source,
|
source: info.source,
|
||||||
tlsMode: tls.mode as 'terminate' | 'terminate-and-reencrypt' | 'passthrough',
|
tlsMode: info.tlsMode,
|
||||||
expiryDate,
|
expiryDate,
|
||||||
issuer,
|
issuer,
|
||||||
issuedAt,
|
issuedAt,
|
||||||
error,
|
error,
|
||||||
canReprovision,
|
canReprovision: info.canReprovision,
|
||||||
|
backoffInfo,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -166,7 +257,10 @@ export class CertificateHandler {
|
|||||||
return summary;
|
return summary;
|
||||||
}
|
}
|
||||||
|
|
||||||
private async reprovisionCertificate(routeName: string): Promise<{ success: boolean; message?: string }> {
|
/**
|
||||||
|
* Legacy route-based reprovisioning
|
||||||
|
*/
|
||||||
|
private async reprovisionCertificateByRoute(routeName: string): Promise<{ success: boolean; message?: string }> {
|
||||||
const dcRouter = this.opsServerRef.dcRouterRef;
|
const dcRouter = this.opsServerRef.dcRouterRef;
|
||||||
const smartProxy = dcRouter.smartProxy;
|
const smartProxy = dcRouter.smartProxy;
|
||||||
|
|
||||||
@@ -176,11 +270,58 @@ export class CertificateHandler {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
await smartProxy.provisionCertificate(routeName);
|
await smartProxy.provisionCertificate(routeName);
|
||||||
// Clear event-based status so it gets refreshed
|
// Clear event-based status for domains in this route
|
||||||
dcRouter.certificateStatusMap.delete(routeName);
|
for (const [domain, entry] of dcRouter.certificateStatusMap) {
|
||||||
|
if (entry.routeNames.includes(routeName)) {
|
||||||
|
dcRouter.certificateStatusMap.delete(domain);
|
||||||
|
}
|
||||||
|
}
|
||||||
return { success: true, message: `Certificate reprovisioning triggered for route '${routeName}'` };
|
return { success: true, message: `Certificate reprovisioning triggered for route '${routeName}'` };
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
return { success: false, message: err.message || 'Failed to reprovision certificate' };
|
return { success: false, message: err.message || 'Failed to reprovision certificate' };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Domain-based reprovisioning — clears backoff first, then triggers provision
|
||||||
|
*/
|
||||||
|
private async reprovisionCertificateDomain(domain: string): Promise<{ success: boolean; message?: string }> {
|
||||||
|
const dcRouter = this.opsServerRef.dcRouterRef;
|
||||||
|
const smartProxy = dcRouter.smartProxy;
|
||||||
|
|
||||||
|
if (!smartProxy) {
|
||||||
|
return { success: false, message: 'SmartProxy is not running' };
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clear backoff for this domain (user override)
|
||||||
|
if (dcRouter.certProvisionScheduler) {
|
||||||
|
await dcRouter.certProvisionScheduler.clearBackoff(domain);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clear status map entry so it gets refreshed
|
||||||
|
dcRouter.certificateStatusMap.delete(domain);
|
||||||
|
|
||||||
|
// Try to provision via SmartAcme directly
|
||||||
|
if (dcRouter.smartAcme) {
|
||||||
|
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}` };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback: try provisioning via the first matching route
|
||||||
|
const routeNames = dcRouter.findRouteNamesForDomain(domain);
|
||||||
|
if (routeNames.length > 0) {
|
||||||
|
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}` };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return { success: false, message: `No routes found for domain '${domain}'` };
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,3 +6,4 @@ export * from './stats.handler.js';
|
|||||||
export * from './radius.handler.js';
|
export * from './radius.handler.js';
|
||||||
export * from './email-ops.handler.js';
|
export * from './email-ops.handler.js';
|
||||||
export * from './certificate.handler.js';
|
export * from './certificate.handler.js';
|
||||||
|
export * from './remoteingress.handler.js';
|
||||||
163
ts/opsserver/handlers/remoteingress.handler.ts
Normal file
163
ts/opsserver/handlers/remoteingress.handler.ts
Normal file
@@ -0,0 +1,163 @@
|
|||||||
|
import * as plugins from '../../plugins.js';
|
||||||
|
import type { OpsServer } from '../classes.opsserver.js';
|
||||||
|
import * as interfaces from '../../../ts_interfaces/index.js';
|
||||||
|
|
||||||
|
export class RemoteIngressHandler {
|
||||||
|
public typedrouter = new plugins.typedrequest.TypedRouter();
|
||||||
|
|
||||||
|
constructor(private opsServerRef: OpsServer) {
|
||||||
|
this.opsServerRef.typedrouter.addTypedRouter(this.typedrouter);
|
||||||
|
this.registerHandlers();
|
||||||
|
}
|
||||||
|
|
||||||
|
private registerHandlers(): void {
|
||||||
|
// Get all remote ingress edges
|
||||||
|
this.typedrouter.addTypedHandler(
|
||||||
|
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_GetRemoteIngresses>(
|
||||||
|
'getRemoteIngresses',
|
||||||
|
async (dataArg, toolsArg) => {
|
||||||
|
const manager = this.opsServerRef.dcRouterRef.remoteIngressManager;
|
||||||
|
if (!manager) {
|
||||||
|
return { edges: [] };
|
||||||
|
}
|
||||||
|
// Return edges without secrets
|
||||||
|
const edges = manager.getAllEdges().map((e) => ({
|
||||||
|
...e,
|
||||||
|
secret: '********', // Never expose secrets via API
|
||||||
|
}));
|
||||||
|
return { edges };
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Create a new remote ingress edge
|
||||||
|
this.typedrouter.addTypedHandler(
|
||||||
|
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_CreateRemoteIngress>(
|
||||||
|
'createRemoteIngress',
|
||||||
|
async (dataArg, toolsArg) => {
|
||||||
|
const manager = this.opsServerRef.dcRouterRef.remoteIngressManager;
|
||||||
|
const tunnelManager = this.opsServerRef.dcRouterRef.tunnelManager;
|
||||||
|
|
||||||
|
if (!manager) {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
edge: null as any,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const edge = await manager.createEdge(
|
||||||
|
dataArg.name,
|
||||||
|
dataArg.listenPorts,
|
||||||
|
dataArg.tags,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Sync allowed edges with the hub
|
||||||
|
if (tunnelManager) {
|
||||||
|
await tunnelManager.syncAllowedEdges();
|
||||||
|
}
|
||||||
|
|
||||||
|
return { success: true, edge };
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Delete a remote ingress edge
|
||||||
|
this.typedrouter.addTypedHandler(
|
||||||
|
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_DeleteRemoteIngress>(
|
||||||
|
'deleteRemoteIngress',
|
||||||
|
async (dataArg, toolsArg) => {
|
||||||
|
const manager = this.opsServerRef.dcRouterRef.remoteIngressManager;
|
||||||
|
const tunnelManager = this.opsServerRef.dcRouterRef.tunnelManager;
|
||||||
|
|
||||||
|
if (!manager) {
|
||||||
|
return { success: false, message: 'RemoteIngress not configured' };
|
||||||
|
}
|
||||||
|
|
||||||
|
const deleted = await manager.deleteEdge(dataArg.id);
|
||||||
|
if (deleted && tunnelManager) {
|
||||||
|
await tunnelManager.syncAllowedEdges();
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: deleted,
|
||||||
|
message: deleted ? undefined : 'Edge not found',
|
||||||
|
};
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Update a remote ingress edge
|
||||||
|
this.typedrouter.addTypedHandler(
|
||||||
|
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_UpdateRemoteIngress>(
|
||||||
|
'updateRemoteIngress',
|
||||||
|
async (dataArg, toolsArg) => {
|
||||||
|
const manager = this.opsServerRef.dcRouterRef.remoteIngressManager;
|
||||||
|
const tunnelManager = this.opsServerRef.dcRouterRef.tunnelManager;
|
||||||
|
|
||||||
|
if (!manager) {
|
||||||
|
return { success: false, edge: null as any };
|
||||||
|
}
|
||||||
|
|
||||||
|
const edge = await manager.updateEdge(dataArg.id, {
|
||||||
|
name: dataArg.name,
|
||||||
|
listenPorts: dataArg.listenPorts,
|
||||||
|
enabled: dataArg.enabled,
|
||||||
|
tags: dataArg.tags,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!edge) {
|
||||||
|
return { success: false, edge: null as any };
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sync allowed edges if enabled status changed
|
||||||
|
if (tunnelManager && dataArg.enabled !== undefined) {
|
||||||
|
await tunnelManager.syncAllowedEdges();
|
||||||
|
}
|
||||||
|
|
||||||
|
return { success: true, edge: { ...edge, secret: '********' } };
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Regenerate secret for an edge
|
||||||
|
this.typedrouter.addTypedHandler(
|
||||||
|
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_RegenerateRemoteIngressSecret>(
|
||||||
|
'regenerateRemoteIngressSecret',
|
||||||
|
async (dataArg, toolsArg) => {
|
||||||
|
const manager = this.opsServerRef.dcRouterRef.remoteIngressManager;
|
||||||
|
const tunnelManager = this.opsServerRef.dcRouterRef.tunnelManager;
|
||||||
|
|
||||||
|
if (!manager) {
|
||||||
|
return { success: false, secret: '' };
|
||||||
|
}
|
||||||
|
|
||||||
|
const secret = await manager.regenerateSecret(dataArg.id);
|
||||||
|
if (!secret) {
|
||||||
|
return { success: false, secret: '' };
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sync allowed edges since secret changed
|
||||||
|
if (tunnelManager) {
|
||||||
|
await tunnelManager.syncAllowedEdges();
|
||||||
|
}
|
||||||
|
|
||||||
|
return { success: true, secret };
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Get runtime status of all edges
|
||||||
|
this.typedrouter.addTypedHandler(
|
||||||
|
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_GetRemoteIngressStatus>(
|
||||||
|
'getRemoteIngressStatus',
|
||||||
|
async (dataArg, toolsArg) => {
|
||||||
|
const tunnelManager = this.opsServerRef.dcRouterRef.tunnelManager;
|
||||||
|
if (!tunnelManager) {
|
||||||
|
return { statuses: [] };
|
||||||
|
}
|
||||||
|
return { statuses: tunnelManager.getEdgeStatuses() };
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -85,11 +85,23 @@ export class SecurityHandler {
|
|||||||
if (this.opsServerRef.dcRouterRef.metricsManager) {
|
if (this.opsServerRef.dcRouterRef.metricsManager) {
|
||||||
const networkStats = await this.opsServerRef.dcRouterRef.metricsManager.getNetworkStats();
|
const networkStats = await this.opsServerRef.dcRouterRef.metricsManager.getNetworkStats();
|
||||||
|
|
||||||
|
// Convert per-IP throughput Map to serializable array
|
||||||
|
const throughputByIP: Array<{ ip: string; in: number; out: number }> = [];
|
||||||
|
if (networkStats.throughputByIP) {
|
||||||
|
for (const [ip, tp] of networkStats.throughputByIP) {
|
||||||
|
throughputByIP.push({ ip, in: tp.in, out: tp.out });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
connectionsByIP: Array.from(networkStats.connectionsByIP.entries()).map(([ip, count]) => ({ ip, count })),
|
connectionsByIP: Array.from(networkStats.connectionsByIP.entries()).map(([ip, count]) => ({ ip, count })),
|
||||||
throughputRate: networkStats.throughputRate,
|
throughputRate: networkStats.throughputRate,
|
||||||
topIPs: networkStats.topIPs,
|
topIPs: networkStats.topIPs,
|
||||||
totalDataTransferred: networkStats.totalDataTransferred,
|
totalDataTransferred: networkStats.totalDataTransferred,
|
||||||
|
throughputHistory: networkStats.throughputHistory || [],
|
||||||
|
throughputByIP,
|
||||||
|
requestsPerSecond: networkStats.requestsPerSecond || 0,
|
||||||
|
requestsTotal: networkStats.requestsTotal || 0,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -99,6 +111,10 @@ export class SecurityHandler {
|
|||||||
throughputRate: { bytesInPerSecond: 0, bytesOutPerSecond: 0 },
|
throughputRate: { bytesInPerSecond: 0, bytesOutPerSecond: 0 },
|
||||||
topIPs: [],
|
topIPs: [],
|
||||||
totalDataTransferred: { bytesIn: 0, bytesOut: 0 },
|
totalDataTransferred: { bytesIn: 0, bytesOut: 0 },
|
||||||
|
throughputHistory: [],
|
||||||
|
throughputByIP: [],
|
||||||
|
requestsPerSecond: 0,
|
||||||
|
requestsTotal: 0,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -251,36 +251,39 @@ export class StatsHandler {
|
|||||||
|
|
||||||
if (sections.network && this.opsServerRef.dcRouterRef.metricsManager) {
|
if (sections.network && this.opsServerRef.dcRouterRef.metricsManager) {
|
||||||
promises.push(
|
promises.push(
|
||||||
this.opsServerRef.dcRouterRef.metricsManager.getNetworkStats().then(stats => {
|
(async () => {
|
||||||
const connectionDetails: interfaces.data.IConnectionDetails[] = [];
|
const stats = await this.opsServerRef.dcRouterRef.metricsManager.getNetworkStats();
|
||||||
stats.connectionsByIP.forEach((count, ip) => {
|
const serverStats = await this.collectServerStats();
|
||||||
connectionDetails.push({
|
|
||||||
remoteAddress: ip,
|
// Build per-IP bandwidth lookup from throughputByIP
|
||||||
protocol: 'https' as any,
|
const ipBandwidth = new Map<string, { in: number; out: number }>();
|
||||||
state: 'established' as any,
|
if (stats.throughputByIP) {
|
||||||
startTime: Date.now(),
|
for (const [ip, tp] of stats.throughputByIP) {
|
||||||
bytesIn: 0,
|
ipBandwidth.set(ip, { in: tp.in, out: tp.out });
|
||||||
bytesOut: 0,
|
}
|
||||||
});
|
}
|
||||||
});
|
|
||||||
|
|
||||||
metrics.network = {
|
metrics.network = {
|
||||||
totalBandwidth: {
|
totalBandwidth: {
|
||||||
in: stats.throughputRate.bytesInPerSecond,
|
in: stats.throughputRate.bytesInPerSecond,
|
||||||
out: stats.throughputRate.bytesOutPerSecond,
|
out: stats.throughputRate.bytesOutPerSecond,
|
||||||
},
|
},
|
||||||
activeConnections: stats.connectionsByIP.size,
|
totalBytes: {
|
||||||
connectionDetails: connectionDetails.slice(0, 50), // Limit to 50 connections
|
in: stats.totalDataTransferred.bytesIn,
|
||||||
|
out: stats.totalDataTransferred.bytesOut,
|
||||||
|
},
|
||||||
|
activeConnections: serverStats.activeConnections,
|
||||||
|
connectionDetails: [],
|
||||||
topEndpoints: stats.topIPs.map(ip => ({
|
topEndpoints: stats.topIPs.map(ip => ({
|
||||||
endpoint: ip.ip,
|
endpoint: ip.ip,
|
||||||
requests: ip.count,
|
requests: ip.count,
|
||||||
bandwidth: {
|
bandwidth: ipBandwidth.get(ip.ip) || { in: 0, out: 0 },
|
||||||
in: 0,
|
|
||||||
out: 0,
|
|
||||||
},
|
|
||||||
})),
|
})),
|
||||||
|
throughputHistory: stats.throughputHistory || [],
|
||||||
|
requestsPerSecond: stats.requestsPerSecond || 0,
|
||||||
|
requestsTotal: stats.requestsTotal || 0,
|
||||||
};
|
};
|
||||||
})
|
})()
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
63
ts/paths.ts
63
ts/paths.ts
@@ -1,7 +1,6 @@
|
|||||||
import * as plugins from './plugins.js';
|
import * as plugins from './plugins.js';
|
||||||
|
|
||||||
// Base directories
|
// Code/asset paths (not affected by baseDir)
|
||||||
export const baseDir = process.cwd();
|
|
||||||
export const packageDir = plugins.path.join(
|
export const packageDir = plugins.path.join(
|
||||||
plugins.smartpath.get.dirnameFromImportMetaUrl(import.meta.url),
|
plugins.smartpath.get.dirnameFromImportMetaUrl(import.meta.url),
|
||||||
'../'
|
'../'
|
||||||
@@ -20,35 +19,37 @@ export const dataDir = process.env.DATA_DIR
|
|||||||
// Default TsmDB path for CacheDb
|
// Default TsmDB path for CacheDb
|
||||||
export const defaultTsmDbPath = plugins.path.join(dcrouterHomeDir, 'tsmdb');
|
export const defaultTsmDbPath = plugins.path.join(dcrouterHomeDir, 'tsmdb');
|
||||||
|
|
||||||
// MTA directories
|
// DNS records directory (only surviving MTA directory reference)
|
||||||
export const keysDir = plugins.path.join(dataDir, 'keys');
|
|
||||||
export const dnsRecordsDir = plugins.path.join(dataDir, 'dns');
|
export const dnsRecordsDir = plugins.path.join(dataDir, 'dns');
|
||||||
export const sentEmailsDir = plugins.path.join(dataDir, 'emails', 'sent');
|
|
||||||
export const receivedEmailsDir = plugins.path.join(dataDir, 'emails', 'received');
|
|
||||||
export const failedEmailsDir = plugins.path.join(dataDir, 'emails', 'failed'); // For failed emails
|
|
||||||
export const logsDir = plugins.path.join(dataDir, 'logs'); // For logs
|
|
||||||
|
|
||||||
// Email template directories
|
/**
|
||||||
export const emailTemplatesDir = plugins.path.join(dataDir, 'templates', 'email');
|
* Resolve all data paths from a given baseDir.
|
||||||
export const MtaAttachmentsDir = plugins.path.join(dataDir, 'attachments'); // For email attachments
|
* When no baseDir is provided, falls back to ~/.serve.zone/dcrouter.
|
||||||
|
* Specific overrides (e.g. DATA_DIR env) take precedence.
|
||||||
// Configuration path
|
*/
|
||||||
export const configPath = process.env.CONFIG_PATH
|
export function resolvePaths(baseDir?: string) {
|
||||||
? process.env.CONFIG_PATH
|
const root = baseDir ?? plugins.path.join(plugins.os.homedir(), '.serve.zone', 'dcrouter');
|
||||||
: plugins.path.join(baseDir, 'config.json');
|
const resolvedDataDir = process.env.DATA_DIR ?? plugins.path.join(root, 'data');
|
||||||
|
return {
|
||||||
// Create directories if they don't exist
|
dcrouterHomeDir: root,
|
||||||
export function ensureDirectories() {
|
dataDir: resolvedDataDir,
|
||||||
// Ensure data directories
|
defaultTsmDbPath: plugins.path.join(root, 'tsmdb'),
|
||||||
plugins.fsUtils.ensureDirSync(dataDir);
|
defaultStoragePath: plugins.path.join(root, 'storage'),
|
||||||
plugins.fsUtils.ensureDirSync(keysDir);
|
dnsRecordsDir: plugins.path.join(resolvedDataDir, 'dns'),
|
||||||
plugins.fsUtils.ensureDirSync(dnsRecordsDir);
|
};
|
||||||
plugins.fsUtils.ensureDirSync(sentEmailsDir);
|
}
|
||||||
plugins.fsUtils.ensureDirSync(receivedEmailsDir);
|
|
||||||
plugins.fsUtils.ensureDirSync(failedEmailsDir);
|
/**
|
||||||
plugins.fsUtils.ensureDirSync(logsDir);
|
* Ensure only the data directories that are actually used exist.
|
||||||
|
*/
|
||||||
// Ensure email template directories
|
export function ensureDataDirectories(resolvedPaths: ReturnType<typeof resolvePaths>) {
|
||||||
plugins.fsUtils.ensureDirSync(emailTemplatesDir);
|
plugins.fsUtils.ensureDirSync(resolvedPaths.dataDir);
|
||||||
plugins.fsUtils.ensureDirSync(MtaAttachmentsDir);
|
plugins.fsUtils.ensureDirSync(resolvedPaths.dnsRecordsDir);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Legacy wrapper — delegates to ensureDataDirectories with module-level defaults.
|
||||||
|
*/
|
||||||
|
export function ensureDirectories() {
|
||||||
|
ensureDataDirectories(resolvePaths());
|
||||||
}
|
}
|
||||||
@@ -23,9 +23,11 @@ export {
|
|||||||
|
|
||||||
// @serve.zone scope
|
// @serve.zone scope
|
||||||
import * as servezoneInterfaces from '@serve.zone/interfaces';
|
import * as servezoneInterfaces from '@serve.zone/interfaces';
|
||||||
|
import * as remoteingress from '@serve.zone/remoteingress';
|
||||||
|
|
||||||
export {
|
export {
|
||||||
servezoneInterfaces
|
servezoneInterfaces,
|
||||||
|
remoteingress,
|
||||||
}
|
}
|
||||||
|
|
||||||
// @api.global scope
|
// @api.global scope
|
||||||
|
|||||||
160
ts/remoteingress/classes.remoteingress-manager.ts
Normal file
160
ts/remoteingress/classes.remoteingress-manager.ts
Normal file
@@ -0,0 +1,160 @@
|
|||||||
|
import * as plugins from '../plugins.js';
|
||||||
|
import type { StorageManager } from '../storage/classes.storagemanager.js';
|
||||||
|
import type { IRemoteIngress } from '../../ts_interfaces/data/remoteingress.js';
|
||||||
|
|
||||||
|
const STORAGE_PREFIX = '/remote-ingress/';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Manages CRUD for remote ingress edge registrations.
|
||||||
|
* Persists edge configs via StorageManager and provides
|
||||||
|
* the allowed edges list for the Rust hub.
|
||||||
|
*/
|
||||||
|
export class RemoteIngressManager {
|
||||||
|
private storageManager: StorageManager;
|
||||||
|
private edges: Map<string, IRemoteIngress> = new Map();
|
||||||
|
|
||||||
|
constructor(storageManager: StorageManager) {
|
||||||
|
this.storageManager = storageManager;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Load all edge registrations from storage into memory.
|
||||||
|
*/
|
||||||
|
public async initialize(): Promise<void> {
|
||||||
|
const keys = await this.storageManager.list(STORAGE_PREFIX);
|
||||||
|
for (const key of keys) {
|
||||||
|
const edge = await this.storageManager.getJSON<IRemoteIngress>(key);
|
||||||
|
if (edge) {
|
||||||
|
this.edges.set(edge.id, edge);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new edge registration.
|
||||||
|
*/
|
||||||
|
public async createEdge(
|
||||||
|
name: string,
|
||||||
|
listenPorts: number[],
|
||||||
|
tags?: string[],
|
||||||
|
): Promise<IRemoteIngress> {
|
||||||
|
const id = plugins.uuid.v4();
|
||||||
|
const secret = plugins.crypto.randomBytes(32).toString('hex');
|
||||||
|
const now = Date.now();
|
||||||
|
|
||||||
|
const edge: IRemoteIngress = {
|
||||||
|
id,
|
||||||
|
name,
|
||||||
|
secret,
|
||||||
|
listenPorts,
|
||||||
|
enabled: true,
|
||||||
|
tags: tags || [],
|
||||||
|
createdAt: now,
|
||||||
|
updatedAt: now,
|
||||||
|
};
|
||||||
|
|
||||||
|
await this.storageManager.setJSON(`${STORAGE_PREFIX}${id}`, edge);
|
||||||
|
this.edges.set(id, edge);
|
||||||
|
return edge;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get an edge by ID.
|
||||||
|
*/
|
||||||
|
public getEdge(id: string): IRemoteIngress | undefined {
|
||||||
|
return this.edges.get(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get all edge registrations.
|
||||||
|
*/
|
||||||
|
public getAllEdges(): IRemoteIngress[] {
|
||||||
|
return Array.from(this.edges.values());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update an edge registration.
|
||||||
|
*/
|
||||||
|
public async updateEdge(
|
||||||
|
id: string,
|
||||||
|
updates: {
|
||||||
|
name?: string;
|
||||||
|
listenPorts?: number[];
|
||||||
|
enabled?: boolean;
|
||||||
|
tags?: string[];
|
||||||
|
},
|
||||||
|
): Promise<IRemoteIngress | null> {
|
||||||
|
const edge = this.edges.get(id);
|
||||||
|
if (!edge) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (updates.name !== undefined) edge.name = updates.name;
|
||||||
|
if (updates.listenPorts !== undefined) edge.listenPorts = updates.listenPorts;
|
||||||
|
if (updates.enabled !== undefined) edge.enabled = updates.enabled;
|
||||||
|
if (updates.tags !== undefined) edge.tags = updates.tags;
|
||||||
|
edge.updatedAt = Date.now();
|
||||||
|
|
||||||
|
await this.storageManager.setJSON(`${STORAGE_PREFIX}${id}`, edge);
|
||||||
|
this.edges.set(id, edge);
|
||||||
|
return edge;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete an edge registration.
|
||||||
|
*/
|
||||||
|
public async deleteEdge(id: string): Promise<boolean> {
|
||||||
|
if (!this.edges.has(id)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
await this.storageManager.delete(`${STORAGE_PREFIX}${id}`);
|
||||||
|
this.edges.delete(id);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Regenerate the secret for an edge.
|
||||||
|
*/
|
||||||
|
public async regenerateSecret(id: string): Promise<string | null> {
|
||||||
|
const edge = this.edges.get(id);
|
||||||
|
if (!edge) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
edge.secret = plugins.crypto.randomBytes(32).toString('hex');
|
||||||
|
edge.updatedAt = Date.now();
|
||||||
|
|
||||||
|
await this.storageManager.setJSON(`${STORAGE_PREFIX}${id}`, edge);
|
||||||
|
this.edges.set(id, edge);
|
||||||
|
return edge.secret;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Verify an edge's secret using constant-time comparison.
|
||||||
|
*/
|
||||||
|
public verifySecret(id: string, secret: string): boolean {
|
||||||
|
const edge = this.edges.get(id);
|
||||||
|
if (!edge) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const expected = Buffer.from(edge.secret);
|
||||||
|
const provided = Buffer.from(secret);
|
||||||
|
if (expected.length !== provided.length) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return plugins.crypto.timingSafeEqual(expected, provided);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the list of allowed edges (enabled only) for the Rust hub.
|
||||||
|
*/
|
||||||
|
public getAllowedEdges(): Array<{ id: string; secret: string }> {
|
||||||
|
const result: Array<{ id: string; secret: string }> = [];
|
||||||
|
for (const edge of this.edges.values()) {
|
||||||
|
if (edge.enabled) {
|
||||||
|
result.push({ id: edge.id, secret: edge.secret });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
||||||
126
ts/remoteingress/classes.tunnel-manager.ts
Normal file
126
ts/remoteingress/classes.tunnel-manager.ts
Normal file
@@ -0,0 +1,126 @@
|
|||||||
|
import * as plugins from '../plugins.js';
|
||||||
|
import type { IRemoteIngressStatus } from '../../ts_interfaces/data/remoteingress.js';
|
||||||
|
import type { RemoteIngressManager } from './classes.remoteingress-manager.js';
|
||||||
|
|
||||||
|
export interface ITunnelManagerConfig {
|
||||||
|
tunnelPort?: number;
|
||||||
|
targetHost?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Manages the RemoteIngressHub instance and tracks connected edge statuses.
|
||||||
|
*/
|
||||||
|
export class TunnelManager {
|
||||||
|
private hub: InstanceType<typeof plugins.remoteingress.RemoteIngressHub>;
|
||||||
|
private manager: RemoteIngressManager;
|
||||||
|
private config: ITunnelManagerConfig;
|
||||||
|
private edgeStatuses: Map<string, IRemoteIngressStatus> = new Map();
|
||||||
|
|
||||||
|
constructor(manager: RemoteIngressManager, config: ITunnelManagerConfig = {}) {
|
||||||
|
this.manager = manager;
|
||||||
|
this.config = config;
|
||||||
|
this.hub = new plugins.remoteingress.RemoteIngressHub();
|
||||||
|
|
||||||
|
// Listen for edge connect/disconnect events
|
||||||
|
this.hub.on('edgeConnected', (data: { edgeId: string }) => {
|
||||||
|
const existing = this.edgeStatuses.get(data.edgeId);
|
||||||
|
this.edgeStatuses.set(data.edgeId, {
|
||||||
|
edgeId: data.edgeId,
|
||||||
|
connected: true,
|
||||||
|
publicIp: existing?.publicIp ?? null,
|
||||||
|
activeTunnels: 0,
|
||||||
|
lastHeartbeat: Date.now(),
|
||||||
|
connectedAt: Date.now(),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
this.hub.on('edgeDisconnected', (data: { edgeId: string }) => {
|
||||||
|
const existing = this.edgeStatuses.get(data.edgeId);
|
||||||
|
if (existing) {
|
||||||
|
existing.connected = false;
|
||||||
|
existing.activeTunnels = 0;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
this.hub.on('streamOpened', (data: { edgeId: string; streamId: number }) => {
|
||||||
|
const existing = this.edgeStatuses.get(data.edgeId);
|
||||||
|
if (existing) {
|
||||||
|
existing.activeTunnels++;
|
||||||
|
existing.lastHeartbeat = Date.now();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
this.hub.on('streamClosed', (data: { edgeId: string; streamId: number }) => {
|
||||||
|
const existing = this.edgeStatuses.get(data.edgeId);
|
||||||
|
if (existing && existing.activeTunnels > 0) {
|
||||||
|
existing.activeTunnels--;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Start the tunnel hub and load allowed edges.
|
||||||
|
*/
|
||||||
|
public async start(): Promise<void> {
|
||||||
|
await this.hub.start({
|
||||||
|
tunnelPort: this.config.tunnelPort ?? 8443,
|
||||||
|
targetHost: this.config.targetHost ?? '127.0.0.1',
|
||||||
|
});
|
||||||
|
|
||||||
|
// Send allowed edges to the hub
|
||||||
|
await this.syncAllowedEdges();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Stop the tunnel hub.
|
||||||
|
*/
|
||||||
|
public async stop(): Promise<void> {
|
||||||
|
await this.hub.stop();
|
||||||
|
this.edgeStatuses.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sync allowed edges from the manager to the hub.
|
||||||
|
* Call this after creating/deleting/updating edges.
|
||||||
|
*/
|
||||||
|
public async syncAllowedEdges(): Promise<void> {
|
||||||
|
const edges = this.manager.getAllowedEdges();
|
||||||
|
await this.hub.updateAllowedEdges(edges);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get runtime statuses for all known edges.
|
||||||
|
*/
|
||||||
|
public getEdgeStatuses(): IRemoteIngressStatus[] {
|
||||||
|
return Array.from(this.edgeStatuses.values());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get status for a specific edge.
|
||||||
|
*/
|
||||||
|
public getEdgeStatus(edgeId: string): IRemoteIngressStatus | undefined {
|
||||||
|
return this.edgeStatuses.get(edgeId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the count of connected edges.
|
||||||
|
*/
|
||||||
|
public getConnectedCount(): number {
|
||||||
|
let count = 0;
|
||||||
|
for (const status of this.edgeStatuses.values()) {
|
||||||
|
if (status.connected) count++;
|
||||||
|
}
|
||||||
|
return count;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the total number of active tunnels across all edges.
|
||||||
|
*/
|
||||||
|
public getTotalActiveTunnels(): number {
|
||||||
|
let total = 0;
|
||||||
|
for (const status of this.edgeStatuses.values()) {
|
||||||
|
total += status.activeTunnels;
|
||||||
|
}
|
||||||
|
return total;
|
||||||
|
}
|
||||||
|
}
|
||||||
2
ts/remoteingress/index.ts
Normal file
2
ts/remoteingress/index.ts
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
export * from './classes.remoteingress-manager.js';
|
||||||
|
export * from './classes.tunnel-manager.js';
|
||||||
@@ -1,5 +1,4 @@
|
|||||||
import * as plugins from '../plugins.js';
|
import * as plugins from '../plugins.js';
|
||||||
import * as paths from '../paths.js';
|
|
||||||
import { logger } from '../logger.js';
|
import { logger } from '../logger.js';
|
||||||
import { Email, type Core } from '@push.rocks/smartmta';
|
import { Email, type Core } from '@push.rocks/smartmta';
|
||||||
type IAttachment = Core.IAttachment;
|
type IAttachment = Core.IAttachment;
|
||||||
|
|||||||
@@ -378,7 +378,7 @@ export class StorageManager {
|
|||||||
*/
|
*/
|
||||||
async getJSON<T = any>(key: string): Promise<T | null> {
|
async getJSON<T = any>(key: string): Promise<T | null> {
|
||||||
const value = await this.get(key);
|
const value = await this.get(key);
|
||||||
if (value === null) {
|
if (value === null || value.trim() === '') {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,2 +1,3 @@
|
|||||||
export * from './auth.js';
|
export * from './auth.js';
|
||||||
export * from './stats.js';
|
export * from './stats.js';
|
||||||
|
export * from './remoteingress.js';
|
||||||
25
ts_interfaces/data/remoteingress.ts
Normal file
25
ts_interfaces/data/remoteingress.ts
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
/**
|
||||||
|
* A stored remote ingress edge registration.
|
||||||
|
*/
|
||||||
|
export interface IRemoteIngress {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
secret: string;
|
||||||
|
listenPorts: number[];
|
||||||
|
enabled: boolean;
|
||||||
|
tags?: string[];
|
||||||
|
createdAt: number;
|
||||||
|
updatedAt: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Runtime status of a remote ingress edge.
|
||||||
|
*/
|
||||||
|
export interface IRemoteIngressStatus {
|
||||||
|
edgeId: string;
|
||||||
|
connected: boolean;
|
||||||
|
publicIp: string | null;
|
||||||
|
activeTunnels: number;
|
||||||
|
lastHeartbeat: number | null;
|
||||||
|
connectedAt: number | null;
|
||||||
|
}
|
||||||
@@ -116,6 +116,10 @@ export interface INetworkMetrics {
|
|||||||
in: number;
|
in: number;
|
||||||
out: number;
|
out: number;
|
||||||
};
|
};
|
||||||
|
totalBytes?: {
|
||||||
|
in: number;
|
||||||
|
out: number;
|
||||||
|
};
|
||||||
activeConnections: number;
|
activeConnections: number;
|
||||||
connectionDetails: IConnectionDetails[];
|
connectionDetails: IConnectionDetails[];
|
||||||
topEndpoints: Array<{
|
topEndpoints: Array<{
|
||||||
@@ -126,6 +130,9 @@ export interface INetworkMetrics {
|
|||||||
out: number;
|
out: number;
|
||||||
};
|
};
|
||||||
}>;
|
}>;
|
||||||
|
throughputHistory?: Array<{ timestamp: number; in: number; out: number }>;
|
||||||
|
requestsPerSecond?: number;
|
||||||
|
requestsTotal?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IConnectionDetails {
|
export interface IConnectionDetails {
|
||||||
|
|||||||
@@ -128,6 +128,37 @@ TypedRequest interfaces for the OpsServer API, organized by domain:
|
|||||||
| `IReq_GetBounceRecords` | `getBounceRecords` | Bounce records |
|
| `IReq_GetBounceRecords` | `getBounceRecords` | Bounce records |
|
||||||
| `IReq_RemoveFromSuppressionList` | `removeFromSuppressionList` | Unsuppress an address |
|
| `IReq_RemoveFromSuppressionList` | `removeFromSuppressionList` | Unsuppress an address |
|
||||||
|
|
||||||
|
#### 🔐 Certificates
|
||||||
|
| Interface | Method | Description |
|
||||||
|
|-----------|--------|-------------|
|
||||||
|
| `IReq_GetCertificateOverview` | `getCertificateOverview` | Domain-centric certificate status |
|
||||||
|
| `IReq_ReprovisionCertificate` | `reprovisionCertificate` | Reprovision by route name (legacy) |
|
||||||
|
| `IReq_ReprovisionCertificateDomain` | `reprovisionCertificateDomain` | Reprovision by domain (preferred) |
|
||||||
|
|
||||||
|
#### Certificate Types
|
||||||
|
```typescript
|
||||||
|
type TCertificateStatus = 'valid' | 'expiring' | 'expired' | 'provisioning' | 'failed' | 'unknown';
|
||||||
|
type TCertificateSource = 'acme' | 'provision-function' | 'static' | 'none';
|
||||||
|
|
||||||
|
interface ICertificateInfo {
|
||||||
|
domain: string;
|
||||||
|
routeNames: string[];
|
||||||
|
status: TCertificateStatus;
|
||||||
|
source: TCertificateSource;
|
||||||
|
tlsMode: 'terminate' | 'terminate-and-reencrypt' | 'passthrough';
|
||||||
|
expiryDate?: string;
|
||||||
|
issuer?: string;
|
||||||
|
issuedAt?: string;
|
||||||
|
error?: string;
|
||||||
|
canReprovision: boolean;
|
||||||
|
backoffInfo?: {
|
||||||
|
failures: number;
|
||||||
|
retryAfter?: string;
|
||||||
|
lastError?: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
#### 📡 RADIUS
|
#### 📡 RADIUS
|
||||||
| Interface | Method | Description |
|
| Interface | Method | Description |
|
||||||
|-----------|--------|-------------|
|
|-----------|--------|-------------|
|
||||||
@@ -173,7 +204,16 @@ console.log('Email:', metrics.emailStats);
|
|||||||
console.log('DNS:', metrics.dnsStats);
|
console.log('DNS:', metrics.dnsStats);
|
||||||
console.log('Security:', metrics.securityMetrics);
|
console.log('Security:', metrics.securityMetrics);
|
||||||
|
|
||||||
// 3. Check email queues
|
// 3. Check certificate status
|
||||||
|
const certClient = new typedrequest.TypedRequest<requests.IReq_GetCertificateOverview>(
|
||||||
|
'https://your-dcrouter:3000/typedrequest',
|
||||||
|
'getCertificateOverview'
|
||||||
|
);
|
||||||
|
|
||||||
|
const certs = await certClient.fire({ identity });
|
||||||
|
console.log(`Certificates: ${certs.summary.valid} valid, ${certs.summary.failed} failed`);
|
||||||
|
|
||||||
|
// 4. Check email queues
|
||||||
const queueClient = new typedrequest.TypedRequest<requests.IReq_GetQueuedEmails>(
|
const queueClient = new typedrequest.TypedRequest<requests.IReq_GetQueuedEmails>(
|
||||||
'https://your-dcrouter:3000/typedrequest',
|
'https://your-dcrouter:3000/typedrequest',
|
||||||
'getQueuedEmails'
|
'getQueuedEmails'
|
||||||
|
|||||||
@@ -5,8 +5,8 @@ export type TCertificateStatus = 'valid' | 'expiring' | 'expired' | 'provisionin
|
|||||||
export type TCertificateSource = 'acme' | 'provision-function' | 'static' | 'none';
|
export type TCertificateSource = 'acme' | 'provision-function' | 'static' | 'none';
|
||||||
|
|
||||||
export interface ICertificateInfo {
|
export interface ICertificateInfo {
|
||||||
routeName: string;
|
domain: string;
|
||||||
domains: string[];
|
routeNames: string[];
|
||||||
status: TCertificateStatus;
|
status: TCertificateStatus;
|
||||||
source: TCertificateSource;
|
source: TCertificateSource;
|
||||||
tlsMode: 'terminate' | 'terminate-and-reencrypt' | 'passthrough';
|
tlsMode: 'terminate' | 'terminate-and-reencrypt' | 'passthrough';
|
||||||
@@ -15,6 +15,11 @@ export interface ICertificateInfo {
|
|||||||
issuedAt?: string; // ISO string
|
issuedAt?: string; // ISO string
|
||||||
error?: string; // if status === 'failed'
|
error?: string; // if status === 'failed'
|
||||||
canReprovision: boolean; // true for acme/provision-function routes
|
canReprovision: boolean; // true for acme/provision-function routes
|
||||||
|
backoffInfo?: {
|
||||||
|
failures: number;
|
||||||
|
retryAfter?: string; // ISO string
|
||||||
|
lastError?: string;
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IReq_GetCertificateOverview extends plugins.typedrequestInterfaces.implementsTR<
|
export interface IReq_GetCertificateOverview extends plugins.typedrequestInterfaces.implementsTR<
|
||||||
@@ -38,6 +43,7 @@ export interface IReq_GetCertificateOverview extends plugins.typedrequestInterfa
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Legacy route-based reprovision (kept for backward compat)
|
||||||
export interface IReq_ReprovisionCertificate extends plugins.typedrequestInterfaces.implementsTR<
|
export interface IReq_ReprovisionCertificate extends plugins.typedrequestInterfaces.implementsTR<
|
||||||
plugins.typedrequestInterfaces.ITypedRequest,
|
plugins.typedrequestInterfaces.ITypedRequest,
|
||||||
IReq_ReprovisionCertificate
|
IReq_ReprovisionCertificate
|
||||||
@@ -52,3 +58,19 @@ export interface IReq_ReprovisionCertificate extends plugins.typedrequestInterfa
|
|||||||
message?: string;
|
message?: string;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Domain-based reprovision (preferred)
|
||||||
|
export interface IReq_ReprovisionCertificateDomain extends plugins.typedrequestInterfaces.implementsTR<
|
||||||
|
plugins.typedrequestInterfaces.ITypedRequest,
|
||||||
|
IReq_ReprovisionCertificateDomain
|
||||||
|
> {
|
||||||
|
method: 'reprovisionCertificateDomain';
|
||||||
|
request: {
|
||||||
|
identity?: authInterfaces.IIdentity;
|
||||||
|
domain: string;
|
||||||
|
};
|
||||||
|
response: {
|
||||||
|
success: boolean;
|
||||||
|
message?: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|||||||
@@ -6,3 +6,4 @@ export * from './combined.stats.js';
|
|||||||
export * from './radius.js';
|
export * from './radius.js';
|
||||||
export * from './email-ops.js';
|
export * from './email-ops.js';
|
||||||
export * from './certificate.js';
|
export * from './certificate.js';
|
||||||
|
export * from './remoteingress.js';
|
||||||
117
ts_interfaces/requests/remoteingress.ts
Normal file
117
ts_interfaces/requests/remoteingress.ts
Normal file
@@ -0,0 +1,117 @@
|
|||||||
|
import * as plugins from '../plugins.js';
|
||||||
|
import * as authInterfaces from '../data/auth.js';
|
||||||
|
import type { IRemoteIngress, IRemoteIngressStatus } from '../data/remoteingress.js';
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// Remote Ingress Edge Management
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new remote ingress edge registration.
|
||||||
|
*/
|
||||||
|
export interface IReq_CreateRemoteIngress extends plugins.typedrequestInterfaces.implementsTR<
|
||||||
|
plugins.typedrequestInterfaces.ITypedRequest,
|
||||||
|
IReq_CreateRemoteIngress
|
||||||
|
> {
|
||||||
|
method: 'createRemoteIngress';
|
||||||
|
request: {
|
||||||
|
identity?: authInterfaces.IIdentity;
|
||||||
|
name: string;
|
||||||
|
listenPorts: number[];
|
||||||
|
tags?: string[];
|
||||||
|
};
|
||||||
|
response: {
|
||||||
|
success: boolean;
|
||||||
|
edge: IRemoteIngress;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete a remote ingress edge registration.
|
||||||
|
*/
|
||||||
|
export interface IReq_DeleteRemoteIngress extends plugins.typedrequestInterfaces.implementsTR<
|
||||||
|
plugins.typedrequestInterfaces.ITypedRequest,
|
||||||
|
IReq_DeleteRemoteIngress
|
||||||
|
> {
|
||||||
|
method: 'deleteRemoteIngress';
|
||||||
|
request: {
|
||||||
|
identity?: authInterfaces.IIdentity;
|
||||||
|
id: string;
|
||||||
|
};
|
||||||
|
response: {
|
||||||
|
success: boolean;
|
||||||
|
message?: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update a remote ingress edge registration.
|
||||||
|
*/
|
||||||
|
export interface IReq_UpdateRemoteIngress extends plugins.typedrequestInterfaces.implementsTR<
|
||||||
|
plugins.typedrequestInterfaces.ITypedRequest,
|
||||||
|
IReq_UpdateRemoteIngress
|
||||||
|
> {
|
||||||
|
method: 'updateRemoteIngress';
|
||||||
|
request: {
|
||||||
|
identity?: authInterfaces.IIdentity;
|
||||||
|
id: string;
|
||||||
|
name?: string;
|
||||||
|
listenPorts?: number[];
|
||||||
|
enabled?: boolean;
|
||||||
|
tags?: string[];
|
||||||
|
};
|
||||||
|
response: {
|
||||||
|
success: boolean;
|
||||||
|
edge: IRemoteIngress;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Regenerate the secret for a remote ingress edge.
|
||||||
|
*/
|
||||||
|
export interface IReq_RegenerateRemoteIngressSecret extends plugins.typedrequestInterfaces.implementsTR<
|
||||||
|
plugins.typedrequestInterfaces.ITypedRequest,
|
||||||
|
IReq_RegenerateRemoteIngressSecret
|
||||||
|
> {
|
||||||
|
method: 'regenerateRemoteIngressSecret';
|
||||||
|
request: {
|
||||||
|
identity?: authInterfaces.IIdentity;
|
||||||
|
id: string;
|
||||||
|
};
|
||||||
|
response: {
|
||||||
|
success: boolean;
|
||||||
|
secret: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get all remote ingress edge registrations.
|
||||||
|
*/
|
||||||
|
export interface IReq_GetRemoteIngresses extends plugins.typedrequestInterfaces.implementsTR<
|
||||||
|
plugins.typedrequestInterfaces.ITypedRequest,
|
||||||
|
IReq_GetRemoteIngresses
|
||||||
|
> {
|
||||||
|
method: 'getRemoteIngresses';
|
||||||
|
request: {
|
||||||
|
identity?: authInterfaces.IIdentity;
|
||||||
|
};
|
||||||
|
response: {
|
||||||
|
edges: IRemoteIngress[];
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get runtime status of all remote ingress edges.
|
||||||
|
*/
|
||||||
|
export interface IReq_GetRemoteIngressStatus extends plugins.typedrequestInterfaces.implementsTR<
|
||||||
|
plugins.typedrequestInterfaces.ITypedRequest,
|
||||||
|
IReq_GetRemoteIngressStatus
|
||||||
|
> {
|
||||||
|
method: 'getRemoteIngressStatus';
|
||||||
|
request: {
|
||||||
|
identity?: authInterfaces.IIdentity;
|
||||||
|
};
|
||||||
|
response: {
|
||||||
|
statuses: IRemoteIngressStatus[];
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -3,6 +3,6 @@
|
|||||||
*/
|
*/
|
||||||
export const commitinfo = {
|
export const commitinfo = {
|
||||||
name: '@serve.zone/dcrouter',
|
name: '@serve.zone/dcrouter',
|
||||||
version: '5.4.0',
|
version: '6.4.4',
|
||||||
description: 'A multifaceted routing service handling mail and SMS delivery functions.'
|
description: 'A multifaceted routing service handling mail and SMS delivery functions.'
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -47,7 +47,12 @@ export interface INetworkState {
|
|||||||
connections: interfaces.data.IConnectionInfo[];
|
connections: interfaces.data.IConnectionInfo[];
|
||||||
connectionsByIP: { [ip: string]: number };
|
connectionsByIP: { [ip: string]: number };
|
||||||
throughputRate: { bytesInPerSecond: number; bytesOutPerSecond: number };
|
throughputRate: { bytesInPerSecond: number; bytesOutPerSecond: number };
|
||||||
|
totalBytes: { in: number; out: number };
|
||||||
topIPs: Array<{ ip: string; count: number }>;
|
topIPs: Array<{ ip: string; count: number }>;
|
||||||
|
throughputByIP: Array<{ ip: string; in: number; out: number }>;
|
||||||
|
throughputHistory: Array<{ timestamp: number; in: number; out: number }>;
|
||||||
|
requestsPerSecond: number;
|
||||||
|
requestsTotal: number;
|
||||||
lastUpdated: number;
|
lastUpdated: number;
|
||||||
isLoading: boolean;
|
isLoading: boolean;
|
||||||
error: string | null;
|
error: string | null;
|
||||||
@@ -111,7 +116,7 @@ export const configStatePart = await appState.getStatePart<IConfigState>(
|
|||||||
// Determine initial view from URL path
|
// Determine initial view from URL path
|
||||||
const getInitialView = (): string => {
|
const getInitialView = (): string => {
|
||||||
const path = typeof window !== 'undefined' ? window.location.pathname : '/';
|
const path = typeof window !== 'undefined' ? window.location.pathname : '/';
|
||||||
const validViews = ['overview', 'network', 'emails', 'logs', 'configuration', 'security', 'certificates'];
|
const validViews = ['overview', 'network', 'emails', 'logs', 'configuration', 'security', 'certificates', 'remoteingress'];
|
||||||
const segments = path.split('/').filter(Boolean);
|
const segments = path.split('/').filter(Boolean);
|
||||||
const view = segments[0];
|
const view = segments[0];
|
||||||
return validViews.includes(view) ? view : 'overview';
|
return validViews.includes(view) ? view : 'overview';
|
||||||
@@ -144,7 +149,12 @@ export const networkStatePart = await appState.getStatePart<INetworkState>(
|
|||||||
connections: [],
|
connections: [],
|
||||||
connectionsByIP: {},
|
connectionsByIP: {},
|
||||||
throughputRate: { bytesInPerSecond: 0, bytesOutPerSecond: 0 },
|
throughputRate: { bytesInPerSecond: 0, bytesOutPerSecond: 0 },
|
||||||
|
totalBytes: { in: 0, out: 0 },
|
||||||
topIPs: [],
|
topIPs: [],
|
||||||
|
throughputByIP: [],
|
||||||
|
throughputHistory: [],
|
||||||
|
requestsPerSecond: 0,
|
||||||
|
requestsTotal: 0,
|
||||||
lastUpdated: 0,
|
lastUpdated: 0,
|
||||||
isLoading: false,
|
isLoading: false,
|
||||||
error: null,
|
error: null,
|
||||||
@@ -182,6 +192,34 @@ export const certificateStatePart = await appState.getStatePart<ICertificateStat
|
|||||||
'soft'
|
'soft'
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// Remote Ingress State
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
export interface IRemoteIngressState {
|
||||||
|
edges: interfaces.data.IRemoteIngress[];
|
||||||
|
statuses: interfaces.data.IRemoteIngressStatus[];
|
||||||
|
selectedEdgeId: string | null;
|
||||||
|
newEdgeSecret: string | null;
|
||||||
|
isLoading: boolean;
|
||||||
|
error: string | null;
|
||||||
|
lastUpdated: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const remoteIngressStatePart = await appState.getStatePart<IRemoteIngressState>(
|
||||||
|
'remoteIngress',
|
||||||
|
{
|
||||||
|
edges: [],
|
||||||
|
statuses: [],
|
||||||
|
selectedEdgeId: null,
|
||||||
|
newEdgeSecret: null,
|
||||||
|
isLoading: false,
|
||||||
|
error: null,
|
||||||
|
lastUpdated: 0,
|
||||||
|
},
|
||||||
|
'soft'
|
||||||
|
);
|
||||||
|
|
||||||
// Actions for state management
|
// Actions for state management
|
||||||
interface IActionContext {
|
interface IActionContext {
|
||||||
identity: interfaces.data.IIdentity | null;
|
identity: interfaces.data.IIdentity | null;
|
||||||
@@ -368,6 +406,13 @@ export const setActiveViewAction = uiStatePart.createAction<string>(async (state
|
|||||||
}, 100);
|
}, 100);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// If switching to remoteingress view, ensure we fetch edge data
|
||||||
|
if (viewName === 'remoteingress' && currentState.activeView !== 'remoteingress') {
|
||||||
|
setTimeout(() => {
|
||||||
|
remoteIngressStatePart.dispatchAction(fetchRemoteIngressAction, null);
|
||||||
|
}, 100);
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...currentState,
|
...currentState,
|
||||||
activeView: viewName,
|
activeView: viewName,
|
||||||
@@ -421,7 +466,14 @@ export const fetchNetworkStatsAction = networkStatePart.createAction(async (stat
|
|||||||
connections: connectionsResponse.connections,
|
connections: connectionsResponse.connections,
|
||||||
connectionsByIP,
|
connectionsByIP,
|
||||||
throughputRate: networkStatsResponse.throughputRate || { bytesInPerSecond: 0, bytesOutPerSecond: 0 },
|
throughputRate: networkStatsResponse.throughputRate || { bytesInPerSecond: 0, bytesOutPerSecond: 0 },
|
||||||
|
totalBytes: networkStatsResponse.totalDataTransferred
|
||||||
|
? { in: networkStatsResponse.totalDataTransferred.bytesIn, out: networkStatsResponse.totalDataTransferred.bytesOut }
|
||||||
|
: { in: 0, out: 0 },
|
||||||
topIPs: networkStatsResponse.topIPs || [],
|
topIPs: networkStatsResponse.topIPs || [],
|
||||||
|
throughputByIP: networkStatsResponse.throughputByIP || [],
|
||||||
|
throughputHistory: networkStatsResponse.throughputHistory || [],
|
||||||
|
requestsPerSecond: networkStatsResponse.requestsPerSecond || 0,
|
||||||
|
requestsTotal: networkStatsResponse.requestsTotal || 0,
|
||||||
lastUpdated: Date.now(),
|
lastUpdated: Date.now(),
|
||||||
isLoading: false,
|
isLoading: false,
|
||||||
error: null,
|
error: null,
|
||||||
@@ -702,18 +754,18 @@ export const fetchCertificateOverviewAction = certificateStatePart.createAction(
|
|||||||
});
|
});
|
||||||
|
|
||||||
export const reprovisionCertificateAction = certificateStatePart.createAction<string>(
|
export const reprovisionCertificateAction = certificateStatePart.createAction<string>(
|
||||||
async (statePartArg, routeName) => {
|
async (statePartArg, domain) => {
|
||||||
const context = getActionContext();
|
const context = getActionContext();
|
||||||
const currentState = statePartArg.getState();
|
const currentState = statePartArg.getState();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const request = new plugins.domtools.plugins.typedrequest.TypedRequest<
|
const request = new plugins.domtools.plugins.typedrequest.TypedRequest<
|
||||||
interfaces.requests.IReq_ReprovisionCertificate
|
interfaces.requests.IReq_ReprovisionCertificateDomain
|
||||||
>('/typedrequest', 'reprovisionCertificate');
|
>('/typedrequest', 'reprovisionCertificateDomain');
|
||||||
|
|
||||||
await request.fire({
|
await request.fire({
|
||||||
identity: context.identity,
|
identity: context.identity,
|
||||||
routeName,
|
domain,
|
||||||
});
|
});
|
||||||
|
|
||||||
// Re-fetch overview after reprovisioning
|
// Re-fetch overview after reprovisioning
|
||||||
@@ -728,6 +780,150 @@ export const reprovisionCertificateAction = certificateStatePart.createAction<st
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// Remote Ingress Actions
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
export const fetchRemoteIngressAction = remoteIngressStatePart.createAction(async (statePartArg) => {
|
||||||
|
const context = getActionContext();
|
||||||
|
const currentState = statePartArg.getState();
|
||||||
|
|
||||||
|
try {
|
||||||
|
const edgesRequest = new plugins.domtools.plugins.typedrequest.TypedRequest<
|
||||||
|
interfaces.requests.IReq_GetRemoteIngresses
|
||||||
|
>('/typedrequest', 'getRemoteIngresses');
|
||||||
|
|
||||||
|
const statusRequest = new plugins.domtools.plugins.typedrequest.TypedRequest<
|
||||||
|
interfaces.requests.IReq_GetRemoteIngressStatus
|
||||||
|
>('/typedrequest', 'getRemoteIngressStatus');
|
||||||
|
|
||||||
|
const [edgesResponse, statusResponse] = await Promise.all([
|
||||||
|
edgesRequest.fire({ identity: context.identity }),
|
||||||
|
statusRequest.fire({ identity: context.identity }),
|
||||||
|
]);
|
||||||
|
|
||||||
|
return {
|
||||||
|
...currentState,
|
||||||
|
edges: edgesResponse.edges,
|
||||||
|
statuses: statusResponse.statuses,
|
||||||
|
isLoading: false,
|
||||||
|
error: null,
|
||||||
|
lastUpdated: Date.now(),
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
return {
|
||||||
|
...currentState,
|
||||||
|
isLoading: false,
|
||||||
|
error: error instanceof Error ? error.message : 'Failed to fetch remote ingress data',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
export const createRemoteIngressAction = remoteIngressStatePart.createAction<{
|
||||||
|
name: string;
|
||||||
|
listenPorts: number[];
|
||||||
|
tags?: string[];
|
||||||
|
}>(async (statePartArg, dataArg) => {
|
||||||
|
const context = getActionContext();
|
||||||
|
const currentState = statePartArg.getState();
|
||||||
|
|
||||||
|
try {
|
||||||
|
const request = new plugins.domtools.plugins.typedrequest.TypedRequest<
|
||||||
|
interfaces.requests.IReq_CreateRemoteIngress
|
||||||
|
>('/typedrequest', 'createRemoteIngress');
|
||||||
|
|
||||||
|
const response = await request.fire({
|
||||||
|
identity: context.identity,
|
||||||
|
name: dataArg.name,
|
||||||
|
listenPorts: dataArg.listenPorts,
|
||||||
|
tags: dataArg.tags,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.success) {
|
||||||
|
// Refresh the list and store the new secret for display
|
||||||
|
await remoteIngressStatePart.dispatchAction(fetchRemoteIngressAction, null);
|
||||||
|
return {
|
||||||
|
...statePartArg.getState(),
|
||||||
|
newEdgeSecret: response.edge.secret,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return currentState;
|
||||||
|
} catch (error) {
|
||||||
|
return {
|
||||||
|
...currentState,
|
||||||
|
error: error instanceof Error ? error.message : 'Failed to create edge',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
export const deleteRemoteIngressAction = remoteIngressStatePart.createAction<string>(
|
||||||
|
async (statePartArg, edgeId) => {
|
||||||
|
const context = getActionContext();
|
||||||
|
const currentState = statePartArg.getState();
|
||||||
|
|
||||||
|
try {
|
||||||
|
const request = new plugins.domtools.plugins.typedrequest.TypedRequest<
|
||||||
|
interfaces.requests.IReq_DeleteRemoteIngress
|
||||||
|
>('/typedrequest', 'deleteRemoteIngress');
|
||||||
|
|
||||||
|
await request.fire({
|
||||||
|
identity: context.identity,
|
||||||
|
id: edgeId,
|
||||||
|
});
|
||||||
|
|
||||||
|
await remoteIngressStatePart.dispatchAction(fetchRemoteIngressAction, null);
|
||||||
|
return statePartArg.getState();
|
||||||
|
} catch (error) {
|
||||||
|
return {
|
||||||
|
...currentState,
|
||||||
|
error: error instanceof Error ? error.message : 'Failed to delete edge',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
export const regenerateRemoteIngressSecretAction = remoteIngressStatePart.createAction<string>(
|
||||||
|
async (statePartArg, edgeId) => {
|
||||||
|
const context = getActionContext();
|
||||||
|
const currentState = statePartArg.getState();
|
||||||
|
|
||||||
|
try {
|
||||||
|
const request = new plugins.domtools.plugins.typedrequest.TypedRequest<
|
||||||
|
interfaces.requests.IReq_RegenerateRemoteIngressSecret
|
||||||
|
>('/typedrequest', 'regenerateRemoteIngressSecret');
|
||||||
|
|
||||||
|
const response = await request.fire({
|
||||||
|
identity: context.identity,
|
||||||
|
id: edgeId,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.success) {
|
||||||
|
return {
|
||||||
|
...currentState,
|
||||||
|
newEdgeSecret: response.secret,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return currentState;
|
||||||
|
} catch (error) {
|
||||||
|
return {
|
||||||
|
...currentState,
|
||||||
|
error: error instanceof Error ? error.message : 'Failed to regenerate secret',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
export const clearNewEdgeSecretAction = remoteIngressStatePart.createAction(
|
||||||
|
async (statePartArg) => {
|
||||||
|
return {
|
||||||
|
...statePartArg.getState(),
|
||||||
|
newEdgeSecret: null,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
// Combined refresh action for efficient polling
|
// Combined refresh action for efficient polling
|
||||||
async function dispatchCombinedRefreshAction() {
|
async function dispatchCombinedRefreshAction() {
|
||||||
const context = getActionContext();
|
const context = getActionContext();
|
||||||
@@ -790,7 +986,12 @@ async function dispatchCombinedRefreshAction() {
|
|||||||
bytesInPerSecond: network.totalBandwidth.in,
|
bytesInPerSecond: network.totalBandwidth.in,
|
||||||
bytesOutPerSecond: network.totalBandwidth.out
|
bytesOutPerSecond: network.totalBandwidth.out
|
||||||
},
|
},
|
||||||
|
totalBytes: network.totalBytes || { in: 0, out: 0 },
|
||||||
topIPs: network.topEndpoints.map(e => ({ ip: e.endpoint, count: e.requests })),
|
topIPs: network.topEndpoints.map(e => ({ ip: e.endpoint, count: e.requests })),
|
||||||
|
throughputByIP: network.topEndpoints.map(e => ({ ip: e.endpoint, in: e.bandwidth?.in || 0, out: e.bandwidth?.out || 0 })),
|
||||||
|
throughputHistory: network.throughputHistory || [],
|
||||||
|
requestsPerSecond: network.requestsPerSecond || 0,
|
||||||
|
requestsTotal: network.requestsTotal || 0,
|
||||||
lastUpdated: Date.now(),
|
lastUpdated: Date.now(),
|
||||||
isLoading: false,
|
isLoading: false,
|
||||||
error: null,
|
error: null,
|
||||||
@@ -805,7 +1006,12 @@ async function dispatchCombinedRefreshAction() {
|
|||||||
bytesInPerSecond: network.totalBandwidth.in,
|
bytesInPerSecond: network.totalBandwidth.in,
|
||||||
bytesOutPerSecond: network.totalBandwidth.out
|
bytesOutPerSecond: network.totalBandwidth.out
|
||||||
},
|
},
|
||||||
|
totalBytes: network.totalBytes || { in: 0, out: 0 },
|
||||||
topIPs: network.topEndpoints.map(e => ({ ip: e.endpoint, count: e.requests })),
|
topIPs: network.topEndpoints.map(e => ({ ip: e.endpoint, count: e.requests })),
|
||||||
|
throughputByIP: network.topEndpoints.map(e => ({ ip: e.endpoint, in: e.bandwidth?.in || 0, out: e.bandwidth?.out || 0 })),
|
||||||
|
throughputHistory: network.throughputHistory || [],
|
||||||
|
requestsPerSecond: network.requestsPerSecond || 0,
|
||||||
|
requestsTotal: network.requestsTotal || 0,
|
||||||
lastUpdated: Date.now(),
|
lastUpdated: Date.now(),
|
||||||
isLoading: false,
|
isLoading: false,
|
||||||
error: null,
|
error: null,
|
||||||
@@ -845,13 +1051,6 @@ let currentRefreshRate = 1000; // Track current refresh rate to avoid unnecessar
|
|||||||
refreshInterval = setInterval(() => {
|
refreshInterval = setInterval(() => {
|
||||||
// Use combined refresh action for efficiency
|
// Use combined refresh action for efficiency
|
||||||
dispatchCombinedRefreshAction();
|
dispatchCombinedRefreshAction();
|
||||||
|
|
||||||
// If network view is active, also ensure we have fresh network data
|
|
||||||
const currentView = uiStatePart.getState().activeView;
|
|
||||||
if (currentView === 'network') {
|
|
||||||
// Network view needs more frequent updates, fetch directly
|
|
||||||
networkStatePart.dispatchAction(fetchNetworkStatsAction, null);
|
|
||||||
}
|
|
||||||
}, uiState.refreshInterval);
|
}, uiState.refreshInterval);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -6,4 +6,5 @@ export * from './ops-view-logs.js';
|
|||||||
export * from './ops-view-config.js';
|
export * from './ops-view-config.js';
|
||||||
export * from './ops-view-security.js';
|
export * from './ops-view-security.js';
|
||||||
export * from './ops-view-certificates.js';
|
export * from './ops-view-certificates.js';
|
||||||
|
export * from './ops-view-remoteingress.js';
|
||||||
export * from './shared/index.js';
|
export * from './shared/index.js';
|
||||||
@@ -20,6 +20,7 @@ import { OpsViewLogs } from './ops-view-logs.js';
|
|||||||
import { OpsViewConfig } from './ops-view-config.js';
|
import { OpsViewConfig } from './ops-view-config.js';
|
||||||
import { OpsViewSecurity } from './ops-view-security.js';
|
import { OpsViewSecurity } from './ops-view-security.js';
|
||||||
import { OpsViewCertificates } from './ops-view-certificates.js';
|
import { OpsViewCertificates } from './ops-view-certificates.js';
|
||||||
|
import { OpsViewRemoteIngress } from './ops-view-remoteingress.js';
|
||||||
|
|
||||||
@customElement('ops-dashboard')
|
@customElement('ops-dashboard')
|
||||||
export class OpsDashboard extends DeesElement {
|
export class OpsDashboard extends DeesElement {
|
||||||
@@ -66,6 +67,10 @@ export class OpsDashboard extends DeesElement {
|
|||||||
name: 'Certificates',
|
name: 'Certificates',
|
||||||
element: OpsViewCertificates,
|
element: OpsViewCertificates,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: 'RemoteIngress',
|
||||||
|
element: OpsViewRemoteIngress,
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -94,13 +94,13 @@ export class OpsViewCertificates extends DeesElement {
|
|||||||
color: ${cssManager.bdTheme('#374151', '#d1d5db')};
|
color: ${cssManager.bdTheme('#374151', '#d1d5db')};
|
||||||
}
|
}
|
||||||
|
|
||||||
.domainPills {
|
.routePills {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
gap: 4px;
|
gap: 4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.domainPill {
|
.routePill {
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
padding: 2px 8px;
|
padding: 2px 8px;
|
||||||
@@ -125,6 +125,17 @@ export class OpsViewCertificates extends DeesElement {
|
|||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.backoffIndicator {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 4px;
|
||||||
|
font-size: 11px;
|
||||||
|
color: ${cssManager.bdTheme('#9a3412', '#fb923c')};
|
||||||
|
padding: 2px 6px;
|
||||||
|
border-radius: 4px;
|
||||||
|
background: ${cssManager.bdTheme('#fff7ed', '#431407')};
|
||||||
|
}
|
||||||
|
|
||||||
.expiryInfo {
|
.expiryInfo {
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
}
|
}
|
||||||
@@ -218,12 +229,14 @@ export class OpsViewCertificates extends DeesElement {
|
|||||||
<dees-table
|
<dees-table
|
||||||
.data=${this.certState.certificates}
|
.data=${this.certState.certificates}
|
||||||
.displayFunction=${(cert: interfaces.requests.ICertificateInfo) => ({
|
.displayFunction=${(cert: interfaces.requests.ICertificateInfo) => ({
|
||||||
Route: cert.routeName,
|
Domain: cert.domain,
|
||||||
Domains: this.renderDomainPills(cert.domains),
|
Routes: this.renderRoutePills(cert.routeNames),
|
||||||
Status: this.renderStatusBadge(cert.status),
|
Status: this.renderStatusBadge(cert.status),
|
||||||
Source: this.renderSourceBadge(cert.source),
|
Source: this.renderSourceBadge(cert.source),
|
||||||
Expires: this.renderExpiry(cert.expiryDate),
|
Expires: this.renderExpiry(cert.expiryDate),
|
||||||
Error: cert.error
|
Error: cert.backoffInfo
|
||||||
|
? html`<span class="backoffIndicator">${cert.backoffInfo.failures} failures, retry ${this.formatRetryTime(cert.backoffInfo.retryAfter)}</span>`
|
||||||
|
: cert.error
|
||||||
? html`<span class="errorText" title="${cert.error}">${cert.error}</span>`
|
? html`<span class="errorText" title="${cert.error}">${cert.error}</span>`
|
||||||
: '',
|
: '',
|
||||||
})}
|
})}
|
||||||
@@ -245,11 +258,11 @@ export class OpsViewCertificates extends DeesElement {
|
|||||||
}
|
}
|
||||||
await appstate.certificateStatePart.dispatchAction(
|
await appstate.certificateStatePart.dispatchAction(
|
||||||
appstate.reprovisionCertificateAction,
|
appstate.reprovisionCertificateAction,
|
||||||
cert.routeName,
|
cert.domain,
|
||||||
);
|
);
|
||||||
const { DeesToast } = await import('@design.estate/dees-catalog');
|
const { DeesToast } = await import('@design.estate/dees-catalog');
|
||||||
DeesToast.show({
|
DeesToast.show({
|
||||||
message: `Reprovisioning triggered for ${cert.routeName}`,
|
message: `Reprovisioning triggered for ${cert.domain}`,
|
||||||
type: 'success',
|
type: 'success',
|
||||||
duration: 3000,
|
duration: 3000,
|
||||||
});
|
});
|
||||||
@@ -263,7 +276,7 @@ export class OpsViewCertificates extends DeesElement {
|
|||||||
const cert = actionData.item;
|
const cert = actionData.item;
|
||||||
const { DeesModal } = await import('@design.estate/dees-catalog');
|
const { DeesModal } = await import('@design.estate/dees-catalog');
|
||||||
await DeesModal.createAndShow({
|
await DeesModal.createAndShow({
|
||||||
heading: `Certificate: ${cert.routeName}`,
|
heading: `Certificate: ${cert.domain}`,
|
||||||
content: html`
|
content: html`
|
||||||
<div style="padding: 20px;">
|
<div style="padding: 20px;">
|
||||||
<dees-dataview-codebox
|
<dees-dataview-codebox
|
||||||
@@ -275,10 +288,10 @@ export class OpsViewCertificates extends DeesElement {
|
|||||||
`,
|
`,
|
||||||
menuOptions: [
|
menuOptions: [
|
||||||
{
|
{
|
||||||
name: 'Copy Route Name',
|
name: 'Copy Domain',
|
||||||
iconName: 'copy',
|
iconName: 'copy',
|
||||||
action: async () => {
|
action: async () => {
|
||||||
await navigator.clipboard.writeText(cert.routeName);
|
await navigator.clipboard.writeText(cert.domain);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
@@ -287,7 +300,7 @@ export class OpsViewCertificates extends DeesElement {
|
|||||||
},
|
},
|
||||||
]}
|
]}
|
||||||
heading1="Certificate Status"
|
heading1="Certificate Status"
|
||||||
heading2="TLS certificates across all routes"
|
heading2="TLS certificates by domain"
|
||||||
searchable
|
searchable
|
||||||
.pagination=${true}
|
.pagination=${true}
|
||||||
.paginationSize=${50}
|
.paginationSize=${50}
|
||||||
@@ -296,14 +309,14 @@ export class OpsViewCertificates extends DeesElement {
|
|||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
private renderDomainPills(domains: string[]): TemplateResult {
|
private renderRoutePills(routeNames: string[]): TemplateResult {
|
||||||
const maxShow = 3;
|
const maxShow = 3;
|
||||||
const visible = domains.slice(0, maxShow);
|
const visible = routeNames.slice(0, maxShow);
|
||||||
const remaining = domains.length - maxShow;
|
const remaining = routeNames.length - maxShow;
|
||||||
|
|
||||||
return html`
|
return html`
|
||||||
<span class="domainPills">
|
<span class="routePills">
|
||||||
${visible.map((d) => html`<span class="domainPill">${d}</span>`)}
|
${visible.map((r) => html`<span class="routePill">${r}</span>`)}
|
||||||
${remaining > 0 ? html`<span class="moreCount">+${remaining} more</span>` : ''}
|
${remaining > 0 ? html`<span class="moreCount">+${remaining} more</span>` : ''}
|
||||||
</span>
|
</span>
|
||||||
`;
|
`;
|
||||||
@@ -352,4 +365,16 @@ export class OpsViewCertificates extends DeesElement {
|
|||||||
</span>
|
</span>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private formatRetryTime(retryAfter?: string): string {
|
||||||
|
if (!retryAfter) return 'soon';
|
||||||
|
const retryDate = new Date(retryAfter);
|
||||||
|
const now = new Date();
|
||||||
|
const diffMs = retryDate.getTime() - now.getTime();
|
||||||
|
if (diffMs <= 0) return 'now';
|
||||||
|
const diffMin = Math.ceil(diffMs / 60000);
|
||||||
|
if (diffMin < 60) return `in ${diffMin}m`;
|
||||||
|
const diffHours = Math.ceil(diffMin / 60);
|
||||||
|
return `in ${diffHours}h`;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -52,8 +52,7 @@ export class OpsViewNetwork extends DeesElement {
|
|||||||
private requestCountHistory = new Map<number, number>(); // Track requests per time bucket
|
private requestCountHistory = new Map<number, number>(); // Track requests per time bucket
|
||||||
private trafficUpdateTimer: any = null;
|
private trafficUpdateTimer: any = null;
|
||||||
private requestsPerSecHistory: number[] = []; // Track requests/sec over time for trend
|
private requestsPerSecHistory: number[] = []; // Track requests/sec over time for trend
|
||||||
|
private historyLoaded = false; // Whether server-side throughput history has been loaded
|
||||||
// Removed byte tracking - now using real-time data from SmartProxy
|
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
super();
|
super();
|
||||||
@@ -111,6 +110,54 @@ export class OpsViewNetwork extends DeesElement {
|
|||||||
this.lastTrafficUpdateTime = now;
|
this.lastTrafficUpdateTime = now;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Load server-side throughput history into the chart.
|
||||||
|
* Called once when history data first arrives from the Rust engine.
|
||||||
|
* This pre-populates the chart so users see historical data immediately
|
||||||
|
* instead of starting from all zeros.
|
||||||
|
*/
|
||||||
|
private loadThroughputHistory() {
|
||||||
|
const history = this.networkState.throughputHistory;
|
||||||
|
if (!history || history.length === 0) return;
|
||||||
|
|
||||||
|
this.historyLoaded = true;
|
||||||
|
|
||||||
|
// Convert history points to chart data format (bytes/sec → Mbit/s)
|
||||||
|
const historyIn = history.map(p => ({
|
||||||
|
x: new Date(p.timestamp).toISOString(),
|
||||||
|
y: Math.round((p.in * 8) / 1000000 * 10) / 10,
|
||||||
|
}));
|
||||||
|
const historyOut = history.map(p => ({
|
||||||
|
x: new Date(p.timestamp).toISOString(),
|
||||||
|
y: Math.round((p.out * 8) / 1000000 * 10) / 10,
|
||||||
|
}));
|
||||||
|
|
||||||
|
// Use history as the chart data, keeping the most recent 60 points (5 min window)
|
||||||
|
const sliceStart = Math.max(0, historyIn.length - 60);
|
||||||
|
this.trafficDataIn = historyIn.slice(sliceStart);
|
||||||
|
this.trafficDataOut = historyOut.slice(sliceStart);
|
||||||
|
|
||||||
|
// If fewer than 60 points, pad the front with zeros
|
||||||
|
if (this.trafficDataIn.length < 60) {
|
||||||
|
const now = Date.now();
|
||||||
|
const range = 5 * 60 * 1000;
|
||||||
|
const bucketSize = range / 60;
|
||||||
|
const padCount = 60 - this.trafficDataIn.length;
|
||||||
|
const firstTimestamp = this.trafficDataIn.length > 0
|
||||||
|
? new Date(this.trafficDataIn[0].x).getTime()
|
||||||
|
: now;
|
||||||
|
|
||||||
|
const padIn = Array.from({ length: padCount }, (_, i) => ({
|
||||||
|
x: new Date(firstTimestamp - ((padCount - i) * bucketSize)).toISOString(),
|
||||||
|
y: 0,
|
||||||
|
}));
|
||||||
|
const padOut = padIn.map(p => ({ ...p }));
|
||||||
|
|
||||||
|
this.trafficDataIn = [...padIn, ...this.trafficDataIn];
|
||||||
|
this.trafficDataOut = [...padOut, ...this.trafficDataOut];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public static styles = [
|
public static styles = [
|
||||||
cssManager.defaultStyles,
|
cssManager.defaultStyles,
|
||||||
viewHostCss,
|
viewHostCss,
|
||||||
@@ -352,21 +399,6 @@ export class OpsViewNetwork extends DeesElement {
|
|||||||
return `${size.toFixed(1)} ${units[unitIndex]}`;
|
return `${size.toFixed(1)} ${units[unitIndex]}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
private calculateRequestsPerSecond(): number {
|
|
||||||
// Calculate from actual request data in the last minute
|
|
||||||
const oneMinuteAgo = Date.now() - 60000;
|
|
||||||
const recentRequests = this.networkRequests.filter(req => req.timestamp >= oneMinuteAgo);
|
|
||||||
const reqPerSec = Math.round(recentRequests.length / 60);
|
|
||||||
|
|
||||||
// Track history for trend (keep last 20 values)
|
|
||||||
this.requestsPerSecHistory.push(reqPerSec);
|
|
||||||
if (this.requestsPerSecHistory.length > 20) {
|
|
||||||
this.requestsPerSecHistory.shift();
|
|
||||||
}
|
|
||||||
|
|
||||||
return reqPerSec;
|
|
||||||
}
|
|
||||||
|
|
||||||
private calculateThroughput(): { in: number; out: number } {
|
private calculateThroughput(): { in: number; out: number } {
|
||||||
// Use real throughput data from network state
|
// Use real throughput data from network state
|
||||||
return {
|
return {
|
||||||
@@ -376,16 +408,17 @@ export class OpsViewNetwork extends DeesElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private renderNetworkStats(): TemplateResult {
|
private renderNetworkStats(): TemplateResult {
|
||||||
const reqPerSec = this.calculateRequestsPerSecond();
|
// Use server-side requests/sec from SmartProxy's Rust engine
|
||||||
|
const reqPerSec = this.networkState.requestsPerSecond || 0;
|
||||||
const throughput = this.calculateThroughput();
|
const throughput = this.calculateThroughput();
|
||||||
const activeConnections = this.statsState.serverStats?.activeConnections || 0;
|
const activeConnections = this.statsState.serverStats?.activeConnections || 0;
|
||||||
|
|
||||||
// Throughput data is now available in the stats tiles
|
// Track requests/sec history for the trend sparkline
|
||||||
|
this.requestsPerSecHistory.push(reqPerSec);
|
||||||
// Use request count history for the requests/sec trend
|
if (this.requestsPerSecHistory.length > 20) {
|
||||||
|
this.requestsPerSecHistory.shift();
|
||||||
|
}
|
||||||
const trendData = [...this.requestsPerSecHistory];
|
const trendData = [...this.requestsPerSecHistory];
|
||||||
|
|
||||||
// If we don't have enough data, pad with zeros
|
|
||||||
while (trendData.length < 20) {
|
while (trendData.length < 20) {
|
||||||
trendData.unshift(0);
|
trendData.unshift(0);
|
||||||
}
|
}
|
||||||
@@ -398,7 +431,7 @@ export class OpsViewNetwork extends DeesElement {
|
|||||||
type: 'number',
|
type: 'number',
|
||||||
icon: 'plug',
|
icon: 'plug',
|
||||||
color: activeConnections > 100 ? '#f59e0b' : '#22c55e',
|
color: activeConnections > 100 ? '#f59e0b' : '#22c55e',
|
||||||
description: `Total: ${this.statsState.serverStats?.totalConnections || 0}`,
|
description: `Total: ${this.networkState.requestsTotal || this.statsState.serverStats?.totalConnections || 0}`,
|
||||||
actions: [
|
actions: [
|
||||||
{
|
{
|
||||||
name: 'View Details',
|
name: 'View Details',
|
||||||
@@ -416,7 +449,7 @@ export class OpsViewNetwork extends DeesElement {
|
|||||||
icon: 'chartLine',
|
icon: 'chartLine',
|
||||||
color: '#3b82f6',
|
color: '#3b82f6',
|
||||||
trendData: trendData,
|
trendData: trendData,
|
||||||
description: `Average over last minute`,
|
description: `Total: ${this.formatNumber(this.networkState.requestsTotal || 0)} requests`,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'throughputIn',
|
id: 'throughputIn',
|
||||||
@@ -426,6 +459,7 @@ export class OpsViewNetwork extends DeesElement {
|
|||||||
type: 'number',
|
type: 'number',
|
||||||
icon: 'download',
|
icon: 'download',
|
||||||
color: '#22c55e',
|
color: '#22c55e',
|
||||||
|
description: `Total: ${this.formatBytes(this.networkState.totalBytes?.in || 0)}`,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'throughputOut',
|
id: 'throughputOut',
|
||||||
@@ -435,6 +469,7 @@ export class OpsViewNetwork extends DeesElement {
|
|||||||
type: 'number',
|
type: 'number',
|
||||||
icon: 'upload',
|
icon: 'upload',
|
||||||
color: '#8b5cf6',
|
color: '#8b5cf6',
|
||||||
|
description: `Total: ${this.formatBytes(this.networkState.totalBytes?.out || 0)}`,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
@@ -461,19 +496,32 @@ export class OpsViewNetwork extends DeesElement {
|
|||||||
return html``;
|
return html``;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Build per-IP bandwidth lookup
|
||||||
|
const bandwidthByIP = new Map<string, { in: number; out: number }>();
|
||||||
|
if (this.networkState.throughputByIP) {
|
||||||
|
for (const entry of this.networkState.throughputByIP) {
|
||||||
|
bandwidthByIP.set(entry.ip, { in: entry.in, out: entry.out });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Calculate total connections across all top IPs
|
// Calculate total connections across all top IPs
|
||||||
const totalConnections = this.networkState.topIPs.reduce((sum, ipData) => sum + ipData.count, 0);
|
const totalConnections = this.networkState.topIPs.reduce((sum, ipData) => sum + ipData.count, 0);
|
||||||
|
|
||||||
return html`
|
return html`
|
||||||
<dees-table
|
<dees-table
|
||||||
.data=${this.networkState.topIPs}
|
.data=${this.networkState.topIPs}
|
||||||
.displayFunction=${(ipData: { ip: string; count: number }) => ({
|
.displayFunction=${(ipData: { ip: string; count: number }) => {
|
||||||
|
const bw = bandwidthByIP.get(ipData.ip);
|
||||||
|
return {
|
||||||
'IP Address': ipData.ip,
|
'IP Address': ipData.ip,
|
||||||
'Connections': ipData.count,
|
'Connections': ipData.count,
|
||||||
'Percentage': totalConnections > 0 ? ((ipData.count / totalConnections) * 100).toFixed(1) + '%' : '0%',
|
'Bandwidth In': bw ? this.formatBitsPerSecond(bw.in) : '0 bit/s',
|
||||||
})}
|
'Bandwidth Out': bw ? this.formatBitsPerSecond(bw.out) : '0 bit/s',
|
||||||
|
'Share': totalConnections > 0 ? ((ipData.count / totalConnections) * 100).toFixed(1) + '%' : '0%',
|
||||||
|
};
|
||||||
|
}}
|
||||||
heading1="Top Connected IPs"
|
heading1="Top Connected IPs"
|
||||||
heading2="IPs with most active connections"
|
heading2="IPs with most active connections and bandwidth"
|
||||||
.pagination=${false}
|
.pagination=${false}
|
||||||
dataName="ip"
|
dataName="ip"
|
||||||
></dees-table>
|
></dees-table>
|
||||||
@@ -513,13 +561,10 @@ export class OpsViewNetwork extends DeesElement {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Generate traffic data based on request history
|
// Load server-side throughput history into chart (once)
|
||||||
this.updateTrafficData();
|
if (!this.historyLoaded && this.networkState.throughputHistory && this.networkState.throughputHistory.length > 0) {
|
||||||
|
this.loadThroughputHistory();
|
||||||
}
|
}
|
||||||
|
|
||||||
private updateTrafficData() {
|
|
||||||
// This method is called when network data updates
|
|
||||||
// The actual chart updates are handled by the timer calling addTrafficDataPoint()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private startTrafficUpdateTimer() {
|
private startTrafficUpdateTimer() {
|
||||||
|
|||||||
290
ts_web/elements/ops-view-remoteingress.ts
Normal file
290
ts_web/elements/ops-view-remoteingress.ts
Normal file
@@ -0,0 +1,290 @@
|
|||||||
|
import {
|
||||||
|
DeesElement,
|
||||||
|
html,
|
||||||
|
customElement,
|
||||||
|
type TemplateResult,
|
||||||
|
css,
|
||||||
|
state,
|
||||||
|
cssManager,
|
||||||
|
} from '@design.estate/dees-element';
|
||||||
|
import * as appstate from '../appstate.js';
|
||||||
|
import * as interfaces from '../../dist_ts_interfaces/index.js';
|
||||||
|
import { viewHostCss } from './shared/css.js';
|
||||||
|
import { type IStatsTile } from '@design.estate/dees-catalog';
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
interface HTMLElementTagNameMap {
|
||||||
|
'ops-view-remoteingress': OpsViewRemoteIngress;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@customElement('ops-view-remoteingress')
|
||||||
|
export class OpsViewRemoteIngress extends DeesElement {
|
||||||
|
@state()
|
||||||
|
accessor riState: appstate.IRemoteIngressState = appstate.remoteIngressStatePart.getState();
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
super();
|
||||||
|
const sub = appstate.remoteIngressStatePart.state.subscribe((newState) => {
|
||||||
|
this.riState = newState;
|
||||||
|
});
|
||||||
|
this.rxSubscriptions.push(sub);
|
||||||
|
}
|
||||||
|
|
||||||
|
async connectedCallback() {
|
||||||
|
await super.connectedCallback();
|
||||||
|
await appstate.remoteIngressStatePart.dispatchAction(appstate.fetchRemoteIngressAction, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static styles = [
|
||||||
|
cssManager.defaultStyles,
|
||||||
|
viewHostCss,
|
||||||
|
css`
|
||||||
|
.remoteIngressContainer {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.statusBadge {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: 3px 10px;
|
||||||
|
border-radius: 12px;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 600;
|
||||||
|
letter-spacing: 0.02em;
|
||||||
|
text-transform: uppercase;
|
||||||
|
}
|
||||||
|
|
||||||
|
.statusBadge.connected {
|
||||||
|
background: ${cssManager.bdTheme('#dcfce7', '#14532d')};
|
||||||
|
color: ${cssManager.bdTheme('#166534', '#4ade80')};
|
||||||
|
}
|
||||||
|
|
||||||
|
.statusBadge.disconnected {
|
||||||
|
background: ${cssManager.bdTheme('#fef2f2', '#450a0a')};
|
||||||
|
color: ${cssManager.bdTheme('#991b1b', '#f87171')};
|
||||||
|
}
|
||||||
|
|
||||||
|
.statusBadge.disabled {
|
||||||
|
background: ${cssManager.bdTheme('#f3f4f6', '#374151')};
|
||||||
|
color: ${cssManager.bdTheme('#6b7280', '#9ca3af')};
|
||||||
|
}
|
||||||
|
|
||||||
|
.secretDialog {
|
||||||
|
padding: 16px;
|
||||||
|
background: ${cssManager.bdTheme('#fffbeb', '#1c1917')};
|
||||||
|
border: 1px solid ${cssManager.bdTheme('#fbbf24', '#92400e')};
|
||||||
|
border-radius: 8px;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.secretDialog code {
|
||||||
|
display: block;
|
||||||
|
padding: 8px 12px;
|
||||||
|
background: ${cssManager.bdTheme('#1f2937', '#111827')};
|
||||||
|
color: #10b981;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-family: monospace;
|
||||||
|
font-size: 13px;
|
||||||
|
word-break: break-all;
|
||||||
|
margin: 8px 0;
|
||||||
|
user-select: all;
|
||||||
|
}
|
||||||
|
|
||||||
|
.secretDialog .warning {
|
||||||
|
font-size: 12px;
|
||||||
|
color: ${cssManager.bdTheme('#92400e', '#fbbf24')};
|
||||||
|
margin-top: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.portsDisplay {
|
||||||
|
display: flex;
|
||||||
|
gap: 4px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.portBadge {
|
||||||
|
display: inline-flex;
|
||||||
|
padding: 2px 8px;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 500;
|
||||||
|
background: ${cssManager.bdTheme('#eff6ff', '#172554')};
|
||||||
|
color: ${cssManager.bdTheme('#1e40af', '#60a5fa')};
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
];
|
||||||
|
|
||||||
|
render(): TemplateResult {
|
||||||
|
const totalEdges = this.riState.edges.length;
|
||||||
|
const connectedEdges = this.riState.statuses.filter(s => s.connected).length;
|
||||||
|
const disconnectedEdges = totalEdges - connectedEdges;
|
||||||
|
const activeTunnels = this.riState.statuses.reduce((sum, s) => sum + s.activeTunnels, 0);
|
||||||
|
|
||||||
|
const statsTiles: IStatsTile[] = [
|
||||||
|
{
|
||||||
|
id: 'totalEdges',
|
||||||
|
title: 'Total Edges',
|
||||||
|
type: 'number',
|
||||||
|
value: totalEdges,
|
||||||
|
icon: 'lucide:server',
|
||||||
|
description: 'Registered edge nodes',
|
||||||
|
color: '#3b82f6',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'connectedEdges',
|
||||||
|
title: 'Connected',
|
||||||
|
type: 'number',
|
||||||
|
value: connectedEdges,
|
||||||
|
icon: 'lucide:link',
|
||||||
|
description: 'Currently connected edges',
|
||||||
|
color: '#10b981',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'disconnectedEdges',
|
||||||
|
title: 'Disconnected',
|
||||||
|
type: 'number',
|
||||||
|
value: disconnectedEdges,
|
||||||
|
icon: 'lucide:unlink',
|
||||||
|
description: 'Offline edge nodes',
|
||||||
|
color: disconnectedEdges > 0 ? '#ef4444' : '#6b7280',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'activeTunnels',
|
||||||
|
title: 'Active Tunnels',
|
||||||
|
type: 'number',
|
||||||
|
value: activeTunnels,
|
||||||
|
icon: 'lucide:cable',
|
||||||
|
description: 'Active client connections',
|
||||||
|
color: '#8b5cf6',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
return html`
|
||||||
|
<ops-sectionheading>Remote Ingress</ops-sectionheading>
|
||||||
|
|
||||||
|
${this.riState.newEdgeSecret ? html`
|
||||||
|
<div class="secretDialog">
|
||||||
|
<strong>Edge Secret (copy now - shown only once):</strong>
|
||||||
|
<code>${this.riState.newEdgeSecret}</code>
|
||||||
|
<div class="warning">This secret will not be shown again. Save it securely.</div>
|
||||||
|
<dees-button
|
||||||
|
@click=${() => appstate.remoteIngressStatePart.dispatchAction(appstate.clearNewEdgeSecretAction, null)}
|
||||||
|
>Dismiss</dees-button>
|
||||||
|
</div>
|
||||||
|
` : ''}
|
||||||
|
|
||||||
|
<div class="remoteIngressContainer">
|
||||||
|
<dees-statsgrid .tiles=${statsTiles}></dees-statsgrid>
|
||||||
|
|
||||||
|
<dees-table
|
||||||
|
.heading1=${'Edge Nodes'}
|
||||||
|
.heading2=${'Manage remote ingress edge registrations'}
|
||||||
|
.data=${this.riState.edges}
|
||||||
|
.displayFunction=${(edge: interfaces.data.IRemoteIngress) => ({
|
||||||
|
name: edge.name,
|
||||||
|
status: this.getEdgeStatusHtml(edge),
|
||||||
|
publicIp: this.getEdgePublicIp(edge.id),
|
||||||
|
ports: this.getPortsHtml(edge.listenPorts),
|
||||||
|
tunnels: this.getEdgeTunnelCount(edge.id),
|
||||||
|
lastHeartbeat: this.getLastHeartbeat(edge.id),
|
||||||
|
})}
|
||||||
|
.dataActions=${[
|
||||||
|
{
|
||||||
|
name: 'Regenerate Secret',
|
||||||
|
iconName: 'lucide:key',
|
||||||
|
action: async (edge: interfaces.data.IRemoteIngress) => {
|
||||||
|
await appstate.remoteIngressStatePart.dispatchAction(
|
||||||
|
appstate.regenerateRemoteIngressSecretAction,
|
||||||
|
edge.id,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Delete',
|
||||||
|
iconName: 'lucide:trash2',
|
||||||
|
action: async (edge: interfaces.data.IRemoteIngress) => {
|
||||||
|
await appstate.remoteIngressStatePart.dispatchAction(
|
||||||
|
appstate.deleteRemoteIngressAction,
|
||||||
|
edge.id,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
.createNewAction=${async () => {
|
||||||
|
const { DeesModal } = await import('@design.estate/dees-catalog');
|
||||||
|
const result = await DeesModal.createAndShow({
|
||||||
|
heading: 'Create Edge Node',
|
||||||
|
content: html`
|
||||||
|
<dees-form>
|
||||||
|
<dees-input-text .key=${'name'} .label=${'Name'} .required=${true}></dees-input-text>
|
||||||
|
<dees-input-text .key=${'listenPorts'} .label=${'Listen Ports (comma-separated)'} .required=${true} .value=${'443,25'}></dees-input-text>
|
||||||
|
<dees-input-text .key=${'tags'} .label=${'Tags (comma-separated, optional)'}></dees-input-text>
|
||||||
|
</dees-form>
|
||||||
|
`,
|
||||||
|
menuOptions: [],
|
||||||
|
});
|
||||||
|
if (result) {
|
||||||
|
const formData = result as any;
|
||||||
|
const ports = (formData.name ? formData.listenPorts : '443')
|
||||||
|
.split(',')
|
||||||
|
.map((p: string) => parseInt(p.trim(), 10))
|
||||||
|
.filter((p: number) => !isNaN(p));
|
||||||
|
const tags = formData.tags
|
||||||
|
? formData.tags.split(',').map((t: string) => t.trim()).filter(Boolean)
|
||||||
|
: undefined;
|
||||||
|
await appstate.remoteIngressStatePart.dispatchAction(
|
||||||
|
appstate.createRemoteIngressAction,
|
||||||
|
{
|
||||||
|
name: formData.name,
|
||||||
|
listenPorts: ports,
|
||||||
|
tags,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
></dees-table>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
private getEdgeStatus(edgeId: string): interfaces.data.IRemoteIngressStatus | undefined {
|
||||||
|
return this.riState.statuses.find(s => s.edgeId === edgeId);
|
||||||
|
}
|
||||||
|
|
||||||
|
private getEdgeStatusHtml(edge: interfaces.data.IRemoteIngress): TemplateResult {
|
||||||
|
if (!edge.enabled) {
|
||||||
|
return html`<span class="statusBadge disabled">Disabled</span>`;
|
||||||
|
}
|
||||||
|
const status = this.getEdgeStatus(edge.id);
|
||||||
|
if (status?.connected) {
|
||||||
|
return html`<span class="statusBadge connected">Connected</span>`;
|
||||||
|
}
|
||||||
|
return html`<span class="statusBadge disconnected">Disconnected</span>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
private getEdgePublicIp(edgeId: string): string {
|
||||||
|
const status = this.getEdgeStatus(edgeId);
|
||||||
|
return status?.publicIp || '-';
|
||||||
|
}
|
||||||
|
|
||||||
|
private getPortsHtml(ports: number[]): TemplateResult {
|
||||||
|
return html`<div class="portsDisplay">${ports.map(p => html`<span class="portBadge">${p}</span>`)}</div>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
private getEdgeTunnelCount(edgeId: string): number {
|
||||||
|
const status = this.getEdgeStatus(edgeId);
|
||||||
|
return status?.activeTunnels || 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
private getLastHeartbeat(edgeId: string): string {
|
||||||
|
const status = this.getEdgeStatus(edgeId);
|
||||||
|
if (!status?.lastHeartbeat) return '-';
|
||||||
|
const ago = Date.now() - status.lastHeartbeat;
|
||||||
|
if (ago < 60000) return `${Math.floor(ago / 1000)}s ago`;
|
||||||
|
if (ago < 3600000) return `${Math.floor(ago / 60000)}m ago`;
|
||||||
|
return `${Math.floor(ago / 3600000)}h ago`;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -34,6 +34,13 @@ For reporting bugs, issues, or security vulnerabilities, please visit [community
|
|||||||
- **Security** — Security incidents from email processing
|
- **Security** — Security incidents from email processing
|
||||||
- Bounce record management and suppression list controls
|
- Bounce record management and suppression list controls
|
||||||
|
|
||||||
|
### 🔐 Certificate Management
|
||||||
|
- Domain-centric certificate overview with status indicators
|
||||||
|
- Certificate source tracking (ACME, provision function, static)
|
||||||
|
- Expiry date monitoring and alerts
|
||||||
|
- Per-domain backoff status for failed provisions
|
||||||
|
- One-click reprovisioning per domain
|
||||||
|
|
||||||
### 📜 Log Viewer
|
### 📜 Log Viewer
|
||||||
- Real-time log streaming
|
- Real-time log streaming
|
||||||
- Filter by log level (error, warning, info, debug)
|
- Filter by log level (error, warning, info, debug)
|
||||||
@@ -77,6 +84,7 @@ ts_web/
|
|||||||
├── ops-view-overview.ts # Overview statistics
|
├── ops-view-overview.ts # Overview statistics
|
||||||
├── ops-view-network.ts # Network monitoring
|
├── ops-view-network.ts # Network monitoring
|
||||||
├── ops-view-emails.ts # Email queue management
|
├── ops-view-emails.ts # Email queue management
|
||||||
|
├── ops-view-certificates.ts # Certificate overview & reprovisioning
|
||||||
├── ops-view-logs.ts # Log viewer
|
├── ops-view-logs.ts # Log viewer
|
||||||
├── ops-view-config.ts # Configuration display
|
├── ops-view-config.ts # Configuration display
|
||||||
├── ops-view-security.ts # Security dashboard
|
├── ops-view-security.ts # Security dashboard
|
||||||
@@ -132,6 +140,7 @@ removeFromSuppressionAction(email) // Remove from suppression list
|
|||||||
/emails/sent → Sent emails
|
/emails/sent → Sent emails
|
||||||
/emails/failed → Failed emails
|
/emails/failed → Failed emails
|
||||||
/emails/security → Security incidents
|
/emails/security → Security incidents
|
||||||
|
/certificates → Certificate management
|
||||||
/logs → Log viewer
|
/logs → Log viewer
|
||||||
/configuration → System configuration
|
/configuration → System configuration
|
||||||
/security → Security dashboard
|
/security → Security dashboard
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import * as appstate from './appstate.js';
|
|||||||
|
|
||||||
const SmartRouter = plugins.domtools.plugins.smartrouter.SmartRouter;
|
const SmartRouter = plugins.domtools.plugins.smartrouter.SmartRouter;
|
||||||
|
|
||||||
export const validViews = ['overview', 'network', 'emails', 'logs', 'configuration', 'security', 'certificates'] as const;
|
export const validViews = ['overview', 'network', 'emails', 'logs', 'configuration', 'security', 'certificates', 'remoteingress'] as const;
|
||||||
export const validEmailFolders = ['queued', 'sent', 'failed', 'security'] as const;
|
export const validEmailFolders = ['queued', 'sent', 'failed', 'security'] as const;
|
||||||
|
|
||||||
export type TValidView = typeof validViews[number];
|
export type TValidView = typeof validViews[number];
|
||||||
|
|||||||
Reference in New Issue
Block a user