feat: security hardening — SSRF guard, template sandbox timeout, webhook log prune, auth & backup polish

- Add outbound URL validation (SSRF) for webhook/Discord/Slack/ntfy/Matrix dispatch
- Template renderer: input/output caps and thread-based render timeout
- Webhook log filter: strip Authorization/signature/token-like headers; atomic prune
- Auth/JWT/backup/config tightening; misc frontend UX fixes
This commit is contained in:
2026-04-16 03:21:45 +03:00
parent 734e5c9340
commit f0739ca949
30 changed files with 567 additions and 105 deletions
+59 -13
View File
@@ -6,6 +6,7 @@
*/
import { createEntityCache } from './entity-cache.svelte';
import { api } from '$lib/api';
import type {
ServiceProvider,
NotificationTarget,
@@ -57,19 +58,56 @@ export const commandTrackersCache = createEntityCache<CommandTracker>('/command-
/** Actions — used by Actions page. */
export const actionsCache = createEntityCache<Action>('/actions');
/** Provider capabilities — used by Template Configs, Command Configs. */
export interface SlotDef {
name: string;
description: string;
required?: boolean;
}
export interface CommandDef {
name: string;
description?: string;
}
export interface ActionTypeDef {
key: string;
name: string;
description?: string;
[key: string]: unknown;
}
export interface ProviderCapabilities {
notification_slots?: SlotDef[];
command_slots?: SlotDef[];
commands?: CommandDef[];
action_types?: ActionTypeDef[];
[key: string]: unknown;
}
export type CapabilitiesMap = Record<string, ProviderCapabilities>;
/** Provider capabilities — used by Template Configs, Command Configs.
* Dedups concurrent fetches so two fast navigations do not double-hit the API. */
export const capabilitiesCache = (() => {
let data = $state<Record<string, any>>({});
let data = $state<CapabilitiesMap>({});
let fetchedAt = $state(0);
const TTL = 60_000; // 1 minute
let inflight: Promise<CapabilitiesMap> | null = null;
const TTL = 60_000;
return {
get items() { return data; },
async fetch(force = false): Promise<Record<string, any>> {
async fetch(force = false): Promise<CapabilitiesMap> {
if (!force && Object.keys(data).length > 0 && Date.now() - fetchedAt < TTL) return data;
const { api } = await import('$lib/api');
data = await api('/providers/capabilities');
fetchedAt = Date.now();
return data;
if (inflight) return inflight;
inflight = (async () => {
try {
data = await api<CapabilitiesMap>('/providers/capabilities');
fetchedAt = Date.now();
return data;
} finally {
inflight = null;
}
})();
return inflight;
},
};
})();
@@ -78,15 +116,23 @@ export const capabilitiesCache = (() => {
export const supportedLocalesCache = (() => {
let data = $state<string[]>(['en', 'ru']);
let fetchedAt = $state(0);
const TTL = 300_000; // 5 minutes
let inflight: Promise<string[]> | null = null;
const TTL = 300_000;
return {
get items() { return data; },
async fetch(force = false): Promise<string[]> {
if (!force && fetchedAt > 0 && Date.now() - fetchedAt < TTL) return data;
const { api } = await import('$lib/api');
data = await api('/settings/locales');
fetchedAt = Date.now();
return data;
if (inflight) return inflight;
inflight = (async () => {
try {
data = await api<string[]>('/settings/locales');
fetchedAt = Date.now();
return data;
} finally {
inflight = null;
}
})();
return inflight;
},
};
})();