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:
@@ -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;
|
||||
},
|
||||
};
|
||||
})();
|
||||
|
||||
Reference in New Issue
Block a user