feat: rebrand Hemmelig to paste.es for cloudhost.es
- 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>
This commit is contained in:
106
api/lib/webhook.ts
Normal file
106
api/lib/webhook.ts
Normal file
@@ -0,0 +1,106 @@
|
||||
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);
|
||||
}
|
||||
})();
|
||||
}
|
||||
Reference in New Issue
Block a user