@serve.zone/dcrouter 14.2.3 → 14.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 +1120 -1058
- package/dist_ts/00_commitinfo_data.js +1 -1
- package/dist_ts/config/classes.route-config-manager.d.ts +2 -0
- package/dist_ts/config/classes.route-config-manager.js +66 -6
- package/dist_ts/config/classes.source-policy-compiler.d.ts +2 -0
- package/dist_ts/config/classes.source-policy-compiler.js +121 -16
- package/dist_ts/http3/http3-route-augmentation.d.ts +1 -0
- package/dist_ts/http3/http3-route-augmentation.js +9 -2
- package/dist_ts_interfaces/data/route-management.d.ts +17 -2
- package/dist_ts_interfaces/data/route-management.js +1 -1
- package/dist_ts_web/00_commitinfo_data.js +1 -1
- package/dist_ts_web/elements/network/ops-view-routes.js +18 -3
- package/dist_ts_web/elements/network/ops-view-sourceprofiles.js +111 -11
- package/package.json +3 -3
- package/readme.md +70 -8
- package/ts/00_commitinfo_data.ts +1 -1
- package/ts/config/classes.route-config-manager.ts +71 -5
- package/ts/config/classes.source-policy-compiler.ts +140 -22
- package/ts/http3/http3-route-augmentation.ts +11 -1
- package/ts_web/00_commitinfo_data.ts +1 -1
- package/ts_web/elements/network/ops-view-routes.ts +30 -5
- package/ts_web/elements/network/ops-view-sourceprofiles.ts +112 -11
|
@@ -21,22 +21,91 @@ function parseOptionalPositiveInteger(value: unknown): number | undefined {
|
|
|
21
21
|
return Number.isInteger(parsed) && parsed > 0 ? parsed : undefined;
|
|
22
22
|
}
|
|
23
23
|
|
|
24
|
-
|
|
25
|
-
|
|
24
|
+
const protectionModeOptions = [
|
|
25
|
+
{ key: 'inherit', option: 'Inherit / unset' },
|
|
26
|
+
{ key: 'none', option: 'None (clear inherited)' },
|
|
27
|
+
{ key: 'custom', option: 'Custom' },
|
|
28
|
+
];
|
|
29
|
+
|
|
30
|
+
const rateLimitExceededOptions = [
|
|
31
|
+
{ key: '429', option: 'Return 429' },
|
|
32
|
+
{ key: 'challenge', option: 'Browser challenge' },
|
|
33
|
+
];
|
|
34
|
+
|
|
35
|
+
function getDropdownKey(value: unknown): string {
|
|
36
|
+
if (typeof value === 'string') {
|
|
37
|
+
return value;
|
|
38
|
+
}
|
|
39
|
+
if (value && typeof value === 'object' && 'key' in value) {
|
|
40
|
+
const key = (value as { key?: unknown }).key;
|
|
41
|
+
return typeof key === 'string' ? key : '';
|
|
42
|
+
}
|
|
43
|
+
return '';
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function protectionModeForValue(value: unknown): 'inherit' | 'none' | 'custom' {
|
|
47
|
+
if (value === null) return 'none';
|
|
48
|
+
if (value === undefined) return 'inherit';
|
|
49
|
+
return 'custom';
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function buildChallengeConfigFromFormData(data: Record<string, unknown>): NonNullable<interfaces.data.IRouteSecurity['challenge']> | undefined {
|
|
53
|
+
const providerId = String(data.challengeProviderId || 'smartchallenge').trim();
|
|
54
|
+
const challengeType = String(data.challengeType || 'wait').trim();
|
|
55
|
+
const ttlSeconds = parseOptionalPositiveInteger(data.challengeTtlSeconds) || 300;
|
|
56
|
+
if (!providerId || !challengeType) {
|
|
57
|
+
alert('Challenge requires Provider ID and Challenge Type.');
|
|
58
|
+
return undefined;
|
|
59
|
+
}
|
|
60
|
+
return {
|
|
61
|
+
providerId,
|
|
62
|
+
challengeType,
|
|
63
|
+
...(challengeType === 'wait' ? { settings: { waitSeconds: 0, ttlSeconds } } : {}),
|
|
64
|
+
clearance: { ttlSeconds, bindToHost: true },
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function buildRateLimitFromFormData(data: Record<string, unknown>): interfaces.data.IRouteSecurity['rateLimit'] | null | undefined {
|
|
69
|
+
const mode = getDropdownKey(data.rateLimitMode) || (Boolean(data.rateLimitEnabled) ? 'custom' : 'inherit');
|
|
70
|
+
if (mode === 'inherit') {
|
|
26
71
|
return undefined;
|
|
27
72
|
}
|
|
73
|
+
if (mode === 'none') {
|
|
74
|
+
return null;
|
|
75
|
+
}
|
|
28
76
|
const maxRequests = parseOptionalPositiveInteger(data.rateLimitMaxRequests);
|
|
29
77
|
const window = parseOptionalPositiveInteger(data.rateLimitWindow);
|
|
30
78
|
if (!maxRequests || !window) {
|
|
31
79
|
alert('Rate limit requires positive Max Requests and Window values.');
|
|
32
80
|
return null;
|
|
33
81
|
}
|
|
34
|
-
|
|
82
|
+
const rateLimit: NonNullable<interfaces.data.IRouteSecurity['rateLimit']> = {
|
|
35
83
|
enabled: true,
|
|
36
84
|
maxRequests,
|
|
37
85
|
window,
|
|
38
86
|
keyBy: 'ip' as const,
|
|
39
87
|
};
|
|
88
|
+
if (getDropdownKey(data.rateLimitExceededMode) === 'challenge') {
|
|
89
|
+
const challenge = buildChallengeConfigFromFormData(data);
|
|
90
|
+
if (!challenge) return null;
|
|
91
|
+
rateLimit.onExceeded = {
|
|
92
|
+
type: 'challenge',
|
|
93
|
+
challenge,
|
|
94
|
+
clearanceEffect: 'bypass-rate-limit',
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
return rateLimit;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function buildChallengeFromFormData(data: Record<string, unknown>): interfaces.data.IRouteSecurity['challenge'] | null | undefined {
|
|
101
|
+
const mode = getDropdownKey(data.challengeMode) || 'inherit';
|
|
102
|
+
if (mode === 'inherit') {
|
|
103
|
+
return undefined;
|
|
104
|
+
}
|
|
105
|
+
if (mode === 'none') {
|
|
106
|
+
return null;
|
|
107
|
+
}
|
|
108
|
+
return buildChallengeConfigFromFormData(data) || null;
|
|
40
109
|
}
|
|
41
110
|
|
|
42
111
|
declare global {
|
|
@@ -105,7 +174,14 @@ export class OpsViewSourceProfiles extends DeesElement {
|
|
|
105
174
|
'IP Allow List': (profile.security?.ipAllowList || []).join(', ') || '-',
|
|
106
175
|
'IP Block List': (profile.security?.ipBlockList || []).join(', ') || '-',
|
|
107
176
|
'Max Connections': profile.security?.maxConnections ?? '-',
|
|
108
|
-
'Rate Limit': profile.security?.rateLimit
|
|
177
|
+
'Rate Limit': profile.security?.rateLimit === null
|
|
178
|
+
? 'none'
|
|
179
|
+
: profile.security?.rateLimit?.enabled
|
|
180
|
+
? `${profile.security.rateLimit.maxRequests}/${profile.security.rateLimit.window}s${profile.security.rateLimit.onExceeded?.type === 'challenge' ? ' -> challenge' : ''}`
|
|
181
|
+
: '-',
|
|
182
|
+
Challenge: profile.security?.challenge === null
|
|
183
|
+
? 'none'
|
|
184
|
+
: profile.security?.challenge ? `${profile.security.challenge.providerId}/${profile.security.challenge.challengeType}` : '-',
|
|
109
185
|
Extends: (profile.extendsProfiles || []).length > 0
|
|
110
186
|
? profile.extendsProfiles!.map(id => {
|
|
111
187
|
const p = profiles.find(pp => pp.id === id);
|
|
@@ -165,9 +241,14 @@ export class OpsViewSourceProfiles extends DeesElement {
|
|
|
165
241
|
<dees-input-list .key=${'ipAllowList'} .label=${'IP Allow List'} .placeholder=${'Add IP or CIDR...'}></dees-input-list>
|
|
166
242
|
<dees-input-list .key=${'ipBlockList'} .label=${'IP Block List'} .placeholder=${'Add IP or CIDR...'}></dees-input-list>
|
|
167
243
|
<dees-input-text .key=${'maxConnections'} .label=${'Max Connections'}></dees-input-text>
|
|
168
|
-
<dees-input-
|
|
244
|
+
<dees-input-dropdown .key=${'rateLimitMode'} .label=${'Rate Limit'} .description=${'Default for route bindings. Use none to clear inherited parent defaults.'} .options=${protectionModeOptions} .selectedOption=${protectionModeOptions[0]}></dees-input-dropdown>
|
|
169
245
|
<dees-input-text .key=${'rateLimitMaxRequests'} .label=${'Max Requests'} .description=${'Requests per source IP'}></dees-input-text>
|
|
170
246
|
<dees-input-text .key=${'rateLimitWindow'} .label=${'Window Seconds'}></dees-input-text>
|
|
247
|
+
<dees-input-dropdown .key=${'rateLimitExceededMode'} .label=${'When Rate Limit Is Exceeded'} .description=${'Return 429 or issue the configured browser challenge.'} .options=${rateLimitExceededOptions} .selectedOption=${rateLimitExceededOptions[0]}></dees-input-dropdown>
|
|
248
|
+
<dees-input-dropdown .key=${'challengeMode'} .label=${'Browser Challenge'} .description=${'Default for HTTP-visible route bindings.'} .options=${protectionModeOptions} .selectedOption=${protectionModeOptions[0]}></dees-input-dropdown>
|
|
249
|
+
<dees-input-text .key=${'challengeProviderId'} .label=${'Challenge Provider ID'} .value=${'smartchallenge'}></dees-input-text>
|
|
250
|
+
<dees-input-text .key=${'challengeType'} .label=${'Challenge Type'} .value=${'wait'}></dees-input-text>
|
|
251
|
+
<dees-input-text .key=${'challengeTtlSeconds'} .label=${'Clearance TTL Seconds'} .value=${'300'}></dees-input-text>
|
|
171
252
|
</dees-form>
|
|
172
253
|
`,
|
|
173
254
|
menuOptions: [
|
|
@@ -182,7 +263,9 @@ export class OpsViewSourceProfiles extends DeesElement {
|
|
|
182
263
|
const ipBlockList: string[] = Array.isArray(data.ipBlockList) ? data.ipBlockList : [];
|
|
183
264
|
const maxConnections = parseOptionalPositiveInteger(data.maxConnections);
|
|
184
265
|
const rateLimit = buildRateLimitFromFormData(data);
|
|
185
|
-
if (rateLimit === null) return;
|
|
266
|
+
if (rateLimit === null && getDropdownKey(data.rateLimitMode) === 'custom') return;
|
|
267
|
+
const challenge = buildChallengeFromFormData(data);
|
|
268
|
+
if (challenge === null && getDropdownKey(data.challengeMode) === 'custom') return;
|
|
186
269
|
|
|
187
270
|
await appstate.profilesTargetsStatePart.dispatchAction(appstate.createProfileAction, {
|
|
188
271
|
name: String(data.name),
|
|
@@ -191,7 +274,8 @@ export class OpsViewSourceProfiles extends DeesElement {
|
|
|
191
274
|
...(ipAllowList.length > 0 ? { ipAllowList } : {}),
|
|
192
275
|
...(ipBlockList.length > 0 ? { ipBlockList } : {}),
|
|
193
276
|
...(maxConnections ? { maxConnections } : {}),
|
|
194
|
-
...(rateLimit ? { rateLimit } : {}),
|
|
277
|
+
...(rateLimit !== undefined ? { rateLimit } : {}),
|
|
278
|
+
...(challenge !== undefined ? { challenge } : {}),
|
|
195
279
|
},
|
|
196
280
|
});
|
|
197
281
|
modalArg.destroy();
|
|
@@ -203,6 +287,16 @@ export class OpsViewSourceProfiles extends DeesElement {
|
|
|
203
287
|
|
|
204
288
|
private async showEditProfileDialog(profile: interfaces.data.ISourceProfile) {
|
|
205
289
|
const { DeesModal } = await import('@design.estate/dees-catalog');
|
|
290
|
+
const rateLimitMode = protectionModeForValue(profile.security?.rateLimit);
|
|
291
|
+
const rateLimitExceededMode = profile.security?.rateLimit && profile.security.rateLimit !== null && profile.security.rateLimit.onExceeded?.type === 'challenge'
|
|
292
|
+
? 'challenge'
|
|
293
|
+
: '429';
|
|
294
|
+
const challengeMode = protectionModeForValue(profile.security?.challenge);
|
|
295
|
+
const challengeDefaults = profile.security?.rateLimit && profile.security.rateLimit !== null && profile.security.rateLimit.onExceeded?.type === 'challenge'
|
|
296
|
+
? profile.security.rateLimit.onExceeded.challenge
|
|
297
|
+
: profile.security?.challenge && profile.security.challenge !== null
|
|
298
|
+
? profile.security.challenge
|
|
299
|
+
: undefined;
|
|
206
300
|
DeesModal.createAndShow({
|
|
207
301
|
heading: `Edit Profile: ${profile.name}`,
|
|
208
302
|
content: html`
|
|
@@ -212,9 +306,14 @@ export class OpsViewSourceProfiles extends DeesElement {
|
|
|
212
306
|
<dees-input-list .key=${'ipAllowList'} .label=${'IP Allow List'} .placeholder=${'Add IP or CIDR...'} .value=${profile.security?.ipAllowList || []}></dees-input-list>
|
|
213
307
|
<dees-input-list .key=${'ipBlockList'} .label=${'IP Block List'} .placeholder=${'Add IP or CIDR...'} .value=${profile.security?.ipBlockList || []}></dees-input-list>
|
|
214
308
|
<dees-input-text .key=${'maxConnections'} .label=${'Max Connections'} .value=${String(profile.security?.maxConnections || '')}></dees-input-text>
|
|
215
|
-
<dees-input-
|
|
309
|
+
<dees-input-dropdown .key=${'rateLimitMode'} .label=${'Rate Limit'} .description=${'Default for route bindings. Use none to clear inherited parent defaults.'} .options=${protectionModeOptions} .selectedOption=${protectionModeOptions.find((option) => option.key === rateLimitMode) || protectionModeOptions[0]}></dees-input-dropdown>
|
|
216
310
|
<dees-input-text .key=${'rateLimitMaxRequests'} .label=${'Max Requests'} .description=${'Requests per source IP'} .value=${String(profile.security?.rateLimit?.maxRequests || '')}></dees-input-text>
|
|
217
311
|
<dees-input-text .key=${'rateLimitWindow'} .label=${'Window Seconds'} .value=${String(profile.security?.rateLimit?.window || '')}></dees-input-text>
|
|
312
|
+
<dees-input-dropdown .key=${'rateLimitExceededMode'} .label=${'When Rate Limit Is Exceeded'} .description=${'Return 429 or issue the configured browser challenge.'} .options=${rateLimitExceededOptions} .selectedOption=${rateLimitExceededOptions.find((option) => option.key === rateLimitExceededMode) || rateLimitExceededOptions[0]}></dees-input-dropdown>
|
|
313
|
+
<dees-input-dropdown .key=${'challengeMode'} .label=${'Browser Challenge'} .description=${'Default for HTTP-visible route bindings.'} .options=${protectionModeOptions} .selectedOption=${protectionModeOptions.find((option) => option.key === challengeMode) || protectionModeOptions[0]}></dees-input-dropdown>
|
|
314
|
+
<dees-input-text .key=${'challengeProviderId'} .label=${'Challenge Provider ID'} .value=${challengeDefaults?.providerId || 'smartchallenge'}></dees-input-text>
|
|
315
|
+
<dees-input-text .key=${'challengeType'} .label=${'Challenge Type'} .value=${challengeDefaults?.challengeType || 'wait'}></dees-input-text>
|
|
316
|
+
<dees-input-text .key=${'challengeTtlSeconds'} .label=${'Clearance TTL Seconds'} .value=${String(challengeDefaults?.clearance?.ttlSeconds || 300)}></dees-input-text>
|
|
218
317
|
</dees-form>
|
|
219
318
|
`,
|
|
220
319
|
menuOptions: [
|
|
@@ -229,7 +328,9 @@ export class OpsViewSourceProfiles extends DeesElement {
|
|
|
229
328
|
const ipBlockList: string[] = Array.isArray(data.ipBlockList) ? data.ipBlockList : [];
|
|
230
329
|
const maxConnections = parseOptionalPositiveInteger(data.maxConnections);
|
|
231
330
|
const rateLimit = buildRateLimitFromFormData(data);
|
|
232
|
-
if (rateLimit === null) return;
|
|
331
|
+
if (rateLimit === null && getDropdownKey(data.rateLimitMode) === 'custom') return;
|
|
332
|
+
const challenge = buildChallengeFromFormData(data);
|
|
333
|
+
if (challenge === null && getDropdownKey(data.challengeMode) === 'custom') return;
|
|
233
334
|
|
|
234
335
|
await appstate.profilesTargetsStatePart.dispatchAction(appstate.updateProfileAction, {
|
|
235
336
|
id: profile.id,
|
|
@@ -239,8 +340,8 @@ export class OpsViewSourceProfiles extends DeesElement {
|
|
|
239
340
|
ipAllowList,
|
|
240
341
|
ipBlockList,
|
|
241
342
|
...(maxConnections ? { maxConnections } : {}),
|
|
242
|
-
...(rateLimit ? { rateLimit } : {}),
|
|
243
|
-
...(
|
|
343
|
+
...(rateLimit !== undefined ? { rateLimit } : {}),
|
|
344
|
+
...(challenge !== undefined ? { challenge } : {}),
|
|
244
345
|
},
|
|
245
346
|
});
|
|
246
347
|
modalArg.destroy();
|