feat(channels): add Synology Chat native channel (#23012)
* feat(channels): add Synology Chat native channel
Webhook-based integration with Synology NAS Chat (DSM 7+).
Supports outgoing webhooks, incoming messages, multi-account,
DM policies, rate limiting, and input sanitization.
- HMAC-based constant-time token validation
- Configurable SSL verification (allowInsecureSsl) for self-signed NAS certs
- 54 unit tests across 5 test suites
- Follows the same ChannelPlugin pattern as LINE/Discord/Telegram
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* feat(synology-chat): add pairing, warnings, messaging, agent hints
- Enable media capability (file_url already supported by client)
- Add pairing.notifyApproval to message approved users
- Add security.collectWarnings for missing token/URL, insecure SSL, open DM policy
- Add messaging.normalizeTarget and targetResolver for user ID resolution
- Add directory stubs (self, listPeers, listGroups)
- Add agentPrompt.messageToolHints with Synology Chat formatting guide
- 63 tests (up from 54), all passing
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-22 00:09:58 +01:00
|
|
|
/**
|
|
|
|
|
* Security module: token validation, rate limiting, input sanitization, user allowlist.
|
|
|
|
|
*/
|
|
|
|
|
|
|
|
|
|
import * as crypto from "node:crypto";
|
|
|
|
|
|
2026-02-24 23:28:24 +00:00
|
|
|
export type DmAuthorizationResult =
|
|
|
|
|
| { allowed: true }
|
|
|
|
|
| { allowed: false; reason: "disabled" | "allowlist-empty" | "not-allowlisted" };
|
|
|
|
|
|
feat(channels): add Synology Chat native channel (#23012)
* feat(channels): add Synology Chat native channel
Webhook-based integration with Synology NAS Chat (DSM 7+).
Supports outgoing webhooks, incoming messages, multi-account,
DM policies, rate limiting, and input sanitization.
- HMAC-based constant-time token validation
- Configurable SSL verification (allowInsecureSsl) for self-signed NAS certs
- 54 unit tests across 5 test suites
- Follows the same ChannelPlugin pattern as LINE/Discord/Telegram
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* feat(synology-chat): add pairing, warnings, messaging, agent hints
- Enable media capability (file_url already supported by client)
- Add pairing.notifyApproval to message approved users
- Add security.collectWarnings for missing token/URL, insecure SSL, open DM policy
- Add messaging.normalizeTarget and targetResolver for user ID resolution
- Add directory stubs (self, listPeers, listGroups)
- Add agentPrompt.messageToolHints with Synology Chat formatting guide
- 63 tests (up from 54), all passing
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-22 00:09:58 +01:00
|
|
|
/**
|
|
|
|
|
* Validate webhook token using constant-time comparison.
|
|
|
|
|
* Prevents timing attacks that could leak token bytes.
|
|
|
|
|
*/
|
|
|
|
|
export function validateToken(received: string, expected: string): boolean {
|
|
|
|
|
if (!received || !expected) return false;
|
|
|
|
|
|
|
|
|
|
// Use HMAC to normalize lengths before comparison,
|
|
|
|
|
// preventing timing side-channel on token length.
|
|
|
|
|
const key = "openclaw-token-cmp";
|
|
|
|
|
const a = crypto.createHmac("sha256", key).update(received).digest();
|
|
|
|
|
const b = crypto.createHmac("sha256", key).update(expected).digest();
|
|
|
|
|
|
|
|
|
|
return crypto.timingSafeEqual(a, b);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Check if a user ID is in the allowed list.
|
2026-02-24 22:55:03 +00:00
|
|
|
* Allowlist mode must be explicit; empty lists should not match any user.
|
feat(channels): add Synology Chat native channel (#23012)
* feat(channels): add Synology Chat native channel
Webhook-based integration with Synology NAS Chat (DSM 7+).
Supports outgoing webhooks, incoming messages, multi-account,
DM policies, rate limiting, and input sanitization.
- HMAC-based constant-time token validation
- Configurable SSL verification (allowInsecureSsl) for self-signed NAS certs
- 54 unit tests across 5 test suites
- Follows the same ChannelPlugin pattern as LINE/Discord/Telegram
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* feat(synology-chat): add pairing, warnings, messaging, agent hints
- Enable media capability (file_url already supported by client)
- Add pairing.notifyApproval to message approved users
- Add security.collectWarnings for missing token/URL, insecure SSL, open DM policy
- Add messaging.normalizeTarget and targetResolver for user ID resolution
- Add directory stubs (self, listPeers, listGroups)
- Add agentPrompt.messageToolHints with Synology Chat formatting guide
- 63 tests (up from 54), all passing
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-22 00:09:58 +01:00
|
|
|
*/
|
|
|
|
|
export function checkUserAllowed(userId: string, allowedUserIds: string[]): boolean {
|
2026-02-25 01:19:43 +00:00
|
|
|
if (allowedUserIds.length === 0) return false;
|
feat(channels): add Synology Chat native channel (#23012)
* feat(channels): add Synology Chat native channel
Webhook-based integration with Synology NAS Chat (DSM 7+).
Supports outgoing webhooks, incoming messages, multi-account,
DM policies, rate limiting, and input sanitization.
- HMAC-based constant-time token validation
- Configurable SSL verification (allowInsecureSsl) for self-signed NAS certs
- 54 unit tests across 5 test suites
- Follows the same ChannelPlugin pattern as LINE/Discord/Telegram
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* feat(synology-chat): add pairing, warnings, messaging, agent hints
- Enable media capability (file_url already supported by client)
- Add pairing.notifyApproval to message approved users
- Add security.collectWarnings for missing token/URL, insecure SSL, open DM policy
- Add messaging.normalizeTarget and targetResolver for user ID resolution
- Add directory stubs (self, listPeers, listGroups)
- Add agentPrompt.messageToolHints with Synology Chat formatting guide
- 63 tests (up from 54), all passing
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-22 00:09:58 +01:00
|
|
|
return allowedUserIds.includes(userId);
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-24 23:28:24 +00:00
|
|
|
/**
|
|
|
|
|
* Resolve DM authorization for a sender across all DM policy modes.
|
|
|
|
|
* Keeps policy semantics in one place so webhook/startup behavior stays consistent.
|
|
|
|
|
*/
|
|
|
|
|
export function authorizeUserForDm(
|
|
|
|
|
userId: string,
|
|
|
|
|
dmPolicy: "open" | "allowlist" | "disabled",
|
|
|
|
|
allowedUserIds: string[],
|
|
|
|
|
): DmAuthorizationResult {
|
|
|
|
|
if (dmPolicy === "disabled") {
|
|
|
|
|
return { allowed: false, reason: "disabled" };
|
|
|
|
|
}
|
|
|
|
|
if (dmPolicy === "open") {
|
|
|
|
|
return { allowed: true };
|
|
|
|
|
}
|
|
|
|
|
if (allowedUserIds.length === 0) {
|
|
|
|
|
return { allowed: false, reason: "allowlist-empty" };
|
|
|
|
|
}
|
|
|
|
|
if (!checkUserAllowed(userId, allowedUserIds)) {
|
|
|
|
|
return { allowed: false, reason: "not-allowlisted" };
|
|
|
|
|
}
|
|
|
|
|
return { allowed: true };
|
|
|
|
|
}
|
|
|
|
|
|
feat(channels): add Synology Chat native channel (#23012)
* feat(channels): add Synology Chat native channel
Webhook-based integration with Synology NAS Chat (DSM 7+).
Supports outgoing webhooks, incoming messages, multi-account,
DM policies, rate limiting, and input sanitization.
- HMAC-based constant-time token validation
- Configurable SSL verification (allowInsecureSsl) for self-signed NAS certs
- 54 unit tests across 5 test suites
- Follows the same ChannelPlugin pattern as LINE/Discord/Telegram
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* feat(synology-chat): add pairing, warnings, messaging, agent hints
- Enable media capability (file_url already supported by client)
- Add pairing.notifyApproval to message approved users
- Add security.collectWarnings for missing token/URL, insecure SSL, open DM policy
- Add messaging.normalizeTarget and targetResolver for user ID resolution
- Add directory stubs (self, listPeers, listGroups)
- Add agentPrompt.messageToolHints with Synology Chat formatting guide
- 63 tests (up from 54), all passing
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-22 00:09:58 +01:00
|
|
|
/**
|
|
|
|
|
* Sanitize user input to prevent prompt injection attacks.
|
|
|
|
|
* Filters known dangerous patterns and truncates long messages.
|
|
|
|
|
*/
|
|
|
|
|
export function sanitizeInput(text: string): string {
|
|
|
|
|
const dangerousPatterns = [
|
|
|
|
|
/ignore\s+(all\s+)?(previous|prior|above)\s+(instructions?|prompts?)/gi,
|
|
|
|
|
/you\s+are\s+now\s+/gi,
|
|
|
|
|
/system:\s*/gi,
|
|
|
|
|
/<\|.*?\|>/g, // special tokens
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
let sanitized = text;
|
|
|
|
|
for (const pattern of dangerousPatterns) {
|
|
|
|
|
sanitized = sanitized.replace(pattern, "[FILTERED]");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const maxLength = 4000;
|
|
|
|
|
if (sanitized.length > maxLength) {
|
|
|
|
|
sanitized = sanitized.slice(0, maxLength) + "... [truncated]";
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return sanitized;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Sliding window rate limiter per user ID.
|
|
|
|
|
*/
|
|
|
|
|
export class RateLimiter {
|
|
|
|
|
private requests: Map<string, number[]> = new Map();
|
|
|
|
|
private limit: number;
|
|
|
|
|
private windowMs: number;
|
|
|
|
|
private lastCleanup = 0;
|
|
|
|
|
private cleanupIntervalMs: number;
|
|
|
|
|
|
|
|
|
|
constructor(limit = 30, windowSeconds = 60) {
|
|
|
|
|
this.limit = limit;
|
|
|
|
|
this.windowMs = windowSeconds * 1000;
|
|
|
|
|
this.cleanupIntervalMs = this.windowMs * 5; // cleanup every 5 windows
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** Returns true if the request is allowed, false if rate-limited. */
|
|
|
|
|
check(userId: string): boolean {
|
|
|
|
|
const now = Date.now();
|
|
|
|
|
const windowStart = now - this.windowMs;
|
|
|
|
|
|
|
|
|
|
// Periodic cleanup of stale entries to prevent memory leak
|
|
|
|
|
if (now - this.lastCleanup > this.cleanupIntervalMs) {
|
|
|
|
|
this.cleanup(windowStart);
|
|
|
|
|
this.lastCleanup = now;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let timestamps = this.requests.get(userId);
|
|
|
|
|
if (timestamps) {
|
|
|
|
|
timestamps = timestamps.filter((ts) => ts > windowStart);
|
|
|
|
|
} else {
|
|
|
|
|
timestamps = [];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (timestamps.length >= this.limit) {
|
|
|
|
|
this.requests.set(userId, timestamps);
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
timestamps.push(now);
|
|
|
|
|
this.requests.set(userId, timestamps);
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** Remove entries with no recent activity. */
|
|
|
|
|
private cleanup(windowStart: number): void {
|
|
|
|
|
for (const [userId, timestamps] of this.requests) {
|
|
|
|
|
const active = timestamps.filter((ts) => ts > windowStart);
|
|
|
|
|
if (active.length === 0) {
|
|
|
|
|
this.requests.delete(userId);
|
|
|
|
|
} else {
|
|
|
|
|
this.requests.set(userId, active);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|