@serve.zone/dcrouter 17.2.3 → 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 +2219 -1887
- 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 +3 -3
- 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
|
@@ -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;
|
|
@@ -2,6 +2,21 @@ import * as plugins from '../../plugins.js';
|
|
|
2
2
|
import { DcRouterDb } from '../classes.dcrouter-db.js';
|
|
3
3
|
import type { IApiTokenPolicy, TGatewayClientType } from '../../../ts_interfaces/data/route-management.js';
|
|
4
4
|
|
|
5
|
+
export interface IGatewayClientPendingProvisioning {
|
|
6
|
+
type: TGatewayClientType;
|
|
7
|
+
name: string;
|
|
8
|
+
description: string | null;
|
|
9
|
+
hostnamePatterns: string[];
|
|
10
|
+
allowedRouteTargets: NonNullable<IApiTokenPolicy['allowedRouteTargets']>;
|
|
11
|
+
capabilities: NonNullable<IApiTokenPolicy['capabilities']>;
|
|
12
|
+
enabled: boolean;
|
|
13
|
+
policyGeneration: number;
|
|
14
|
+
policyDigest: string;
|
|
15
|
+
credentialSequence: number;
|
|
16
|
+
stagedAt: number;
|
|
17
|
+
stagedBy: string;
|
|
18
|
+
}
|
|
19
|
+
|
|
5
20
|
const getDb = () => DcRouterDb.getInstance().getDb();
|
|
6
21
|
|
|
7
22
|
@plugins.smartdata.Collection(() => getDb())
|
|
@@ -31,6 +46,23 @@ export class GatewayClientDoc extends plugins.smartdata.SmartDataDbDoc<GatewayCl
|
|
|
31
46
|
@plugins.smartdata.svDb()
|
|
32
47
|
public enabled: boolean = true;
|
|
33
48
|
|
|
49
|
+
@plugins.smartdata.svDb()
|
|
50
|
+
public policyGeneration: number = 1;
|
|
51
|
+
|
|
52
|
+
@plugins.smartdata.svDb()
|
|
53
|
+
public credentialSequence: number = 0;
|
|
54
|
+
|
|
55
|
+
/** Exact provisioned credential sequence authorized for the active policy. */
|
|
56
|
+
@plugins.smartdata.svDb()
|
|
57
|
+
public activeCredentialSequence: number = 0;
|
|
58
|
+
|
|
59
|
+
/** Monotonic compare-and-set authority for every mutable client field. */
|
|
60
|
+
@plugins.smartdata.svDb()
|
|
61
|
+
public mutationRevision: number = 0;
|
|
62
|
+
|
|
63
|
+
@plugins.smartdata.svDb()
|
|
64
|
+
public pendingProvisioning: IGatewayClientPendingProvisioning | null = null;
|
|
65
|
+
|
|
34
66
|
@plugins.smartdata.svDb()
|
|
35
67
|
public createdAt!: number;
|
|
36
68
|
|
|
@@ -48,6 +80,60 @@ export class GatewayClientDoc extends plugins.smartdata.SmartDataDbDoc<GatewayCl
|
|
|
48
80
|
return await GatewayClientDoc.getInstance({ id });
|
|
49
81
|
}
|
|
50
82
|
|
|
83
|
+
private static async getNativeCollection() {
|
|
84
|
+
const smartdataCollection = (GatewayClientDoc as typeof GatewayClientDoc & {
|
|
85
|
+
collection: plugins.smartdata.SmartdataCollection<GatewayClientDoc>;
|
|
86
|
+
}).collection;
|
|
87
|
+
await smartdataCollection.init();
|
|
88
|
+
return smartdataCollection.mongoDbCollection;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
private static buildRevisionSelector(idArg: string, revisionArg: number): Record<string, unknown> {
|
|
92
|
+
if (revisionArg === 0) {
|
|
93
|
+
return {
|
|
94
|
+
id: idArg,
|
|
95
|
+
$or: [
|
|
96
|
+
{ mutationRevision: 0 },
|
|
97
|
+
{ mutationRevision: { $exists: false } },
|
|
98
|
+
],
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
return { id: idArg, mutationRevision: revisionArg };
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
public static async compareAndSet(
|
|
105
|
+
idArg: string,
|
|
106
|
+
expectedRevisionArg: number,
|
|
107
|
+
setArg: Record<string, unknown>,
|
|
108
|
+
unsetArg: string[] = [],
|
|
109
|
+
): Promise<GatewayClientDoc | null> {
|
|
110
|
+
const collection = await GatewayClientDoc.getNativeCollection();
|
|
111
|
+
const update: Record<string, unknown> = {
|
|
112
|
+
$set: {
|
|
113
|
+
...setArg,
|
|
114
|
+
mutationRevision: expectedRevisionArg + 1,
|
|
115
|
+
_updatedAt: new Date().toISOString(),
|
|
116
|
+
},
|
|
117
|
+
};
|
|
118
|
+
if (unsetArg.length > 0) {
|
|
119
|
+
update.$unset = Object.fromEntries(unsetArg.map((fieldArg) => [fieldArg, '']));
|
|
120
|
+
}
|
|
121
|
+
const result = await collection.updateOne(
|
|
122
|
+
GatewayClientDoc.buildRevisionSelector(idArg, expectedRevisionArg),
|
|
123
|
+
update,
|
|
124
|
+
);
|
|
125
|
+
if (result.matchedCount !== 1) return null;
|
|
126
|
+
return await GatewayClientDoc.findById(idArg);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
public static async deleteIfRevision(idArg: string, expectedRevisionArg: number): Promise<boolean> {
|
|
130
|
+
const collection = await GatewayClientDoc.getNativeCollection();
|
|
131
|
+
const result = await collection.deleteOne(
|
|
132
|
+
GatewayClientDoc.buildRevisionSelector(idArg, expectedRevisionArg),
|
|
133
|
+
);
|
|
134
|
+
return result.deletedCount === 1;
|
|
135
|
+
}
|
|
136
|
+
|
|
51
137
|
public static async findAll(): Promise<GatewayClientDoc[]> {
|
|
52
138
|
return await GatewayClientDoc.getInstances({});
|
|
53
139
|
}
|