@serve.zone/dcrouter 17.2.4 → 17.4.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 +11 -1
- package/dist_ts/classes.dcrouter.js +63 -3
- 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.cached.email.d.ts +10 -1
- package/dist_ts/db/documents/classes.cached.email.js +27 -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/email/classes.accepted-email-spool.d.ts +8 -1
- package/dist_ts/email/classes.accepted-email-spool.js +93 -1
- package/dist_ts/email/classes.smartmta-storage-manager.d.ts +7 -1
- package/dist_ts/email/classes.smartmta-storage-manager.js +46 -1
- package/dist_ts/email/smartmta-storage-policy.js +3 -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/email-ops.handler.d.ts +6 -0
- package/dist_ts/opsserver/handlers/email-ops.handler.js +40 -9
- 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 +4 -4
- package/readme.plan.md +22 -0
- package/ts/00_commitinfo_data.ts +1 -1
- package/ts/classes.dcrouter.ts +72 -2
- 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.cached.email.ts +21 -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/email/classes.accepted-email-spool.ts +111 -0
- package/ts/email/classes.smartmta-storage-manager.ts +57 -1
- package/ts/email/smartmta-storage-policy.ts +2 -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/email-ops.handler.ts +42 -8
- 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
|
@@ -1,12 +1,37 @@
|
|
|
1
1
|
import * as plugins from '../plugins.js';
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
GatewayClientDoc,
|
|
4
|
+
type IGatewayClientPendingProvisioning,
|
|
5
|
+
} from '../db/index.js';
|
|
3
6
|
import type { IGatewayClient } from '../../ts_interfaces/data/workhoster.js';
|
|
7
|
+
import type { IGatewayCredentialLifecycle } from '../../ts_interfaces/data/route-management.js';
|
|
8
|
+
|
|
9
|
+
export interface IStagedGatewayClientProvisioning {
|
|
10
|
+
action: plugins.servezoneInterfaces.data.TGatewayClientProvisioningAction;
|
|
11
|
+
gatewayClient: IGatewayClient;
|
|
12
|
+
credentialSequence: number;
|
|
13
|
+
policyDigest: string;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
interface INormalizedGatewayClientSpec {
|
|
17
|
+
type: IGatewayClient['type'];
|
|
18
|
+
name: string;
|
|
19
|
+
description: string | null;
|
|
20
|
+
hostnamePatterns: string[];
|
|
21
|
+
allowedRouteTargets: IGatewayClient['allowedRouteTargets'];
|
|
22
|
+
capabilities: IGatewayClient['capabilities'];
|
|
23
|
+
enabled: boolean;
|
|
24
|
+
}
|
|
4
25
|
|
|
5
26
|
const defaultCapabilities: IGatewayClient['capabilities'] = {
|
|
6
27
|
readDomains: true,
|
|
7
28
|
readDnsRecords: true,
|
|
29
|
+
readRoutes: true,
|
|
8
30
|
syncRoutes: true,
|
|
9
31
|
syncDnsRecords: false,
|
|
32
|
+
readMail: false,
|
|
33
|
+
manageMail: false,
|
|
34
|
+
readCertificates: false,
|
|
10
35
|
requestCertificates: false,
|
|
11
36
|
};
|
|
12
37
|
|
|
@@ -32,6 +57,7 @@ export class GatewayClientManager {
|
|
|
32
57
|
allowedRouteTargets?: IGatewayClient['allowedRouteTargets'];
|
|
33
58
|
capabilities?: IGatewayClient['capabilities'];
|
|
34
59
|
createdBy: string;
|
|
60
|
+
applyCapabilityDefaults?: boolean;
|
|
35
61
|
}): Promise<IGatewayClient> {
|
|
36
62
|
const id = this.normalizeId(options.id || `${options.type}-${plugins.uuid.v4()}`);
|
|
37
63
|
if (!id) {
|
|
@@ -47,10 +73,17 @@ export class GatewayClientManager {
|
|
|
47
73
|
doc.type = options.type;
|
|
48
74
|
doc.name = options.name.trim();
|
|
49
75
|
doc.description = options.description?.trim() || undefined;
|
|
50
|
-
doc.hostnamePatterns = this.
|
|
76
|
+
doc.hostnamePatterns = this.normalizeHostnamePatterns(options.hostnamePatterns || []);
|
|
51
77
|
doc.allowedRouteTargets = this.normalizeAllowedRouteTargets(options.allowedRouteTargets || []);
|
|
52
|
-
doc.capabilities =
|
|
78
|
+
doc.capabilities = options.applyCapabilityDefaults === false
|
|
79
|
+
? { ...(options.capabilities || {}) }
|
|
80
|
+
: { ...defaultCapabilities, ...(options.capabilities || {}) };
|
|
53
81
|
doc.enabled = true;
|
|
82
|
+
doc.policyGeneration = 1;
|
|
83
|
+
doc.credentialSequence = 0;
|
|
84
|
+
doc.activeCredentialSequence = 0;
|
|
85
|
+
doc.mutationRevision = 1;
|
|
86
|
+
doc.pendingProvisioning = null;
|
|
54
87
|
doc.createdAt = now;
|
|
55
88
|
doc.updatedAt = now;
|
|
56
89
|
doc.createdBy = options.createdBy;
|
|
@@ -61,42 +94,452 @@ export class GatewayClientManager {
|
|
|
61
94
|
public async updateClient(
|
|
62
95
|
id: string,
|
|
63
96
|
patch: Partial<Pick<IGatewayClient, 'name' | 'description' | 'hostnamePatterns' | 'allowedRouteTargets' | 'capabilities' | 'enabled'>>,
|
|
97
|
+
options: { applyCapabilityDefaults?: boolean } = {},
|
|
64
98
|
): Promise<IGatewayClient | null> {
|
|
65
99
|
const doc = await GatewayClientDoc.findById(id);
|
|
66
100
|
if (!doc) return null;
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
101
|
+
const expectedRevision = this.normalizeMutationRevision(doc.mutationRevision);
|
|
102
|
+
const priorPolicy = JSON.stringify({
|
|
103
|
+
hostnamePatterns: doc.hostnamePatterns || [],
|
|
104
|
+
allowedRouteTargets: doc.allowedRouteTargets || [],
|
|
105
|
+
capabilities: doc.capabilities || {},
|
|
106
|
+
enabled: doc.enabled,
|
|
107
|
+
});
|
|
108
|
+
const nextName = patch.name !== undefined ? patch.name.trim() : doc.name;
|
|
109
|
+
const nextDescription = patch.description !== undefined
|
|
110
|
+
? patch.description.trim() || undefined
|
|
111
|
+
: doc.description;
|
|
112
|
+
const nextHostnamePatterns = patch.hostnamePatterns !== undefined
|
|
113
|
+
? this.normalizeHostnamePatterns(patch.hostnamePatterns)
|
|
114
|
+
: doc.hostnamePatterns || [];
|
|
115
|
+
const nextAllowedRouteTargets = patch.allowedRouteTargets !== undefined
|
|
116
|
+
? this.normalizeAllowedRouteTargets(patch.allowedRouteTargets)
|
|
117
|
+
: doc.allowedRouteTargets || [];
|
|
118
|
+
const nextCapabilities = patch.capabilities !== undefined
|
|
119
|
+
? options.applyCapabilityDefaults === false
|
|
120
|
+
? { ...patch.capabilities }
|
|
121
|
+
: { ...defaultCapabilities, ...patch.capabilities }
|
|
122
|
+
: doc.capabilities || {};
|
|
123
|
+
const nextEnabled = patch.enabled !== undefined ? patch.enabled : doc.enabled;
|
|
124
|
+
const nextPolicy = JSON.stringify({
|
|
125
|
+
hostnamePatterns: nextHostnamePatterns,
|
|
126
|
+
allowedRouteTargets: nextAllowedRouteTargets,
|
|
127
|
+
capabilities: nextCapabilities,
|
|
128
|
+
enabled: nextEnabled,
|
|
129
|
+
});
|
|
130
|
+
const policyGeneration = this.normalizePolicyGeneration(doc.policyGeneration)
|
|
131
|
+
+ (priorPolicy === nextPolicy ? 0 : 1);
|
|
132
|
+
const updatedAt = Date.now();
|
|
133
|
+
const updated = await GatewayClientDoc.compareAndSet(id, expectedRevision, {
|
|
134
|
+
name: nextName,
|
|
135
|
+
hostnamePatterns: structuredClone(nextHostnamePatterns),
|
|
136
|
+
allowedRouteTargets: structuredClone(nextAllowedRouteTargets),
|
|
137
|
+
capabilities: structuredClone(nextCapabilities),
|
|
138
|
+
enabled: nextEnabled,
|
|
139
|
+
policyGeneration,
|
|
140
|
+
pendingProvisioning: null,
|
|
141
|
+
updatedAt,
|
|
142
|
+
...(nextDescription ? { description: nextDescription } : {}),
|
|
143
|
+
}, nextDescription ? [] : ['description']);
|
|
144
|
+
if (!updated) {
|
|
145
|
+
if (await GatewayClientDoc.findById(id)) throw new Error('Gateway client changed concurrently; retry the update');
|
|
146
|
+
return null;
|
|
147
|
+
}
|
|
148
|
+
return this.toPublicClient(updated);
|
|
76
149
|
}
|
|
77
150
|
|
|
78
151
|
public async deleteClient(id: string): Promise<boolean> {
|
|
79
152
|
const doc = await GatewayClientDoc.findById(id);
|
|
80
153
|
if (!doc) return false;
|
|
81
|
-
await
|
|
82
|
-
|
|
154
|
+
const deleted = await GatewayClientDoc.deleteIfRevision(
|
|
155
|
+
id,
|
|
156
|
+
this.normalizeMutationRevision(doc.mutationRevision),
|
|
157
|
+
);
|
|
158
|
+
if (!deleted && await GatewayClientDoc.findById(id)) {
|
|
159
|
+
throw new Error('Gateway client changed concurrently; retry the deletion');
|
|
160
|
+
}
|
|
161
|
+
return deleted;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
public async upsertClient(
|
|
165
|
+
spec: plugins.servezoneInterfaces.data.IGatewayClientProvisioningSpec,
|
|
166
|
+
createdBy: string,
|
|
167
|
+
): Promise<{
|
|
168
|
+
action: plugins.servezoneInterfaces.data.TGatewayClientProvisioningAction;
|
|
169
|
+
gatewayClient: IGatewayClient;
|
|
170
|
+
}> {
|
|
171
|
+
const id = this.normalizeId(spec.id);
|
|
172
|
+
if (!id) throw new Error('gateway client id is required');
|
|
173
|
+
const normalized = {
|
|
174
|
+
type: spec.type,
|
|
175
|
+
name: spec.name.trim(),
|
|
176
|
+
description: spec.description?.trim() || undefined,
|
|
177
|
+
hostnamePatterns: this.normalizeHostnamePatterns(spec.hostnamePatterns || []),
|
|
178
|
+
allowedRouteTargets: this.normalizeAllowedRouteTargets(spec.allowedRouteTargets || []),
|
|
179
|
+
capabilities: { ...(spec.capabilities || {}) },
|
|
180
|
+
enabled: spec.enabled ?? true,
|
|
181
|
+
};
|
|
182
|
+
if (!normalized.name) throw new Error('gateway client name is required');
|
|
183
|
+
|
|
184
|
+
const existing = await GatewayClientDoc.findById(id);
|
|
185
|
+
if (!existing) {
|
|
186
|
+
const gatewayClient = await this.createClient({
|
|
187
|
+
id,
|
|
188
|
+
type: normalized.type,
|
|
189
|
+
name: normalized.name,
|
|
190
|
+
description: normalized.description,
|
|
191
|
+
hostnamePatterns: normalized.hostnamePatterns,
|
|
192
|
+
allowedRouteTargets: normalized.allowedRouteTargets,
|
|
193
|
+
capabilities: normalized.capabilities,
|
|
194
|
+
createdBy,
|
|
195
|
+
applyCapabilityDefaults: false,
|
|
196
|
+
});
|
|
197
|
+
if (!normalized.enabled) {
|
|
198
|
+
return {
|
|
199
|
+
action: 'created',
|
|
200
|
+
gatewayClient: (await this.updateClient(id, { enabled: false }))!,
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
return { action: 'created', gatewayClient };
|
|
204
|
+
}
|
|
205
|
+
if (existing.type !== normalized.type) {
|
|
206
|
+
throw new Error('gateway client type cannot be changed');
|
|
207
|
+
}
|
|
208
|
+
const current = {
|
|
209
|
+
type: existing.type,
|
|
210
|
+
name: existing.name,
|
|
211
|
+
description: existing.description?.trim() || undefined,
|
|
212
|
+
hostnamePatterns: this.normalizeHostnamePatterns(existing.hostnamePatterns || []),
|
|
213
|
+
allowedRouteTargets: this.normalizeAllowedRouteTargets(existing.allowedRouteTargets || []),
|
|
214
|
+
capabilities: { ...(existing.capabilities || {}) },
|
|
215
|
+
enabled: existing.enabled,
|
|
216
|
+
};
|
|
217
|
+
if (JSON.stringify(current) === JSON.stringify(normalized)) {
|
|
218
|
+
return { action: 'unchanged', gatewayClient: this.toPublicClient(existing) };
|
|
219
|
+
}
|
|
220
|
+
const gatewayClient = await this.updateClient(id, normalized, { applyCapabilityDefaults: false });
|
|
221
|
+
if (!gatewayClient) throw new Error('gateway client disappeared during provisioning');
|
|
222
|
+
return { action: 'updated', gatewayClient };
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/**
|
|
226
|
+
* Stage an idempotent provisioning policy without changing the live policy.
|
|
227
|
+
* Sequence gaps after a later credential write failure are intentional.
|
|
228
|
+
*/
|
|
229
|
+
public async stageClientProvisioning(
|
|
230
|
+
spec: plugins.servezoneInterfaces.data.IGatewayClientProvisioningSpec,
|
|
231
|
+
stagedBy: string,
|
|
232
|
+
): Promise<IStagedGatewayClientProvisioning> {
|
|
233
|
+
const id = this.normalizeId(spec.id);
|
|
234
|
+
if (!id) throw new Error('gateway client id is required');
|
|
235
|
+
const normalized = this.normalizeProvisioningSpec(spec);
|
|
236
|
+
const existing = await GatewayClientDoc.findById(id);
|
|
237
|
+
const now = Date.now();
|
|
238
|
+
|
|
239
|
+
if (!existing) {
|
|
240
|
+
const doc = new GatewayClientDoc();
|
|
241
|
+
doc.id = id;
|
|
242
|
+
doc.type = normalized.type;
|
|
243
|
+
doc.name = normalized.name;
|
|
244
|
+
doc.description = normalized.description || undefined;
|
|
245
|
+
doc.hostnamePatterns = normalized.hostnamePatterns;
|
|
246
|
+
doc.allowedRouteTargets = normalized.allowedRouteTargets;
|
|
247
|
+
doc.capabilities = normalized.capabilities;
|
|
248
|
+
doc.enabled = normalized.enabled;
|
|
249
|
+
doc.policyGeneration = 1;
|
|
250
|
+
doc.credentialSequence = 1;
|
|
251
|
+
doc.activeCredentialSequence = 0;
|
|
252
|
+
doc.mutationRevision = 1;
|
|
253
|
+
doc.createdAt = now;
|
|
254
|
+
doc.updatedAt = now;
|
|
255
|
+
doc.createdBy = stagedBy;
|
|
256
|
+
const policyDigest = this.digestPolicy(id, normalized, 1);
|
|
257
|
+
doc.pendingProvisioning = {
|
|
258
|
+
...structuredClone(normalized),
|
|
259
|
+
policyGeneration: 1,
|
|
260
|
+
policyDigest,
|
|
261
|
+
credentialSequence: 1,
|
|
262
|
+
stagedAt: now,
|
|
263
|
+
stagedBy,
|
|
264
|
+
};
|
|
265
|
+
await doc.save();
|
|
266
|
+
return {
|
|
267
|
+
action: 'created',
|
|
268
|
+
gatewayClient: this.toPublicPendingClient(doc, doc.pendingProvisioning),
|
|
269
|
+
credentialSequence: 1,
|
|
270
|
+
policyDigest,
|
|
271
|
+
};
|
|
272
|
+
}
|
|
273
|
+
if (existing.type !== normalized.type) {
|
|
274
|
+
throw new Error('gateway client type cannot be changed');
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
const active = this.activePolicyFromDoc(existing);
|
|
278
|
+
const activeGeneration = this.normalizePolicyGeneration(existing.policyGeneration);
|
|
279
|
+
const credentialSequence = this.normalizeCredentialSequence(existing.credentialSequence) + 1;
|
|
280
|
+
const policyUnchanged = this.digestPolicy(id, active, activeGeneration)
|
|
281
|
+
=== this.digestPolicy(id, normalized, activeGeneration);
|
|
282
|
+
const metadataChanged = active.name !== normalized.name
|
|
283
|
+
|| active.description !== normalized.description;
|
|
284
|
+
const policyGeneration = policyUnchanged ? activeGeneration : activeGeneration + 1;
|
|
285
|
+
const policyDigest = this.digestPolicy(id, normalized, policyGeneration);
|
|
286
|
+
const pendingProvisioning: IGatewayClientPendingProvisioning = {
|
|
287
|
+
...structuredClone(normalized),
|
|
288
|
+
policyGeneration,
|
|
289
|
+
policyDigest,
|
|
290
|
+
credentialSequence,
|
|
291
|
+
stagedAt: now,
|
|
292
|
+
stagedBy,
|
|
293
|
+
};
|
|
294
|
+
const set: Record<string, unknown> = {
|
|
295
|
+
credentialSequence,
|
|
296
|
+
pendingProvisioning,
|
|
297
|
+
updatedAt: now,
|
|
298
|
+
};
|
|
299
|
+
const unset: string[] = [];
|
|
300
|
+
if (policyUnchanged) {
|
|
301
|
+
set.name = normalized.name;
|
|
302
|
+
if (normalized.description) {
|
|
303
|
+
set.description = normalized.description;
|
|
304
|
+
} else {
|
|
305
|
+
unset.push('description');
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
const updated = await GatewayClientDoc.compareAndSet(
|
|
309
|
+
id,
|
|
310
|
+
this.normalizeMutationRevision(existing.mutationRevision),
|
|
311
|
+
set,
|
|
312
|
+
unset,
|
|
313
|
+
);
|
|
314
|
+
if (!updated) throw new Error('Gateway client changed concurrently; retry provisioning');
|
|
315
|
+
|
|
316
|
+
return {
|
|
317
|
+
action: policyUnchanged && !metadataChanged ? 'unchanged' : 'updated',
|
|
318
|
+
gatewayClient: this.toPublicPendingClient(updated, updated.pendingProvisioning!),
|
|
319
|
+
credentialSequence,
|
|
320
|
+
policyDigest,
|
|
321
|
+
};
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
/** Resolve the exact durable policy a provisioned credential was issued for. */
|
|
325
|
+
public async getClientForProvisionedCredential(
|
|
326
|
+
idArg: string,
|
|
327
|
+
typeArg: IGatewayClient['type'],
|
|
328
|
+
lifecycleArg: IGatewayCredentialLifecycle,
|
|
329
|
+
): Promise<IGatewayClient | null> {
|
|
330
|
+
const doc = await GatewayClientDoc.findById(idArg);
|
|
331
|
+
if (!doc || doc.type !== typeArg) return null;
|
|
332
|
+
const activeGeneration = this.normalizePolicyGeneration(doc.policyGeneration);
|
|
333
|
+
if (
|
|
334
|
+
lifecycleArg.state === 'active'
|
|
335
|
+
&& lifecycleArg.sequence === this.normalizeCredentialSequence(doc.activeCredentialSequence)
|
|
336
|
+
&& lifecycleArg.policyGeneration === activeGeneration
|
|
337
|
+
&& lifecycleArg.policyDigest === this.digestPolicy(doc.id, this.activePolicyFromDoc(doc), activeGeneration)
|
|
338
|
+
) {
|
|
339
|
+
return this.toPublicClient(doc);
|
|
340
|
+
}
|
|
341
|
+
const pending = doc.pendingProvisioning;
|
|
342
|
+
if (
|
|
343
|
+
lifecycleArg.state === 'candidate'
|
|
344
|
+
&& pending
|
|
345
|
+
&& this.normalizeCredentialSequence(doc.credentialSequence) === lifecycleArg.sequence
|
|
346
|
+
&& pending.credentialSequence === lifecycleArg.sequence
|
|
347
|
+
&& pending.policyGeneration === lifecycleArg.policyGeneration
|
|
348
|
+
&& pending.policyDigest === lifecycleArg.policyDigest
|
|
349
|
+
) {
|
|
350
|
+
return this.toPublicPendingClient(doc, pending);
|
|
351
|
+
}
|
|
352
|
+
return null;
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
/** Promote only the pending policy owned by this exact credential sequence. */
|
|
356
|
+
public async promoteProvisionedPolicy(
|
|
357
|
+
idArg: string,
|
|
358
|
+
typeArg: IGatewayClient['type'],
|
|
359
|
+
lifecycleArg: IGatewayCredentialLifecycle,
|
|
360
|
+
): Promise<IGatewayClient> {
|
|
361
|
+
const doc = await GatewayClientDoc.findById(idArg);
|
|
362
|
+
if (!doc || doc.type !== typeArg) {
|
|
363
|
+
throw new Error('Gateway client is missing or has the wrong type');
|
|
364
|
+
}
|
|
365
|
+
const expectedRevision = this.normalizeMutationRevision(doc.mutationRevision);
|
|
366
|
+
const activeGeneration = this.normalizePolicyGeneration(doc.policyGeneration);
|
|
367
|
+
const activeDigest = this.digestPolicy(doc.id, this.activePolicyFromDoc(doc), activeGeneration);
|
|
368
|
+
if (
|
|
369
|
+
lifecycleArg.state === 'active'
|
|
370
|
+
&& lifecycleArg.sequence === this.normalizeCredentialSequence(doc.activeCredentialSequence)
|
|
371
|
+
&& lifecycleArg.policyGeneration === activeGeneration
|
|
372
|
+
&& lifecycleArg.policyDigest === activeDigest
|
|
373
|
+
) {
|
|
374
|
+
return this.toPublicClient(doc);
|
|
375
|
+
}
|
|
376
|
+
const pending = doc.pendingProvisioning;
|
|
377
|
+
if (
|
|
378
|
+
(lifecycleArg.state !== 'candidate' && lifecycleArg.state !== 'active')
|
|
379
|
+
|| !pending
|
|
380
|
+
|| this.normalizeCredentialSequence(doc.credentialSequence) !== lifecycleArg.sequence
|
|
381
|
+
|| pending.credentialSequence !== lifecycleArg.sequence
|
|
382
|
+
|| pending.policyGeneration !== lifecycleArg.policyGeneration
|
|
383
|
+
|| pending.policyDigest !== lifecycleArg.policyDigest
|
|
384
|
+
) {
|
|
385
|
+
throw new Error('Gateway credential policy was superseded before finalization');
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
const updated = await GatewayClientDoc.compareAndSet(idArg, expectedRevision, {
|
|
389
|
+
name: pending.name,
|
|
390
|
+
hostnamePatterns: structuredClone(pending.hostnamePatterns),
|
|
391
|
+
allowedRouteTargets: structuredClone(pending.allowedRouteTargets),
|
|
392
|
+
capabilities: structuredClone(pending.capabilities),
|
|
393
|
+
enabled: pending.enabled,
|
|
394
|
+
policyGeneration: pending.policyGeneration,
|
|
395
|
+
activeCredentialSequence: pending.credentialSequence,
|
|
396
|
+
pendingProvisioning: null,
|
|
397
|
+
updatedAt: Date.now(),
|
|
398
|
+
...(pending.description ? { description: pending.description } : {}),
|
|
399
|
+
}, pending.description ? [] : ['description']);
|
|
400
|
+
if (!updated) {
|
|
401
|
+
throw new Error('Gateway client changed concurrently; retry credential finalization');
|
|
402
|
+
}
|
|
403
|
+
return this.toPublicClient(updated);
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
public digestPublicClientPolicy(clientArg: IGatewayClient): string {
|
|
407
|
+
return this.digestPolicy(
|
|
408
|
+
clientArg.id,
|
|
409
|
+
{
|
|
410
|
+
type: clientArg.type,
|
|
411
|
+
name: clientArg.name,
|
|
412
|
+
description: clientArg.description || null,
|
|
413
|
+
hostnamePatterns: structuredClone(clientArg.hostnamePatterns),
|
|
414
|
+
allowedRouteTargets: structuredClone(clientArg.allowedRouteTargets),
|
|
415
|
+
capabilities: structuredClone(clientArg.capabilities),
|
|
416
|
+
enabled: clientArg.enabled,
|
|
417
|
+
},
|
|
418
|
+
clientArg.policyGeneration,
|
|
419
|
+
);
|
|
83
420
|
}
|
|
84
421
|
|
|
85
422
|
private normalizeId(id: string): string {
|
|
86
423
|
return id.trim().toLowerCase().replace(/[^a-z0-9._-]/g, '-').replace(/-+/g, '-').replace(/^-|-$/g, '');
|
|
87
424
|
}
|
|
88
425
|
|
|
89
|
-
private
|
|
90
|
-
return values.map((value) =>
|
|
426
|
+
private normalizeHostnamePatterns(values: string[]): string[] {
|
|
427
|
+
return Array.from(new Set(values.map((value) => this.normalizeHostnamePattern(value))));
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
private normalizeHostnamePattern(valueArg: string): string {
|
|
431
|
+
const value = valueArg.trim().toLowerCase().replace(/\.+$/, '');
|
|
432
|
+
if (value === '*') return value;
|
|
433
|
+
const wildcard = value.startsWith('*.');
|
|
434
|
+
const hostname = wildcard ? value.slice(2) : value;
|
|
435
|
+
const ascii = plugins.url.domainToASCII(hostname);
|
|
436
|
+
if (!ascii || ascii.includes('*')) throw new Error(`invalid hostname pattern: ${valueArg}`);
|
|
437
|
+
return wildcard ? `*.${ascii}` : ascii;
|
|
91
438
|
}
|
|
92
439
|
|
|
93
440
|
private normalizeAllowedRouteTargets(targets: IGatewayClient['allowedRouteTargets']): IGatewayClient['allowedRouteTargets'] {
|
|
94
441
|
return targets
|
|
95
442
|
.map((target) => ({
|
|
96
|
-
host: target.host
|
|
97
|
-
ports: target.ports.filter((port) => Number.isInteger(port) && port > 0 && port <= 65535),
|
|
443
|
+
host: this.normalizeTargetHost(target.host),
|
|
444
|
+
ports: Array.from(new Set(target.ports.filter((port) => Number.isInteger(port) && port > 0 && port <= 65535))).sort((a, b) => a - b),
|
|
445
|
+
...(target.allowAnyPort ? { allowAnyPort: true } : {}),
|
|
98
446
|
}))
|
|
99
|
-
.filter((target) => target.host && target.ports.length > 0);
|
|
447
|
+
.filter((target) => target.host && (target.allowAnyPort || target.ports.length > 0));
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
private normalizeTargetHost(valueArg: string): string {
|
|
451
|
+
const value = valueArg.trim().toLowerCase().replace(/\.+$/, '');
|
|
452
|
+
if (plugins.net.isIP(value)) return value;
|
|
453
|
+
const ascii = plugins.url.domainToASCII(value);
|
|
454
|
+
if (!ascii) throw new Error(`invalid route target host: ${valueArg}`);
|
|
455
|
+
return ascii;
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
private normalizeProvisioningSpec(
|
|
459
|
+
specArg: plugins.servezoneInterfaces.data.IGatewayClientProvisioningSpec,
|
|
460
|
+
): INormalizedGatewayClientSpec {
|
|
461
|
+
const normalized: INormalizedGatewayClientSpec = {
|
|
462
|
+
type: specArg.type,
|
|
463
|
+
name: specArg.name.trim(),
|
|
464
|
+
description: specArg.description?.trim() || null,
|
|
465
|
+
hostnamePatterns: this.normalizeHostnamePatterns(specArg.hostnamePatterns || []),
|
|
466
|
+
allowedRouteTargets: this.normalizeAllowedRouteTargets(specArg.allowedRouteTargets || []),
|
|
467
|
+
capabilities: { ...(specArg.capabilities || {}) },
|
|
468
|
+
enabled: specArg.enabled ?? true,
|
|
469
|
+
};
|
|
470
|
+
if (!normalized.name) throw new Error('gateway client name is required');
|
|
471
|
+
return normalized;
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
private activePolicyFromDoc(docArg: GatewayClientDoc): INormalizedGatewayClientSpec {
|
|
475
|
+
return {
|
|
476
|
+
type: docArg.type,
|
|
477
|
+
name: docArg.name,
|
|
478
|
+
description: docArg.description?.trim() || null,
|
|
479
|
+
hostnamePatterns: this.normalizeHostnamePatterns(docArg.hostnamePatterns || []),
|
|
480
|
+
allowedRouteTargets: this.normalizeAllowedRouteTargets(docArg.allowedRouteTargets || []),
|
|
481
|
+
capabilities: { ...(docArg.capabilities || {}) },
|
|
482
|
+
enabled: docArg.enabled,
|
|
483
|
+
};
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
private digestPolicy(
|
|
487
|
+
idArg: string,
|
|
488
|
+
policyArg: INormalizedGatewayClientSpec,
|
|
489
|
+
generationArg: number,
|
|
490
|
+
): string {
|
|
491
|
+
const canonicalize = (valueArg: unknown): unknown => {
|
|
492
|
+
if (Array.isArray(valueArg)) return valueArg.map((entryArg) => canonicalize(entryArg));
|
|
493
|
+
if (valueArg && typeof valueArg === 'object') {
|
|
494
|
+
return Object.fromEntries(Object.keys(valueArg as Record<string, unknown>)
|
|
495
|
+
.sort((left, right) => left.localeCompare(right))
|
|
496
|
+
.map((keyArg) => [keyArg, canonicalize((valueArg as Record<string, unknown>)[keyArg])]));
|
|
497
|
+
}
|
|
498
|
+
return valueArg;
|
|
499
|
+
};
|
|
500
|
+
return plugins.crypto.createHash('sha256').update(JSON.stringify(canonicalize({
|
|
501
|
+
id: idArg,
|
|
502
|
+
generation: generationArg,
|
|
503
|
+
policy: {
|
|
504
|
+
type: policyArg.type,
|
|
505
|
+
hostnamePatterns: policyArg.hostnamePatterns,
|
|
506
|
+
allowedRouteTargets: policyArg.allowedRouteTargets,
|
|
507
|
+
capabilities: policyArg.capabilities,
|
|
508
|
+
enabled: policyArg.enabled,
|
|
509
|
+
},
|
|
510
|
+
}))).digest('hex');
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
private normalizeCredentialSequence(valueArg: unknown): number {
|
|
514
|
+
return Number.isSafeInteger(valueArg) && Number(valueArg) >= 0 ? Number(valueArg) : 0;
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
private normalizePolicyGeneration(valueArg: unknown): number {
|
|
518
|
+
return Number.isSafeInteger(valueArg) && Number(valueArg) >= 1 ? Number(valueArg) : 1;
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
private normalizeMutationRevision(valueArg: unknown): number {
|
|
522
|
+
return Number.isSafeInteger(valueArg) && Number(valueArg) >= 0 ? Number(valueArg) : 0;
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
private toPublicPendingClient(
|
|
526
|
+
docArg: GatewayClientDoc,
|
|
527
|
+
pendingArg: IGatewayClientPendingProvisioning,
|
|
528
|
+
): IGatewayClient {
|
|
529
|
+
return {
|
|
530
|
+
id: docArg.id,
|
|
531
|
+
type: pendingArg.type,
|
|
532
|
+
name: pendingArg.name,
|
|
533
|
+
description: pendingArg.description || undefined,
|
|
534
|
+
hostnamePatterns: structuredClone(pendingArg.hostnamePatterns),
|
|
535
|
+
allowedRouteTargets: structuredClone(pendingArg.allowedRouteTargets),
|
|
536
|
+
capabilities: structuredClone(pendingArg.capabilities),
|
|
537
|
+
enabled: pendingArg.enabled,
|
|
538
|
+
policyGeneration: pendingArg.policyGeneration,
|
|
539
|
+
createdAt: docArg.createdAt,
|
|
540
|
+
updatedAt: pendingArg.stagedAt,
|
|
541
|
+
createdBy: docArg.createdBy,
|
|
542
|
+
};
|
|
100
543
|
}
|
|
101
544
|
|
|
102
545
|
private toPublicClient(doc: GatewayClientDoc): IGatewayClient {
|
|
@@ -109,6 +552,7 @@ export class GatewayClientManager {
|
|
|
109
552
|
allowedRouteTargets: doc.allowedRouteTargets || [],
|
|
110
553
|
capabilities: doc.capabilities || {},
|
|
111
554
|
enabled: doc.enabled,
|
|
555
|
+
policyGeneration: this.normalizePolicyGeneration(doc.policyGeneration),
|
|
112
556
|
createdAt: doc.createdAt,
|
|
113
557
|
updatedAt: doc.updatedAt,
|
|
114
558
|
createdBy: doc.createdBy,
|
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
import * as plugins from '../../plugins.js';
|
|
2
2
|
import { DcRouterDb } from '../classes.dcrouter-db.js';
|
|
3
|
-
import type {
|
|
3
|
+
import type {
|
|
4
|
+
IApiTokenPolicy,
|
|
5
|
+
IGatewayCredentialLifecycle,
|
|
6
|
+
TApiTokenScope,
|
|
7
|
+
} from '../../../ts_interfaces/data/route-management.js';
|
|
4
8
|
|
|
5
9
|
const getDb = () => DcRouterDb.getInstance().getDb();
|
|
6
10
|
|
|
@@ -21,6 +25,9 @@ export class ApiTokenDoc extends plugins.smartdata.SmartDataDbDoc<ApiTokenDoc, A
|
|
|
21
25
|
|
|
22
26
|
@plugins.smartdata.svDb()
|
|
23
27
|
public policy?: IApiTokenPolicy;
|
|
28
|
+
@plugins.smartdata.svDb()
|
|
29
|
+
public gatewayCredentialLifecycle: IGatewayCredentialLifecycle | null = null;
|
|
30
|
+
|
|
24
31
|
|
|
25
32
|
@plugins.smartdata.svDb()
|
|
26
33
|
public createdAt!: number;
|
|
@@ -6,7 +6,7 @@ const TTL = plugins.smartdata.smartdataTtlValues;
|
|
|
6
6
|
/**
|
|
7
7
|
* Email status in the cache
|
|
8
8
|
*/
|
|
9
|
-
export type TCachedEmailStatus = 'pending' | 'processing' | 'queued' | 'delivered' | 'failed' | 'deferred';
|
|
9
|
+
export type TCachedEmailStatus = 'pending' | 'processing' | 'queued' | 'delivered' | 'failed' | 'deferred' | 'stored';
|
|
10
10
|
|
|
11
11
|
/**
|
|
12
12
|
* Direction of an accepted email: 'inbound' = received from a remote peer,
|
|
@@ -141,6 +141,14 @@ export class CachedEmail extends plugins.smartdata.SmartdataCachedDocument<Cache
|
|
|
141
141
|
@plugins.smartdata.svDb()
|
|
142
142
|
public status!: TCachedEmailStatus;
|
|
143
143
|
|
|
144
|
+
/** Attachment count derived from the parsed message at acceptance. */
|
|
145
|
+
@plugins.smartdata.svDb()
|
|
146
|
+
public attachmentCount?: number;
|
|
147
|
+
|
|
148
|
+
/** JSON-serialized inbound SPF/DKIM/DMARC verdicts from the SMTP session. */
|
|
149
|
+
@plugins.smartdata.svDb()
|
|
150
|
+
public inboundSecurityResults?: string;
|
|
151
|
+
|
|
144
152
|
/**
|
|
145
153
|
* Number of delivery attempts
|
|
146
154
|
*/
|
|
@@ -344,6 +352,18 @@ export class CachedEmail extends plugins.smartdata.SmartdataCachedDocument<Cache
|
|
|
344
352
|
this.deliveredAt = new Date();
|
|
345
353
|
}
|
|
346
354
|
|
|
355
|
+
/**
|
|
356
|
+
* Mark as terminally stored (catch-all inbound): visible in the email log,
|
|
357
|
+
* pruned after the retention window.
|
|
358
|
+
*/
|
|
359
|
+
public markStored(retentionMs: number): void {
|
|
360
|
+
this.status = 'stored';
|
|
361
|
+
this.deliveredAt = new Date();
|
|
362
|
+
// Base-class TTL: the CacheCleaner deletes expired rows and their raw
|
|
363
|
+
// payloads via beforeDeleteCachedEmail.
|
|
364
|
+
this.setTTL(retentionMs);
|
|
365
|
+
}
|
|
366
|
+
|
|
347
367
|
/**
|
|
348
368
|
* Mark as failed with error
|
|
349
369
|
*/
|