@serve.zone/dcrouter 15.0.1 → 15.0.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/deno.json +1 -1
- package/dist_serve/bundle.js +768 -768
- package/dist_ts/00_commitinfo_data.js +1 -1
- package/dist_ts/acme/classes.smartacme-lifecycle.d.ts +25 -0
- package/dist_ts/acme/classes.smartacme-lifecycle.js +144 -0
- package/dist_ts/acme/index.d.ts +1 -0
- package/dist_ts/acme/index.js +2 -1
- package/dist_ts/classes.dcrouter.d.ts +21 -139
- package/dist_ts/classes.dcrouter.js +71 -1585
- package/dist_ts/dns/classes.dns-server-runtime.d.ts +37 -0
- package/dist_ts/dns/classes.dns-server-runtime.js +449 -0
- package/dist_ts/dns/index.d.ts +1 -0
- package/dist_ts/dns/index.js +2 -1
- package/dist_ts/email/classes.accepted-email-spool.d.ts +55 -0
- package/dist_ts/email/classes.accepted-email-spool.js +345 -0
- package/dist_ts/email/classes.email-route-builder.d.ts +28 -0
- package/dist_ts/email/classes.email-route-builder.js +260 -0
- package/dist_ts/email/index.d.ts +2 -0
- package/dist_ts/email/index.js +3 -1
- package/dist_ts/opsserver/handlers/gatewayclient.handler.js +10 -8
- package/dist_ts/remoteingress/classes.hub-lifecycle.d.ts +27 -0
- package/dist_ts/remoteingress/classes.hub-lifecycle.js +241 -0
- package/dist_ts/remoteingress/classes.remoteingress-manager.d.ts +1 -2
- package/dist_ts/remoteingress/index.d.ts +1 -0
- package/dist_ts/remoteingress/index.js +2 -1
- package/dist_ts/security/classes.route-policy-augmenter.d.ts +22 -0
- package/dist_ts/security/classes.route-policy-augmenter.js +120 -0
- package/dist_ts/security/index.d.ts +1 -0
- package/dist_ts/security/index.js +2 -1
- package/dist_ts/vpn/classes.vpn-access-resolver.d.ts +34 -0
- package/dist_ts/vpn/classes.vpn-access-resolver.js +101 -0
- package/dist_ts/vpn/index.d.ts +1 -0
- package/dist_ts/vpn/index.js +2 -1
- package/dist_ts_migrations/index.js +92 -9
- package/dist_ts_web/00_commitinfo_data.js +1 -1
- package/dist_ts_web/appstate/acme.d.ts +17 -0
- package/dist_ts_web/appstate/acme.js +64 -0
- package/dist_ts_web/appstate/certificates.d.ts +37 -0
- package/dist_ts_web/appstate/certificates.js +107 -0
- package/dist_ts_web/appstate/config.d.ts +9 -0
- package/dist_ts_web/appstate/config.js +35 -0
- package/dist_ts_web/appstate/domains.d.ts +80 -0
- package/dist_ts_web/appstate/domains.js +324 -0
- package/dist_ts_web/appstate/email-domains.d.ts +25 -0
- package/dist_ts_web/appstate/email-domains.js +104 -0
- package/dist_ts_web/appstate/email-ops.d.ts +10 -0
- package/dist_ts_web/appstate/email-ops.js +40 -0
- package/dist_ts_web/appstate/login.d.ts +30 -0
- package/dist_ts_web/appstate/login.js +83 -0
- package/dist_ts_web/appstate/logs.d.ts +16 -0
- package/dist_ts_web/appstate/logs.js +27 -0
- package/dist_ts_web/appstate/network.d.ts +50 -0
- package/dist_ts_web/appstate/network.js +122 -0
- package/dist_ts_web/appstate/profiles-targets.d.ts +45 -0
- package/dist_ts_web/appstate/profiles-targets.js +173 -0
- package/dist_ts_web/appstate/remoteingress.d.ts +47 -0
- package/dist_ts_web/appstate/remoteingress.js +204 -0
- package/dist_ts_web/appstate/routes.d.ts +76 -0
- package/dist_ts_web/appstate/routes.js +316 -0
- package/dist_ts_web/appstate/runtime.d.ts +1 -0
- package/dist_ts_web/appstate/runtime.js +276 -0
- package/dist_ts_web/appstate/security.d.ts +29 -0
- package/dist_ts_web/appstate/security.js +167 -0
- package/dist_ts_web/appstate/shared.d.ts +3 -0
- package/dist_ts_web/appstate/shared.js +13 -0
- package/dist_ts_web/appstate/stats.d.ts +15 -0
- package/dist_ts_web/appstate/stats.js +59 -0
- package/dist_ts_web/appstate/target-profiles.d.ts +37 -0
- package/dist_ts_web/appstate/target-profiles.js +118 -0
- package/dist_ts_web/appstate/ui.d.ts +11 -0
- package/dist_ts_web/appstate/ui.js +55 -0
- package/dist_ts_web/appstate/users.d.ts +27 -0
- package/dist_ts_web/appstate/users.js +85 -0
- package/dist_ts_web/appstate/vpn.d.ts +44 -0
- package/dist_ts_web/appstate/vpn.js +148 -0
- package/dist_ts_web/appstate.d.ts +20 -568
- package/dist_ts_web/appstate.js +24 -2418
- package/package.json +1 -1
- package/ts/00_commitinfo_data.ts +1 -1
- package/ts/acme/classes.smartacme-lifecycle.ts +155 -0
- package/ts/acme/index.ts +1 -0
- package/ts/classes.dcrouter.ts +118 -1919
- package/ts/dns/classes.dns-server-runtime.ts +525 -0
- package/ts/dns/index.ts +1 -0
- package/ts/email/classes.accepted-email-spool.ts +434 -0
- package/ts/email/classes.email-route-builder.ts +312 -0
- package/ts/email/index.ts +2 -0
- package/ts/opsserver/handlers/gatewayclient.handler.ts +9 -7
- package/ts/remoteingress/classes.hub-lifecycle.ts +278 -0
- package/ts/remoteingress/classes.remoteingress-manager.ts +1 -1
- package/ts/remoteingress/index.ts +1 -0
- package/ts/security/classes.route-policy-augmenter.ts +140 -0
- package/ts/security/index.ts +1 -0
- package/ts/vpn/classes.vpn-access-resolver.ts +126 -0
- package/ts/vpn/index.ts +1 -0
- package/ts_web/00_commitinfo_data.ts +1 -1
- package/ts_web/appstate/acme.ts +93 -0
- package/ts_web/appstate/certificates.ts +159 -0
- package/ts_web/appstate/config.ts +49 -0
- package/ts_web/appstate/domains.ts +429 -0
- package/ts_web/appstate/email-domains.ts +155 -0
- package/ts_web/appstate/email-ops.ts +57 -0
- package/ts_web/appstate/login.ts +128 -0
- package/ts_web/appstate/logs.ts +50 -0
- package/ts_web/appstate/network.ts +161 -0
- package/ts_web/appstate/profiles-targets.ts +240 -0
- package/ts_web/appstate/remoteingress.ts +300 -0
- package/ts_web/appstate/routes.ts +447 -0
- package/ts_web/appstate/runtime.ts +308 -0
- package/ts_web/appstate/security.ts +229 -0
- package/ts_web/appstate/shared.ts +15 -0
- package/ts_web/appstate/stats.ts +79 -0
- package/ts_web/appstate/target-profiles.ts +164 -0
- package/ts_web/appstate/ui.ts +75 -0
- package/ts_web/appstate/users.ts +133 -0
- package/ts_web/appstate/vpn.ts +234 -0
- package/ts_web/appstate.ts +24 -3403
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
import * as plugins from '../plugins.js';
|
|
2
|
+
import type { IDcRouterRouteConfig } from '../../ts_interfaces/data/remoteingress.js';
|
|
3
|
+
import type { ISecurityCompiledPolicy } from '../../ts_interfaces/data/security-policy.js';
|
|
4
|
+
import type { DcRouter } from '../classes.dcrouter.js';
|
|
5
|
+
|
|
6
|
+
type TInboundProxyProtocolPolicy = NonNullable<plugins.smartproxy.IRouteMatch['inboundProxyProtocol']>;
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Pure route-policy transformations applied before routes reach SmartProxy:
|
|
10
|
+
* per-listener inbound PROXY protocol policies (driven by RemoteIngress and
|
|
11
|
+
* VPN) and merging of compiled security policies.
|
|
12
|
+
*/
|
|
13
|
+
export class RoutePolicyAugmenter {
|
|
14
|
+
constructor(private dcRouterRef: DcRouter) {}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Ensure every route on a shared listener carries a compatible
|
|
18
|
+
* inboundProxyProtocol policy, deriving defaults from RemoteIngress/VPN.
|
|
19
|
+
*/
|
|
20
|
+
public applyInboundProxyProtocolPolicies(
|
|
21
|
+
routes: plugins.smartproxy.IRouteConfig[],
|
|
22
|
+
): plugins.smartproxy.IRouteConfig[] {
|
|
23
|
+
const policiesByListener = new Map<string, TInboundProxyProtocolPolicy>();
|
|
24
|
+
|
|
25
|
+
for (const route of routes) {
|
|
26
|
+
const policy = route.match?.inboundProxyProtocol || this.getDesiredInboundProxyProtocolPolicy(route);
|
|
27
|
+
if (!policy) {
|
|
28
|
+
continue;
|
|
29
|
+
}
|
|
30
|
+
for (const listenerKey of this.getInboundProxyListenerKeys(route)) {
|
|
31
|
+
const mergedPolicy = this.mergeInboundProxyProtocolPolicies(
|
|
32
|
+
policiesByListener.get(listenerKey),
|
|
33
|
+
policy,
|
|
34
|
+
);
|
|
35
|
+
if (mergedPolicy) {
|
|
36
|
+
policiesByListener.set(listenerKey, mergedPolicy);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
if (policiesByListener.size === 0) {
|
|
42
|
+
return routes;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
return routes.map((route) => {
|
|
46
|
+
if (route.match?.inboundProxyProtocol) {
|
|
47
|
+
return route;
|
|
48
|
+
}
|
|
49
|
+
let listenerPolicy: TInboundProxyProtocolPolicy | undefined;
|
|
50
|
+
for (const listenerKey of this.getInboundProxyListenerKeys(route)) {
|
|
51
|
+
listenerPolicy = this.mergeInboundProxyProtocolPolicies(
|
|
52
|
+
listenerPolicy,
|
|
53
|
+
policiesByListener.get(listenerKey),
|
|
54
|
+
);
|
|
55
|
+
}
|
|
56
|
+
if (!listenerPolicy) {
|
|
57
|
+
return route;
|
|
58
|
+
}
|
|
59
|
+
return {
|
|
60
|
+
...route,
|
|
61
|
+
match: {
|
|
62
|
+
...route.match,
|
|
63
|
+
inboundProxyProtocol: listenerPolicy,
|
|
64
|
+
},
|
|
65
|
+
};
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** Union blocked IPs/CIDRs across configured and compiled security policies. */
|
|
70
|
+
public mergeSecurityPolicies(
|
|
71
|
+
...policies: Array<Partial<ISecurityCompiledPolicy> | undefined>
|
|
72
|
+
): ISecurityCompiledPolicy | undefined {
|
|
73
|
+
const blockedIps = new Set<string>();
|
|
74
|
+
const blockedCidrs = new Set<string>();
|
|
75
|
+
|
|
76
|
+
for (const policy of policies) {
|
|
77
|
+
for (const ip of policy?.blockedIps || []) {
|
|
78
|
+
if (ip) blockedIps.add(ip);
|
|
79
|
+
}
|
|
80
|
+
for (const cidr of policy?.blockedCidrs || []) {
|
|
81
|
+
if (cidr) blockedCidrs.add(cidr);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
if (blockedIps.size === 0 && blockedCidrs.size === 0) {
|
|
86
|
+
return undefined;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
return {
|
|
90
|
+
blockedIps: [...blockedIps].sort(),
|
|
91
|
+
blockedCidrs: [...blockedCidrs].sort(),
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
private getDesiredInboundProxyProtocolPolicy(
|
|
96
|
+
route: plugins.smartproxy.IRouteConfig,
|
|
97
|
+
): TInboundProxyProtocolPolicy | undefined {
|
|
98
|
+
const dcRoute = route as IDcRouterRouteConfig;
|
|
99
|
+
if (this.dcRouterRef.isRemoteIngressHubEnabled() && dcRoute.remoteIngress?.enabled) {
|
|
100
|
+
const ports = plugins.smartproxy.expandPortRange(route.match.ports as any) as number[];
|
|
101
|
+
if (ports.some((port) => port === 25 || port === 587)) {
|
|
102
|
+
return { mode: 'required' };
|
|
103
|
+
}
|
|
104
|
+
return { mode: 'optional' };
|
|
105
|
+
}
|
|
106
|
+
if (this.dcRouterRef.options.vpnConfig?.enabled) {
|
|
107
|
+
return { mode: 'optional' };
|
|
108
|
+
}
|
|
109
|
+
return undefined;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
private getInboundProxyListenerKeys(route: plugins.smartproxy.IRouteConfig): string[] {
|
|
113
|
+
const ports = plugins.smartproxy.expandPortRange(route.match.ports as any) as number[];
|
|
114
|
+
const transports = route.match.transport === 'udp'
|
|
115
|
+
? ['udp']
|
|
116
|
+
: route.match.transport === 'all'
|
|
117
|
+
? ['tcp', 'udp']
|
|
118
|
+
: ['tcp'];
|
|
119
|
+
const keys: string[] = [];
|
|
120
|
+
for (const port of ports) {
|
|
121
|
+
for (const transport of transports) {
|
|
122
|
+
keys.push(`${transport}:${port}`);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
return keys;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
private mergeInboundProxyProtocolPolicies(
|
|
129
|
+
current?: TInboundProxyProtocolPolicy,
|
|
130
|
+
next?: TInboundProxyProtocolPolicy,
|
|
131
|
+
): TInboundProxyProtocolPolicy | undefined {
|
|
132
|
+
if (!current) return next;
|
|
133
|
+
if (!next) return current;
|
|
134
|
+
if (current.mode === 'required') return current;
|
|
135
|
+
if (next.mode === 'required') return next;
|
|
136
|
+
if (current.mode === 'optional') return current;
|
|
137
|
+
if (next.mode === 'optional') return next;
|
|
138
|
+
return current;
|
|
139
|
+
}
|
|
140
|
+
}
|
package/ts/security/index.ts
CHANGED
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import { logger } from '../logger.js';
|
|
2
|
+
import type { TVpnClientAllowEntry } from '../config/classes.route-config-manager.js';
|
|
3
|
+
import type { IDcRouterRouteConfig } from '../../ts_interfaces/data/remoteingress.js';
|
|
4
|
+
import type { DcRouter } from '../classes.dcrouter.js';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Resolves which VPN clients may access which routes and which IPs belong in
|
|
8
|
+
* a client's WireGuard AllowedIPs, including cached DNS resolution of
|
|
9
|
+
* VPN-gated route domains.
|
|
10
|
+
*/
|
|
11
|
+
export class VpnAccessResolver {
|
|
12
|
+
/** Cache for DNS-resolved IPs of VPN-gated domains. TTL: 5 minutes. */
|
|
13
|
+
private domainIpCache = new Map<string, { ips: string[]; expiresAt: number }>();
|
|
14
|
+
/** Deduplicate wildcard-resolution warnings for WireGuard AllowedIPs generation. */
|
|
15
|
+
private warnedWildcardDomains = new Set<string>();
|
|
16
|
+
|
|
17
|
+
constructor(private dcRouterRef: DcRouter) {}
|
|
18
|
+
|
|
19
|
+
/** Clear DNS and warning caches, e.g. after a VPN config change. */
|
|
20
|
+
public reset(): void {
|
|
21
|
+
this.domainIpCache.clear();
|
|
22
|
+
this.warnedWildcardDomains.clear();
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Build the per-route VPN client allow resolver handed to RouteConfigManager,
|
|
27
|
+
* or undefined when VPN is disabled.
|
|
28
|
+
*/
|
|
29
|
+
public createRouteAllowResolver(): ((
|
|
30
|
+
route: IDcRouterRouteConfig,
|
|
31
|
+
routeId?: string,
|
|
32
|
+
) => TVpnClientAllowEntry[]) | undefined {
|
|
33
|
+
if (!this.dcRouterRef.options.vpnConfig?.enabled) {
|
|
34
|
+
return undefined;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
return (route: IDcRouterRouteConfig, routeId?: string) => {
|
|
38
|
+
if (!this.dcRouterRef.vpnManager || !this.dcRouterRef.targetProfileManager) {
|
|
39
|
+
// VPN not ready yet — deny all until re-apply after VPN starts.
|
|
40
|
+
return [];
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
return this.dcRouterRef.targetProfileManager.getMatchingVpnClients(
|
|
44
|
+
route,
|
|
45
|
+
routeId,
|
|
46
|
+
this.dcRouterRef.vpnManager.listClients(),
|
|
47
|
+
this.dcRouterRef.routeConfigManager?.getRoutes() || new Map(),
|
|
48
|
+
);
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Compute the WireGuard AllowedIPs for a client from its target profiles:
|
|
54
|
+
* the VPN subnet, direct target IPs, and DNS-resolved route domains.
|
|
55
|
+
*/
|
|
56
|
+
public async getClientAllowedIPs(targetProfileIds: string[]): Promise<string[]> {
|
|
57
|
+
const subnet = this.dcRouterRef.options.vpnConfig?.subnet || '10.8.0.0/24';
|
|
58
|
+
const ips = new Set<string>([subnet]);
|
|
59
|
+
|
|
60
|
+
const targetProfileManager = this.dcRouterRef.targetProfileManager;
|
|
61
|
+
if (!targetProfileManager) return [...ips];
|
|
62
|
+
|
|
63
|
+
const allRoutes = this.dcRouterRef.routeConfigManager?.getRoutes() || new Map();
|
|
64
|
+
|
|
65
|
+
const { domains, targetIps } = targetProfileManager.getClientAccessSpec(
|
|
66
|
+
targetProfileIds,
|
|
67
|
+
allRoutes,
|
|
68
|
+
);
|
|
69
|
+
|
|
70
|
+
// Add target IPs directly
|
|
71
|
+
for (const ip of targetIps) {
|
|
72
|
+
ips.add(`${ip}/32`);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Resolve DNS A records for matched domains (with caching)
|
|
76
|
+
for (const domain of domains) {
|
|
77
|
+
if (this.isWildcardDomain(domain)) {
|
|
78
|
+
this.logSkippedWildcardAllowedIp(domain);
|
|
79
|
+
continue;
|
|
80
|
+
}
|
|
81
|
+
const resolvedIps = await this.resolveDomainIPs(domain);
|
|
82
|
+
for (const ip of resolvedIps) {
|
|
83
|
+
ips.add(`${ip}/32`);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
return [...ips];
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Resolve a domain's A record(s) for VPN AllowedIPs, with a 5-minute cache.
|
|
92
|
+
*/
|
|
93
|
+
private async resolveDomainIPs(domain: string): Promise<string[]> {
|
|
94
|
+
const cached = this.domainIpCache.get(domain);
|
|
95
|
+
if (cached && cached.expiresAt > Date.now()) {
|
|
96
|
+
return cached.ips;
|
|
97
|
+
}
|
|
98
|
+
try {
|
|
99
|
+
const { promises: dnsPromises } = await import('dns');
|
|
100
|
+
const ips = await dnsPromises.resolve4(domain);
|
|
101
|
+
this.domainIpCache.set(domain, { ips, expiresAt: Date.now() + 5 * 60 * 1000 });
|
|
102
|
+
// Evict oldest entries if cache exceeds 1000 entries
|
|
103
|
+
if (this.domainIpCache.size > 1000) {
|
|
104
|
+
const firstKey = this.domainIpCache.keys().next().value;
|
|
105
|
+
if (firstKey) this.domainIpCache.delete(firstKey);
|
|
106
|
+
}
|
|
107
|
+
return ips;
|
|
108
|
+
} catch (err) {
|
|
109
|
+
logger.log('warn', `VPN: Failed to resolve ${domain} for AllowedIPs: ${(err as Error).message}`);
|
|
110
|
+
return cached?.ips || []; // Return stale cache on failure, or empty
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
private isWildcardDomain(domain: string): boolean {
|
|
115
|
+
return domain.includes('*');
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
private logSkippedWildcardAllowedIp(domain: string): void {
|
|
119
|
+
if (this.warnedWildcardDomains.has(domain)) return;
|
|
120
|
+
this.warnedWildcardDomains.add(domain);
|
|
121
|
+
logger.log(
|
|
122
|
+
'warn',
|
|
123
|
+
`VPN: Skipping wildcard domain '${domain}' for WireGuard AllowedIPs; wildcard patterns must be resolved to concrete hostnames by matching routes.`,
|
|
124
|
+
);
|
|
125
|
+
}
|
|
126
|
+
}
|
package/ts/vpn/index.ts
CHANGED
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import * as plugins from '../plugins.js';
|
|
2
|
+
import * as interfaces from '../../ts_interfaces/index.js';
|
|
3
|
+
import { appState } from './shared.js';
|
|
4
|
+
import { getActionContext } from './login.js';
|
|
5
|
+
|
|
6
|
+
// ============================================================================
|
|
7
|
+
// ACME Config State (DB-backed singleton, managed via Domains > Certificates)
|
|
8
|
+
// ============================================================================
|
|
9
|
+
|
|
10
|
+
export interface IAcmeConfigState {
|
|
11
|
+
config: interfaces.data.IAcmeConfig | null;
|
|
12
|
+
isLoading: boolean;
|
|
13
|
+
error: string | null;
|
|
14
|
+
lastUpdated: number;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export const acmeConfigStatePart = await appState.getStatePart<IAcmeConfigState>(
|
|
18
|
+
'acmeConfig',
|
|
19
|
+
{
|
|
20
|
+
config: null,
|
|
21
|
+
isLoading: false,
|
|
22
|
+
error: null,
|
|
23
|
+
lastUpdated: 0,
|
|
24
|
+
},
|
|
25
|
+
'soft',
|
|
26
|
+
);
|
|
27
|
+
|
|
28
|
+
// ACME Config Actions
|
|
29
|
+
// ============================================================================
|
|
30
|
+
|
|
31
|
+
export const fetchAcmeConfigAction = acmeConfigStatePart.createAction(
|
|
32
|
+
async (statePartArg): Promise<IAcmeConfigState> => {
|
|
33
|
+
const context = getActionContext();
|
|
34
|
+
const currentState = statePartArg.getState()!;
|
|
35
|
+
if (!context.identity) return currentState;
|
|
36
|
+
|
|
37
|
+
try {
|
|
38
|
+
const request = new plugins.domtools.plugins.typedrequest.TypedRequest<
|
|
39
|
+
interfaces.requests.IReq_GetAcmeConfig
|
|
40
|
+
>('/typedrequest', 'getAcmeConfig');
|
|
41
|
+
const response = await request.fire({ identity: context.identity });
|
|
42
|
+
return {
|
|
43
|
+
config: response.config,
|
|
44
|
+
isLoading: false,
|
|
45
|
+
error: null,
|
|
46
|
+
lastUpdated: Date.now(),
|
|
47
|
+
};
|
|
48
|
+
} catch (error: unknown) {
|
|
49
|
+
return {
|
|
50
|
+
...currentState,
|
|
51
|
+
isLoading: false,
|
|
52
|
+
error: error instanceof Error ? error.message : 'Failed to fetch ACME config',
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
},
|
|
56
|
+
);
|
|
57
|
+
|
|
58
|
+
export const updateAcmeConfigAction = acmeConfigStatePart.createAction<{
|
|
59
|
+
accountEmail?: string;
|
|
60
|
+
enabled?: boolean;
|
|
61
|
+
useProduction?: boolean;
|
|
62
|
+
autoRenew?: boolean;
|
|
63
|
+
renewThresholdDays?: number;
|
|
64
|
+
}>(async (statePartArg, dataArg, actionContext): Promise<IAcmeConfigState> => {
|
|
65
|
+
const context = getActionContext();
|
|
66
|
+
try {
|
|
67
|
+
const request = new plugins.domtools.plugins.typedrequest.TypedRequest<
|
|
68
|
+
interfaces.requests.IReq_UpdateAcmeConfig
|
|
69
|
+
>('/typedrequest', 'updateAcmeConfig');
|
|
70
|
+
const response = await request.fire({
|
|
71
|
+
identity: context.identity!,
|
|
72
|
+
accountEmail: dataArg.accountEmail,
|
|
73
|
+
enabled: dataArg.enabled,
|
|
74
|
+
useProduction: dataArg.useProduction,
|
|
75
|
+
autoRenew: dataArg.autoRenew,
|
|
76
|
+
renewThresholdDays: dataArg.renewThresholdDays,
|
|
77
|
+
});
|
|
78
|
+
if (!response.success) {
|
|
79
|
+
return {
|
|
80
|
+
...statePartArg.getState()!,
|
|
81
|
+
error: response.message || 'Failed to update ACME config',
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
return await actionContext!.dispatch(fetchAcmeConfigAction, null);
|
|
85
|
+
} catch (error: unknown) {
|
|
86
|
+
return {
|
|
87
|
+
...statePartArg.getState()!,
|
|
88
|
+
error: error instanceof Error ? error.message : 'Failed to update ACME config',
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
// ============================================================================
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
import * as plugins from '../plugins.js';
|
|
2
|
+
import type * as servezoneInterfaces from '@serve.zone/interfaces';
|
|
3
|
+
import * as interfaces from '../../ts_interfaces/index.js';
|
|
4
|
+
import { appState } from './shared.js';
|
|
5
|
+
import { getActionContext } from './login.js';
|
|
6
|
+
|
|
7
|
+
export interface ICertificateState {
|
|
8
|
+
certificates: interfaces.requests.ICertificateInfo[];
|
|
9
|
+
summary: { total: number; valid: number; expiring: number; expired: number; failed: number; unknown: number };
|
|
10
|
+
isLoading: boolean;
|
|
11
|
+
error: string | null;
|
|
12
|
+
lastUpdated: number;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export const certificateStatePart = await appState.getStatePart<ICertificateState>(
|
|
16
|
+
'certificates',
|
|
17
|
+
{
|
|
18
|
+
certificates: [],
|
|
19
|
+
summary: { total: 0, valid: 0, expiring: 0, expired: 0, failed: 0, unknown: 0 },
|
|
20
|
+
isLoading: false,
|
|
21
|
+
error: null,
|
|
22
|
+
lastUpdated: 0,
|
|
23
|
+
},
|
|
24
|
+
'soft'
|
|
25
|
+
);
|
|
26
|
+
|
|
27
|
+
// Certificate Actions
|
|
28
|
+
// ============================================================================
|
|
29
|
+
|
|
30
|
+
export const fetchCertificateOverviewAction = certificateStatePart.createAction(async (statePartArg): Promise<ICertificateState> => {
|
|
31
|
+
const context = getActionContext();
|
|
32
|
+
const currentState = statePartArg.getState()!;
|
|
33
|
+
if (!context.identity) return currentState;
|
|
34
|
+
|
|
35
|
+
try {
|
|
36
|
+
const request = new plugins.domtools.plugins.typedrequest.TypedRequest<
|
|
37
|
+
interfaces.requests.IReq_GetCertificateOverview
|
|
38
|
+
>('/typedrequest', 'getCertificateOverview');
|
|
39
|
+
|
|
40
|
+
const response = await request.fire({
|
|
41
|
+
identity: context.identity,
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
return {
|
|
45
|
+
certificates: response.certificates,
|
|
46
|
+
summary: response.summary,
|
|
47
|
+
isLoading: false,
|
|
48
|
+
error: null,
|
|
49
|
+
lastUpdated: Date.now(),
|
|
50
|
+
};
|
|
51
|
+
} catch (error) {
|
|
52
|
+
return {
|
|
53
|
+
...currentState,
|
|
54
|
+
isLoading: false,
|
|
55
|
+
error: error instanceof Error ? error.message : 'Failed to fetch certificate overview',
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
export const reprovisionCertificateAction = certificateStatePart.createAction<{ domain: string; forceRenew?: boolean }>(
|
|
61
|
+
async (statePartArg, dataArg, actionContext): Promise<ICertificateState> => {
|
|
62
|
+
const context = getActionContext();
|
|
63
|
+
const currentState = statePartArg.getState()!;
|
|
64
|
+
|
|
65
|
+
try {
|
|
66
|
+
const request = new plugins.domtools.plugins.typedrequest.TypedRequest<
|
|
67
|
+
interfaces.requests.IReq_ReprovisionCertificateDomain
|
|
68
|
+
>('/typedrequest', 'reprovisionCertificateDomain');
|
|
69
|
+
|
|
70
|
+
await request.fire({
|
|
71
|
+
identity: context.identity!,
|
|
72
|
+
domain: dataArg.domain,
|
|
73
|
+
forceRenew: dataArg.forceRenew,
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
// Re-fetch overview after reprovisioning
|
|
77
|
+
return await actionContext!.dispatch(fetchCertificateOverviewAction, null);
|
|
78
|
+
} catch (error: unknown) {
|
|
79
|
+
return {
|
|
80
|
+
...currentState,
|
|
81
|
+
error: error instanceof Error ? error.message : 'Failed to reprovision certificate',
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
);
|
|
86
|
+
|
|
87
|
+
export const deleteCertificateAction = certificateStatePart.createAction<string>(
|
|
88
|
+
async (statePartArg, domain, actionContext): Promise<ICertificateState> => {
|
|
89
|
+
const context = getActionContext();
|
|
90
|
+
const currentState = statePartArg.getState()!;
|
|
91
|
+
|
|
92
|
+
try {
|
|
93
|
+
const request = new plugins.domtools.plugins.typedrequest.TypedRequest<
|
|
94
|
+
interfaces.requests.IReq_DeleteCertificate
|
|
95
|
+
>('/typedrequest', 'deleteCertificate');
|
|
96
|
+
|
|
97
|
+
await request.fire({
|
|
98
|
+
identity: context.identity!,
|
|
99
|
+
domain,
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
// Re-fetch overview after deletion
|
|
103
|
+
return await actionContext!.dispatch(fetchCertificateOverviewAction, null);
|
|
104
|
+
} catch (error: unknown) {
|
|
105
|
+
return {
|
|
106
|
+
...currentState,
|
|
107
|
+
error: error instanceof Error ? error.message : 'Failed to delete certificate',
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
);
|
|
112
|
+
|
|
113
|
+
export const importCertificateAction = certificateStatePart.createAction<{
|
|
114
|
+
id: string;
|
|
115
|
+
domainName: string;
|
|
116
|
+
created: number;
|
|
117
|
+
validUntil: number;
|
|
118
|
+
privateKey: string;
|
|
119
|
+
publicKey: string;
|
|
120
|
+
csr: string;
|
|
121
|
+
}>(
|
|
122
|
+
async (statePartArg, cert, actionContext): Promise<ICertificateState> => {
|
|
123
|
+
const context = getActionContext();
|
|
124
|
+
const currentState = statePartArg.getState()!;
|
|
125
|
+
|
|
126
|
+
try {
|
|
127
|
+
const request = new plugins.domtools.plugins.typedrequest.TypedRequest<
|
|
128
|
+
servezoneInterfaces.requests.gateway.IReq_ImportCertificate
|
|
129
|
+
>('/typedrequest', 'importCertificate');
|
|
130
|
+
|
|
131
|
+
await request.fire({
|
|
132
|
+
identity: context.identity!,
|
|
133
|
+
cert,
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
// Re-fetch overview after import
|
|
137
|
+
return await actionContext!.dispatch(fetchCertificateOverviewAction, null);
|
|
138
|
+
} catch (error: unknown) {
|
|
139
|
+
return {
|
|
140
|
+
...currentState,
|
|
141
|
+
error: error instanceof Error ? error.message : 'Failed to import certificate',
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
);
|
|
146
|
+
|
|
147
|
+
export async function fetchCertificateExport(domain: string) {
|
|
148
|
+
const context = getActionContext();
|
|
149
|
+
const request = new plugins.domtools.plugins.typedrequest.TypedRequest<
|
|
150
|
+
servezoneInterfaces.requests.gateway.IReq_ExportCertificate
|
|
151
|
+
>('/typedrequest', 'exportCertificate');
|
|
152
|
+
|
|
153
|
+
return request.fire({
|
|
154
|
+
identity: context.identity!,
|
|
155
|
+
domain,
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// ============================================================================
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import * as plugins from '../plugins.js';
|
|
2
|
+
import * as interfaces from '../../ts_interfaces/index.js';
|
|
3
|
+
import { appState } from './shared.js';
|
|
4
|
+
import { getActionContext } from './login.js';
|
|
5
|
+
|
|
6
|
+
export interface IConfigState {
|
|
7
|
+
config: interfaces.requests.IConfigData | null;
|
|
8
|
+
isLoading: boolean;
|
|
9
|
+
error: string | null;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export const configStatePart = await appState.getStatePart<IConfigState>(
|
|
13
|
+
'config',
|
|
14
|
+
{
|
|
15
|
+
config: null,
|
|
16
|
+
isLoading: false,
|
|
17
|
+
error: null,
|
|
18
|
+
}
|
|
19
|
+
);
|
|
20
|
+
|
|
21
|
+
// Fetch Configuration Action (read-only)
|
|
22
|
+
export const fetchConfigurationAction = configStatePart.createAction(async (statePartArg): Promise<IConfigState> => {
|
|
23
|
+
const context = getActionContext();
|
|
24
|
+
const currentState = statePartArg.getState()!;
|
|
25
|
+
if (!context.identity) return currentState;
|
|
26
|
+
|
|
27
|
+
try {
|
|
28
|
+
const configRequest = new plugins.domtools.plugins.typedrequest.TypedRequest<
|
|
29
|
+
interfaces.requests.IReq_GetConfiguration
|
|
30
|
+
>('/typedrequest', 'getConfiguration');
|
|
31
|
+
|
|
32
|
+
const response = await configRequest.fire({
|
|
33
|
+
identity: context.identity,
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
return {
|
|
37
|
+
config: response.config,
|
|
38
|
+
isLoading: false,
|
|
39
|
+
error: null,
|
|
40
|
+
};
|
|
41
|
+
} catch (error: unknown) {
|
|
42
|
+
return {
|
|
43
|
+
...currentState,
|
|
44
|
+
isLoading: false,
|
|
45
|
+
error: (error as Error).message || 'Failed to fetch configuration',
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
});
|
|
49
|
+
|