@serve.zone/dcrouter 17.0.0 → 17.1.1
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/.smartconfig.json +7 -4
- package/deno.json +1 -1
- package/dist_serve/bundle.js +1316 -1151
- package/dist_ts/00_commitinfo_data.js +1 -1
- package/dist_ts/classes.dcrouter.d.ts +3 -1
- package/dist_ts/classes.dcrouter.js +113 -52
- package/dist_ts/db/classes.cache.cleaner.d.ts +3 -0
- package/dist_ts/db/classes.cache.cleaner.js +7 -3
- package/dist_ts/db/classes.dcrouter-db.js +1 -2
- package/dist_ts/db/documents/classes.cached.email.d.ts +41 -4
- package/dist_ts/db/documents/classes.cached.email.js +36 -5
- package/dist_ts/db/documents/classes.dns-record.doc.js +3 -1
- package/dist_ts/dns/classes.dns-server-runtime.d.ts +0 -1
- package/dist_ts/dns/classes.dns-server-runtime.js +4 -36
- package/dist_ts/dns/index.d.ts +1 -0
- package/dist_ts/dns/index.js +2 -1
- package/dist_ts/dns/manager.dns.d.ts +1 -0
- package/dist_ts/dns/manager.dns.js +49 -14
- package/dist_ts/dns/providers/cloudflare.provider.js +19 -4
- package/dist_ts/dns/txt-record-presentation.d.ts +20 -0
- package/dist_ts/dns/txt-record-presentation.js +131 -0
- package/dist_ts/email/classes.accepted-email-spool.d.ts +32 -3
- package/dist_ts/email/classes.accepted-email-spool.js +174 -13
- package/dist_ts/email/classes.email-domain.manager.d.ts +14 -1
- package/dist_ts/email/classes.email-domain.manager.js +151 -8
- package/dist_ts/email/classes.email-settings.manager.js +2 -2
- package/dist_ts/email/classes.mail-dns-sync.d.ts +31 -5
- package/dist_ts/email/classes.mail-dns-sync.js +508 -117
- package/dist_ts/email/classes.smartmta-blob-storage-manager.d.ts +2 -2
- package/dist_ts/email/classes.smartmta-blob-storage-manager.js +4 -3
- package/dist_ts/email/classes.workapp-mail-manager.d.ts +1 -0
- package/dist_ts/email/classes.workapp-mail-manager.js +26 -2
- package/dist_ts/email/index.d.ts +2 -0
- package/dist_ts/email/index.js +3 -1
- package/dist_ts/email/mail-dns-txt-policy.d.ts +18 -0
- package/dist_ts/email/mail-dns-txt-policy.js +466 -0
- package/dist_ts/email/reconcile-mail-dns-singleton.d.ts +47 -0
- package/dist_ts/email/reconcile-mail-dns-singleton.js +159 -0
- package/dist_ts/opsserver/classes.opsserver.d.ts +8 -0
- package/dist_ts/opsserver/classes.opsserver.js +34 -1
- package/dist_ts/opsserver/handlers/certificate.handler.js +25 -4
- package/dist_ts/opsserver/handlers/config.handler.js +9 -5
- package/dist_ts/opsserver/handlers/dns-provider.handler.js +19 -2
- package/dist_ts/opsserver/handlers/dns-record.handler.js +25 -4
- package/dist_ts/opsserver/handlers/domain.handler.js +37 -3
- package/dist_ts/opsserver/handlers/email-domain.handler.js +5 -7
- package/dist_ts/opsserver/handlers/email-ops.handler.d.ts +1 -0
- package/dist_ts/opsserver/handlers/email-ops.handler.js +26 -7
- package/dist_ts/opsserver/handlers/index.d.ts +1 -0
- package/dist_ts/opsserver/handlers/index.js +2 -1
- package/dist_ts/opsserver/handlers/logs.handler.d.ts +1 -1
- package/dist_ts/opsserver/handlers/logs.handler.js +3 -3
- package/dist_ts/opsserver/handlers/realtime.handler.d.ts +37 -0
- package/dist_ts/opsserver/handlers/realtime.handler.js +154 -0
- package/dist_ts/opsserver/handlers/remoteingress.handler.js +22 -1
- package/dist_ts/opsserver/handlers/route-management.handler.js +47 -5
- package/dist_ts/opsserver/handlers/stats.handler.js +5 -1
- package/dist_ts/paths.d.ts +0 -11
- package/dist_ts/paths.js +1 -22
- package/dist_ts/plugins.d.ts +0 -34
- package/dist_ts/plugins.js +1 -62
- package/dist_ts_interfaces/data/email-domain.d.ts +14 -0
- package/dist_ts_interfaces/requests/config.d.ts +1 -1
- package/dist_ts_interfaces/requests/email-ops.d.ts +34 -1
- package/dist_ts_interfaces/requests/index.d.ts +1 -0
- package/dist_ts_interfaces/requests/index.js +2 -1
- package/dist_ts_interfaces/requests/realtime.d.ts +38 -0
- package/dist_ts_interfaces/requests/realtime.js +11 -0
- package/dist_ts_migrations/index.d.ts +6 -0
- package/dist_ts_migrations/index.js +208 -9
- package/dist_ts_web/00_commitinfo_data.js +1 -1
- package/dist_ts_web/appstate/certificates.js +9 -2
- package/dist_ts_web/appstate/domains.js +16 -5
- package/dist_ts_web/appstate/email-domains.d.ts +3 -0
- package/dist_ts_web/appstate/email-domains.js +47 -14
- package/dist_ts_web/appstate/email-ops.d.ts +1 -0
- package/dist_ts_web/appstate/email-ops.js +20 -2
- package/dist_ts_web/appstate/realtime.d.ts +27 -0
- package/dist_ts_web/appstate/realtime.js +71 -0
- package/dist_ts_web/appstate/remoteingress.js +9 -3
- package/dist_ts_web/appstate/routes.js +16 -5
- package/dist_ts_web/appstate/runtime.js +135 -7
- package/dist_ts_web/appstate.d.ts +1 -0
- package/dist_ts_web/appstate.js +2 -1
- package/dist_ts_web/elements/email/email-domain-creation-progress.d.ts +33 -0
- package/dist_ts_web/elements/email/email-domain-creation-progress.js +257 -0
- package/dist_ts_web/elements/email/ops-view-email-domains.js +233 -68
- package/dist_ts_web/elements/email/ops-view-emails.d.ts +4 -2
- package/dist_ts_web/elements/email/ops-view-emails.js +25 -13
- package/dist_ts_web/elements/overview/ops-view-config.js +5 -5
- package/package.json +22 -19
- package/ts/00_commitinfo_data.ts +1 -1
- package/ts/classes.dcrouter.ts +124 -57
- package/ts/db/classes.cache.cleaner.ts +18 -3
- package/ts/db/classes.dcrouter-db.ts +0 -2
- package/ts/db/documents/classes.cached.email.ts +78 -4
- package/ts/db/documents/classes.dns-record.doc.ts +1 -0
- package/ts/dns/classes.dns-server-runtime.ts +9 -45
- package/ts/dns/index.ts +1 -0
- package/ts/dns/manager.dns.ts +50 -16
- package/ts/dns/providers/cloudflare.provider.ts +21 -3
- package/ts/dns/txt-record-presentation.ts +134 -0
- package/ts/email/classes.accepted-email-spool.ts +220 -14
- package/ts/email/classes.email-domain.manager.ts +201 -7
- package/ts/email/classes.email-settings.manager.ts +1 -1
- package/ts/email/classes.mail-dns-sync.ts +588 -122
- package/ts/email/classes.smartmta-blob-storage-manager.ts +3 -2
- package/ts/email/classes.workapp-mail-manager.ts +34 -1
- package/ts/email/index.ts +2 -0
- package/ts/email/mail-dns-txt-policy.ts +505 -0
- package/ts/email/reconcile-mail-dns-singleton.ts +224 -0
- package/ts/opsserver/classes.opsserver.ts +42 -0
- package/ts/opsserver/handlers/certificate.handler.ts +24 -3
- package/ts/opsserver/handlers/config.handler.ts +9 -5
- package/ts/opsserver/handlers/dns-provider.handler.ts +18 -1
- package/ts/opsserver/handlers/dns-record.handler.ts +24 -3
- package/ts/opsserver/handlers/domain.handler.ts +36 -2
- package/ts/opsserver/handlers/email-domain.handler.ts +4 -5
- package/ts/opsserver/handlers/email-ops.handler.ts +29 -6
- package/ts/opsserver/handlers/index.ts +1 -0
- package/ts/opsserver/handlers/logs.handler.ts +2 -2
- package/ts/opsserver/handlers/realtime.handler.ts +222 -0
- package/ts/opsserver/handlers/remoteingress.handler.ts +21 -0
- package/ts/opsserver/handlers/route-management.handler.ts +46 -4
- package/ts/opsserver/handlers/stats.handler.ts +4 -0
- package/ts/paths.ts +0 -25
- package/ts/plugins.ts +0 -68
- package/ts_web/00_commitinfo_data.ts +1 -1
- package/ts_web/appstate/certificates.ts +6 -1
- package/ts_web/appstate/domains.ts +11 -4
- package/ts_web/appstate/email-domains.ts +68 -14
- package/ts_web/appstate/email-ops.ts +20 -1
- package/ts_web/appstate/realtime.ts +109 -0
- package/ts_web/appstate/remoteingress.ts +6 -2
- package/ts_web/appstate/routes.ts +11 -4
- package/ts_web/appstate/runtime.ts +176 -7
- package/ts_web/appstate.ts +1 -0
- package/ts_web/elements/email/email-domain-creation-progress.ts +340 -0
- package/ts_web/elements/email/ops-view-email-domains.ts +273 -74
- package/ts_web/elements/email/ops-view-emails.ts +23 -16
- package/ts_web/elements/overview/ops-view-config.ts +4 -4
|
@@ -193,7 +193,7 @@ export class LogsHandler {
|
|
|
193
193
|
|
|
194
194
|
/**
|
|
195
195
|
* Add a log destination to the base logger that pushes entries
|
|
196
|
-
* to
|
|
196
|
+
* only to server-authenticated Ops UI realtime subscribers.
|
|
197
197
|
*
|
|
198
198
|
* Uses a module-level singleton so the destination is added only once,
|
|
199
199
|
* even across OpsServer restart cycles. The destination reads
|
|
@@ -218,7 +218,7 @@ export class LogsHandler {
|
|
|
218
218
|
|
|
219
219
|
let connections: any[];
|
|
220
220
|
try {
|
|
221
|
-
connections = await
|
|
221
|
+
connections = await opsServer.getAuthenticatedRealtimeConnections();
|
|
222
222
|
} catch {
|
|
223
223
|
return;
|
|
224
224
|
}
|
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
import * as plugins from '../../plugins.js';
|
|
2
|
+
import type { OpsServer } from '../classes.opsserver.js';
|
|
3
|
+
import * as interfaces from '../../../ts_interfaces/index.js';
|
|
4
|
+
import { requireOpsAuth } from '../helpers/auth.js';
|
|
5
|
+
|
|
6
|
+
const REALTIME_TAG_PREFIX = 'dcrouterOpsRealtime:user:';
|
|
7
|
+
|
|
8
|
+
export const getOpsRealtimeUserTag = (userIdArg: string): string => {
|
|
9
|
+
return REALTIME_TAG_PREFIX + encodeURIComponent(userIdArg);
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
const createInitialRevisions = (): interfaces.requests.IOpsRealtimeRevisionMap => {
|
|
13
|
+
return Object.fromEntries(
|
|
14
|
+
interfaces.requests.opsRealtimeResources.map((resource) => [resource, 1]),
|
|
15
|
+
) as unknown as interfaces.requests.IOpsRealtimeRevisionMap;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
export class OpsRealtimeRevisionClock {
|
|
19
|
+
public readonly serverEpoch: string;
|
|
20
|
+
private revisions = createInitialRevisions();
|
|
21
|
+
|
|
22
|
+
constructor(serverEpochArg = plugins.uuid.v4()) {
|
|
23
|
+
this.serverEpoch = serverEpochArg;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
public getRevisions(): interfaces.requests.IOpsRealtimeRevisionMap {
|
|
27
|
+
return { ...this.revisions };
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
public invalidate(
|
|
31
|
+
resourceArg: interfaces.requests.TOpsRealtimeResource,
|
|
32
|
+
optionsArg: {
|
|
33
|
+
changedIds?: string[];
|
|
34
|
+
reason?: string;
|
|
35
|
+
timestamp?: number;
|
|
36
|
+
} = {},
|
|
37
|
+
): interfaces.requests.IOpsRealtimeInvalidation {
|
|
38
|
+
const revision = this.revisions[resourceArg] + 1;
|
|
39
|
+
this.revisions[resourceArg] = revision;
|
|
40
|
+
return {
|
|
41
|
+
resource: resourceArg,
|
|
42
|
+
revision,
|
|
43
|
+
changedIds: optionsArg.changedIds?.length
|
|
44
|
+
? Array.from(new Set(optionsArg.changedIds))
|
|
45
|
+
: undefined,
|
|
46
|
+
reason: optionsArg.reason,
|
|
47
|
+
timestamp: optionsArg.timestamp ?? Date.now(),
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
public getCatchup(
|
|
52
|
+
knownServerEpochArg?: string,
|
|
53
|
+
knownRevisionsArg: Partial<interfaces.requests.IOpsRealtimeRevisionMap> = {},
|
|
54
|
+
): interfaces.requests.IOpsRealtimeInvalidation[] {
|
|
55
|
+
const epochChanged = knownServerEpochArg !== this.serverEpoch;
|
|
56
|
+
return interfaces.requests.opsRealtimeResources
|
|
57
|
+
.filter((resource) => epochChanged || (knownRevisionsArg[resource] ?? 0) < this.revisions[resource])
|
|
58
|
+
.map((resource) => ({
|
|
59
|
+
resource,
|
|
60
|
+
revision: this.revisions[resource],
|
|
61
|
+
timestamp: Date.now(),
|
|
62
|
+
reason: epochChanged ? 'initial-catchup' : 'revision-catchup',
|
|
63
|
+
}));
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
interface IWebSocketPeerForRealtime {
|
|
68
|
+
id: string;
|
|
69
|
+
tags: Set<string>;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
interface IRealtimeInvalidationOptions {
|
|
73
|
+
changedIds?: string[];
|
|
74
|
+
reason?: string;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export class RealtimeHandler {
|
|
78
|
+
public typedrouter = new plugins.typedrequest.TypedRouter();
|
|
79
|
+
public readonly revisionClock = new OpsRealtimeRevisionClock();
|
|
80
|
+
|
|
81
|
+
private subscribedUserIds = new Set<string>();
|
|
82
|
+
private pendingInvalidations = new Map<
|
|
83
|
+
interfaces.requests.TOpsRealtimeResource,
|
|
84
|
+
interfaces.requests.IOpsRealtimeInvalidation
|
|
85
|
+
>();
|
|
86
|
+
private pushScheduled = false;
|
|
87
|
+
private stopped = false;
|
|
88
|
+
|
|
89
|
+
constructor(private opsServerRef: OpsServer) {
|
|
90
|
+
this.opsServerRef.viewRouter.addTypedRouter(this.typedrouter);
|
|
91
|
+
this.registerHandlers();
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
public invalidate(
|
|
95
|
+
resourcesArg:
|
|
96
|
+
| interfaces.requests.TOpsRealtimeResource
|
|
97
|
+
| interfaces.requests.TOpsRealtimeResource[],
|
|
98
|
+
optionsArg: IRealtimeInvalidationOptions = {},
|
|
99
|
+
): void {
|
|
100
|
+
if (this.stopped) return;
|
|
101
|
+
const resources = Array.isArray(resourcesArg) ? resourcesArg : [resourcesArg];
|
|
102
|
+
for (const resource of new Set(resources)) {
|
|
103
|
+
const invalidation = this.revisionClock.invalidate(resource, optionsArg);
|
|
104
|
+
const existing = this.pendingInvalidations.get(resource);
|
|
105
|
+
this.pendingInvalidations.set(resource, {
|
|
106
|
+
...invalidation,
|
|
107
|
+
changedIds: Array.from(new Set([
|
|
108
|
+
...(existing?.changedIds || []),
|
|
109
|
+
...(invalidation.changedIds || []),
|
|
110
|
+
])),
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
this.schedulePush();
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
public async getAuthenticatedConnections(): Promise<
|
|
117
|
+
plugins.typedsocket.ISmartServeConnectionWrapper[]
|
|
118
|
+
> {
|
|
119
|
+
const typedsocket = this.opsServerRef.server?.typedserver?.typedsocket;
|
|
120
|
+
if (!typedsocket) return [];
|
|
121
|
+
|
|
122
|
+
const connections = new Map<string, plugins.typedsocket.ISmartServeConnectionWrapper>();
|
|
123
|
+
for (const userId of Array.from(this.subscribedUserIds)) {
|
|
124
|
+
const userConnections = await typedsocket.findAllTargetConnectionsByTag(
|
|
125
|
+
getOpsRealtimeUserTag(userId),
|
|
126
|
+
);
|
|
127
|
+
if (userConnections.length === 0) {
|
|
128
|
+
this.subscribedUserIds.delete(userId);
|
|
129
|
+
continue;
|
|
130
|
+
}
|
|
131
|
+
for (const connection of userConnections) {
|
|
132
|
+
connections.set(connection.peer.id, connection);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
return Array.from(connections.values());
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
public cleanup(): void {
|
|
139
|
+
this.stopped = true;
|
|
140
|
+
this.pendingInvalidations.clear();
|
|
141
|
+
this.subscribedUserIds.clear();
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
private registerHandlers(): void {
|
|
145
|
+
this.typedrouter.addTypedHandler(
|
|
146
|
+
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_SubscribeOpsRealtime>(
|
|
147
|
+
'subscribeOpsRealtime',
|
|
148
|
+
async (dataArg, toolsArg) => {
|
|
149
|
+
const auth = await requireOpsAuth(this.opsServerRef, dataArg);
|
|
150
|
+
if (auth.type !== 'identity') {
|
|
151
|
+
throw new plugins.typedrequest.TypedResponseError(
|
|
152
|
+
'Realtime UI subscriptions require a user identity',
|
|
153
|
+
);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
const peer = toolsArg?.localData?.peer as IWebSocketPeerForRealtime | undefined;
|
|
157
|
+
if (!peer?.id || !peer.tags) {
|
|
158
|
+
throw new plugins.typedrequest.TypedResponseError(
|
|
159
|
+
'Realtime subscriptions require a WebSocket connection',
|
|
160
|
+
);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
for (const tag of Array.from(peer.tags)) {
|
|
164
|
+
if (tag.startsWith(REALTIME_TAG_PREFIX)) {
|
|
165
|
+
peer.tags.delete(tag);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
peer.tags.add(getOpsRealtimeUserTag(auth.userId));
|
|
169
|
+
this.subscribedUserIds.add(auth.userId);
|
|
170
|
+
|
|
171
|
+
return {
|
|
172
|
+
serverEpoch: this.revisionClock.serverEpoch,
|
|
173
|
+
revisions: this.revisionClock.getRevisions(),
|
|
174
|
+
invalidations: this.revisionClock.getCatchup(
|
|
175
|
+
dataArg.knownServerEpoch,
|
|
176
|
+
dataArg.knownRevisions,
|
|
177
|
+
),
|
|
178
|
+
};
|
|
179
|
+
},
|
|
180
|
+
),
|
|
181
|
+
);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
private schedulePush(): void {
|
|
185
|
+
if (this.pushScheduled) return;
|
|
186
|
+
this.pushScheduled = true;
|
|
187
|
+
queueMicrotask(() => {
|
|
188
|
+
this.pushScheduled = false;
|
|
189
|
+
void this.flushPushes();
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
private async flushPushes(): Promise<void> {
|
|
194
|
+
if (this.stopped || this.pendingInvalidations.size === 0) return;
|
|
195
|
+
const invalidations = Array.from(this.pendingInvalidations.values());
|
|
196
|
+
this.pendingInvalidations.clear();
|
|
197
|
+
|
|
198
|
+
let connections: plugins.typedsocket.ISmartServeConnectionWrapper[];
|
|
199
|
+
try {
|
|
200
|
+
connections = await this.getAuthenticatedConnections();
|
|
201
|
+
} catch {
|
|
202
|
+
return;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
const typedsocket = this.opsServerRef.server?.typedserver?.typedsocket;
|
|
206
|
+
if (!typedsocket) return;
|
|
207
|
+
|
|
208
|
+
await Promise.allSettled(
|
|
209
|
+
connections.map(async (connection) => {
|
|
210
|
+
const request =
|
|
211
|
+
typedsocket.createTypedRequest<interfaces.requests.IReq_PushOpsRealtimeInvalidation>(
|
|
212
|
+
'pushOpsRealtimeInvalidation',
|
|
213
|
+
connection,
|
|
214
|
+
);
|
|
215
|
+
await request.fire({
|
|
216
|
+
serverEpoch: this.revisionClock.serverEpoch,
|
|
217
|
+
invalidations,
|
|
218
|
+
});
|
|
219
|
+
}),
|
|
220
|
+
);
|
|
221
|
+
}
|
|
222
|
+
}
|
|
@@ -70,6 +70,10 @@ export class RemoteIngressHandler {
|
|
|
70
70
|
mailHostname: dataArg.mailHostname,
|
|
71
71
|
},
|
|
72
72
|
));
|
|
73
|
+
this.opsServerRef.invalidateRealtime?.(['edges', 'emailDomains', 'dns', 'routes'], {
|
|
74
|
+
changedIds: [edge.id],
|
|
75
|
+
reason: 'remote-ingress-created',
|
|
76
|
+
});
|
|
73
77
|
return { success: true, edge };
|
|
74
78
|
} catch (err: unknown) {
|
|
75
79
|
return {
|
|
@@ -100,6 +104,12 @@ export class RemoteIngressHandler {
|
|
|
100
104
|
throw err;
|
|
101
105
|
});
|
|
102
106
|
|
|
107
|
+
if (deleted) {
|
|
108
|
+
this.opsServerRef.invalidateRealtime?.(['edges', 'emailDomains', 'dns', 'routes'], {
|
|
109
|
+
changedIds: [dataArg.id],
|
|
110
|
+
reason: 'remote-ingress-deleted',
|
|
111
|
+
});
|
|
112
|
+
}
|
|
103
113
|
return {
|
|
104
114
|
success: deleted,
|
|
105
115
|
message: deleted ? undefined : 'Edge not found or RemoteIngress not configured',
|
|
@@ -155,6 +165,10 @@ export class RemoteIngressHandler {
|
|
|
155
165
|
if (!result) {
|
|
156
166
|
return { success: false, edge: null as any, message: failureMessage };
|
|
157
167
|
}
|
|
168
|
+
this.opsServerRef.invalidateRealtime?.(['edges', 'emailDomains', 'dns', 'routes'], {
|
|
169
|
+
changedIds: [dataArg.id],
|
|
170
|
+
reason: 'remote-ingress-updated',
|
|
171
|
+
});
|
|
158
172
|
return {
|
|
159
173
|
success: true,
|
|
160
174
|
edge: result,
|
|
@@ -184,6 +198,10 @@ export class RemoteIngressHandler {
|
|
|
184
198
|
return { success: false, secret: '' };
|
|
185
199
|
}
|
|
186
200
|
|
|
201
|
+
this.opsServerRef.invalidateRealtime?.('edges', {
|
|
202
|
+
changedIds: [dataArg.id],
|
|
203
|
+
reason: 'remote-ingress-secret-regenerated',
|
|
204
|
+
});
|
|
187
205
|
return { success: true, secret };
|
|
188
206
|
},
|
|
189
207
|
),
|
|
@@ -252,6 +270,9 @@ export class RemoteIngressHandler {
|
|
|
252
270
|
updates,
|
|
253
271
|
auth.userId,
|
|
254
272
|
);
|
|
273
|
+
this.opsServerRef.invalidateRealtime?.(['edges', 'emailDomains', 'dns', 'routes'], {
|
|
274
|
+
reason: 'remote-ingress-hub-settings-updated',
|
|
275
|
+
});
|
|
255
276
|
return { success: true, settings };
|
|
256
277
|
} catch (err: unknown) {
|
|
257
278
|
return { success: false, message: (err as Error).message };
|
|
@@ -68,6 +68,10 @@ export class RouteManagementHandler {
|
|
|
68
68
|
return { success: false, message: 'Route management not initialized' };
|
|
69
69
|
}
|
|
70
70
|
const id = await manager.createRoute(dataArg.route, userId, dataArg.enabled ?? true, dataArg.metadata);
|
|
71
|
+
this.opsServerRef.invalidateRealtime?.('routes', {
|
|
72
|
+
changedIds: [id],
|
|
73
|
+
reason: 'route-created',
|
|
74
|
+
});
|
|
71
75
|
return { success: true, routeId: id };
|
|
72
76
|
},
|
|
73
77
|
),
|
|
@@ -88,6 +92,12 @@ export class RouteManagementHandler {
|
|
|
88
92
|
enabled: dataArg.enabled,
|
|
89
93
|
metadata: dataArg.metadata,
|
|
90
94
|
});
|
|
95
|
+
if (result.success) {
|
|
96
|
+
this.opsServerRef.invalidateRealtime?.('routes', {
|
|
97
|
+
changedIds: [dataArg.id],
|
|
98
|
+
reason: 'route-updated',
|
|
99
|
+
});
|
|
100
|
+
}
|
|
91
101
|
return result;
|
|
92
102
|
},
|
|
93
103
|
),
|
|
@@ -103,7 +113,14 @@ export class RouteManagementHandler {
|
|
|
103
113
|
if (!manager) {
|
|
104
114
|
return { success: false, message: 'Route management not initialized' };
|
|
105
115
|
}
|
|
106
|
-
|
|
116
|
+
const result = await manager.deleteRoute(dataArg.id);
|
|
117
|
+
if (result.success) {
|
|
118
|
+
this.opsServerRef.invalidateRealtime?.('routes', {
|
|
119
|
+
changedIds: [dataArg.id],
|
|
120
|
+
reason: 'route-deleted',
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
return result;
|
|
107
124
|
},
|
|
108
125
|
),
|
|
109
126
|
);
|
|
@@ -118,7 +135,14 @@ export class RouteManagementHandler {
|
|
|
118
135
|
if (!manager) {
|
|
119
136
|
return { success: false, message: 'Route management not initialized' };
|
|
120
137
|
}
|
|
121
|
-
|
|
138
|
+
const result = await manager.toggleRoute(dataArg.id, dataArg.enabled);
|
|
139
|
+
if (result.success) {
|
|
140
|
+
this.opsServerRef.invalidateRealtime?.('routes', {
|
|
141
|
+
changedIds: [dataArg.id],
|
|
142
|
+
reason: 'route-toggled',
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
return result;
|
|
122
146
|
},
|
|
123
147
|
),
|
|
124
148
|
);
|
|
@@ -138,6 +162,10 @@ export class RouteManagementHandler {
|
|
|
138
162
|
userId,
|
|
139
163
|
dataArg.enabled ?? true,
|
|
140
164
|
);
|
|
165
|
+
this.opsServerRef.invalidateRealtime?.('routes', {
|
|
166
|
+
changedIds: [routeId],
|
|
167
|
+
reason: 'lets-encrypt-forward-created',
|
|
168
|
+
});
|
|
141
169
|
return { success: true, routeId };
|
|
142
170
|
} catch (error) {
|
|
143
171
|
return { success: false, message: (error as Error).message };
|
|
@@ -156,11 +184,18 @@ export class RouteManagementHandler {
|
|
|
156
184
|
return { success: false, message: 'Route management not initialized' };
|
|
157
185
|
}
|
|
158
186
|
try {
|
|
159
|
-
|
|
187
|
+
const result = await manager.updateLetsEncryptHttp01Forward(
|
|
160
188
|
dataArg.id,
|
|
161
189
|
dataArg.config,
|
|
162
190
|
dataArg.enabled,
|
|
163
191
|
);
|
|
192
|
+
if (result.success) {
|
|
193
|
+
this.opsServerRef.invalidateRealtime?.('routes', {
|
|
194
|
+
changedIds: [dataArg.id],
|
|
195
|
+
reason: 'lets-encrypt-forward-updated',
|
|
196
|
+
});
|
|
197
|
+
}
|
|
198
|
+
return result;
|
|
164
199
|
} catch (error) {
|
|
165
200
|
return { success: false, message: (error as Error).message };
|
|
166
201
|
}
|
|
@@ -177,7 +212,14 @@ export class RouteManagementHandler {
|
|
|
177
212
|
if (!manager) {
|
|
178
213
|
return { success: false, message: 'Route management not initialized' };
|
|
179
214
|
}
|
|
180
|
-
|
|
215
|
+
const result = await manager.deleteLetsEncryptHttp01Forward(dataArg.id);
|
|
216
|
+
if (result.success) {
|
|
217
|
+
this.opsServerRef.invalidateRealtime?.('routes', {
|
|
218
|
+
changedIds: [dataArg.id],
|
|
219
|
+
reason: 'lets-encrypt-forward-deleted',
|
|
220
|
+
});
|
|
221
|
+
}
|
|
222
|
+
return result;
|
|
181
223
|
},
|
|
182
224
|
),
|
|
183
225
|
);
|
|
@@ -432,6 +432,7 @@ export class StatsHandler {
|
|
|
432
432
|
// Fallback to basic stats if MetricsManager not available
|
|
433
433
|
const uptime = process.uptime();
|
|
434
434
|
const memUsage = process.memoryUsage();
|
|
435
|
+
const hostMemoryBytes = Math.max(1, plugins.os.totalmem());
|
|
435
436
|
const cpuUsage = plugins.os.loadavg()[0] * 100 / plugins.os.cpus().length;
|
|
436
437
|
|
|
437
438
|
return {
|
|
@@ -445,6 +446,9 @@ export class StatsHandler {
|
|
|
445
446
|
heapTotal: memUsage.heapTotal,
|
|
446
447
|
external: memUsage.external,
|
|
447
448
|
rss: memUsage.rss,
|
|
449
|
+
maxMemoryMB: hostMemoryBytes / (1024 * 1024),
|
|
450
|
+
actualUsageBytes: memUsage.rss,
|
|
451
|
+
actualUsagePercentage: (memUsage.rss / hostMemoryBytes) * 100,
|
|
448
452
|
},
|
|
449
453
|
requestsPerSecond: 0,
|
|
450
454
|
activeConnections: 0,
|
package/ts/paths.ts
CHANGED
|
@@ -10,18 +10,9 @@ export const distServe = plugins.path.join(packageDir, './dist_serve');
|
|
|
10
10
|
// Default base for all dcrouter data (always user-writable)
|
|
11
11
|
export const dcrouterHomeDir = plugins.path.join(plugins.os.homedir(), '.serve.zone', 'dcrouter');
|
|
12
12
|
|
|
13
|
-
// Configure data directory with environment variable or default to ~/.serve.zone/dcrouter/data
|
|
14
|
-
const DEFAULT_DATA_PATH = plugins.path.join(dcrouterHomeDir, 'data');
|
|
15
|
-
export const dataDir = process.env.DATA_DIR
|
|
16
|
-
? process.env.DATA_DIR
|
|
17
|
-
: DEFAULT_DATA_PATH;
|
|
18
|
-
|
|
19
13
|
// Default TsmDB path for CacheDb
|
|
20
14
|
export const defaultTsmDbPath = plugins.path.join(dcrouterHomeDir, 'tsmdb');
|
|
21
15
|
|
|
22
|
-
// DNS records directory (only surviving MTA directory reference)
|
|
23
|
-
export const dnsRecordsDir = plugins.path.join(dataDir, 'dns');
|
|
24
|
-
|
|
25
16
|
/**
|
|
26
17
|
* Resolve all data paths from a given baseDir.
|
|
27
18
|
* When no baseDir is provided, falls back to ~/.serve.zone/dcrouter.
|
|
@@ -34,21 +25,5 @@ export function resolvePaths(baseDir?: string) {
|
|
|
34
25
|
dcrouterHomeDir: root,
|
|
35
26
|
dataDir: resolvedDataDir,
|
|
36
27
|
defaultTsmDbPath: plugins.path.join(root, 'tsmdb'),
|
|
37
|
-
dnsRecordsDir: plugins.path.join(resolvedDataDir, 'dns'),
|
|
38
28
|
};
|
|
39
29
|
}
|
|
40
|
-
|
|
41
|
-
/**
|
|
42
|
-
* Ensure only the data directories that are actually used exist.
|
|
43
|
-
*/
|
|
44
|
-
export function ensureDataDirectories(resolvedPaths: ReturnType<typeof resolvePaths>) {
|
|
45
|
-
plugins.fsUtils.ensureDirSync(resolvedPaths.dataDir);
|
|
46
|
-
plugins.fsUtils.ensureDirSync(resolvedPaths.dnsRecordsDir);
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
/**
|
|
50
|
-
* Legacy wrapper — delegates to ensureDataDirectories with module-level defaults.
|
|
51
|
-
*/
|
|
52
|
-
export function ensureDirectories() {
|
|
53
|
-
ensureDataDirectories(resolvePaths());
|
|
54
|
-
}
|
package/ts/plugins.ts
CHANGED
|
@@ -102,71 +102,3 @@ import * as uuid from 'uuid';
|
|
|
102
102
|
export {
|
|
103
103
|
uuid,
|
|
104
104
|
}
|
|
105
|
-
|
|
106
|
-
// Filesystem utilities
|
|
107
|
-
export const fsUtils = {
|
|
108
|
-
/**
|
|
109
|
-
* Ensure a directory exists, creating it recursively if needed (sync)
|
|
110
|
-
*/
|
|
111
|
-
ensureDirSync: (dirPath: string): void => {
|
|
112
|
-
fs.mkdirSync(dirPath, { recursive: true });
|
|
113
|
-
},
|
|
114
|
-
|
|
115
|
-
/**
|
|
116
|
-
* Ensure a directory exists, creating it recursively if needed (async)
|
|
117
|
-
*/
|
|
118
|
-
ensureDir: async (dirPath: string): Promise<void> => {
|
|
119
|
-
await fs.promises.mkdir(dirPath, { recursive: true });
|
|
120
|
-
},
|
|
121
|
-
|
|
122
|
-
/**
|
|
123
|
-
* Write JSON content to a file synchronously
|
|
124
|
-
*/
|
|
125
|
-
toFsSync: (content: any, filePath: string): void => {
|
|
126
|
-
const data = typeof content === 'string' ? content : JSON.stringify(content, null, 2);
|
|
127
|
-
fs.writeFileSync(filePath, data);
|
|
128
|
-
},
|
|
129
|
-
|
|
130
|
-
/**
|
|
131
|
-
* Write JSON content to a file asynchronously
|
|
132
|
-
*/
|
|
133
|
-
toFs: async (content: any, filePath: string): Promise<void> => {
|
|
134
|
-
const data = typeof content === 'string' ? content : JSON.stringify(content, null, 2);
|
|
135
|
-
await fs.promises.writeFile(filePath, data);
|
|
136
|
-
},
|
|
137
|
-
|
|
138
|
-
/**
|
|
139
|
-
* Check if a file or directory exists
|
|
140
|
-
*/
|
|
141
|
-
fileExistsSync: (filePath: string): boolean => {
|
|
142
|
-
return fs.existsSync(filePath);
|
|
143
|
-
},
|
|
144
|
-
|
|
145
|
-
/**
|
|
146
|
-
* Check if a file or directory exists (async)
|
|
147
|
-
*/
|
|
148
|
-
fileExists: async (filePath: string): Promise<boolean> => {
|
|
149
|
-
try {
|
|
150
|
-
await fs.promises.access(filePath);
|
|
151
|
-
return true;
|
|
152
|
-
} catch {
|
|
153
|
-
return false;
|
|
154
|
-
}
|
|
155
|
-
},
|
|
156
|
-
|
|
157
|
-
/**
|
|
158
|
-
* Read a JSON file synchronously
|
|
159
|
-
*/
|
|
160
|
-
toObjectSync: <T = any>(filePath: string): T => {
|
|
161
|
-
const content = fs.readFileSync(filePath, 'utf8');
|
|
162
|
-
return JSON.parse(content) as T;
|
|
163
|
-
},
|
|
164
|
-
|
|
165
|
-
/**
|
|
166
|
-
* Read a JSON file asynchronously
|
|
167
|
-
*/
|
|
168
|
-
toObject: async <T = any>(filePath: string): Promise<T> => {
|
|
169
|
-
const content = await fs.promises.readFile(filePath, 'utf8');
|
|
170
|
-
return JSON.parse(content) as T;
|
|
171
|
-
},
|
|
172
|
-
};
|
|
@@ -3,6 +3,7 @@ import type * as servezoneInterfaces from '@serve.zone/interfaces';
|
|
|
3
3
|
import * as interfaces from '../../ts_interfaces/index.js';
|
|
4
4
|
import { appState } from './shared.js';
|
|
5
5
|
import { getActionContext } from './login.js';
|
|
6
|
+
import { captureRealtimeTicket, isRealtimeTicketCurrent } from './realtime.js';
|
|
6
7
|
|
|
7
8
|
export interface ICertificateState {
|
|
8
9
|
certificates: interfaces.requests.ICertificateInfo[];
|
|
@@ -31,6 +32,7 @@ export const fetchCertificateOverviewAction = certificateStatePart.createAction(
|
|
|
31
32
|
const context = getActionContext();
|
|
32
33
|
const currentState = statePartArg.getState()!;
|
|
33
34
|
if (!context.identity) return currentState;
|
|
35
|
+
const ticket = captureRealtimeTicket('certificates');
|
|
34
36
|
|
|
35
37
|
try {
|
|
36
38
|
const request = new plugins.domtools.plugins.typedrequest.TypedRequest<
|
|
@@ -40,8 +42,10 @@ export const fetchCertificateOverviewAction = certificateStatePart.createAction(
|
|
|
40
42
|
const response = await request.fire({
|
|
41
43
|
identity: context.identity,
|
|
42
44
|
});
|
|
45
|
+
if (!isRealtimeTicketCurrent(ticket)) return statePartArg.getState()!;
|
|
43
46
|
|
|
44
47
|
return {
|
|
48
|
+
...statePartArg.getState()!,
|
|
45
49
|
certificates: response.certificates,
|
|
46
50
|
summary: response.summary,
|
|
47
51
|
isLoading: false,
|
|
@@ -49,8 +53,9 @@ export const fetchCertificateOverviewAction = certificateStatePart.createAction(
|
|
|
49
53
|
lastUpdated: Date.now(),
|
|
50
54
|
};
|
|
51
55
|
} catch (error) {
|
|
56
|
+
if (!isRealtimeTicketCurrent(ticket)) return statePartArg.getState()!;
|
|
52
57
|
return {
|
|
53
|
-
...
|
|
58
|
+
...statePartArg.getState()!,
|
|
54
59
|
isLoading: false,
|
|
55
60
|
error: error instanceof Error ? error.message : 'Failed to fetch certificate overview',
|
|
56
61
|
};
|
|
@@ -2,6 +2,7 @@ import * as plugins from '../plugins.js';
|
|
|
2
2
|
import * as interfaces from '../../ts_interfaces/index.js';
|
|
3
3
|
import { appState } from './shared.js';
|
|
4
4
|
import { getActionContext } from './login.js';
|
|
5
|
+
import { captureRealtimeTicket, isRealtimeTicketCurrent } from './realtime.js';
|
|
5
6
|
|
|
6
7
|
// ============================================================================
|
|
7
8
|
// Domains State (DNS providers + domains + records)
|
|
@@ -37,6 +38,7 @@ export const fetchDomainsAndProvidersAction = domainsStatePart.createAction(
|
|
|
37
38
|
const context = getActionContext();
|
|
38
39
|
const currentState = statePartArg.getState()!;
|
|
39
40
|
if (!context.identity) return currentState;
|
|
41
|
+
const ticket = captureRealtimeTicket('dns');
|
|
40
42
|
|
|
41
43
|
try {
|
|
42
44
|
const providersRequest = new plugins.domtools.plugins.typedrequest.TypedRequest<
|
|
@@ -50,9 +52,10 @@ export const fetchDomainsAndProvidersAction = domainsStatePart.createAction(
|
|
|
50
52
|
providersRequest.fire({ identity: context.identity }),
|
|
51
53
|
domainsRequest.fire({ identity: context.identity }),
|
|
52
54
|
]);
|
|
55
|
+
if (!isRealtimeTicketCurrent(ticket)) return statePartArg.getState()!;
|
|
53
56
|
|
|
54
57
|
return {
|
|
55
|
-
...
|
|
58
|
+
...statePartArg.getState()!,
|
|
56
59
|
providers: providersResponse.providers,
|
|
57
60
|
domains: domainsResponse.domains,
|
|
58
61
|
isLoading: false,
|
|
@@ -60,8 +63,9 @@ export const fetchDomainsAndProvidersAction = domainsStatePart.createAction(
|
|
|
60
63
|
lastUpdated: Date.now(),
|
|
61
64
|
};
|
|
62
65
|
} catch (error: unknown) {
|
|
66
|
+
if (!isRealtimeTicketCurrent(ticket)) return statePartArg.getState()!;
|
|
63
67
|
return {
|
|
64
|
-
...
|
|
68
|
+
...statePartArg.getState()!,
|
|
65
69
|
isLoading: false,
|
|
66
70
|
error: error instanceof Error ? error.message : 'Failed to fetch domains/providers',
|
|
67
71
|
};
|
|
@@ -74,6 +78,7 @@ export const fetchDnsRecordsForDomainAction = domainsStatePart.createAction<{ do
|
|
|
74
78
|
const context = getActionContext();
|
|
75
79
|
const currentState = statePartArg.getState()!;
|
|
76
80
|
if (!context.identity) return currentState;
|
|
81
|
+
const ticket = captureRealtimeTicket('dns');
|
|
77
82
|
|
|
78
83
|
try {
|
|
79
84
|
const request = new plugins.domtools.plugins.typedrequest.TypedRequest<
|
|
@@ -83,15 +88,17 @@ export const fetchDnsRecordsForDomainAction = domainsStatePart.createAction<{ do
|
|
|
83
88
|
identity: context.identity,
|
|
84
89
|
domainId: dataArg.domainId,
|
|
85
90
|
});
|
|
91
|
+
if (!isRealtimeTicketCurrent(ticket)) return statePartArg.getState()!;
|
|
86
92
|
return {
|
|
87
|
-
...
|
|
93
|
+
...statePartArg.getState()!,
|
|
88
94
|
records: response.records,
|
|
89
95
|
selectedDomainId: dataArg.domainId,
|
|
90
96
|
error: null,
|
|
91
97
|
};
|
|
92
98
|
} catch (error: unknown) {
|
|
99
|
+
if (!isRealtimeTicketCurrent(ticket)) return statePartArg.getState()!;
|
|
93
100
|
return {
|
|
94
|
-
...
|
|
101
|
+
...statePartArg.getState()!,
|
|
95
102
|
error: error instanceof Error ? error.message : 'Failed to fetch DNS records',
|
|
96
103
|
};
|
|
97
104
|
}
|