Compare commits

...

33 Commits
v6.0.0 ... main

Author SHA1 Message Date
31a6510d8b v6.7.0
Some checks failed
Docker (tags) / security (push) Failing after 1s
Docker (tags) / test (push) Has been skipped
Docker (tags) / release (push) Has been skipped
Docker (tags) / metadata (push) Has been skipped
2026-02-17 11:56:54 +00:00
b5e760ae07 feat(remote-ingress): Support auto-derived effective listen ports, make listenPorts optional, add toggle action and refine remote ingress creation/management UI 2026-02-17 11:56:54 +00:00
ea32babaac v6.6.1
Some checks failed
Docker (tags) / security (push) Failing after 1s
Docker (tags) / test (push) Has been skipped
Docker (tags) / release (push) Has been skipped
Docker (tags) / metadata (push) Has been skipped
2026-02-17 10:57:27 +00:00
a4ddedaf46 fix(icons): standardize icon identifiers to lucide-prefixed names across operational views 2026-02-17 10:57:27 +00:00
7ce09c53ca v6.6.0
Some checks failed
Docker (tags) / security (push) Failing after 1s
Docker (tags) / test (push) Has been skipped
Docker (tags) / release (push) Has been skipped
Docker (tags) / metadata (push) Has been skipped
2026-02-17 10:55:31 +00:00
69be2295f1 feat(remoteingress): derive effective remote ingress listen ports from route configs and expose them via ops API 2026-02-17 10:55:31 +00:00
018efa32f6 v6.5.0
Some checks failed
Docker (tags) / security (push) Failing after 1s
Docker (tags) / test (push) Has been skipped
Docker (tags) / release (push) Has been skipped
Docker (tags) / metadata (push) Has been skipped
2026-02-16 22:42:30 +00:00
2530918dc6 v6.4.5
Some checks failed
Docker (tags) / security (push) Failing after 1s
Docker (tags) / test (push) Has been skipped
Docker (tags) / release (push) Has been skipped
Docker (tags) / metadata (push) Has been skipped
2026-02-16 17:47:43 +00:00
0b09ea1573 fix(remoteingress): mark remote ingress data actions as row actions and bump @design.estate/dees-catalog dependency 2026-02-16 17:47:43 +00:00
21157477b4 v6.4.4
Some checks failed
Docker (tags) / security (push) Failing after 1s
Docker (tags) / test (push) Has been skipped
Docker (tags) / release (push) Has been skipped
Docker (tags) / metadata (push) Has been skipped
2026-02-16 14:50:44 +00:00
fcf36e5cd5 fix(deps): bump @push.rocks/smartproxy to ^25.7.3 2026-02-16 14:50:44 +00:00
f5740fa565 v6.4.3
Some checks failed
Docker (tags) / security (push) Failing after 1s
Docker (tags) / test (push) Has been skipped
Docker (tags) / release (push) Has been skipped
Docker (tags) / metadata (push) Has been skipped
2026-02-16 13:44:38 +00:00
4a9fba53a9 fix(deps): bump @push.rocks/smartproxy to ^25.7.2 2026-02-16 13:44:38 +00:00
da61adc9a2 v6.4.2
Some checks failed
Docker (tags) / security (push) Failing after 1s
Docker (tags) / test (push) Has been skipped
Docker (tags) / release (push) Has been skipped
Docker (tags) / metadata (push) Has been skipped
2026-02-16 13:32:24 +00:00
616066ffd0 fix(smartproxy): bump @push.rocks/smartproxy to ^25.7.1 2026-02-16 13:32:24 +00:00
bd5cccb405 v6.4.1
Some checks failed
Docker (tags) / security (push) Failing after 1s
Docker (tags) / test (push) Has been skipped
Docker (tags) / release (push) Has been skipped
Docker (tags) / metadata (push) Has been skipped
2026-02-16 13:16:50 +00:00
fbade85cda fix(deps): bump dependencies: @push.rocks/smartproxy to ^25.7.0 and @serve.zone/remoteingress to ^3.0.2 2026-02-16 13:16:50 +00:00
9060d26f3a v6.4.0
Some checks failed
Docker (tags) / security (push) Failing after 1s
Docker (tags) / test (push) Has been skipped
Docker (tags) / release (push) Has been skipped
Docker (tags) / metadata (push) Has been skipped
2026-02-16 11:25:16 +00:00
c889141ec3 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 2026-02-16 11:25:16 +00:00
fb472f353c v6.3.0
Some checks failed
Docker (tags) / security (push) Failing after 1s
Docker (tags) / test (push) Has been skipped
Docker (tags) / release (push) Has been skipped
Docker (tags) / metadata (push) Has been skipped
2026-02-16 09:52:38 +00:00
090bd747e1 feat(dcrouter): add configurable baseDir and centralized path resolution; use resolved data paths for storage, cache and DNS 2026-02-16 09:52:38 +00:00
4d77a94bbb v6.2.4
Some checks failed
Docker (tags) / security (push) Failing after 1s
Docker (tags) / test (push) Has been skipped
Docker (tags) / release (push) Has been skipped
Docker (tags) / metadata (push) Has been skipped
2026-02-16 09:02:57 +00:00
7f5284b10f fix(deps): bump @push.rocks/smartproxy to ^25.5.0 2026-02-16 09:02:57 +00:00
9cd5db2d81 v6.2.3
Some checks failed
Docker (tags) / security (push) Failing after 1s
Docker (tags) / test (push) Has been skipped
Docker (tags) / release (push) Has been skipped
Docker (tags) / metadata (push) Has been skipped
2026-02-16 02:50:25 +00:00
de0b7d1fe0 fix(dcrouter): persist proxy certificate validity dates and improve certificate status initialization 2026-02-16 02:50:25 +00:00
4e32745a8f v6.2.2
Some checks failed
Docker (tags) / security (push) Failing after 1s
Docker (tags) / test (push) Has been skipped
Docker (tags) / release (push) Has been skipped
Docker (tags) / metadata (push) Has been skipped
2026-02-16 01:58:39 +00:00
121573de2f fix(certs): Populate certificate status for cert-store-loaded certificates after SmartProxy startup and check proxy-certs in opsserver certificate handler 2026-02-16 01:58:39 +00:00
cd957526e2 v6.2.1
Some checks failed
Docker (tags) / security (push) Failing after 1s
Docker (tags) / test (push) Has been skipped
Docker (tags) / release (push) Has been skipped
Docker (tags) / metadata (push) Has been skipped
2026-02-16 00:56:41 +00:00
7aa5f07731 fix(smartacme,storage): Respect wildcard domain requests when retrieving certificates and treat empty/whitespace storage values as null in getJSON 2026-02-16 00:56:41 +00:00
5b6f7b30c3 v6.2.0
Some checks failed
Docker (tags) / security (push) Failing after 1s
Docker (tags) / test (push) Has been skipped
Docker (tags) / release (push) Has been skipped
Docker (tags) / metadata (push) Has been skipped
2026-02-16 00:26:35 +00:00
18cc21a49e feat(ts_web): add Certificate Management documentation and ops-view-certificates reference 2026-02-16 00:26:35 +00:00
46fa2f6ade v6.1.0
Some checks failed
Docker (tags) / security (push) Failing after 1s
Docker (tags) / test (push) Has been skipped
Docker (tags) / release (push) Has been skipped
Docker (tags) / metadata (push) Has been skipped
2026-02-16 00:22:23 +00:00
0a6315f177 feat(certs): integrate smartacme v9 for ACME certificate provisioning and add certificate management features, docs, dashboard views, API endpoints, and per-domain backoff scheduler 2026-02-16 00:22:23 +00:00
35 changed files with 1941 additions and 439 deletions

View File

@@ -1,5 +1,135 @@
# Changelog # Changelog
## 2026-02-17 - 6.7.0 - feat(remote-ingress)
Support auto-derived effective listen ports, make listenPorts optional, add toggle action and refine remote ingress creation/management UI
- Add effectiveListenPorts?: number[] to IRemoteIngress interface (present in API responses)
- Make createRemoteIngressAction.listenPorts optional and update creation modal to allow empty ports (auto-derived)
- Add toggleRemoteIngressAction to enable/disable remote ingress edges and wire up Enable/Disable row/context-menu actions
- Update getPortsHtml to prefer manual listenPorts, fall back to effectiveListenPorts, show '(auto)' when derived and 'none' when no ports
- Standardize UI actions to use inRow/contextmenu and actionFunc signatures; update create modal to use explicit Cancel/Create menu options and collect form data programmatically
## 2026-02-17 - 6.6.1 - fix(icons)
standardize icon identifiers to lucide-prefixed names across operational views
- Replaced legacy/ambiguous icon names with 'lucide:...' identifiers in four UI modules: ts_web/elements/ops-view-certificates.ts, ops-view-network.ts, ops-view-overview.ts, and ops-view-security.ts.
- Updated common action/menu icons (e.g. arrowsRotate -> lucide:RefreshCw, magnifyingGlass -> lucide:Search, copy -> lucide:Copy, fileExport -> lucide:FileOutput).
- Mapped dashboard/tile icons to lucide equivalents (e.g. server -> lucide:Server, networkWired/sitemap -> lucide:Network, download/upload -> lucide:Download/Upload, microchip/memory -> lucide:Cpu/MemoryStick).
- Normalized alert and status icons to lucide names (e.g. triangleExclamation -> lucide:TriangleAlert, shield/userShield -> lucide:Shield/ShieldCheck, clock/clockRotateLeft -> lucide:Clock/History).
## 2026-02-17 - 6.6.0 - feat(remoteingress)
derive effective remote ingress listen ports from route configs and expose them via ops API
- Derive listen ports from SmartProxy route configs with remoteIngress.enabled; supports optional edgeFilter to target edges by id or tags.
- Add RemoteIngressManager.setRoutes(), derivePortsForEdge(), and getEffectiveListenPorts() which falls back to manual listenPorts when present.
- dcrouter now supplies route configs to RemoteIngressManager during initialization and when updating SmartProxy configuration to keep derived ports in sync.
- Ops API now returns effectiveListenPorts for edges; createRemoteIngress.listenPorts is optional and createEdge defaults listenPorts to an empty array.
- Bump dependency @serve.zone/remoteingress to ^3.0.4 to align types/behavior.
## 2026-02-16 - 6.5.0 - feat(ops-view-remoteingress)
add 'Create Edge Node' header action to remote ingress table and remove duplicate createNewAction
- Add a 'Create Edge Node' header action in dataActions that opens DeesModal to collect name, listenPorts and tags
- Parse comma-separated listenPorts into integer array and normalize optional tags
- Dispatch appstate.createRemoteIngressAction with the collected payload
- Remove the previously duplicated createNewAction prop from the dees-table
## 2026-02-16 - 6.4.5 - fix(remoteingress)
mark remote ingress data actions as row actions and bump @design.estate/dees-catalog dependency
- Add type:['row'] to 'Regenerate Secret' and 'Delete' dataActions in ts_web/elements/ops-view-remoteingress.ts to ensure they are treated as row actions in the UI
- Bump @design.estate/dees-catalog from ^3.42.0 to ^3.42.2 in package.json
## 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, oneclick 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) ## 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. 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.

View File

@@ -1,7 +1,7 @@
{ {
"name": "@serve.zone/dcrouter", "name": "@serve.zone/dcrouter",
"private": false, "private": false,
"version": "6.0.0", "version": "6.7.0",
"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": {
@@ -32,11 +32,11 @@
"@api.global/typedserver": "^8.3.0", "@api.global/typedserver": "^8.3.0",
"@api.global/typedsocket": "^4.1.0", "@api.global/typedsocket": "^4.1.0",
"@apiclient.xyz/cloudflare": "^7.1.0", "@apiclient.xyz/cloudflare": "^7.1.0",
"@design.estate/dees-catalog": "^3.42.0", "@design.estate/dees-catalog": "^3.42.2",
"@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",
@@ -49,13 +49,14 @@
"@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.3.1", "@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.4",
"@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"

393
pnpm-lock.yaml generated
View File

@@ -24,8 +24,8 @@ importers:
specifier: ^7.1.0 specifier: ^7.1.0
version: 7.1.0 version: 7.1.0
'@design.estate/dees-catalog': '@design.estate/dees-catalog':
specifier: ^3.42.0 specifier: ^3.42.2
version: 3.42.0(@tiptap/pm@2.27.2) version: 3.42.2(@tiptap/pm@2.27.2)
'@design.estate/dees-element': '@design.estate/dees-element':
specifier: ^2.1.6 specifier: ^2.1.6
version: 2.1.6 version: 2.1.6
@@ -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(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)
@@ -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.3.1 specifier: ^25.7.3
version: 25.3.1 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.4
version: 3.0.4
'@tsclass/tsclass': '@tsclass/tsclass':
specifier: ^9.3.0 specifier: ^9.3.0
version: 9.3.0 version: 9.3.0
@@ -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==}
@@ -351,8 +351,8 @@ packages:
'@configvault.io/interfaces@1.0.17': '@configvault.io/interfaces@1.0.17':
resolution: {integrity: sha512-bEcCUR2VBDJsTin8HQh8Uw/mlYl2v8A3jMIaQ+MTB9Hrqd6CZL2dL7iJdWyFl/3EIX+LDxWFR+Oq7liIq7w+1Q==} resolution: {integrity: sha512-bEcCUR2VBDJsTin8HQh8Uw/mlYl2v8A3jMIaQ+MTB9Hrqd6CZL2dL7iJdWyFl/3EIX+LDxWFR+Oq7liIq7w+1Q==}
'@design.estate/dees-catalog@3.42.0': '@design.estate/dees-catalog@3.42.2':
resolution: {integrity: sha512-pArkafnrhRsHsSxKUMUM2YP5ei/AbcchPEKZY2PyHHAdXcNxyT3pE2Oh1FPcs1pqF2LpEgJRq8KFQbFhvhp8Nw==} resolution: {integrity: sha512-e/d5XpIjuOmQIxHnBq81Uq+TyBHX92Ie1n7jEFBCYtxvi3+P2LU1sQ3VDrvLTpkwGxq7iyagu7BYWHYRtPLPmw==}
'@design.estate/dees-comms@1.0.30': '@design.estate/dees-comms@1.0.30':
resolution: {integrity: sha512-KchMlklJfKAjQiJiR0xmofXtQ27VgZtBIxcMwPE9d+h3jJRv+lPZxzBQVOM0eyM0uS44S5vJMZ11IeV4uDXSHg==} resolution: {integrity: sha512-KchMlklJfKAjQiJiR0xmofXtQ27VgZtBIxcMwPE9d+h3jJRv+lPZxzBQVOM0eyM0uS44S5vJMZ11IeV4uDXSHg==}
@@ -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==}
@@ -684,74 +681,74 @@ packages:
'@mongodb-js/saslprep@1.4.6': '@mongodb-js/saslprep@1.4.6':
resolution: {integrity: sha512-y+x3H1xBZd38n10NZF/rEBlvDOOMQ6LKUTHqr8R9VkJ+mmQOYtJFxIlkkK8fZrtOiL6VixbOBWMbZGBdal3Z1g==} resolution: {integrity: sha512-y+x3H1xBZd38n10NZF/rEBlvDOOMQ6LKUTHqr8R9VkJ+mmQOYtJFxIlkkK8fZrtOiL6VixbOBWMbZGBdal3Z1g==}
'@napi-rs/canvas-android-arm64@0.1.91': '@napi-rs/canvas-android-arm64@0.1.92':
resolution: {integrity: sha512-SLLzXXgSnfct4zy/BVAfweZQkYkPJsNsJ2e5DOE8DFEHC6PufyUrwb12yqeu2So2IOIDpWJJaDAxKY/xpy6MYQ==} resolution: {integrity: sha512-rDOtq53ujfOuevD5taxAuIFALuf1QsQWZe1yS/N4MtT+tNiDBEdjufvQRPWZ11FubL2uwgP8ApYU3YOaNu1ZsQ==}
engines: {node: '>= 10'} engines: {node: '>= 10'}
cpu: [arm64] cpu: [arm64]
os: [android] os: [android]
'@napi-rs/canvas-darwin-arm64@0.1.91': '@napi-rs/canvas-darwin-arm64@0.1.92':
resolution: {integrity: sha512-bzdbCjIjw3iRuVFL+uxdSoMra/l09ydGNX9gsBxO/zg+5nlppscIpj6gg+nL6VNG85zwUarDleIrUJ+FWHvmuA==} resolution: {integrity: sha512-4PT6GRGCr7yMRehp42x0LJb1V0IEy1cDZDDayv7eKbFUIGbPFkV7CRC9Bee5MPkjg1EB4ZPXXUyy3gjQm7mR8Q==}
engines: {node: '>= 10'} engines: {node: '>= 10'}
cpu: [arm64] cpu: [arm64]
os: [darwin] os: [darwin]
'@napi-rs/canvas-darwin-x64@0.1.91': '@napi-rs/canvas-darwin-x64@0.1.92':
resolution: {integrity: sha512-q3qpkpw0IsG9fAS/dmcGIhCVoNxj8ojbexZKWwz3HwxlEWsLncEQRl4arnxrwbpLc2nTNTyj4WwDn7QR5NDAaA==} resolution: {integrity: sha512-5e/3ZapP7CqPtDcZPtmowCsjoyQwuNMMD7c0GKPtZQ8pgQhLkeq/3fmk0HqNSD1i227FyJN/9pDrhw/UMTkaWA==}
engines: {node: '>= 10'} engines: {node: '>= 10'}
cpu: [x64] cpu: [x64]
os: [darwin] os: [darwin]
'@napi-rs/canvas-linux-arm-gnueabihf@0.1.91': '@napi-rs/canvas-linux-arm-gnueabihf@0.1.92':
resolution: {integrity: sha512-Io3g8wJZVhK8G+Fpg1363BE90pIPqg+ZbeehYNxPWDSzbgwU3xV0l8r/JBzODwC7XHi1RpFEk+xyUTMa2POj6w==} resolution: {integrity: sha512-j6KaLL9iir68lwpzzY+aBGag1PZp3+gJE2mQ3ar4VJVmyLRVOh+1qsdNK1gfWoAVy5w6U7OEYFrLzN2vOFUSng==}
engines: {node: '>= 10'} engines: {node: '>= 10'}
cpu: [arm] cpu: [arm]
os: [linux] os: [linux]
'@napi-rs/canvas-linux-arm64-gnu@0.1.91': '@napi-rs/canvas-linux-arm64-gnu@0.1.92':
resolution: {integrity: sha512-HBnto+0rxx1bQSl8bCWA9PyBKtlk2z/AI32r3cu4kcNO+M/5SD4b0v1MWBWZyqMQyxFjWgy3ECyDjDKMC6tY1A==} resolution: {integrity: sha512-s3NlnJMHOSotUYVoTCoC1OcomaChFdKmZg0VsHFeIkeHbwX0uPHP4eCX1irjSfMykyvsGHTQDfBAtGYuqxCxhQ==}
engines: {node: '>= 10'} engines: {node: '>= 10'}
cpu: [arm64] cpu: [arm64]
os: [linux] os: [linux]
'@napi-rs/canvas-linux-arm64-musl@0.1.91': '@napi-rs/canvas-linux-arm64-musl@0.1.92':
resolution: {integrity: sha512-/eJtVe2Xw9A86I4kwXpxxoNagdGclu12/NSMsfoL8q05QmeRCbfjhg1PJS7ENAuAvaiUiALGrbVfeY1KU1gztQ==} resolution: {integrity: sha512-xV0GQnukYq5qY+ebkAwHjnP2OrSGBxS3vSi1zQNQj0bkXU6Ou+Tw7JjCM7pZcQ28MUyEBS1yKfo7rc7ip2IPFQ==}
engines: {node: '>= 10'} engines: {node: '>= 10'}
cpu: [arm64] cpu: [arm64]
os: [linux] os: [linux]
'@napi-rs/canvas-linux-riscv64-gnu@0.1.91': '@napi-rs/canvas-linux-riscv64-gnu@0.1.92':
resolution: {integrity: sha512-floNK9wQuRWevUhhXRcuis7h0zirdytVxPgkonWO+kQlbvxV7gEUHGUFQyq4n55UHYFwgck1SAfJ1HuXv/+ppQ==} resolution: {integrity: sha512-+GKvIFbQ74eB/TopEdH6XIXcvOGcuKvCITLGXy7WLJAyNp3Kdn1ncjxg91ihatBaPR+t63QOE99yHuIWn3UQ9w==}
engines: {node: '>= 10'} engines: {node: '>= 10'}
cpu: [riscv64] cpu: [riscv64]
os: [linux] os: [linux]
'@napi-rs/canvas-linux-x64-gnu@0.1.91': '@napi-rs/canvas-linux-x64-gnu@0.1.92':
resolution: {integrity: sha512-c3YDqBdf7KETuZy2AxsHFMsBBX1dWT43yFfWUq+j1IELdgesWtxf/6N7csi3VPf6VA3PmnT9EhMyb+M1wfGtqw==} resolution: {integrity: sha512-tFd6MwbEhZ1g64iVY2asV+dOJC+GT3Yd6UH4G3Hp0/VHQ6qikB+nvXEULskFYZ0+wFqlGPtXjG1Jmv7sJy+3Ww==}
engines: {node: '>= 10'} engines: {node: '>= 10'}
cpu: [x64] cpu: [x64]
os: [linux] os: [linux]
'@napi-rs/canvas-linux-x64-musl@0.1.91': '@napi-rs/canvas-linux-x64-musl@0.1.92':
resolution: {integrity: sha512-RpZ3RPIwgEcNBHSHSX98adm+4VP8SMT5FN6250s5jQbWpX/XNUX5aLMfAVJS/YnDjS1QlsCgQxFOPU0aCCWgag==} resolution: {integrity: sha512-uSuqeSveB/ZGd72VfNbHCSXO9sArpZTvznMVsb42nqPP7gBGEH6NJQ0+hmF+w24unEmxBhPYakP/Wiosm16KkA==}
engines: {node: '>= 10'} engines: {node: '>= 10'}
cpu: [x64] cpu: [x64]
os: [linux] os: [linux]
'@napi-rs/canvas-win32-arm64-msvc@0.1.91': '@napi-rs/canvas-win32-arm64-msvc@0.1.92':
resolution: {integrity: sha512-gF8MBp4X134AgVurxqlCdDA2qO0WaDdi9o6Sd5rWRVXRhWhYQ6wkdEzXNLIrmmros0Tsp2J0hQzx4ej/9O8trQ==} resolution: {integrity: sha512-20SK5AU/OUNz9ZuoAPj5ekWai45EIBDh/XsdrVZ8le/pJVlhjFU3olbumSQUXRFn7lBRS+qwM8kA//uLaDx6iQ==}
engines: {node: '>= 10'} engines: {node: '>= 10'}
cpu: [arm64] cpu: [arm64]
os: [win32] os: [win32]
'@napi-rs/canvas-win32-x64-msvc@0.1.91': '@napi-rs/canvas-win32-x64-msvc@0.1.92':
resolution: {integrity: sha512-++gtW9EV/neKI8TshD8WFxzBYALSPag2kFRahIJV+LYsyt5kBn21b1dBhEUDHf7O+wiZmuFCeUa7QKGHnYRZBA==} resolution: {integrity: sha512-KEhyZLzq1MXCNlXybz4k25MJmHFp+uK1SIb8yJB0xfrQjz5aogAMhyseSzewo+XxAq3OAOdyKvfHGNzT3w1RPg==}
engines: {node: '>= 10'} engines: {node: '>= 10'}
cpu: [x64] cpu: [x64]
os: [win32] os: [win32]
'@napi-rs/canvas@0.1.91': '@napi-rs/canvas@0.1.92':
resolution: {integrity: sha512-eeIe1GoB74P1B0Nkw6pV8BCQ3hfCfvyYr4BntzlCsnFXzVJiPMDnLeIx3gVB0xQMblHYnjK/0nCLvirEhOjr5g==} resolution: {integrity: sha512-q7ZaUCJkEU5BeOdE7fBx1XWRd2T5Ady65nxq4brMf5L4cE1VV/ACq5w9Z5b/IVJs8CwSSIwc30nlthH0gFo4Ig==}
engines: {node: '>= 10'} engines: {node: '>= 10'}
'@napi-rs/wasm-runtime@1.0.7': '@napi-rs/wasm-runtime@1.0.7':
@@ -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==}
@@ -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.3.1': '@push.rocks/smartproxy@25.7.3':
resolution: {integrity: sha512-kGJGpx3KBUz+qWU2L9B2gbZoUbQEG2BFe6ZzK0b68Y32nHoSIMjol14hzc3sRgW1p/loWy+Gj+5j0KuVytKWmA==} 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,6 +1125,9 @@ 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==}
@@ -1340,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.4':
resolution: {integrity: sha512-ZD66Y8fvW7SjealziOlhaC7+Y/3gxQkZlj/X8rxgVHmGhlc/YQtn6H6LNVazbM88BXK5ns004Qo6ongAB6Ho0Q==}
'@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'}
@@ -1563,31 +1566,6 @@ packages:
'@socket.io/component-emitter@3.1.2': '@socket.io/component-emitter@3.1.2':
resolution: {integrity: sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA==} resolution: {integrity: sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA==}
'@svgdotjs/svg.draggable.js@3.0.6':
resolution: {integrity: sha512-7iJFm9lL3C40HQcqzEfezK2l+dW2CpoVY3b77KQGqc8GXWa6LhhmX5Ckv7alQfUXBuZbjpICZ+Dvq1czlGx7gA==}
peerDependencies:
'@svgdotjs/svg.js': ^3.2.4
'@svgdotjs/svg.filter.js@3.0.9':
resolution: {integrity: sha512-/69XMRCDoam2HgC4ldHIaDgeQf1ViHIsa0Ld4uWgiXtZ+E24DWHe/9Ib6kbNiZ7WRIdlVokUDR1Fg0kjIpkfbw==}
engines: {node: '>= 0.8.0'}
'@svgdotjs/svg.js@3.2.5':
resolution: {integrity: sha512-/VNHWYhNu+BS7ktbYoVGrCmsXDh+chFMaONMwGNdIBcFHrWqk2jY8fNyr3DLdtQUIalvkPfM554ZSFa3dm3nxQ==}
'@svgdotjs/svg.resize.js@2.0.5':
resolution: {integrity: sha512-4heRW4B1QrJeENfi7326lUPYBCevj78FJs8kfeDxn5st0IYPIRXoTtOSYvTzFWgaWWXd3YCDE6ao4fmv91RthA==}
engines: {node: '>= 14.18'}
peerDependencies:
'@svgdotjs/svg.js': ^3.2.4
'@svgdotjs/svg.select.js': ^4.0.1
'@svgdotjs/svg.select.js@4.0.3':
resolution: {integrity: sha512-qkMgso1sd2hXKd1FZ1weO7ANq12sNmQJeGDjs46QwDVsxSRcHmvWKL2NDF7Yimpwf3sl5esOLkPqtV2bQ3v/Jg==}
engines: {node: '>= 14.18'}
peerDependencies:
'@svgdotjs/svg.js': ^3.2.4
'@szmarczak/http-timer@5.0.1': '@szmarczak/http-timer@5.0.1':
resolution: {integrity: sha512-+PmQX0PiAYPMeVYe237LJAYvOMYW1j2rH5YROyS3b4CTVJum34HfRvKvAzozHAQG0TnHNdUfY9nCeUyRAs//cw==} resolution: {integrity: sha512-+PmQX0PiAYPMeVYe237LJAYvOMYW1j2rH5YROyS3b4CTVJum34HfRvKvAzozHAQG0TnHNdUfY9nCeUyRAs//cw==}
engines: {node: '>=14.16'} engines: {node: '>=14.16'}
@@ -1757,18 +1735,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==}
@@ -1787,12 +1759,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==}
@@ -2000,8 +1966,8 @@ packages:
resolution: {integrity: sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==} resolution: {integrity: sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==}
engines: {node: '>=12'} engines: {node: '>=12'}
apexcharts@5.3.6: apexcharts@5.5.0:
resolution: {integrity: sha512-sVEPw+J0Gp0IHQabKu8cfdsxlfME0e36Wid7RIaPclGM2OUt+O7O4+6mfAmTUYhy5bDk8cNHzEhPfVtLCIXEJA==} resolution: {integrity: sha512-r0GzBUmIAihVDHiPTWrKzd2I+T2Dw+oZTDBRJeBExUuCyqEaCe2pAMEKZnTbJQXyDAhCBzPgkM2SeeKQuW4Ddw==}
argparse@1.0.10: argparse@1.0.10:
resolution: {integrity: sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==} resolution: {integrity: sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==}
@@ -2096,9 +2062,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'}
@@ -2119,9 +2082,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'}
@@ -2281,6 +2241,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'}
@@ -2374,10 +2338,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==}
@@ -2407,9 +2367,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==}
@@ -2742,9 +2699,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'}
@@ -2766,9 +2720,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'}
@@ -3068,8 +3019,8 @@ packages:
resolution: {integrity: sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==} resolution: {integrity: sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==}
engines: {node: '>=12'} engines: {node: '>=12'}
lucide@0.563.0: lucide@0.564.0:
resolution: {integrity: sha512-2zBzDJ5n2Plj3d0ksj6h9TWPOSiKu9gtxJxnBAye11X/8gfWied6IYJn6ADYBp1NPoJmgpyOYP3wMrVx69+2AA==} resolution: {integrity: sha512-FasyXKHWon773WIl3HeCQpd5xS6E0aLjqxiQStlHNKktni+HDncc1sqY+6vRUbCfmDsIaKQz43EEQLAUDLZO0g==}
mailparser@3.9.3: mailparser@3.9.3:
resolution: {integrity: sha512-AnB0a3zROum6fLaa52L+/K2SoRJVyFDk78Ea6q1D0ofcZLxWEWDtsS1+OrVqKbV7r5dulKL/AwYQccFGAPpuYQ==} resolution: {integrity: sha512-AnB0a3zROum6fLaa52L+/K2SoRJVyFDk78Ea6q1D0ofcZLxWEWDtsS1+OrVqKbV7r5dulKL/AwYQccFGAPpuYQ==}
@@ -3279,12 +3230,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}
@@ -3638,8 +3583,8 @@ packages:
property-information@7.1.0: property-information@7.1.0:
resolution: {integrity: sha512-TwEZ+X+yCJmYfL7TPUOcvBZ4QfoT5YenQiJuX//0th53DE6w0xxLEtfK3iyryQFddXuvkIk51EEgrJQ0WJkOmQ==} resolution: {integrity: sha512-TwEZ+X+yCJmYfL7TPUOcvBZ4QfoT5YenQiJuX//0th53DE6w0xxLEtfK3iyryQFddXuvkIk51EEgrJQ0WJkOmQ==}
prosemirror-changeset@2.3.1: prosemirror-changeset@2.4.0:
resolution: {integrity: sha512-j0kORIBm8ayJNl3zQvD1TTPHJX3g042et6y/KQhZhnPrruO8exkTgG8X+NRpj7kIyMMEx74Xb3DyMIBtO0IKkQ==} resolution: {integrity: sha512-LvqH2v7Q2SF6yxatuPP2e8vSUKS/L+xAU7dPDC4RMyHMhZoGDfBC74mYuyYF4gLqOEG758wajtyhNnsTkuhvng==}
prosemirror-collab@1.3.1: prosemirror-collab@1.3.1:
resolution: {integrity: sha512-4SnynYR9TTYaQVXd/ieUvsVV4PDMBzrq2xPUWutHivDuOshZXqQ5rGbZM84HEaXKbLdItse7weMGOUdDVcLKEQ==} resolution: {integrity: sha512-4SnynYR9TTYaQVXd/ieUvsVV4PDMBzrq2xPUWutHivDuOshZXqQ5rGbZM84HEaXKbLdItse7weMGOUdDVcLKEQ==}
@@ -4302,11 +4247,13 @@ packages:
xterm-addon-fit@0.8.0: xterm-addon-fit@0.8.0:
resolution: {integrity: sha512-yj3Np7XlvxxhYF/EJ7p3KHaMt6OdwQ+HDu573Vx1lRXsVxOcnVJs51RgjZOouIZOczTsskaS+CpXspK81/DLqw==} resolution: {integrity: sha512-yj3Np7XlvxxhYF/EJ7p3KHaMt6OdwQ+HDu573Vx1lRXsVxOcnVJs51RgjZOouIZOczTsskaS+CpXspK81/DLqw==}
deprecated: This package is now deprecated. Move to @xterm/addon-fit instead.
peerDependencies: peerDependencies:
xterm: ^5.0.0 xterm: ^5.0.0
xterm@5.3.0: xterm@5.3.0:
resolution: {integrity: sha512-8QqjlekLUFTrU6x7xck1MsPzPA571K5zNqWm0M0oroYEWVOptZ0+ubQSkQ3uxIEhcIHRujJy6emDWX4A7qyFzg==} resolution: {integrity: sha512-8QqjlekLUFTrU6x7xck1MsPzPA571K5zNqWm0M0oroYEWVOptZ0+ubQSkQ3uxIEhcIHRujJy6emDWX4A7qyFzg==}
deprecated: This package is now deprecated. Move to @xterm/xterm instead.
y18n@5.0.8: y18n@5.0.8:
resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==} resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==}
@@ -4418,7 +4365,7 @@ snapshots:
'@api.global/typedrequest-interfaces': 3.0.19 '@api.global/typedrequest-interfaces': 3.0.19
'@api.global/typedsocket': 4.1.0(@push.rocks/smartserve@2.0.1) '@api.global/typedsocket': 4.1.0(@push.rocks/smartserve@2.0.1)
'@cloudflare/workers-types': 4.20260210.0 '@cloudflare/workers-types': 4.20260210.0
'@design.estate/dees-catalog': 3.42.0(@tiptap/pm@2.27.2) '@design.estate/dees-catalog': 3.42.2(@tiptap/pm@2.27.2)
'@design.estate/dees-comms': 1.0.30 '@design.estate/dees-comms': 1.0.30
'@push.rocks/lik': 6.2.2 '@push.rocks/lik': 6.2.2
'@push.rocks/smartdelay': 3.0.5 '@push.rocks/smartdelay': 3.0.5
@@ -4491,18 +4438,6 @@ 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.11
'@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
@@ -5028,7 +4963,7 @@ snapshots:
dependencies: dependencies:
'@api.global/typedrequest-interfaces': 3.0.19 '@api.global/typedrequest-interfaces': 3.0.19
'@design.estate/dees-catalog@3.42.0(@tiptap/pm@2.27.2)': '@design.estate/dees-catalog@3.42.2(@tiptap/pm@2.27.2)':
dependencies: dependencies:
'@design.estate/dees-domtools': 2.3.8 '@design.estate/dees-domtools': 2.3.8
'@design.estate/dees-element': 2.1.6 '@design.estate/dees-element': 2.1.6
@@ -5048,10 +4983,10 @@ snapshots:
'@tiptap/extension-underline': 2.27.2(@tiptap/core@2.27.2(@tiptap/pm@2.27.2)) '@tiptap/extension-underline': 2.27.2(@tiptap/core@2.27.2(@tiptap/pm@2.27.2))
'@tiptap/starter-kit': 2.27.2 '@tiptap/starter-kit': 2.27.2
'@tsclass/tsclass': 9.3.0 '@tsclass/tsclass': 9.3.0
apexcharts: 5.3.6 apexcharts: 5.5.0
highlight.js: 11.11.1 highlight.js: 11.11.1
ibantools: 4.5.1 ibantools: 4.5.1
lucide: 0.563.0 lucide: 0.564.0
monaco-editor: 0.55.1 monaco-editor: 0.55.1
pdfjs-dist: 4.10.38 pdfjs-dist: 4.10.38
xterm: 5.3.0 xterm: 5.3.0
@@ -5500,8 +5435,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':
@@ -5539,52 +5472,52 @@ snapshots:
dependencies: dependencies:
sparse-bitfield: 3.0.3 sparse-bitfield: 3.0.3
'@napi-rs/canvas-android-arm64@0.1.91': '@napi-rs/canvas-android-arm64@0.1.92':
optional: true optional: true
'@napi-rs/canvas-darwin-arm64@0.1.91': '@napi-rs/canvas-darwin-arm64@0.1.92':
optional: true optional: true
'@napi-rs/canvas-darwin-x64@0.1.91': '@napi-rs/canvas-darwin-x64@0.1.92':
optional: true optional: true
'@napi-rs/canvas-linux-arm-gnueabihf@0.1.91': '@napi-rs/canvas-linux-arm-gnueabihf@0.1.92':
optional: true optional: true
'@napi-rs/canvas-linux-arm64-gnu@0.1.91': '@napi-rs/canvas-linux-arm64-gnu@0.1.92':
optional: true optional: true
'@napi-rs/canvas-linux-arm64-musl@0.1.91': '@napi-rs/canvas-linux-arm64-musl@0.1.92':
optional: true optional: true
'@napi-rs/canvas-linux-riscv64-gnu@0.1.91': '@napi-rs/canvas-linux-riscv64-gnu@0.1.92':
optional: true optional: true
'@napi-rs/canvas-linux-x64-gnu@0.1.91': '@napi-rs/canvas-linux-x64-gnu@0.1.92':
optional: true optional: true
'@napi-rs/canvas-linux-x64-musl@0.1.91': '@napi-rs/canvas-linux-x64-musl@0.1.92':
optional: true optional: true
'@napi-rs/canvas-win32-arm64-msvc@0.1.91': '@napi-rs/canvas-win32-arm64-msvc@0.1.92':
optional: true optional: true
'@napi-rs/canvas-win32-x64-msvc@0.1.91': '@napi-rs/canvas-win32-x64-msvc@0.1.92':
optional: true optional: true
'@napi-rs/canvas@0.1.91': '@napi-rs/canvas@0.1.92':
optionalDependencies: optionalDependencies:
'@napi-rs/canvas-android-arm64': 0.1.91 '@napi-rs/canvas-android-arm64': 0.1.92
'@napi-rs/canvas-darwin-arm64': 0.1.91 '@napi-rs/canvas-darwin-arm64': 0.1.92
'@napi-rs/canvas-darwin-x64': 0.1.91 '@napi-rs/canvas-darwin-x64': 0.1.92
'@napi-rs/canvas-linux-arm-gnueabihf': 0.1.91 '@napi-rs/canvas-linux-arm-gnueabihf': 0.1.92
'@napi-rs/canvas-linux-arm64-gnu': 0.1.91 '@napi-rs/canvas-linux-arm64-gnu': 0.1.92
'@napi-rs/canvas-linux-arm64-musl': 0.1.91 '@napi-rs/canvas-linux-arm64-musl': 0.1.92
'@napi-rs/canvas-linux-riscv64-gnu': 0.1.91 '@napi-rs/canvas-linux-riscv64-gnu': 0.1.92
'@napi-rs/canvas-linux-x64-gnu': 0.1.91 '@napi-rs/canvas-linux-x64-gnu': 0.1.92
'@napi-rs/canvas-linux-x64-musl': 0.1.91 '@napi-rs/canvas-linux-x64-musl': 0.1.92
'@napi-rs/canvas-win32-arm64-msvc': 0.1.91 '@napi-rs/canvas-win32-arm64-msvc': 0.1.92
'@napi-rs/canvas-win32-x64-msvc': 0.1.91 '@napi-rs/canvas-win32-x64-msvc': 0.1.92
optional: true optional: true
'@napi-rs/wasm-runtime@1.0.7': '@napi-rs/wasm-runtime@1.0.7':
@@ -5832,24 +5765,21 @@ snapshots:
'@push.rocks/smartlog': 3.1.11 '@push.rocks/smartlog': 3.1.11
'@push.rocks/smartpath': 6.0.0 '@push.rocks/smartpath': 6.0.0
'@push.rocks/smartacme@8.0.0(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.11
'@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'
@@ -6036,22 +5966,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
@@ -6438,7 +6352,7 @@ snapshots:
'@push.rocks/smartpromise@4.2.3': {} '@push.rocks/smartpromise@4.2.3': {}
'@push.rocks/smartproxy@25.3.1': '@push.rocks/smartproxy@25.7.3':
dependencies: dependencies:
'@push.rocks/smartcrypto': 2.0.4 '@push.rocks/smartcrypto': 2.0.4
'@push.rocks/smartlog': 3.1.11 '@push.rocks/smartlog': 3.1.11
@@ -6621,6 +6535,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
@@ -6688,6 +6613,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
@@ -6889,6 +6830,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.4':
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':
@@ -7231,25 +7177,6 @@ snapshots:
'@socket.io/component-emitter@3.1.2': {} '@socket.io/component-emitter@3.1.2': {}
'@svgdotjs/svg.draggable.js@3.0.6(@svgdotjs/svg.js@3.2.5)':
dependencies:
'@svgdotjs/svg.js': 3.2.5
'@svgdotjs/svg.filter.js@3.0.9':
dependencies:
'@svgdotjs/svg.js': 3.2.5
'@svgdotjs/svg.js@3.2.5': {}
'@svgdotjs/svg.resize.js@2.0.5(@svgdotjs/svg.js@3.2.5)(@svgdotjs/svg.select.js@4.0.3(@svgdotjs/svg.js@3.2.5))':
dependencies:
'@svgdotjs/svg.js': 3.2.5
'@svgdotjs/svg.select.js': 4.0.3(@svgdotjs/svg.js@3.2.5)
'@svgdotjs/svg.select.js@4.0.3(@svgdotjs/svg.js@3.2.5)':
dependencies:
'@svgdotjs/svg.js': 3.2.5
'@szmarczak/http-timer@5.0.1': '@szmarczak/http-timer@5.0.1':
dependencies: dependencies:
defer-to-connect: 2.0.1 defer-to-connect: 2.0.1
@@ -7365,7 +7292,7 @@ snapshots:
'@tiptap/pm@2.27.2': '@tiptap/pm@2.27.2':
dependencies: dependencies:
prosemirror-changeset: 2.3.1 prosemirror-changeset: 2.4.0
prosemirror-collab: 1.3.1 prosemirror-collab: 1.3.1
prosemirror-commands: 1.7.1 prosemirror-commands: 1.7.1
prosemirror-dropcursor: 1.8.2 prosemirror-dropcursor: 1.8.2
@@ -7423,10 +7350,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
@@ -7436,10 +7359,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
@@ -7464,14 +7383,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
@@ -7694,13 +7605,8 @@ snapshots:
ansi-styles@6.2.3: {} ansi-styles@6.2.3: {}
apexcharts@5.3.6: apexcharts@5.5.0:
dependencies: dependencies:
'@svgdotjs/svg.draggable.js': 3.0.6(@svgdotjs/svg.js@3.2.5)
'@svgdotjs/svg.filter.js': 3.0.9
'@svgdotjs/svg.js': 3.2.5
'@svgdotjs/svg.resize.js': 2.0.5(@svgdotjs/svg.js@3.2.5)(@svgdotjs/svg.select.js@4.0.3(@svgdotjs/svg.js@3.2.5))
'@svgdotjs/svg.select.js': 4.0.3(@svgdotjs/svg.js@3.2.5)
'@yr/monotone-cubic-spline': 1.0.3 '@yr/monotone-cubic-spline': 1.0.3
argparse@1.0.10: argparse@1.0.10:
@@ -7788,8 +7694,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
@@ -7826,8 +7730,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: {}
@@ -7978,6 +7880,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:
@@ -8050,10 +7954,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
@@ -8090,16 +7990,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: {}
@@ -8529,11 +8419,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
@@ -8566,12 +8451,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
@@ -8883,7 +8762,7 @@ snapshots:
lru-cache@7.18.3: {} lru-cache@7.18.3: {}
lucide@0.563.0: {} lucide@0.564.0: {}
mailparser@3.9.3: mailparser@3.9.3:
dependencies: dependencies:
@@ -9280,10 +9159,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
@@ -9552,7 +9427,7 @@ snapshots:
pdfjs-dist@4.10.38: pdfjs-dist@4.10.38:
optionalDependencies: optionalDependencies:
'@napi-rs/canvas': 0.1.91 '@napi-rs/canvas': 0.1.92
peberminta@0.9.0: {} peberminta@0.9.0: {}
@@ -9591,7 +9466,7 @@ snapshots:
property-information@7.1.0: {} property-information@7.1.0: {}
prosemirror-changeset@2.3.1: prosemirror-changeset@2.4.0:
dependencies: dependencies:
prosemirror-transform: 1.11.0 prosemirror-transform: 1.11.0

102
readme.md
View File

@@ -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

View File

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

View File

@@ -11,31 +11,22 @@ interface IBackoffEntry {
/** /**
* Manages certificate provisioning scheduling with: * Manages certificate provisioning scheduling with:
* - Per-domain exponential backoff persisted in StorageManager * - Per-domain exponential backoff persisted in StorageManager
* - Serial stagger queue with configurable delay between provisions *
* Note: Serial stagger queue was removed — smartacme v9 handles
* concurrency, per-domain dedup, and rate limiting internally.
*/ */
export class CertProvisionScheduler { export class CertProvisionScheduler {
private storageManager: StorageManager; private storageManager: StorageManager;
private staggerDelayMs: number;
private maxBackoffHours: number; private maxBackoffHours: number;
// In-memory serial queue
private queue: Array<{
domain: string;
fn: () => Promise<any>;
resolve: (value: any) => void;
reject: (err: any) => void;
}> = [];
private processing = false;
// In-memory backoff cache (mirrors storage for fast lookups) // In-memory backoff cache (mirrors storage for fast lookups)
private backoffCache = new Map<string, IBackoffEntry>(); private backoffCache = new Map<string, IBackoffEntry>();
constructor( constructor(
storageManager: StorageManager, storageManager: StorageManager,
options?: { staggerDelayMs?: number; maxBackoffHours?: number } options?: { maxBackoffHours?: number }
) { ) {
this.storageManager = storageManager; this.storageManager = storageManager;
this.staggerDelayMs = options?.staggerDelayMs ?? 3000;
this.maxBackoffHours = options?.maxBackoffHours ?? 24; this.maxBackoffHours = options?.maxBackoffHours ?? 24;
} }
@@ -136,41 +127,4 @@ export class CertProvisionScheduler {
lastError: entry.lastError, lastError: entry.lastError,
}; };
} }
/**
* Enqueue a provision operation for serial execution with stagger delay.
* Returns the result of the provision function.
*/
enqueueProvision<T>(domain: string, fn: () => Promise<T>): Promise<T> {
return new Promise<T>((resolve, reject) => {
this.queue.push({ domain, fn, resolve, reject });
this.processQueue();
});
}
/**
* Process the stagger queue serially
*/
private async processQueue(): Promise<void> {
if (this.processing) return;
this.processing = true;
while (this.queue.length > 0) {
const item = this.queue.shift()!;
try {
logger.log('info', `Processing cert provision for ${item.domain}`);
const result = await item.fn();
item.resolve(result);
} catch (err) {
item.reject(err);
}
// Stagger delay between provisions
if (this.queue.length > 0) {
await new Promise<void>((r) => setTimeout(r, this.staggerDelayMs));
}
}
this.processing = false;
}
} }

View File

@@ -21,8 +21,12 @@ 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
@@ -152,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;
};
};
} }
/** /**
@@ -170,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;
@@ -185,6 +206,10 @@ export class DcRouter {
public cacheDb?: CacheDb; public cacheDb?: CacheDb;
public cacheCleaner?: CacheCleaner; public cacheCleaner?: CacheCleaner;
// Remote Ingress
public remoteIngressManager?: RemoteIngressManager;
public tunnelManager?: TunnelManager;
// Certificate status tracking from SmartProxy events (keyed by domain) // 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';
@@ -195,7 +220,7 @@ export class DcRouter {
error?: string; error?: string;
}>(); }>();
// Certificate provisioning scheduler with backoff + stagger // Certificate provisioning scheduler with per-domain backoff
public certProvisionScheduler?: CertProvisionScheduler; public certProvisionScheduler?: CertProvisionScheduler;
// TypedRouter for API endpoints // TypedRouter for API endpoints
@@ -210,10 +235,13 @@ 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 // Default storage to filesystem if not configured
if (!this.options.storage) { if (!this.options.storage) {
this.options.storage = { this.options.storage = {
fsPath: plugins.path.join(paths.dcrouterHomeDir, 'storage'), fsPath: this.resolvedPaths.defaultStoragePath,
}; };
} }
@@ -259,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);
@@ -345,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:');
@@ -372,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,
}); });
@@ -445,6 +488,9 @@ 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,
@@ -456,13 +502,23 @@ export class DcRouter {
const certs: Array<{ domain: string; publicKey: string; privateKey: string; ca?: string }> = []; const certs: Array<{ domain: string; publicKey: string; privateKey: string; ca?: string }> = [];
for (const key of keys) { for (const key of keys) {
const data = await this.storageManager.getJSON(key); const data = await this.storageManager.getJSON(key);
if (data) certs.push(data); if (data) {
certs.push(data);
loadedCertEntries.push({ domain: data.domain, publicKey: data.publicKey, validUntil: data.validUntil, validFrom: data.validFrom });
}
} }
return certs; return certs;
}, },
save: async (domain: string, publicKey: string, privateKey: string, ca?: string) => { 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}`, { await this.storageManager.setJSON(`/proxy-certs/${domain}`, {
domain, publicKey, privateKey, ca, domain, publicKey, privateKey, ca, validUntil, validFrom,
}); });
}, },
remove: async (domain: string) => { remove: async (domain: string) => {
@@ -496,14 +552,17 @@ export class DcRouter {
} }
try { try {
const result = await scheduler.enqueueProvision(domain, async () => { // 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,
@@ -512,7 +571,6 @@ export class DcRouter {
publicKey: cert.publicKey, publicKey: cert.publicKey,
csr: cert.csr, csr: cert.csr,
}; };
});
// Success — clear any backoff // Success — clear any backoff
await scheduler.clearBackoff(domain); await scheduler.clearBackoff(domain);
@@ -578,6 +636,59 @@ export class DcRouter {
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`);
} }
} }
@@ -811,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()
]); ]);
@@ -840,6 +956,11 @@ export class DcRouter {
// Update configuration // Update configuration
this.options.smartProxyConfig = config; this.options.smartProxyConfig = config;
// Update routes on RemoteIngressManager so derived ports stay in sync
if (this.remoteIngressManager && config.routes) {
this.remoteIngressManager.setRoutes(config.routes as any[]);
}
// Start new SmartProxy with updated configuration (will include email routes if configured) // Start new SmartProxy with updated configuration (will include email routes if configured)
await this.setupSmartProxy(); await this.setupSmartProxy();
@@ -1238,7 +1359,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)) {
@@ -1302,7 +1423,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) {
@@ -1457,6 +1578,35 @@ 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();
// Pass current routes so the manager can derive edge ports from remoteIngress-tagged routes
const currentRoutes = this.options.smartProxyConfig?.routes || [];
this.remoteIngressManager.setRoutes(currentRoutes as any[]);
// 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
*/ */

View File

@@ -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 () => {};

View File

@@ -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');
} }

View File

@@ -156,13 +156,29 @@ export class CertificateHandler {
// Check persisted cert data from StorageManager // Check persisted cert data from StorageManager
if (status === 'unknown') { if (status === 'unknown') {
const cleanDomain = domain.replace(/^\*\.?/, ''); const cleanDomain = domain.replace(/^\*\.?/, '');
const certData = await dcRouter.storageManager.getJSON(`/certs/${cleanDomain}`); 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) { if (certData?.validUntil) {
expiryDate = new Date(certData.validUntil).toISOString(); expiryDate = new Date(certData.validUntil).toISOString();
if (certData.created) { if (certData.created) {
issuedAt = new Date(certData.created).toISOString(); issuedAt = new Date(certData.created).toISOString();
} }
issuer = 'smartacme-dns-01'; 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';
} }
} }

View File

@@ -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';

View File

@@ -0,0 +1,164 @@
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, enriched with effective listen ports
const edges = manager.getAllEdges().map((e) => ({
...e,
secret: '********', // Never expose secrets via API
effectiveListenPorts: manager.getEffectiveListenPorts(e),
}));
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() };
},
),
);
}
}

View File

@@ -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());
} }

View File

@@ -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

View File

@@ -0,0 +1,230 @@
import * as plugins from '../plugins.js';
import type { StorageManager } from '../storage/classes.storagemanager.js';
import type { IRemoteIngress, IDcRouterRouteConfig } from '../../ts_interfaces/data/remoteingress.js';
const STORAGE_PREFIX = '/remote-ingress/';
/**
* Flatten a port range (number | number[] | Array<{from, to}>) to a sorted unique number array.
*/
function extractPorts(portRange: number | number[] | Array<{ from: number; to: number }>): number[] {
const ports = new Set<number>();
if (typeof portRange === 'number') {
ports.add(portRange);
} else if (Array.isArray(portRange)) {
for (const entry of portRange) {
if (typeof entry === 'number') {
ports.add(entry);
} else if (typeof entry === 'object' && 'from' in entry && 'to' in entry) {
for (let p = entry.from; p <= entry.to; p++) {
ports.add(p);
}
}
}
}
return [...ports].sort((a, b) => a - b);
}
/**
* 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();
private routes: IDcRouterRouteConfig[] = [];
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);
}
}
}
/**
* Store the current route configs for port derivation.
*/
public setRoutes(routes: IDcRouterRouteConfig[]): void {
this.routes = routes;
}
/**
* Derive listen ports for an edge from routes tagged with remoteIngress.enabled.
* When a route specifies edgeFilter, only edges whose id or tags match get that route's ports.
* When edgeFilter is absent, the route applies to all edges.
*/
public derivePortsForEdge(edgeId: string, edgeTags?: string[]): number[] {
const ports = new Set<number>();
for (const route of this.routes) {
if (!route.remoteIngress?.enabled) continue;
// Apply edge filter if present
const filter = route.remoteIngress.edgeFilter;
if (filter && filter.length > 0) {
const idMatch = filter.includes(edgeId);
const tagMatch = edgeTags?.some((tag) => filter.includes(tag)) ?? false;
if (!idMatch && !tagMatch) continue;
}
// Extract ports from the route match
if (route.match?.ports) {
for (const p of extractPorts(route.match.ports)) {
ports.add(p);
}
}
}
return [...ports].sort((a, b) => a - b);
}
/**
* Get the effective listen ports for an edge.
* Returns manual listenPorts if non-empty, otherwise derives ports from tagged routes.
*/
public getEffectiveListenPorts(edge: IRemoteIngress): number[] {
if (edge.listenPorts && edge.listenPorts.length > 0) {
return edge.listenPorts;
}
return this.derivePortsForEdge(edge.id, edge.tags);
}
/**
* 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;
}
}

View 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;
}
}

View File

@@ -0,0 +1,2 @@
export * from './classes.remoteingress-manager.js';
export * from './classes.tunnel-manager.js';

View File

@@ -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;

View File

@@ -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;
} }

View File

@@ -1,2 +1,3 @@
export * from './auth.js'; export * from './auth.js';
export * from './stats.js'; export * from './stats.js';
export * from './remoteingress.js';

View File

@@ -0,0 +1,51 @@
import type { IRouteConfig } from '@push.rocks/smartproxy';
/**
* 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;
/** Effective ports derived from route configs — only present in API responses. */
effectiveListenPorts?: 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;
}
/**
* Route-level remote ingress configuration.
* When attached to a route, signals that traffic for this route
* should be accepted from remote edge nodes.
*/
export interface IRouteRemoteIngress {
/** Whether this route receives traffic from edge nodes */
enabled: boolean;
/** Optional filter: only edges whose id or tags match get this route's ports.
* When absent, the route applies to all edges. */
edgeFilter?: string[];
}
/**
* Extended route config used within dcrouter.
* Adds the optional `remoteIngress` property to SmartProxy's IRouteConfig.
* SmartProxy ignores unknown properties at runtime.
*/
export type IDcRouterRouteConfig = IRouteConfig & {
remoteIngress?: IRouteRemoteIngress;
};

View File

@@ -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'

View File

@@ -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';

View 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[];
};
}

View File

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

View File

@@ -116,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';
@@ -192,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;
@@ -378,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,
@@ -745,6 +780,178 @@ 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,
};
}
);
export const toggleRemoteIngressAction = remoteIngressStatePart.createAction<{
id: string;
enabled: boolean;
}>(async (statePartArg, dataArg) => {
const context = getActionContext();
const currentState = statePartArg.getState();
try {
const request = new plugins.domtools.plugins.typedrequest.TypedRequest<
interfaces.requests.IReq_UpdateRemoteIngress
>('/typedrequest', 'updateRemoteIngress');
await request.fire({
identity: context.identity,
id: dataArg.id,
enabled: dataArg.enabled,
});
await remoteIngressStatePart.dispatchAction(fetchRemoteIngressAction, null);
return statePartArg.getState();
} catch (error) {
return {
...currentState,
error: error instanceof Error ? error.message : 'Failed to toggle edge',
};
}
});
// Combined refresh action for efficient polling // Combined refresh action for efficient polling
async function dispatchCombinedRefreshAction() { async function dispatchCombinedRefreshAction() {
const context = getActionContext(); const context = getActionContext();

View File

@@ -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';

View File

@@ -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,
},
]; ];
/** /**

View File

@@ -175,7 +175,7 @@ export class OpsViewCertificates extends DeesElement {
title: 'Total Certificates', title: 'Total Certificates',
value: summary.total, value: summary.total,
type: 'number', type: 'number',
icon: 'shieldHalved', icon: 'lucide:ShieldHalf',
color: '#3b82f6', color: '#3b82f6',
}, },
{ {
@@ -183,7 +183,7 @@ export class OpsViewCertificates extends DeesElement {
title: 'Valid', title: 'Valid',
value: summary.valid, value: summary.valid,
type: 'number', type: 'number',
icon: 'check', icon: 'lucide:Check',
color: '#22c55e', color: '#22c55e',
}, },
{ {
@@ -191,7 +191,7 @@ export class OpsViewCertificates extends DeesElement {
title: 'Expiring Soon', title: 'Expiring Soon',
value: summary.expiring, value: summary.expiring,
type: 'number', type: 'number',
icon: 'clock', icon: 'lucide:Clock',
color: '#f59e0b', color: '#f59e0b',
}, },
{ {
@@ -199,7 +199,7 @@ export class OpsViewCertificates extends DeesElement {
title: 'Failed / Expired', title: 'Failed / Expired',
value: summary.failed + summary.expired, value: summary.failed + summary.expired,
type: 'number', type: 'number',
icon: 'triangleExclamation', icon: 'lucide:TriangleAlert',
color: '#ef4444', color: '#ef4444',
}, },
]; ];
@@ -211,7 +211,7 @@ export class OpsViewCertificates extends DeesElement {
.gridActions=${[ .gridActions=${[
{ {
name: 'Refresh', name: 'Refresh',
iconName: 'arrowsRotate', iconName: 'lucide:RefreshCw',
action: async () => { action: async () => {
await appstate.certificateStatePart.dispatchAction( await appstate.certificateStatePart.dispatchAction(
appstate.fetchCertificateOverviewAction, appstate.fetchCertificateOverviewAction,
@@ -243,7 +243,7 @@ export class OpsViewCertificates extends DeesElement {
.dataActions=${[ .dataActions=${[
{ {
name: 'Reprovision', name: 'Reprovision',
iconName: 'arrowsRotate', iconName: 'lucide:RefreshCw',
type: ['inRow'], type: ['inRow'],
actionFunc: async (actionData: { item: interfaces.requests.ICertificateInfo }) => { actionFunc: async (actionData: { item: interfaces.requests.ICertificateInfo }) => {
const cert = actionData.item; const cert = actionData.item;
@@ -270,7 +270,7 @@ export class OpsViewCertificates extends DeesElement {
}, },
{ {
name: 'View Details', name: 'View Details',
iconName: 'magnifyingGlass', iconName: 'lucide:Search',
type: ['doubleClick', 'contextmenu'], type: ['doubleClick', 'contextmenu'],
actionFunc: async (actionData: { item: interfaces.requests.ICertificateInfo }) => { actionFunc: async (actionData: { item: interfaces.requests.ICertificateInfo }) => {
const cert = actionData.item; const cert = actionData.item;
@@ -289,7 +289,7 @@ export class OpsViewCertificates extends DeesElement {
menuOptions: [ menuOptions: [
{ {
name: 'Copy Domain', name: 'Copy Domain',
iconName: 'copy', iconName: 'lucide:Copy',
action: async () => { action: async () => {
await navigator.clipboard.writeText(cert.domain); await navigator.clipboard.writeText(cert.domain);
}, },

View File

@@ -287,7 +287,7 @@ export class OpsViewNetwork extends DeesElement {
.dataActions=${[ .dataActions=${[
{ {
name: 'View Details', name: 'View Details',
iconName: 'magnifyingGlass', iconName: 'lucide:Search',
type: ['inRow', 'doubleClick', 'contextmenu'], type: ['inRow', 'doubleClick', 'contextmenu'],
actionFunc: async (actionData) => { actionFunc: async (actionData) => {
await this.showRequestDetails(actionData.item); await this.showRequestDetails(actionData.item);
@@ -336,7 +336,7 @@ export class OpsViewNetwork extends DeesElement {
menuOptions: [ menuOptions: [
{ {
name: 'Copy Request ID', name: 'Copy Request ID',
iconName: 'copy', iconName: 'lucide:Copy',
action: async () => { action: async () => {
await navigator.clipboard.writeText(request.id); await navigator.clipboard.writeText(request.id);
} }
@@ -429,13 +429,13 @@ export class OpsViewNetwork extends DeesElement {
title: 'Active Connections', title: 'Active Connections',
value: activeConnections, value: activeConnections,
type: 'number', type: 'number',
icon: 'plug', icon: 'lucide:Plug',
color: activeConnections > 100 ? '#f59e0b' : '#22c55e', color: activeConnections > 100 ? '#f59e0b' : '#22c55e',
description: `Total: ${this.networkState.requestsTotal || this.statsState.serverStats?.totalConnections || 0}`, description: `Total: ${this.networkState.requestsTotal || this.statsState.serverStats?.totalConnections || 0}`,
actions: [ actions: [
{ {
name: 'View Details', name: 'View Details',
iconName: 'magnifyingGlass', iconName: 'lucide:Search',
action: async () => { action: async () => {
}, },
}, },
@@ -446,7 +446,7 @@ export class OpsViewNetwork extends DeesElement {
title: 'Requests/sec', title: 'Requests/sec',
value: reqPerSec, value: reqPerSec,
type: 'trend', type: 'trend',
icon: 'chartLine', icon: 'lucide:ChartLine',
color: '#3b82f6', color: '#3b82f6',
trendData: trendData, trendData: trendData,
description: `Total: ${this.formatNumber(this.networkState.requestsTotal || 0)} requests`, description: `Total: ${this.formatNumber(this.networkState.requestsTotal || 0)} requests`,
@@ -457,7 +457,7 @@ export class OpsViewNetwork extends DeesElement {
value: this.formatBitsPerSecond(throughput.in), value: this.formatBitsPerSecond(throughput.in),
unit: '', unit: '',
type: 'number', type: 'number',
icon: 'download', icon: 'lucide:Download',
color: '#22c55e', color: '#22c55e',
description: `Total: ${this.formatBytes(this.networkState.totalBytes?.in || 0)}`, description: `Total: ${this.formatBytes(this.networkState.totalBytes?.in || 0)}`,
}, },
@@ -467,7 +467,7 @@ export class OpsViewNetwork extends DeesElement {
value: this.formatBitsPerSecond(throughput.out), value: this.formatBitsPerSecond(throughput.out),
unit: '', unit: '',
type: 'number', type: 'number',
icon: 'upload', icon: 'lucide:Upload',
color: '#8b5cf6', color: '#8b5cf6',
description: `Total: ${this.formatBytes(this.networkState.totalBytes?.out || 0)}`, description: `Total: ${this.formatBytes(this.networkState.totalBytes?.out || 0)}`,
}, },
@@ -480,7 +480,7 @@ export class OpsViewNetwork extends DeesElement {
.gridActions=${[ .gridActions=${[
{ {
name: 'Export Data', name: 'Export Data',
iconName: 'fileExport', iconName: 'lucide:FileOutput',
action: async () => { action: async () => {
console.log('Export feature coming soon'); console.log('Export feature coming soon');
}, },

View File

@@ -163,7 +163,7 @@ export class OpsViewOverview extends DeesElement {
title: 'Server Status', title: 'Server Status',
value: this.statsState.serverStats.uptime ? 'Online' : 'Offline', value: this.statsState.serverStats.uptime ? 'Online' : 'Offline',
type: 'text', type: 'text',
icon: 'server', icon: 'lucide:Server',
color: this.statsState.serverStats.uptime ? '#22c55e' : '#ef4444', color: this.statsState.serverStats.uptime ? '#22c55e' : '#ef4444',
description: `Uptime: ${this.formatUptime(this.statsState.serverStats.uptime)}`, description: `Uptime: ${this.formatUptime(this.statsState.serverStats.uptime)}`,
}, },
@@ -172,7 +172,7 @@ export class OpsViewOverview extends DeesElement {
title: 'Active Connections', title: 'Active Connections',
value: this.statsState.serverStats.activeConnections, value: this.statsState.serverStats.activeConnections,
type: 'number', type: 'number',
icon: 'networkWired', icon: 'lucide:Network',
color: '#3b82f6', color: '#3b82f6',
description: `Total: ${this.statsState.serverStats.totalConnections}`, description: `Total: ${this.statsState.serverStats.totalConnections}`,
}, },
@@ -181,7 +181,7 @@ export class OpsViewOverview extends DeesElement {
title: 'Throughput In', title: 'Throughput In',
value: this.formatBitsPerSecond(this.statsState.serverStats.throughput?.bytesInPerSecond || 0), value: this.formatBitsPerSecond(this.statsState.serverStats.throughput?.bytesInPerSecond || 0),
type: 'text', type: 'text',
icon: 'download', icon: 'lucide:Download',
color: '#22c55e', color: '#22c55e',
description: `Total: ${this.formatBytes(this.statsState.serverStats.throughput?.bytesIn || 0)}`, description: `Total: ${this.formatBytes(this.statsState.serverStats.throughput?.bytesIn || 0)}`,
}, },
@@ -190,7 +190,7 @@ export class OpsViewOverview extends DeesElement {
title: 'Throughput Out', title: 'Throughput Out',
value: this.formatBitsPerSecond(this.statsState.serverStats.throughput?.bytesOutPerSecond || 0), value: this.formatBitsPerSecond(this.statsState.serverStats.throughput?.bytesOutPerSecond || 0),
type: 'text', type: 'text',
icon: 'upload', icon: 'lucide:Upload',
color: '#8b5cf6', color: '#8b5cf6',
description: `Total: ${this.formatBytes(this.statsState.serverStats.throughput?.bytesOut || 0)}`, description: `Total: ${this.formatBytes(this.statsState.serverStats.throughput?.bytesOut || 0)}`,
}, },
@@ -199,7 +199,7 @@ export class OpsViewOverview extends DeesElement {
title: 'CPU Usage', title: 'CPU Usage',
value: cpuUsage, value: cpuUsage,
type: 'gauge', type: 'gauge',
icon: 'microchip', icon: 'lucide:Cpu',
gaugeOptions: { gaugeOptions: {
min: 0, min: 0,
max: 100, max: 100,
@@ -215,7 +215,7 @@ export class OpsViewOverview extends DeesElement {
title: 'Memory Usage', title: 'Memory Usage',
value: memoryUsage, value: memoryUsage,
type: 'percentage', type: 'percentage',
icon: 'memory', icon: 'lucide:MemoryStick',
color: memoryUsage > 80 ? '#ef4444' : memoryUsage > 60 ? '#f59e0b' : '#22c55e', color: memoryUsage > 80 ? '#ef4444' : memoryUsage > 60 ? '#f59e0b' : '#22c55e',
description: this.statsState.serverStats.memoryUsage.actualUsageBytes !== undefined && this.statsState.serverStats.memoryUsage.maxMemoryMB !== undefined description: this.statsState.serverStats.memoryUsage.actualUsageBytes !== undefined && this.statsState.serverStats.memoryUsage.maxMemoryMB !== undefined
? `${this.formatBytes(this.statsState.serverStats.memoryUsage.actualUsageBytes)} / ${this.formatBytes(this.statsState.serverStats.memoryUsage.maxMemoryMB * 1024 * 1024)}` ? `${this.formatBytes(this.statsState.serverStats.memoryUsage.actualUsageBytes)} / ${this.formatBytes(this.statsState.serverStats.memoryUsage.maxMemoryMB * 1024 * 1024)}`
@@ -229,7 +229,7 @@ export class OpsViewOverview extends DeesElement {
.gridActions=${[ .gridActions=${[
{ {
name: 'Refresh', name: 'Refresh',
iconName: 'arrowsRotate', iconName: 'lucide:RefreshCw',
action: async () => { action: async () => {
await appstate.statsStatePart.dispatchAction(appstate.fetchAllStatsAction, null); await appstate.statsStatePart.dispatchAction(appstate.fetchAllStatsAction, null);
}, },
@@ -251,7 +251,7 @@ export class OpsViewOverview extends DeesElement {
title: 'Emails Sent', title: 'Emails Sent',
value: this.statsState.emailStats.sent, value: this.statsState.emailStats.sent,
type: 'number', type: 'number',
icon: 'paperPlane', icon: 'lucide:Send',
color: '#22c55e', color: '#22c55e',
description: `Delivery rate: ${(deliveryRate * 100).toFixed(1)}%`, description: `Delivery rate: ${(deliveryRate * 100).toFixed(1)}%`,
}, },
@@ -260,7 +260,7 @@ export class OpsViewOverview extends DeesElement {
title: 'Emails Received', title: 'Emails Received',
value: this.statsState.emailStats.received, value: this.statsState.emailStats.received,
type: 'number', type: 'number',
icon: 'envelope', icon: 'lucide:Mail',
color: '#3b82f6', color: '#3b82f6',
}, },
{ {
@@ -268,7 +268,7 @@ export class OpsViewOverview extends DeesElement {
title: 'Queued', title: 'Queued',
value: this.statsState.emailStats.queued, value: this.statsState.emailStats.queued,
type: 'number', type: 'number',
icon: 'clock', icon: 'lucide:Clock',
color: '#f59e0b', color: '#f59e0b',
description: 'Pending delivery', description: 'Pending delivery',
}, },
@@ -277,7 +277,7 @@ export class OpsViewOverview extends DeesElement {
title: 'Failed', title: 'Failed',
value: this.statsState.emailStats.failed, value: this.statsState.emailStats.failed,
type: 'number', type: 'number',
icon: 'triangleExclamation', icon: 'lucide:TriangleAlert',
color: '#ef4444', color: '#ef4444',
description: `Bounce rate: ${(bounceRate * 100).toFixed(1)}%`, description: `Bounce rate: ${(bounceRate * 100).toFixed(1)}%`,
}, },
@@ -300,7 +300,7 @@ export class OpsViewOverview extends DeesElement {
title: 'DNS Queries', title: 'DNS Queries',
value: this.statsState.dnsStats.totalQueries, value: this.statsState.dnsStats.totalQueries,
type: 'number', type: 'number',
icon: 'globe', icon: 'lucide:Globe',
color: '#3b82f6', color: '#3b82f6',
description: 'Total queries handled', description: 'Total queries handled',
}, },
@@ -309,7 +309,7 @@ export class OpsViewOverview extends DeesElement {
title: 'Cache Hit Rate', title: 'Cache Hit Rate',
value: cacheHitRate, value: cacheHitRate,
type: 'percentage', type: 'percentage',
icon: 'database', icon: 'lucide:Database',
color: cacheHitRate > 80 ? '#22c55e' : cacheHitRate > 60 ? '#f59e0b' : '#ef4444', color: cacheHitRate > 80 ? '#22c55e' : cacheHitRate > 60 ? '#f59e0b' : '#ef4444',
description: `${this.statsState.dnsStats.cacheHits} hits / ${this.statsState.dnsStats.cacheMisses} misses`, description: `${this.statsState.dnsStats.cacheHits} hits / ${this.statsState.dnsStats.cacheMisses} misses`,
}, },
@@ -318,7 +318,7 @@ export class OpsViewOverview extends DeesElement {
title: 'Active Domains', title: 'Active Domains',
value: this.statsState.dnsStats.activeDomains, value: this.statsState.dnsStats.activeDomains,
type: 'number', type: 'number',
icon: 'sitemap', icon: 'lucide:Network',
color: '#8b5cf6', color: '#8b5cf6',
}, },
{ {
@@ -327,7 +327,7 @@ export class OpsViewOverview extends DeesElement {
value: this.statsState.dnsStats.averageResponseTime.toFixed(1), value: this.statsState.dnsStats.averageResponseTime.toFixed(1),
unit: 'ms', unit: 'ms',
type: 'number', type: 'number',
icon: 'clockRotateLeft', icon: 'lucide:History',
color: this.statsState.dnsStats.averageResponseTime < 50 ? '#22c55e' : '#f59e0b', color: this.statsState.dnsStats.averageResponseTime < 50 ? '#22c55e' : '#f59e0b',
}, },
]; ];

View File

@@ -0,0 +1,342 @@
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),
tunnels: this.getEdgeTunnelCount(edge.id),
lastHeartbeat: this.getLastHeartbeat(edge.id),
})}
.dataActions=${[
{
name: 'Create Edge Node',
iconName: 'lucide:plus',
type: ['header'],
actionFunc: async () => {
const { DeesModal } = await import('@design.estate/dees-catalog');
const modal = 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, auto-derived if empty)'}></dees-input-text>
<dees-input-text .key=${'tags'} .label=${'Tags (comma-separated, optional)'}></dees-input-text>
</dees-form>
`,
menuOptions: [
{
name: 'Cancel',
iconName: 'lucide:x',
action: async (modalArg: any) => await modalArg.destroy(),
},
{
name: 'Create',
iconName: 'lucide:plus',
action: async (modalArg: any) => {
const form = modalArg.shadowRoot?.querySelector('.content')?.querySelector('dees-form');
if (!form) return;
const formData = await form.collectFormData();
const name = formData.name;
if (!name) return;
const portsStr = formData.listenPorts?.trim();
const listenPorts = portsStr
? portsStr.split(',').map((p: string) => parseInt(p.trim(), 10)).filter((p: number) => !isNaN(p))
: undefined;
const tags = formData.tags
? formData.tags.split(',').map((t: string) => t.trim()).filter(Boolean)
: undefined;
await appstate.remoteIngressStatePart.dispatchAction(
appstate.createRemoteIngressAction,
{ name, listenPorts, tags },
);
await modalArg.destroy();
},
},
],
});
},
},
{
name: 'Enable',
iconName: 'lucide:play',
type: ['inRow', 'contextmenu'] as any,
actionRelevancyCheckFunc: (actionData: any) => !actionData.item.enabled,
actionFunc: async (actionData: any) => {
const edge = actionData.item as interfaces.data.IRemoteIngress;
await appstate.remoteIngressStatePart.dispatchAction(
appstate.toggleRemoteIngressAction,
{ id: edge.id, enabled: true },
);
},
},
{
name: 'Disable',
iconName: 'lucide:pause',
type: ['inRow', 'contextmenu'] as any,
actionRelevancyCheckFunc: (actionData: any) => actionData.item.enabled,
actionFunc: async (actionData: any) => {
const edge = actionData.item as interfaces.data.IRemoteIngress;
await appstate.remoteIngressStatePart.dispatchAction(
appstate.toggleRemoteIngressAction,
{ id: edge.id, enabled: false },
);
},
},
{
name: 'Regenerate Secret',
iconName: 'lucide:key',
type: ['inRow', 'contextmenu'] as any,
actionFunc: async (actionData: any) => {
const edge = actionData.item as interfaces.data.IRemoteIngress;
await appstate.remoteIngressStatePart.dispatchAction(
appstate.regenerateRemoteIngressSecretAction,
edge.id,
);
},
},
{
name: 'Delete',
iconName: 'lucide:trash2',
type: ['inRow', 'contextmenu'] as any,
actionFunc: async (actionData: any) => {
const edge = actionData.item as interfaces.data.IRemoteIngress;
await appstate.remoteIngressStatePart.dispatchAction(
appstate.deleteRemoteIngressAction,
edge.id,
);
},
},
]}
></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(edge: interfaces.data.IRemoteIngress): TemplateResult {
const hasManualPorts = edge.listenPorts && edge.listenPorts.length > 0;
const ports = hasManualPorts ? edge.listenPorts : (edge.effectiveListenPorts || []);
const isAuto = !hasManualPorts && ports.length > 0;
if (ports.length === 0) {
return html`<span style="color: var(--text-muted, #6b7280); font-size: 12px;">none</span>`;
}
return html`<div class="portsDisplay">${ports.map(p => html`<span class="portBadge">${p}</span>`)}${isAuto ? html`<span style="font-size: 11px; color: var(--text-muted, #6b7280); align-self: center;">(auto)</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`;
}
}

View File

@@ -256,7 +256,7 @@ export class OpsViewSecurity extends DeesElement {
title: 'Threat Level', title: 'Threat Level',
value: threatScore, value: threatScore,
type: 'gauge', type: 'gauge',
icon: 'shield', icon: 'lucide:Shield',
gaugeOptions: { gaugeOptions: {
min: 0, min: 0,
max: 100, max: 100,
@@ -273,7 +273,7 @@ export class OpsViewSecurity extends DeesElement {
title: 'Blocked Threats', title: 'Blocked Threats',
value: metrics.blockedIPs.length + metrics.spamDetected, value: metrics.blockedIPs.length + metrics.spamDetected,
type: 'number', type: 'number',
icon: 'userShield', icon: 'lucide:ShieldCheck',
color: '#ef4444', color: '#ef4444',
description: 'Total threats blocked today', description: 'Total threats blocked today',
}, },
@@ -282,7 +282,7 @@ export class OpsViewSecurity extends DeesElement {
title: 'Active Sessions', title: 'Active Sessions',
value: 0, value: 0,
type: 'number', type: 'number',
icon: 'users', icon: 'lucide:Users',
color: '#22c55e', color: '#22c55e',
description: 'Current authenticated sessions', description: 'Current authenticated sessions',
}, },
@@ -291,7 +291,7 @@ export class OpsViewSecurity extends DeesElement {
title: 'Auth Failures', title: 'Auth Failures',
value: metrics.authenticationFailures, value: metrics.authenticationFailures,
type: 'number', type: 'number',
icon: 'lockOpen', icon: 'lucide:LockOpen',
color: metrics.authenticationFailures > 10 ? '#ef4444' : '#f59e0b', color: metrics.authenticationFailures > 10 ? '#ef4444' : '#f59e0b',
description: 'Failed login attempts today', description: 'Failed login attempts today',
}, },
@@ -355,7 +355,7 @@ export class OpsViewSecurity extends DeesElement {
title: 'Authentication Failures', title: 'Authentication Failures',
value: metrics.authenticationFailures, value: metrics.authenticationFailures,
type: 'number', type: 'number',
icon: 'lockOpen', icon: 'lucide:LockOpen',
color: metrics.authenticationFailures > 10 ? '#ef4444' : '#f59e0b', color: metrics.authenticationFailures > 10 ? '#ef4444' : '#f59e0b',
description: 'Failed authentication attempts today', description: 'Failed authentication attempts today',
}, },
@@ -364,7 +364,7 @@ export class OpsViewSecurity extends DeesElement {
title: 'Successful Logins', title: 'Successful Logins',
value: 0, value: 0,
type: 'number', type: 'number',
icon: 'lock', icon: 'lucide:Lock',
color: '#22c55e', color: '#22c55e',
description: 'Successful logins today', description: 'Successful logins today',
}, },
@@ -399,7 +399,7 @@ export class OpsViewSecurity extends DeesElement {
title: 'Malware Detection', title: 'Malware Detection',
value: metrics.malwareDetected, value: metrics.malwareDetected,
type: 'number', type: 'number',
icon: 'virusSlash', icon: 'lucide:BugOff',
color: metrics.malwareDetected > 0 ? '#ef4444' : '#22c55e', color: metrics.malwareDetected > 0 ? '#ef4444' : '#22c55e',
description: 'Malware detected', description: 'Malware detected',
}, },
@@ -408,7 +408,7 @@ export class OpsViewSecurity extends DeesElement {
title: 'Phishing Detection', title: 'Phishing Detection',
value: metrics.phishingDetected, value: metrics.phishingDetected,
type: 'number', type: 'number',
icon: 'fishFins', icon: 'lucide:Fish',
color: metrics.phishingDetected > 0 ? '#ef4444' : '#22c55e', color: metrics.phishingDetected > 0 ? '#ef4444' : '#22c55e',
description: 'Phishing attempts detected', description: 'Phishing attempts detected',
}, },
@@ -417,7 +417,7 @@ export class OpsViewSecurity extends DeesElement {
title: 'Suspicious Activities', title: 'Suspicious Activities',
value: metrics.suspiciousActivities, value: metrics.suspiciousActivities,
type: 'number', type: 'number',
icon: 'triangleExclamation', icon: 'lucide:TriangleAlert',
color: metrics.suspiciousActivities > 5 ? '#ef4444' : '#f59e0b', color: metrics.suspiciousActivities > 5 ? '#ef4444' : '#f59e0b',
description: 'Suspicious activities detected', description: 'Suspicious activities detected',
}, },
@@ -426,7 +426,7 @@ export class OpsViewSecurity extends DeesElement {
title: 'Spam Detection', title: 'Spam Detection',
value: metrics.spamDetected, value: metrics.spamDetected,
type: 'number', type: 'number',
icon: 'ban', icon: 'lucide:Ban',
color: '#f59e0b', color: '#f59e0b',
description: 'Spam emails blocked', description: 'Spam emails blocked',
}, },

View File

@@ -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

View File

@@ -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];