@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.
Files changed (117) hide show
  1. package/deno.json +1 -1
  2. package/dist_serve/bundle.js +768 -768
  3. package/dist_ts/00_commitinfo_data.js +1 -1
  4. package/dist_ts/acme/classes.smartacme-lifecycle.d.ts +25 -0
  5. package/dist_ts/acme/classes.smartacme-lifecycle.js +144 -0
  6. package/dist_ts/acme/index.d.ts +1 -0
  7. package/dist_ts/acme/index.js +2 -1
  8. package/dist_ts/classes.dcrouter.d.ts +21 -139
  9. package/dist_ts/classes.dcrouter.js +71 -1585
  10. package/dist_ts/dns/classes.dns-server-runtime.d.ts +37 -0
  11. package/dist_ts/dns/classes.dns-server-runtime.js +449 -0
  12. package/dist_ts/dns/index.d.ts +1 -0
  13. package/dist_ts/dns/index.js +2 -1
  14. package/dist_ts/email/classes.accepted-email-spool.d.ts +55 -0
  15. package/dist_ts/email/classes.accepted-email-spool.js +345 -0
  16. package/dist_ts/email/classes.email-route-builder.d.ts +28 -0
  17. package/dist_ts/email/classes.email-route-builder.js +260 -0
  18. package/dist_ts/email/index.d.ts +2 -0
  19. package/dist_ts/email/index.js +3 -1
  20. package/dist_ts/opsserver/handlers/gatewayclient.handler.js +10 -8
  21. package/dist_ts/remoteingress/classes.hub-lifecycle.d.ts +27 -0
  22. package/dist_ts/remoteingress/classes.hub-lifecycle.js +241 -0
  23. package/dist_ts/remoteingress/classes.remoteingress-manager.d.ts +1 -2
  24. package/dist_ts/remoteingress/index.d.ts +1 -0
  25. package/dist_ts/remoteingress/index.js +2 -1
  26. package/dist_ts/security/classes.route-policy-augmenter.d.ts +22 -0
  27. package/dist_ts/security/classes.route-policy-augmenter.js +120 -0
  28. package/dist_ts/security/index.d.ts +1 -0
  29. package/dist_ts/security/index.js +2 -1
  30. package/dist_ts/vpn/classes.vpn-access-resolver.d.ts +34 -0
  31. package/dist_ts/vpn/classes.vpn-access-resolver.js +101 -0
  32. package/dist_ts/vpn/index.d.ts +1 -0
  33. package/dist_ts/vpn/index.js +2 -1
  34. package/dist_ts_migrations/index.js +92 -9
  35. package/dist_ts_web/00_commitinfo_data.js +1 -1
  36. package/dist_ts_web/appstate/acme.d.ts +17 -0
  37. package/dist_ts_web/appstate/acme.js +64 -0
  38. package/dist_ts_web/appstate/certificates.d.ts +37 -0
  39. package/dist_ts_web/appstate/certificates.js +107 -0
  40. package/dist_ts_web/appstate/config.d.ts +9 -0
  41. package/dist_ts_web/appstate/config.js +35 -0
  42. package/dist_ts_web/appstate/domains.d.ts +80 -0
  43. package/dist_ts_web/appstate/domains.js +324 -0
  44. package/dist_ts_web/appstate/email-domains.d.ts +25 -0
  45. package/dist_ts_web/appstate/email-domains.js +104 -0
  46. package/dist_ts_web/appstate/email-ops.d.ts +10 -0
  47. package/dist_ts_web/appstate/email-ops.js +40 -0
  48. package/dist_ts_web/appstate/login.d.ts +30 -0
  49. package/dist_ts_web/appstate/login.js +83 -0
  50. package/dist_ts_web/appstate/logs.d.ts +16 -0
  51. package/dist_ts_web/appstate/logs.js +27 -0
  52. package/dist_ts_web/appstate/network.d.ts +50 -0
  53. package/dist_ts_web/appstate/network.js +122 -0
  54. package/dist_ts_web/appstate/profiles-targets.d.ts +45 -0
  55. package/dist_ts_web/appstate/profiles-targets.js +173 -0
  56. package/dist_ts_web/appstate/remoteingress.d.ts +47 -0
  57. package/dist_ts_web/appstate/remoteingress.js +204 -0
  58. package/dist_ts_web/appstate/routes.d.ts +76 -0
  59. package/dist_ts_web/appstate/routes.js +316 -0
  60. package/dist_ts_web/appstate/runtime.d.ts +1 -0
  61. package/dist_ts_web/appstate/runtime.js +276 -0
  62. package/dist_ts_web/appstate/security.d.ts +29 -0
  63. package/dist_ts_web/appstate/security.js +167 -0
  64. package/dist_ts_web/appstate/shared.d.ts +3 -0
  65. package/dist_ts_web/appstate/shared.js +13 -0
  66. package/dist_ts_web/appstate/stats.d.ts +15 -0
  67. package/dist_ts_web/appstate/stats.js +59 -0
  68. package/dist_ts_web/appstate/target-profiles.d.ts +37 -0
  69. package/dist_ts_web/appstate/target-profiles.js +118 -0
  70. package/dist_ts_web/appstate/ui.d.ts +11 -0
  71. package/dist_ts_web/appstate/ui.js +55 -0
  72. package/dist_ts_web/appstate/users.d.ts +27 -0
  73. package/dist_ts_web/appstate/users.js +85 -0
  74. package/dist_ts_web/appstate/vpn.d.ts +44 -0
  75. package/dist_ts_web/appstate/vpn.js +148 -0
  76. package/dist_ts_web/appstate.d.ts +20 -568
  77. package/dist_ts_web/appstate.js +24 -2418
  78. package/package.json +1 -1
  79. package/ts/00_commitinfo_data.ts +1 -1
  80. package/ts/acme/classes.smartacme-lifecycle.ts +155 -0
  81. package/ts/acme/index.ts +1 -0
  82. package/ts/classes.dcrouter.ts +118 -1919
  83. package/ts/dns/classes.dns-server-runtime.ts +525 -0
  84. package/ts/dns/index.ts +1 -0
  85. package/ts/email/classes.accepted-email-spool.ts +434 -0
  86. package/ts/email/classes.email-route-builder.ts +312 -0
  87. package/ts/email/index.ts +2 -0
  88. package/ts/opsserver/handlers/gatewayclient.handler.ts +9 -7
  89. package/ts/remoteingress/classes.hub-lifecycle.ts +278 -0
  90. package/ts/remoteingress/classes.remoteingress-manager.ts +1 -1
  91. package/ts/remoteingress/index.ts +1 -0
  92. package/ts/security/classes.route-policy-augmenter.ts +140 -0
  93. package/ts/security/index.ts +1 -0
  94. package/ts/vpn/classes.vpn-access-resolver.ts +126 -0
  95. package/ts/vpn/index.ts +1 -0
  96. package/ts_web/00_commitinfo_data.ts +1 -1
  97. package/ts_web/appstate/acme.ts +93 -0
  98. package/ts_web/appstate/certificates.ts +159 -0
  99. package/ts_web/appstate/config.ts +49 -0
  100. package/ts_web/appstate/domains.ts +429 -0
  101. package/ts_web/appstate/email-domains.ts +155 -0
  102. package/ts_web/appstate/email-ops.ts +57 -0
  103. package/ts_web/appstate/login.ts +128 -0
  104. package/ts_web/appstate/logs.ts +50 -0
  105. package/ts_web/appstate/network.ts +161 -0
  106. package/ts_web/appstate/profiles-targets.ts +240 -0
  107. package/ts_web/appstate/remoteingress.ts +300 -0
  108. package/ts_web/appstate/routes.ts +447 -0
  109. package/ts_web/appstate/runtime.ts +308 -0
  110. package/ts_web/appstate/security.ts +229 -0
  111. package/ts_web/appstate/shared.ts +15 -0
  112. package/ts_web/appstate/stats.ts +79 -0
  113. package/ts_web/appstate/target-profiles.ts +164 -0
  114. package/ts_web/appstate/ui.ts +75 -0
  115. package/ts_web/appstate/users.ts +133 -0
  116. package/ts_web/appstate/vpn.ts +234 -0
  117. 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
+ }
@@ -25,3 +25,4 @@ export {
25
25
  type ISecurityPolicyManagerOptions,
26
26
  type IRemoteIngressFirewallSnapshot,
27
27
  } from './classes.security-policy-manager.js';
28
+ export * from './classes.route-policy-augmenter.js';
@@ -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
@@ -1 +1,2 @@
1
1
  export * from './classes.vpn-manager.js';
2
+ export * from './classes.vpn-access-resolver.js';
@@ -3,6 +3,6 @@
3
3
  */
4
4
  export const commitinfo = {
5
5
  name: '@serve.zone/dcrouter',
6
- version: '15.0.1',
6
+ version: '15.0.3',
7
7
  description: 'A multifaceted routing service handling mail and SMS delivery functions.'
8
8
  }
@@ -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
+