/* ---------- Contact-form webhook secret: WRITE-ONLY client ---------- Posts a raw shared secret to the panel endpoint and gets back an opaque id. The raw secret is never stored anywhere on the client: it is passed to `storeWebhookSecret` from local component state, and only the returned `secret_id` is ever written to a craft prop. Craft props are serialised into the saved project and into published output, which is the wrong tier for a credential -- that is the whole reason this endpoint exists. THERE IS NO READ. The panel endpoint (web-files/api/form-webhook-secret.php) is POST-only by design: a "show me my secret" route would re-open the exact problem this closes. Rotation is another POST, which mints a NEW id. So the UI can offer set / replace / remove, and never "view". */ export interface StoreSecretResult { ok: boolean; /** Present only on success -- `whs__`. */ secretId?: string; /** Customer-facing message on failure (the endpoint's own, when it sent one). */ error?: string; } /** * Derive the secret endpoint from the configured API url, so a deployment that * moves the panel API (or the vite dev proxy) doesn't need a second constant * kept in sync: `/api/site-builder.php` -> `/api/form-webhook-secret.php`. */ export function webhookSecretEndpoint(apiUrl?: string): string { const base = typeof apiUrl === 'string' ? apiUrl.trim() : ''; if (base.includes('/')) return base.replace(/[^/]*$/, 'form-webhook-secret.php'); return '/api/form-webhook-secret.php'; } /** * Store a raw webhook secret for the current site; resolves with its opaque id. * * Never throws and never returns the secret. Errors are returned as text fit to * show a customer -- including the endpoint's 429 ("Too many webhook secrets * stored for this site"), which is actionable (contact support) and so is * passed through rather than flattened into a generic failure. */ export async function storeWebhookSecret(secret: string): Promise { const cfg = (window as any).WHP_CONFIG; if (!cfg) { return { ok: false, error: 'Saving a webhook secret needs the builder to be open inside the control panel.' }; } try { const resp = await fetch(webhookSecretEndpoint(cfg.apiUrl), { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-CSRF-Token': cfg.csrfToken }, body: JSON.stringify({ site_id: cfg.siteId, secret }), }); const data = await resp.json().catch(() => null); if (resp.ok && data && data.success === true && typeof data.secret_id === 'string' && data.secret_id !== '') { return { ok: true, secretId: data.secret_id }; } const message = data && typeof data.error === 'string' && data.error !== '' ? data.error : `Could not store the secret (HTTP ${resp.status}).`; return { ok: false, error: message }; } catch { return { ok: false, error: 'Could not reach the control panel to store the secret.' }; } }