@serve.zone/dcrouter 16.5.0 → 17.1.0
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 +5 -2
- 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 +6 -1
- package/dist_ts/classes.dcrouter.js +119 -41
- 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/db/documents/classes.smartmta-storage.doc.d.ts +8 -0
- package/dist_ts/db/documents/classes.smartmta-storage.doc.js +91 -0
- package/dist_ts/db/documents/index.d.ts +1 -0
- package/dist_ts/db/documents/index.js +2 -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 -1
- 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 +27 -0
- package/dist_ts/email/classes.smartmta-blob-storage-manager.js +110 -0
- package/dist_ts/email/classes.smartmta-storage-manager.d.ts +6 -8
- package/dist_ts/email/classes.smartmta-storage-manager.js +63 -86
- package/dist_ts/email/classes.workapp-mail-manager.d.ts +1 -0
- package/dist_ts/email/classes.workapp-mail-manager.js +31 -4
- package/dist_ts/email/index.d.ts +4 -0
- package/dist_ts/email/index.js +5 -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/email/smartmta-storage-policy.d.ts +9 -0
- package/dist_ts/email/smartmta-storage-policy.js +58 -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 +3 -5
- package/dist_ts/paths.d.ts +0 -11
- package/dist_ts/paths.js +1 -22
- package/dist_ts/plugins.d.ts +2 -35
- package/dist_ts/plugins.js +3 -63
- 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 +8 -0
- package/dist_ts_migrations/index.js +489 -12
- 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 +23 -19
- package/ts/00_commitinfo_data.ts +1 -1
- package/ts/classes.dcrouter.ts +136 -44
- 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/db/documents/classes.smartmta-storage.doc.ts +25 -0
- package/ts/db/documents/index.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 -0
- package/ts/email/classes.mail-dns-sync.ts +588 -122
- package/ts/email/classes.smartmta-blob-storage-manager.ts +139 -0
- package/ts/email/classes.smartmta-storage-manager.ts +86 -83
- package/ts/email/classes.workapp-mail-manager.ts +47 -4
- package/ts/email/index.ts +4 -0
- package/ts/email/mail-dns-txt-policy.ts +505 -0
- package/ts/email/reconcile-mail-dns-singleton.ts +224 -0
- package/ts/email/smartmta-storage-policy.ts +73 -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 +2 -4
- package/ts/paths.ts +0 -25
- package/ts/plugins.ts +2 -69
- 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
|
@@ -134,7 +134,7 @@ export class EmailOpsHandler {
|
|
|
134
134
|
if (this.isCachedEmailStoreAvailable()) {
|
|
135
135
|
const cachedEmail = await CachedEmail.findById(emailId);
|
|
136
136
|
if (cachedEmail) {
|
|
137
|
-
return this.mapCachedEmailToEmailDetail(cachedEmail);
|
|
137
|
+
return await this.mapCachedEmailToEmailDetail(cachedEmail);
|
|
138
138
|
}
|
|
139
139
|
}
|
|
140
140
|
return null;
|
|
@@ -158,23 +158,34 @@ export class EmailOpsHandler {
|
|
|
158
158
|
subject: doc.subject || '',
|
|
159
159
|
timestamp: new Date(doc.acceptedAt || 0).toISOString(),
|
|
160
160
|
messageId: doc.messageId || '',
|
|
161
|
-
size: this.formatSize(doc.
|
|
161
|
+
size: this.formatSize(doc.rawContentSize || 0),
|
|
162
162
|
};
|
|
163
163
|
}
|
|
164
164
|
|
|
165
165
|
/**
|
|
166
166
|
* Map a CachedEmail row to catalog IEmailDetail format
|
|
167
167
|
*/
|
|
168
|
-
private mapCachedEmailToEmailDetail(
|
|
168
|
+
private async mapCachedEmailToEmailDetail(
|
|
169
|
+
doc: CachedEmail,
|
|
170
|
+
): Promise<interfaces.requests.IEmailDetail> {
|
|
169
171
|
const base = this.mapCachedEmailToEmail(doc);
|
|
170
|
-
|
|
172
|
+
let rawContent = '';
|
|
173
|
+
try {
|
|
174
|
+
rawContent = (await this.opsServerRef.dcRouterRef.acceptedEmailSpool.readRawMessage(doc))
|
|
175
|
+
.toString('utf8');
|
|
176
|
+
} catch {
|
|
177
|
+
// Transaction history and delivery state remain available when a retained
|
|
178
|
+
// legacy row no longer has its raw payload.
|
|
179
|
+
}
|
|
180
|
+
const { headers, body } = this.parseRawEmail(rawContent);
|
|
171
181
|
const session = this.parseCachedEmailSession(doc);
|
|
172
182
|
|
|
173
183
|
return {
|
|
174
184
|
...base,
|
|
185
|
+
smtpTransactions: structuredClone(doc.smtpTransactions || []),
|
|
175
186
|
toList: doc.to || [],
|
|
176
187
|
cc: doc.cc || [],
|
|
177
|
-
smtpLog: [],
|
|
188
|
+
smtpLog: this.flattenSmtpTransactions(doc.smtpTransactions || []),
|
|
178
189
|
connectionInfo: {
|
|
179
190
|
sourceIp: session.remoteAddress || '',
|
|
180
191
|
sourceHostname: session.clientHostname || '',
|
|
@@ -344,9 +355,10 @@ export class EmailOpsHandler {
|
|
|
344
355
|
|
|
345
356
|
return {
|
|
346
357
|
...base,
|
|
358
|
+
smtpTransactions: structuredClone(item.smtpTransactions || []),
|
|
347
359
|
toList,
|
|
348
360
|
cc,
|
|
349
|
-
smtpLog: [],
|
|
361
|
+
smtpLog: this.flattenSmtpTransactions(item.smtpTransactions || []),
|
|
350
362
|
connectionInfo: {
|
|
351
363
|
sourceIp: '',
|
|
352
364
|
sourceHostname: '',
|
|
@@ -393,6 +405,17 @@ export class EmailOpsHandler {
|
|
|
393
405
|
}
|
|
394
406
|
}
|
|
395
407
|
|
|
408
|
+
private flattenSmtpTransactions(
|
|
409
|
+
transactionsArg: interfaces.requests.ISmtpTransactionAttempt[],
|
|
410
|
+
): interfaces.requests.ISmtpLogEntry[] {
|
|
411
|
+
return transactionsArg.flatMap((transaction) => transaction.transcript.map((entry) => ({
|
|
412
|
+
timestamp: new Date(entry.timestampMs).toISOString(),
|
|
413
|
+
direction: entry.direction,
|
|
414
|
+
command: entry.text,
|
|
415
|
+
responseCode: entry.responseCode,
|
|
416
|
+
})));
|
|
417
|
+
}
|
|
418
|
+
|
|
396
419
|
/**
|
|
397
420
|
* Format byte size to human-readable string
|
|
398
421
|
*/
|
|
@@ -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
|
);
|
|
@@ -562,10 +562,8 @@ export class StatsHandler {
|
|
|
562
562
|
const emailLike = item.processingResult;
|
|
563
563
|
const recipients = Array.isArray(emailLike?.to)
|
|
564
564
|
? emailLike.to
|
|
565
|
-
:
|
|
566
|
-
|
|
567
|
-
: [];
|
|
568
|
-
const subject = emailLike?.subject || emailLike?.email?.subject || '';
|
|
565
|
+
: [];
|
|
566
|
+
const subject = emailLike?.subject || '';
|
|
569
567
|
return {
|
|
570
568
|
id: item.id,
|
|
571
569
|
recipient: recipients[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
|
@@ -54,6 +54,7 @@ export {
|
|
|
54
54
|
import * as projectinfo from '@push.rocks/projectinfo';
|
|
55
55
|
import * as qenv from '@push.rocks/qenv';
|
|
56
56
|
import * as smartacme from '@push.rocks/smartacme';
|
|
57
|
+
import * as smartbucket from '@push.rocks/smartbucket';
|
|
57
58
|
import * as smartchallenge from '@push.rocks/smartchallenge';
|
|
58
59
|
import * as smartdata from '@push.rocks/smartdata';
|
|
59
60
|
import * as smartdns from '@push.rocks/smartdns';
|
|
@@ -76,7 +77,7 @@ import * as smartrx from '@push.rocks/smartrx';
|
|
|
76
77
|
import * as smartunique from '@push.rocks/smartunique';
|
|
77
78
|
import * as taskbuffer from '@push.rocks/taskbuffer';
|
|
78
79
|
|
|
79
|
-
export { projectinfo, qenv, smartacme, smartchallenge, smartdata, smartdns, smartfs, smartguard, smartjwt, smartlog, smartmetrics, smartmcp, smartdb, smartmta, smartnetwork, smartpath, smartproxy, smartpromise, smartradius, smartrequest, smartrx, smartunique, smartvpn, taskbuffer };
|
|
80
|
+
export { projectinfo, qenv, smartacme, smartbucket, smartchallenge, smartdata, smartdns, smartfs, smartguard, smartjwt, smartlog, smartmetrics, smartmcp, smartdb, smartmta, smartnetwork, smartpath, smartproxy, smartpromise, smartradius, smartrequest, smartrx, smartunique, smartvpn, taskbuffer };
|
|
80
81
|
|
|
81
82
|
// Define SmartLog types for use in error handling
|
|
82
83
|
export type TLogLevel = 'error' | 'warn' | 'info' | 'success' | 'debug';
|
|
@@ -101,71 +102,3 @@ import * as uuid from 'uuid';
|
|
|
101
102
|
export {
|
|
102
103
|
uuid,
|
|
103
104
|
}
|
|
104
|
-
|
|
105
|
-
// Filesystem utilities
|
|
106
|
-
export const fsUtils = {
|
|
107
|
-
/**
|
|
108
|
-
* Ensure a directory exists, creating it recursively if needed (sync)
|
|
109
|
-
*/
|
|
110
|
-
ensureDirSync: (dirPath: string): void => {
|
|
111
|
-
fs.mkdirSync(dirPath, { recursive: true });
|
|
112
|
-
},
|
|
113
|
-
|
|
114
|
-
/**
|
|
115
|
-
* Ensure a directory exists, creating it recursively if needed (async)
|
|
116
|
-
*/
|
|
117
|
-
ensureDir: async (dirPath: string): Promise<void> => {
|
|
118
|
-
await fs.promises.mkdir(dirPath, { recursive: true });
|
|
119
|
-
},
|
|
120
|
-
|
|
121
|
-
/**
|
|
122
|
-
* Write JSON content to a file synchronously
|
|
123
|
-
*/
|
|
124
|
-
toFsSync: (content: any, filePath: string): void => {
|
|
125
|
-
const data = typeof content === 'string' ? content : JSON.stringify(content, null, 2);
|
|
126
|
-
fs.writeFileSync(filePath, data);
|
|
127
|
-
},
|
|
128
|
-
|
|
129
|
-
/**
|
|
130
|
-
* Write JSON content to a file asynchronously
|
|
131
|
-
*/
|
|
132
|
-
toFs: async (content: any, filePath: string): Promise<void> => {
|
|
133
|
-
const data = typeof content === 'string' ? content : JSON.stringify(content, null, 2);
|
|
134
|
-
await fs.promises.writeFile(filePath, data);
|
|
135
|
-
},
|
|
136
|
-
|
|
137
|
-
/**
|
|
138
|
-
* Check if a file or directory exists
|
|
139
|
-
*/
|
|
140
|
-
fileExistsSync: (filePath: string): boolean => {
|
|
141
|
-
return fs.existsSync(filePath);
|
|
142
|
-
},
|
|
143
|
-
|
|
144
|
-
/**
|
|
145
|
-
* Check if a file or directory exists (async)
|
|
146
|
-
*/
|
|
147
|
-
fileExists: async (filePath: string): Promise<boolean> => {
|
|
148
|
-
try {
|
|
149
|
-
await fs.promises.access(filePath);
|
|
150
|
-
return true;
|
|
151
|
-
} catch {
|
|
152
|
-
return false;
|
|
153
|
-
}
|
|
154
|
-
},
|
|
155
|
-
|
|
156
|
-
/**
|
|
157
|
-
* Read a JSON file synchronously
|
|
158
|
-
*/
|
|
159
|
-
toObjectSync: <T = any>(filePath: string): T => {
|
|
160
|
-
const content = fs.readFileSync(filePath, 'utf8');
|
|
161
|
-
return JSON.parse(content) as T;
|
|
162
|
-
},
|
|
163
|
-
|
|
164
|
-
/**
|
|
165
|
-
* Read a JSON file asynchronously
|
|
166
|
-
*/
|
|
167
|
-
toObject: async <T = any>(filePath: string): Promise<T> => {
|
|
168
|
-
const content = await fs.promises.readFile(filePath, 'utf8');
|
|
169
|
-
return JSON.parse(content) as T;
|
|
170
|
-
},
|
|
171
|
-
};
|
|
@@ -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
|
};
|