@serve.zone/dcrouter 14.2.3 → 14.3.1
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/dns/manager.dns.js +14 -3
- 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/dns/manager.dns.ts +17 -2
- 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
|
@@ -31,7 +31,17 @@ const tlsCertOptions = [
|
|
|
31
31
|
{ key: 'custom', option: 'Custom certificate' },
|
|
32
32
|
];
|
|
33
33
|
const giteaSourcePolicyProfileNames = ['TRUSTED NETWORKS', 'AI CRAWLERS', 'PUBLIC'] as const;
|
|
34
|
-
type
|
|
34
|
+
type TSzRouteSecurityBase = NonNullable<ISzSourceProfileOption['security']>;
|
|
35
|
+
type TSzRouteSecurity = Omit<TSzRouteSecurityBase, 'rateLimit'> & {
|
|
36
|
+
rateLimit?: interfaces.data.IRouteSecurity['rateLimit'];
|
|
37
|
+
challenge?: interfaces.data.IRouteSecurity['challenge'];
|
|
38
|
+
};
|
|
39
|
+
type TSourceProfileOption = Omit<ISzSourceProfileOption, 'security'> & {
|
|
40
|
+
security?: TSzRouteSecurity;
|
|
41
|
+
};
|
|
42
|
+
type TSzRouteSourcePolicyPreset = Omit<ISzRouteSourcePolicyPreset, 'bindings'> & {
|
|
43
|
+
bindings: interfaces.data.IRouteSourceBinding[];
|
|
44
|
+
};
|
|
35
45
|
|
|
36
46
|
function rateLimit(maxRequests: number): interfaces.data.IRouteSecurity['rateLimit'] {
|
|
37
47
|
return { enabled: true, maxRequests, window: 60, keyBy: 'ip' };
|
|
@@ -92,7 +102,7 @@ function buildGiteaSourceBindingsMetadata(profileRefs: string[]): interfaces.dat
|
|
|
92
102
|
];
|
|
93
103
|
}
|
|
94
104
|
|
|
95
|
-
function getGiteaSourcePolicyPresets(profiles: interfaces.data.ISourceProfile[]):
|
|
105
|
+
function getGiteaSourcePolicyPresets(profiles: interfaces.data.ISourceProfile[]): TSzRouteSourcePolicyPreset[] {
|
|
96
106
|
const { refs, missingNames } = getGiteaPresetProfileRefs(profiles);
|
|
97
107
|
if (missingNames.length > 0) {
|
|
98
108
|
return [];
|
|
@@ -136,25 +146,40 @@ function sourceProfileHasSourceMatches(profile: interfaces.data.ISourceProfile):
|
|
|
136
146
|
function normalizeCatalogRateLimit(
|
|
137
147
|
rateLimitValue: interfaces.data.IRouteSecurity['rateLimit'] | undefined,
|
|
138
148
|
): TSzRouteSecurity['rateLimit'] | undefined {
|
|
149
|
+
if (rateLimitValue === null) return null;
|
|
139
150
|
if (!rateLimitValue) return undefined;
|
|
151
|
+
const keyBy = ['ip', 'path', 'header'].includes(String(rateLimitValue.keyBy))
|
|
152
|
+
? rateLimitValue.keyBy as 'ip' | 'path' | 'header'
|
|
153
|
+
: undefined;
|
|
140
154
|
return {
|
|
141
155
|
enabled: Boolean(rateLimitValue.enabled),
|
|
142
156
|
maxRequests: Number(rateLimitValue.maxRequests) || 0,
|
|
143
157
|
window: Number(rateLimitValue.window) || 0,
|
|
144
|
-
...(
|
|
158
|
+
...(keyBy ? { keyBy } : {}),
|
|
159
|
+
...(rateLimitValue.onExceeded ? { onExceeded: rateLimitValue.onExceeded } : {}),
|
|
145
160
|
};
|
|
146
161
|
}
|
|
147
162
|
|
|
148
|
-
function
|
|
163
|
+
function normalizeCatalogChallenge(
|
|
164
|
+
challengeValue: interfaces.data.IRouteSecurity['challenge'] | undefined,
|
|
165
|
+
): TSzRouteSecurity['challenge'] | undefined {
|
|
166
|
+
if (challengeValue === null) return null;
|
|
167
|
+
if (!challengeValue) return undefined;
|
|
168
|
+
return structuredClone(challengeValue) as TSzRouteSecurity['challenge'];
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function getSourceProfileOptions(profiles: interfaces.data.ISourceProfile[]): TSourceProfileOption[] {
|
|
149
172
|
return profiles.map((profile) => {
|
|
150
173
|
const ipAllowList = normalizeSecurityListEntries(profile.security?.ipAllowList);
|
|
151
174
|
const ipBlockList = normalizeSecurityListEntries(profile.security?.ipBlockList);
|
|
152
175
|
const rateLimitValue = normalizeCatalogRateLimit(profile.security?.rateLimit);
|
|
176
|
+
const challengeValue = normalizeCatalogChallenge(profile.security?.challenge);
|
|
153
177
|
const security: TSzRouteSecurity = {
|
|
154
178
|
...(ipAllowList.length ? { ipAllowList } : {}),
|
|
155
179
|
...(ipBlockList.length ? { ipBlockList } : {}),
|
|
156
180
|
...(typeof profile.security?.maxConnections === 'number' ? { maxConnections: profile.security.maxConnections } : {}),
|
|
157
|
-
...(rateLimitValue ? { rateLimit: rateLimitValue } : {}),
|
|
181
|
+
...(rateLimitValue !== undefined ? { rateLimit: rateLimitValue } : {}),
|
|
182
|
+
...(challengeValue !== undefined ? { challenge: challengeValue } : {}),
|
|
158
183
|
};
|
|
159
184
|
return {
|
|
160
185
|
id: profile.id,
|
|
@@ -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();
|