- Set Spanish as default language with ephemeral/encrypted privacy focus - Translate all user-facing strings and legal pages to Spanish - Replace Norwegian flag with Spanish flag in footer - Remove Hemmelig/terces.cloud links, add cloudhost.es sponsorship - Rewrite PrivacyPage: zero data collection, ephemeral design emphasis - Rewrite TermsPage: Spanish law, RGPD, paste.es/CloudHost.es references - Update PWA manifest, HTML meta tags, package.json branding - Rename webhook headers to X-Paste-Event / X-Paste-Signature - Update API docs title and contact to paste.es / cloudhost.es Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
107 lines
3.1 KiB
TypeScript
107 lines
3.1 KiB
TypeScript
import { createHmac } from 'crypto';
|
|
import config from '../config';
|
|
import { getInstanceSettings } from './settings';
|
|
|
|
export type WebhookEvent = 'secret.viewed' | 'secret.burned' | 'apikey.created';
|
|
|
|
interface SecretWebhookData {
|
|
secretId: string;
|
|
hasPassword: boolean;
|
|
hasIpRestriction: boolean;
|
|
viewsRemaining?: number;
|
|
}
|
|
|
|
interface ApiKeyWebhookData {
|
|
apiKeyId: string;
|
|
name: string;
|
|
expiresAt: string | null;
|
|
userId: string;
|
|
}
|
|
|
|
interface WebhookPayload {
|
|
event: WebhookEvent;
|
|
timestamp: string;
|
|
data: SecretWebhookData | ApiKeyWebhookData;
|
|
}
|
|
|
|
function signPayload(payload: string, secret: string): string {
|
|
return createHmac('sha256', secret).update(payload).digest('hex');
|
|
}
|
|
|
|
async function sendWithRetry(
|
|
url: string,
|
|
headers: Record<string, string>,
|
|
body: string
|
|
): Promise<void> {
|
|
const maxRetries = 3;
|
|
|
|
for (let attempt = 0; attempt < maxRetries; attempt++) {
|
|
try {
|
|
const response = await fetch(url, {
|
|
method: 'POST',
|
|
headers,
|
|
body,
|
|
signal: AbortSignal.timeout(5000),
|
|
redirect: 'error',
|
|
});
|
|
|
|
if (response.ok) return;
|
|
|
|
if (response.status >= 400 && response.status < 500) {
|
|
console.error(`Webhook delivery failed: ${response.status}`);
|
|
return;
|
|
}
|
|
} catch (error) {
|
|
if (attempt === maxRetries - 1) {
|
|
console.error('Webhook delivery failed after retries:', error);
|
|
return;
|
|
}
|
|
}
|
|
|
|
await new Promise((resolve) => setTimeout(resolve, 1000 * Math.pow(2, attempt)));
|
|
}
|
|
}
|
|
|
|
export function sendWebhook(event: WebhookEvent, data: WebhookPayload['data']): void {
|
|
(async () => {
|
|
try {
|
|
const settings = config.isManaged()
|
|
? config.getManagedSettings()
|
|
: await getInstanceSettings();
|
|
|
|
if (!settings?.webhookEnabled || !settings.webhookUrl) {
|
|
return;
|
|
}
|
|
|
|
if (event === 'secret.viewed' && !settings.webhookOnView) {
|
|
return;
|
|
}
|
|
if (event === 'secret.burned' && !settings.webhookOnBurn) {
|
|
return;
|
|
}
|
|
|
|
const payload: WebhookPayload = {
|
|
event,
|
|
timestamp: new Date().toISOString(),
|
|
data,
|
|
};
|
|
|
|
const payloadString = JSON.stringify(payload);
|
|
const headers: Record<string, string> = {
|
|
'Content-Type': 'application/json',
|
|
'X-Paste-Event': event,
|
|
'User-Agent': 'Paste-ES-Webhook/1.0',
|
|
};
|
|
|
|
if (settings.webhookSecret) {
|
|
const signature = signPayload(payloadString, settings.webhookSecret);
|
|
headers['X-Paste-Signature'] = `sha256=${signature}`;
|
|
}
|
|
|
|
await sendWithRetry(settings.webhookUrl, headers, payloadString);
|
|
} catch (error) {
|
|
console.error('Error preparing webhook:', error);
|
|
}
|
|
})();
|
|
}
|