@serve.zone/dcrouter 17.2.4 → 17.3.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/deno.json +1 -1
- package/dist_serve/bundle.js +2024 -1758
- package/dist_ts/00_commitinfo_data.js +1 -1
- package/dist_ts/classes.dcrouter.d.ts +4 -1
- package/dist_ts/classes.dcrouter.js +42 -2
- package/dist_ts/config/classes.api-token-manager.d.ts +20 -2
- package/dist_ts/config/classes.api-token-manager.js +201 -6
- package/dist_ts/config/classes.gateway-client-manager.d.ts +36 -2
- package/dist_ts/config/classes.gateway-client-manager.js +391 -27
- package/dist_ts/db/documents/classes.api-token.doc.d.ts +2 -1
- package/dist_ts/db/documents/classes.api-token.doc.js +8 -2
- package/dist_ts/db/documents/classes.gateway-client.doc.d.ts +25 -0
- package/dist_ts/db/documents/classes.gateway-client.doc.js +73 -2
- package/dist_ts/db/documents/classes.ops-config-event.doc.d.ts +55 -0
- package/dist_ts/db/documents/classes.ops-config-event.doc.js +296 -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.gateway-route-dns-reconciler.d.ts +52 -0
- package/dist_ts/dns/classes.gateway-route-dns-reconciler.js +609 -0
- 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 +19 -0
- package/dist_ts/dns/manager.dns.js +25 -1
- package/dist_ts/monitoring/classes.ops-event-manager.d.ts +32 -0
- package/dist_ts/monitoring/classes.ops-event-manager.js +71 -0
- package/dist_ts/monitoring/index.d.ts +1 -0
- package/dist_ts/monitoring/index.js +2 -1
- package/dist_ts/opsserver/classes.opsserver.d.ts +1 -0
- package/dist_ts/opsserver/classes.opsserver.js +3 -1
- package/dist_ts/opsserver/handlers/certificate.handler.d.ts +1 -0
- package/dist_ts/opsserver/handlers/certificate.handler.js +35 -5
- package/dist_ts/opsserver/handlers/config-event.handler.d.ts +12 -0
- package/dist_ts/opsserver/handlers/config-event.handler.js +47 -0
- package/dist_ts/opsserver/handlers/dns-record.handler.d.ts +1 -0
- package/dist_ts/opsserver/handlers/dns-record.handler.js +82 -3
- package/dist_ts/opsserver/handlers/gatewayclient.handler.d.ts +9 -1
- package/dist_ts/opsserver/handlers/gatewayclient.handler.js +714 -162
- package/dist_ts/opsserver/handlers/index.d.ts +1 -0
- package/dist_ts/opsserver/handlers/index.js +2 -1
- package/dist_ts/opsserver/helpers/gateway-client-auth.d.ts +25 -0
- package/dist_ts/opsserver/helpers/gateway-client-auth.js +168 -0
- package/dist_ts/plugins.d.ts +2 -1
- package/dist_ts/plugins.js +3 -2
- package/dist_ts_apiclient/classes.gatewayclient.d.ts +13 -1
- package/dist_ts_apiclient/classes.gatewayclient.js +18 -2
- package/dist_ts_interfaces/data/config-events.d.ts +34 -0
- package/dist_ts_interfaces/data/config-events.js +8 -0
- package/dist_ts_interfaces/data/index.d.ts +1 -0
- package/dist_ts_interfaces/data/index.js +2 -1
- package/dist_ts_interfaces/data/route-management.d.ts +28 -1
- package/dist_ts_interfaces/data/route-management.js +3 -1
- package/dist_ts_interfaces/data/workhoster.d.ts +1 -0
- package/dist_ts_interfaces/requests/config-events.d.ts +35 -0
- package/dist_ts_interfaces/requests/config-events.js +4 -0
- 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 +1 -1
- package/dist_ts_interfaces/requests/realtime.js +2 -1
- package/dist_ts_migrations/index.js +148 -9
- package/dist_ts_web/00_commitinfo_data.js +1 -1
- package/dist_ts_web/appstate/config-events.d.ts +19 -0
- package/dist_ts_web/appstate/config-events.js +68 -0
- package/dist_ts_web/appstate/runtime.js +7 -1
- package/dist_ts_web/appstate.d.ts +1 -0
- package/dist_ts_web/appstate.js +2 -1
- package/dist_ts_web/elements/ops-platform-events.d.ts +21 -0
- package/dist_ts_web/elements/ops-platform-events.js +361 -0
- package/dist_ts_web/elements/ops-view-logs.d.ts +4 -0
- package/dist_ts_web/elements/ops-view-logs.js +96 -9
- package/package.json +2 -2
- package/ts/00_commitinfo_data.ts +1 -1
- package/ts/classes.dcrouter.ts +48 -1
- package/ts/config/classes.api-token-manager.ts +237 -4
- package/ts/config/classes.gateway-client-manager.ts +463 -19
- package/ts/db/documents/classes.api-token.doc.ts +8 -1
- package/ts/db/documents/classes.gateway-client.doc.ts +86 -0
- package/ts/db/documents/classes.ops-config-event.doc.ts +257 -0
- package/ts/db/documents/index.ts +1 -0
- package/ts/dns/classes.gateway-route-dns-reconciler.ts +801 -0
- package/ts/dns/index.ts +1 -0
- package/ts/dns/manager.dns.ts +36 -0
- package/ts/monitoring/classes.ops-event-manager.ts +99 -0
- package/ts/monitoring/index.ts +2 -1
- package/ts/opsserver/classes.opsserver.ts +2 -0
- package/ts/opsserver/handlers/certificate.handler.ts +51 -4
- package/ts/opsserver/handlers/config-event.handler.ts +62 -0
- package/ts/opsserver/handlers/dns-record.handler.ts +101 -2
- package/ts/opsserver/handlers/gatewayclient.handler.ts +804 -136
- package/ts/opsserver/handlers/index.ts +1 -0
- package/ts/opsserver/helpers/gateway-client-auth.ts +198 -0
- package/ts/plugins.ts +2 -0
- package/ts_apiclient/classes.gatewayclient.ts +45 -0
- package/ts_web/00_commitinfo_data.ts +1 -1
- package/ts_web/appstate/config-events.ts +98 -0
- package/ts_web/appstate/runtime.ts +8 -1
- package/ts_web/appstate.ts +1 -0
- package/ts_web/elements/ops-platform-events.ts +315 -0
- package/ts_web/elements/ops-view-logs.ts +84 -7
package/ts/dns/index.ts
CHANGED
package/ts/dns/manager.dns.ts
CHANGED
|
@@ -961,6 +961,42 @@ export class DnsManager {
|
|
|
961
961
|
return { success: true, id: doc.id };
|
|
962
962
|
}
|
|
963
963
|
|
|
964
|
+
/**
|
|
965
|
+
* Adopt an already-correct record into an automation lifecycle without
|
|
966
|
+
* mutating provider DNS. The expected value fields make adoption fail closed
|
|
967
|
+
* if the provider mirror changed between inspection and persistence.
|
|
968
|
+
*/
|
|
969
|
+
public async adoptRecordManagement(args: {
|
|
970
|
+
id: string;
|
|
971
|
+
name: string;
|
|
972
|
+
type: TDnsRecordType;
|
|
973
|
+
value: string;
|
|
974
|
+
ttl: number;
|
|
975
|
+
proxied: boolean;
|
|
976
|
+
managedBy: string;
|
|
977
|
+
managedOwnerId: string;
|
|
978
|
+
managedRecordKey: string;
|
|
979
|
+
}): Promise<{ success: boolean; message?: string }> {
|
|
980
|
+
const doc = await DnsRecordDoc.findById(args.id);
|
|
981
|
+
if (!doc) return { success: false, message: 'Record not found' };
|
|
982
|
+
if (
|
|
983
|
+
doc.name.toLowerCase().replace(/\.+$/, '') !== args.name.toLowerCase().replace(/\.+$/, '')
|
|
984
|
+
|| doc.type !== args.type
|
|
985
|
+
|| doc.value !== args.value
|
|
986
|
+
|| doc.ttl !== args.ttl
|
|
987
|
+
|| doc.proxied !== args.proxied
|
|
988
|
+
|| doc.managedBy
|
|
989
|
+
) {
|
|
990
|
+
return { success: false, message: 'DNS record changed before provenance adoption' };
|
|
991
|
+
}
|
|
992
|
+
doc.managedBy = args.managedBy;
|
|
993
|
+
doc.managedOwnerId = args.managedOwnerId;
|
|
994
|
+
doc.managedRecordKey = args.managedRecordKey;
|
|
995
|
+
doc.updatedAt = Date.now();
|
|
996
|
+
await doc.save();
|
|
997
|
+
return { success: true };
|
|
998
|
+
}
|
|
999
|
+
|
|
964
1000
|
public async updateRecord(args: {
|
|
965
1001
|
id: string;
|
|
966
1002
|
name?: string;
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import * as plugins from '../plugins.js';
|
|
2
|
+
import { logger } from '../logger.js';
|
|
3
|
+
import {
|
|
4
|
+
OpsConfigEventDoc,
|
|
5
|
+
type IOpsConfigEventFilter,
|
|
6
|
+
} from '../db/documents/classes.ops-config-event.doc.js';
|
|
7
|
+
import type {
|
|
8
|
+
IOpsConfigEvent,
|
|
9
|
+
IOpsConfigEventCursor,
|
|
10
|
+
IOpsConfigEventContext,
|
|
11
|
+
TOpsConfigEventSeverity,
|
|
12
|
+
} from '../../ts_interfaces/data/config-events.js';
|
|
13
|
+
|
|
14
|
+
export interface IRecordOpsConfigEventOptions {
|
|
15
|
+
severity: TOpsConfigEventSeverity;
|
|
16
|
+
category: string;
|
|
17
|
+
title: string;
|
|
18
|
+
detail: string;
|
|
19
|
+
context?: IOpsConfigEventContext;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** Retention for acknowledged events (90 days). */
|
|
23
|
+
const ACKNOWLEDGED_RETENTION_MS = 90 * 24 * 60 * 60 * 1000;
|
|
24
|
+
const PRUNE_INTERVAL_MS = 24 * 60 * 60 * 1000;
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* OpsEventManager — durable, acknowledgeable platform configuration events.
|
|
28
|
+
*
|
|
29
|
+
* These are operator-facing config conflicts (e.g. gateway DNS automation
|
|
30
|
+
* overwriting a manual record), not application runtime errors. Producers call
|
|
31
|
+
* `recordEvent()` fire-and-forget; the ops UI lists and acknowledges them.
|
|
32
|
+
*
|
|
33
|
+
* Repeated occurrences of the same unacknowledged conflict coalesce into one
|
|
34
|
+
* event (refreshed timestamp/detail) so reconcile loops cannot flood the inbox.
|
|
35
|
+
*/
|
|
36
|
+
export class OpsEventManager {
|
|
37
|
+
private pruneInterval: ReturnType<typeof setInterval> | null = null;
|
|
38
|
+
|
|
39
|
+
public async start(): Promise<void> {
|
|
40
|
+
this.pruneInterval = setInterval(() => {
|
|
41
|
+
OpsConfigEventDoc.pruneAcknowledged(ACKNOWLEDGED_RETENTION_MS).catch((error: unknown) => {
|
|
42
|
+
logger.log('warn', `OpsEventManager prune failed: ${(error as Error).message}`);
|
|
43
|
+
});
|
|
44
|
+
}, PRUNE_INTERVAL_MS);
|
|
45
|
+
// Prune once at startup so long-stopped instances converge immediately.
|
|
46
|
+
await OpsConfigEventDoc.pruneAcknowledged(ACKNOWLEDGED_RETENTION_MS).catch(() => 0);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
public async stop(): Promise<void> {
|
|
50
|
+
if (this.pruneInterval) {
|
|
51
|
+
clearInterval(this.pruneInterval);
|
|
52
|
+
this.pruneInterval = null;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
public async recordEvent(options: IRecordOpsConfigEventOptions): Promise<IOpsConfigEvent> {
|
|
57
|
+
const context = options.context || {};
|
|
58
|
+
const sourceKey = [
|
|
59
|
+
options.title,
|
|
60
|
+
context.recordName || '',
|
|
61
|
+
context.domain || '',
|
|
62
|
+
context.routeRef || '',
|
|
63
|
+
].join('|');
|
|
64
|
+
const openDedupeKey = plugins.crypto
|
|
65
|
+
.createHash('sha256')
|
|
66
|
+
.update(sourceKey)
|
|
67
|
+
.digest('hex');
|
|
68
|
+
const now = Date.now();
|
|
69
|
+
const { event, created } = await OpsConfigEventDoc.upsertOpenEvent({
|
|
70
|
+
id: `oce_${now.toString(36)}_${Math.random().toString(36).slice(2, 10)}`,
|
|
71
|
+
openDedupeKey,
|
|
72
|
+
severity: options.severity,
|
|
73
|
+
category: options.category,
|
|
74
|
+
title: options.title,
|
|
75
|
+
detail: options.detail,
|
|
76
|
+
context,
|
|
77
|
+
createdAt: now,
|
|
78
|
+
});
|
|
79
|
+
if (created) {
|
|
80
|
+
logger.log(
|
|
81
|
+
options.severity === 'error' ? 'error' : 'warn',
|
|
82
|
+
`Platform config event [${options.category}] ${options.title}`,
|
|
83
|
+
);
|
|
84
|
+
}
|
|
85
|
+
return event.toApiObject();
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
public async listEvents(
|
|
89
|
+
filter: IOpsConfigEventFilter = {},
|
|
90
|
+
): Promise<{ events: IOpsConfigEvent[]; total: number; unacknowledgedCount: number; nextCursor?: IOpsConfigEventCursor }> {
|
|
91
|
+
const { events, total, nextCursor } = await OpsConfigEventDoc.findFiltered(filter);
|
|
92
|
+
const unacknowledgedCount = await OpsConfigEventDoc.countUnacknowledged();
|
|
93
|
+
return { events: events.map((doc) => doc.toApiObject()), total, unacknowledgedCount, nextCursor };
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
public async acknowledgeEvents(ids: string[], userId: string): Promise<number> {
|
|
97
|
+
return OpsConfigEventDoc.acknowledgeByIds(ids, userId);
|
|
98
|
+
}
|
|
99
|
+
}
|
package/ts/monitoring/index.ts
CHANGED
|
@@ -1 +1,2 @@
|
|
|
1
|
-
export * from './classes.metricsmanager.js';
|
|
1
|
+
export * from './classes.metricsmanager.js';
|
|
2
|
+
export * from './classes.ops-event-manager.js';
|
|
@@ -41,6 +41,7 @@ export class OpsServer {
|
|
|
41
41
|
private acmeConfigHandler!: handlers.AcmeConfigHandler;
|
|
42
42
|
private emailDomainHandler!: handlers.EmailDomainHandler;
|
|
43
43
|
private gatewayClientHandler!: handlers.GatewayClientHandler;
|
|
44
|
+
private configEventHandler!: handlers.ConfigEventHandler;
|
|
44
45
|
private smtpRealtimeUnsubscribe?: () => void;
|
|
45
46
|
private emailQueueRealtimeUnsubscribe?: () => void;
|
|
46
47
|
|
|
@@ -105,6 +106,7 @@ export class OpsServer {
|
|
|
105
106
|
this.acmeConfigHandler = new handlers.AcmeConfigHandler(this);
|
|
106
107
|
this.emailDomainHandler = new handlers.EmailDomainHandler(this);
|
|
107
108
|
this.gatewayClientHandler = new handlers.GatewayClientHandler(this);
|
|
109
|
+
this.configEventHandler = new handlers.ConfigEventHandler(this);
|
|
108
110
|
this.smtpRealtimeUnsubscribe =
|
|
109
111
|
this.dcRouterRef.acceptedEmailSpool?.onSmtpTransactionPersisted?.((eventArg) => {
|
|
110
112
|
this.invalidateRealtime(['smtpAttempts', 'emailQueue'], {
|
|
@@ -4,6 +4,12 @@ import * as interfaces from '../../../ts_interfaces/index.js';
|
|
|
4
4
|
import { AcmeCertDoc, ProxyCertDoc } from '../../db/index.js';
|
|
5
5
|
import { logger } from '../../logger.js';
|
|
6
6
|
import { requireOpsAuth } from '../helpers/auth.js';
|
|
7
|
+
import {
|
|
8
|
+
hasOwnedEnabledGatewayRoute,
|
|
9
|
+
matchesGatewayHostnamePatterns,
|
|
10
|
+
normalizeGatewayHostname,
|
|
11
|
+
requireGatewayMachineAuth,
|
|
12
|
+
} from '../helpers/gateway-client-auth.js';
|
|
7
13
|
|
|
8
14
|
/**
|
|
9
15
|
* Mirrors `SmartacmeCertMatcher.getCertificateDomainNameByDomainName` from
|
|
@@ -45,6 +51,40 @@ export class CertificateHandler {
|
|
|
45
51
|
return auth.userId;
|
|
46
52
|
}
|
|
47
53
|
|
|
54
|
+
private async requireCertificateAuth(
|
|
55
|
+
requestArg: { identity?: interfaces.data.IIdentity; apiToken?: string },
|
|
56
|
+
domainArg: string,
|
|
57
|
+
capabilityArg: 'readCertificates' | 'requestCertificates',
|
|
58
|
+
): Promise<{ userId: string; domain: string }> {
|
|
59
|
+
const tokenManager = this.opsServerRef.dcRouterRef.apiTokenManager;
|
|
60
|
+
const candidate = requestArg.apiToken && tokenManager
|
|
61
|
+
? await tokenManager.validateToken(requestArg.apiToken)
|
|
62
|
+
: null;
|
|
63
|
+
if (candidate?.policy?.role === 'gatewayClient') {
|
|
64
|
+
const auth = await requireGatewayMachineAuth(
|
|
65
|
+
this.opsServerRef,
|
|
66
|
+
requestArg,
|
|
67
|
+
capabilityArg === 'readCertificates' ? 'gateway-clients:read' : 'gateway-clients:write',
|
|
68
|
+
);
|
|
69
|
+
if (auth.policy?.capabilities?.[capabilityArg] !== true) {
|
|
70
|
+
throw new plugins.typedrequest.TypedResponseError(`token capability missing: ${capabilityArg}`);
|
|
71
|
+
}
|
|
72
|
+
const domain = normalizeGatewayHostname(domainArg);
|
|
73
|
+
if (!matchesGatewayHostnamePatterns(domain, auth.policy.hostnamePatterns || [], auth.managedZoneNames)) {
|
|
74
|
+
throw new plugins.typedrequest.TypedResponseError('certificate hostname is outside gateway-client policy');
|
|
75
|
+
}
|
|
76
|
+
if (!hasOwnedEnabledGatewayRoute(this.opsServerRef, auth, domain)) {
|
|
77
|
+
throw new plugins.typedrequest.TypedResponseError('certificate hostname has no owned enabled gateway route');
|
|
78
|
+
}
|
|
79
|
+
return { userId: auth.userId, domain };
|
|
80
|
+
}
|
|
81
|
+
const userId = await this.requireAuth(
|
|
82
|
+
requestArg,
|
|
83
|
+
capabilityArg === 'readCertificates' ? 'certificates:read' : 'certificates:write',
|
|
84
|
+
);
|
|
85
|
+
return { userId, domain: normalizeGatewayHostname(domainArg) };
|
|
86
|
+
}
|
|
87
|
+
|
|
48
88
|
private registerHandlers(): void {
|
|
49
89
|
const router = this.typedrouter;
|
|
50
90
|
|
|
@@ -66,8 +106,8 @@ export class CertificateHandler {
|
|
|
66
106
|
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_ReprovisionCertificateDomain>(
|
|
67
107
|
'reprovisionCertificateDomain',
|
|
68
108
|
async (dataArg) => {
|
|
69
|
-
await this.
|
|
70
|
-
const result = await this.reprovisionCertificateDomain(
|
|
109
|
+
const auth = await this.requireCertificateAuth(dataArg, dataArg.domain, 'requestCertificates');
|
|
110
|
+
const result = await this.reprovisionCertificateDomain(auth.domain, dataArg.forceRenew);
|
|
71
111
|
if (result.success) {
|
|
72
112
|
this.opsServerRef.invalidateRealtime?.('certificates', {
|
|
73
113
|
changedIds: [dataArg.domain],
|
|
@@ -102,8 +142,8 @@ export class CertificateHandler {
|
|
|
102
142
|
new plugins.typedrequest.TypedHandler<plugins.servezoneInterfaces.requests.gateway.IReq_ExportCertificate>(
|
|
103
143
|
'exportCertificate',
|
|
104
144
|
async (dataArg) => {
|
|
105
|
-
await this.
|
|
106
|
-
return this.exportCertificate(
|
|
145
|
+
const auth = await this.requireCertificateAuth(dataArg, dataArg.domain, 'readCertificates');
|
|
146
|
+
return this.exportCertificate(auth.domain);
|
|
107
147
|
}
|
|
108
148
|
)
|
|
109
149
|
);
|
|
@@ -113,6 +153,13 @@ export class CertificateHandler {
|
|
|
113
153
|
new plugins.typedrequest.TypedHandler<plugins.servezoneInterfaces.requests.gateway.IReq_ImportCertificate>(
|
|
114
154
|
'importCertificate',
|
|
115
155
|
async (dataArg) => {
|
|
156
|
+
const tokenManager = this.opsServerRef.dcRouterRef.apiTokenManager;
|
|
157
|
+
const candidate = dataArg.apiToken && tokenManager
|
|
158
|
+
? await tokenManager.validateToken(dataArg.apiToken)
|
|
159
|
+
: null;
|
|
160
|
+
if (candidate?.policy?.role === 'gatewayClient') {
|
|
161
|
+
throw new plugins.typedrequest.TypedResponseError('gateway-client credentials cannot import certificates');
|
|
162
|
+
}
|
|
116
163
|
await this.requireAuth(dataArg, 'certificates:write');
|
|
117
164
|
const result = await this.importCertificate(dataArg.cert);
|
|
118
165
|
if (result.success) {
|
|
@@ -0,0 +1,62 @@
|
|
|
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
|
+
/**
|
|
7
|
+
* Handlers for the platform configuration event inbox (list + acknowledge).
|
|
8
|
+
*/
|
|
9
|
+
export class ConfigEventHandler {
|
|
10
|
+
public typedrouter = new plugins.typedrequest.TypedRouter();
|
|
11
|
+
|
|
12
|
+
constructor(private opsServerRef: OpsServer) {
|
|
13
|
+
this.opsServerRef.typedrouter.addTypedRouter(this.typedrouter);
|
|
14
|
+
this.registerHandlers();
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
private registerHandlers(): void {
|
|
18
|
+
this.typedrouter.addTypedHandler(
|
|
19
|
+
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_ListConfigEvents>(
|
|
20
|
+
'listConfigEvents',
|
|
21
|
+
async (dataArg) => {
|
|
22
|
+
await requireOpsAuth(this.opsServerRef, dataArg, { scope: 'config-events:read' });
|
|
23
|
+
const opsEventManager = this.opsServerRef.dcRouterRef.opsEventManager;
|
|
24
|
+
if (!opsEventManager) {
|
|
25
|
+
return { events: [], total: 0, unacknowledgedCount: 0, nextCursor: undefined };
|
|
26
|
+
}
|
|
27
|
+
return opsEventManager.listEvents({
|
|
28
|
+
acknowledged: dataArg.acknowledged,
|
|
29
|
+
severity: dataArg.severity,
|
|
30
|
+
category: dataArg.category,
|
|
31
|
+
limit: dataArg.limit,
|
|
32
|
+
cursor: dataArg.cursor,
|
|
33
|
+
});
|
|
34
|
+
},
|
|
35
|
+
),
|
|
36
|
+
);
|
|
37
|
+
|
|
38
|
+
this.typedrouter.addTypedHandler(
|
|
39
|
+
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_AcknowledgeConfigEvents>(
|
|
40
|
+
'acknowledgeConfigEvents',
|
|
41
|
+
async (dataArg) => {
|
|
42
|
+
const auth = await requireOpsAuth(this.opsServerRef, dataArg, {
|
|
43
|
+
scope: 'config-events:write',
|
|
44
|
+
requireAdminIdentity: true,
|
|
45
|
+
});
|
|
46
|
+
const opsEventManager = this.opsServerRef.dcRouterRef.opsEventManager;
|
|
47
|
+
if (!opsEventManager) {
|
|
48
|
+
return { success: false, acknowledgedCount: 0, message: 'OpsEventManager not initialized' };
|
|
49
|
+
}
|
|
50
|
+
const acknowledgedCount = await opsEventManager.acknowledgeEvents(
|
|
51
|
+
dataArg.ids || [],
|
|
52
|
+
auth.userId,
|
|
53
|
+
);
|
|
54
|
+
this.opsServerRef.invalidateRealtime?.(['configEvents'], {
|
|
55
|
+
reason: 'config-events-acknowledged',
|
|
56
|
+
});
|
|
57
|
+
return { success: true, acknowledgedCount };
|
|
58
|
+
},
|
|
59
|
+
),
|
|
60
|
+
);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
import * as plugins from '../../plugins.js';
|
|
2
2
|
import type { OpsServer } from '../classes.opsserver.js';
|
|
3
3
|
import * as interfaces from '../../../ts_interfaces/index.js';
|
|
4
|
+
import { logger } from '../../logger.js';
|
|
4
5
|
import { requireOpsAuth } from '../helpers/auth.js';
|
|
6
|
+
import { normalizeGatewayHostname } from '../helpers/gateway-client-auth.js';
|
|
7
|
+
import { getActiveGatewayDnsClaims } from '../../dns/classes.gateway-route-dns-reconciler.js';
|
|
5
8
|
|
|
6
9
|
/**
|
|
7
10
|
* CRUD handlers for DnsRecordDoc.
|
|
@@ -62,6 +65,14 @@ export class DnsRecordHandler {
|
|
|
62
65
|
const userId = await this.requireAuth(dataArg, 'dns-records:write');
|
|
63
66
|
const dnsManager = this.opsServerRef.dcRouterRef.dnsManager;
|
|
64
67
|
if (!dnsManager) return { success: false, message: 'DnsManager not initialized' };
|
|
68
|
+
const claimError = await this.rejectClaimedHostnameMutation(
|
|
69
|
+
dataArg.domainId,
|
|
70
|
+
dataArg.name,
|
|
71
|
+
dataArg.type,
|
|
72
|
+
userId,
|
|
73
|
+
'create',
|
|
74
|
+
);
|
|
75
|
+
if (claimError) return { success: false, message: claimError };
|
|
65
76
|
const result = await dnsManager.createRecord({
|
|
66
77
|
domainId: dataArg.domainId,
|
|
67
78
|
name: dataArg.name,
|
|
@@ -87,9 +98,30 @@ export class DnsRecordHandler {
|
|
|
87
98
|
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_UpdateDnsRecord>(
|
|
88
99
|
'updateDnsRecord',
|
|
89
100
|
async (dataArg) => {
|
|
90
|
-
await this.requireAuth(dataArg, 'dns-records:write');
|
|
101
|
+
const userId = await this.requireAuth(dataArg, 'dns-records:write');
|
|
91
102
|
const dnsManager = this.opsServerRef.dcRouterRef.dnsManager;
|
|
92
103
|
if (!dnsManager) return { success: false, message: 'DnsManager not initialized' };
|
|
104
|
+
const existing = await dnsManager.getRecord(dataArg.id);
|
|
105
|
+
if (existing) {
|
|
106
|
+
const currentClaimError = await this.rejectClaimedHostnameMutation(
|
|
107
|
+
existing.domainId,
|
|
108
|
+
existing.name,
|
|
109
|
+
existing.type,
|
|
110
|
+
userId,
|
|
111
|
+
'update',
|
|
112
|
+
);
|
|
113
|
+
if (currentClaimError) return { success: false, message: currentClaimError };
|
|
114
|
+
if (dataArg.name !== undefined) {
|
|
115
|
+
const nextClaimError = await this.rejectClaimedHostnameMutation(
|
|
116
|
+
existing.domainId,
|
|
117
|
+
dataArg.name,
|
|
118
|
+
existing.type,
|
|
119
|
+
userId,
|
|
120
|
+
'update',
|
|
121
|
+
);
|
|
122
|
+
if (nextClaimError) return { success: false, message: nextClaimError };
|
|
123
|
+
}
|
|
124
|
+
}
|
|
93
125
|
const result = await dnsManager.updateRecord({
|
|
94
126
|
id: dataArg.id,
|
|
95
127
|
name: dataArg.name,
|
|
@@ -113,9 +145,20 @@ export class DnsRecordHandler {
|
|
|
113
145
|
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_DeleteDnsRecord>(
|
|
114
146
|
'deleteDnsRecord',
|
|
115
147
|
async (dataArg) => {
|
|
116
|
-
await this.requireAuth(dataArg, 'dns-records:write');
|
|
148
|
+
const userId = await this.requireAuth(dataArg, 'dns-records:write');
|
|
117
149
|
const dnsManager = this.opsServerRef.dcRouterRef.dnsManager;
|
|
118
150
|
if (!dnsManager) return { success: false, message: 'DnsManager not initialized' };
|
|
151
|
+
const existing = await dnsManager.getRecord(dataArg.id);
|
|
152
|
+
if (existing) {
|
|
153
|
+
const claimError = await this.rejectClaimedHostnameMutation(
|
|
154
|
+
existing.domainId,
|
|
155
|
+
existing.name,
|
|
156
|
+
existing.type,
|
|
157
|
+
userId,
|
|
158
|
+
'delete',
|
|
159
|
+
);
|
|
160
|
+
if (claimError) return { success: false, message: claimError };
|
|
161
|
+
}
|
|
119
162
|
const result = await dnsManager.deleteRecord(dataArg.id);
|
|
120
163
|
if (result.success) {
|
|
121
164
|
this.opsServerRef.invalidateRealtime?.(['dns', 'emailDomains'], {
|
|
@@ -128,4 +171,60 @@ export class DnsRecordHandler {
|
|
|
128
171
|
),
|
|
129
172
|
);
|
|
130
173
|
}
|
|
174
|
+
|
|
175
|
+
private async rejectClaimedHostnameMutation(
|
|
176
|
+
domainIdArg: string,
|
|
177
|
+
hostnameArg: string,
|
|
178
|
+
recordTypeArg: interfaces.data.TDnsRecordType,
|
|
179
|
+
actorArg: string,
|
|
180
|
+
actionArg: 'create' | 'update' | 'delete',
|
|
181
|
+
): Promise<string | undefined> {
|
|
182
|
+
const dnsManager = this.opsServerRef.dcRouterRef.dnsManager;
|
|
183
|
+
if (!dnsManager) return undefined;
|
|
184
|
+
if (recordTypeArg !== 'A' && recordTypeArg !== 'AAAA' && recordTypeArg !== 'CNAME') return undefined;
|
|
185
|
+
let hostname: string;
|
|
186
|
+
try {
|
|
187
|
+
hostname = normalizeGatewayHostname(hostnameArg);
|
|
188
|
+
} catch {
|
|
189
|
+
return undefined;
|
|
190
|
+
}
|
|
191
|
+
const claims = getActiveGatewayDnsClaims(this.opsServerRef.dcRouterRef, hostname);
|
|
192
|
+
if (!claims.length) return undefined;
|
|
193
|
+
const ownerSummary = claims
|
|
194
|
+
.map((claimArg) => `${claimArg.gatewayClientId} (${claimArg.routeExternalKeys.join(', ')})`)
|
|
195
|
+
.join('; ');
|
|
196
|
+
const message = `DNS hostname ${hostname} is claimed by gateway client route(s) ${ownerSummary}; actor ${actorArg} cannot ${actionArg} ${recordTypeArg} records at this hostname`;
|
|
197
|
+
let domainName: string | undefined;
|
|
198
|
+
try {
|
|
199
|
+
domainName = (await dnsManager.getDomain(domainIdArg))?.name;
|
|
200
|
+
} catch (error) {
|
|
201
|
+
logger.log('warn', `Failed to load DNS domain context for rejected mutation on ${hostname}: ${(error as Error).message}`);
|
|
202
|
+
}
|
|
203
|
+
if (this.opsServerRef.dcRouterRef.opsEventManager) {
|
|
204
|
+
try {
|
|
205
|
+
await this.opsServerRef.dcRouterRef.opsEventManager.recordEvent({
|
|
206
|
+
severity: 'error',
|
|
207
|
+
category: 'dns-conflict',
|
|
208
|
+
title: 'Rejected manual DNS mutation on gateway-managed hostname',
|
|
209
|
+
detail: message,
|
|
210
|
+
context: {
|
|
211
|
+
domain: domainName,
|
|
212
|
+
recordName: hostname,
|
|
213
|
+
routeRef: claims.flatMap((claimArg) => claimArg.routeExternalKeys).join(', '),
|
|
214
|
+
actor: actorArg,
|
|
215
|
+
},
|
|
216
|
+
});
|
|
217
|
+
} catch (error) {
|
|
218
|
+
logger.log('warn', `Failed to record rejected gateway DNS mutation for ${hostname}: ${(error as Error).message}`);
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
try {
|
|
222
|
+
this.opsServerRef.invalidateRealtime?.('configEvents', {
|
|
223
|
+
reason: 'gateway-dns-manual-mutation-rejected',
|
|
224
|
+
});
|
|
225
|
+
} catch (error) {
|
|
226
|
+
logger.log('warn', `Failed to invalidate config events after rejected DNS mutation for ${hostname}: ${(error as Error).message}`);
|
|
227
|
+
}
|
|
228
|
+
return message;
|
|
229
|
+
}
|
|
131
230
|
}
|