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:
@@ -4,6 +4,13 @@
|
||||
|
||||
const API_BASE = '/api';
|
||||
|
||||
/** Normalize a caught error to a user-safe message. */
|
||||
export function errMsg(err: unknown, fallback = 'Unexpected error'): string {
|
||||
if (err instanceof Error && err.message) return err.message;
|
||||
if (typeof err === 'string' && err) return err;
|
||||
return fallback;
|
||||
}
|
||||
|
||||
function getToken(): string | null {
|
||||
if (typeof window === 'undefined') return null;
|
||||
return localStorage.getItem('access_token');
|
||||
|
||||
@@ -21,20 +21,22 @@
|
||||
</script>
|
||||
|
||||
<button type="button" bind:this={btnEl}
|
||||
class="inline-flex items-center justify-center w-3.5 h-3.5 rounded-full text-[9px] font-bold leading-none
|
||||
class="inline-flex items-center justify-center w-4 h-4 rounded-full text-[11px] font-bold leading-none
|
||||
border border-[var(--color-border)] bg-[var(--color-muted)] text-[var(--color-muted-foreground)]
|
||||
hover:bg-[var(--color-border)] hover:text-[var(--color-foreground)]
|
||||
focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-[var(--color-primary)]
|
||||
transition-colors cursor-help align-middle ml-2 flex-shrink-0"
|
||||
onmouseenter={show}
|
||||
onmouseleave={hide}
|
||||
onfocus={show}
|
||||
onblur={hide}
|
||||
aria-label={text}
|
||||
title={text}
|
||||
tabindex="0"
|
||||
>?</button>
|
||||
|
||||
{#if visible}
|
||||
<div role="tooltip" style="{tooltipStyle} background:var(--color-card); color:var(--color-foreground); border:1px solid var(--color-border); box-shadow:0 10px 30px rgba(0,0,0,0.3); padding:0.625rem 0.75rem; border-radius:0.5rem; font-size:0.75rem; white-space:normal; line-height:1.625; pointer-events:none;">
|
||||
<div role="tooltip" style="{tooltipStyle} background:var(--color-card); color:var(--color-foreground); border:1px solid var(--color-border); box-shadow:0 10px 30px rgba(0,0,0,0.3); padding:0.625rem 0.75rem; border-radius:0.5rem; font-size:0.8125rem; white-space:normal; line-height:1.625; pointer-events:none;">
|
||||
{text}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
@@ -40,7 +40,7 @@ export interface ConfigField {
|
||||
min?: number;
|
||||
max?: number;
|
||||
/** Default value for this field. */
|
||||
defaultValue?: string | number;
|
||||
defaultValue?: string | number | boolean;
|
||||
}
|
||||
|
||||
// ── Event tracking (TrackingConfig form) ─────────────────────────────
|
||||
@@ -60,14 +60,14 @@ export interface EventTrackingField {
|
||||
export interface ExtraTrackingField {
|
||||
key: string;
|
||||
label: string;
|
||||
type: 'number' | 'grid-select';
|
||||
type: 'number' | 'grid-select' | 'toggle';
|
||||
/** Grid-select item source function name from grid-items.ts. */
|
||||
gridItems?: string;
|
||||
gridColumns?: number;
|
||||
hint?: string;
|
||||
min?: number;
|
||||
max?: number;
|
||||
defaultValue?: string | number;
|
||||
defaultValue?: string | number | boolean;
|
||||
}
|
||||
|
||||
/** A feature section like periodic summary, scheduled assets, memory mode. */
|
||||
|
||||
@@ -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