/* * Copyright (C) 2024-present Puter Technologies Inc. * * This file is part of Puter. * * Puter is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as published * by the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see . */ const crypto = require('crypto'); const fs = require('fs'); const path = require('path'); const readline = require('readline'); const CONFIG_PATH = path.join(__dirname, '.test-webhook-config.json'); function randomHex (bytes) { return crypto.randomBytes(bytes).toString('hex'); } function loadConfig () { try { const raw = fs.readFileSync(CONFIG_PATH, 'utf8'); const data = JSON.parse(raw); if ( data && typeof data.key === 'string' && typeof data.webhook_secret === 'string' ) { const out = { key: data.key, webhook_secret: data.webhook_secret, nonce: typeof data.nonce === 'number' ? data.nonce : 0, }; if ( typeof data.instance_url === 'string' && data.instance_url.trim() !== '' ) { out.instance_url = data.instance_url.trim().replace(/\/+$/, ''); } return out; } } catch (e) { const is_not_found = e.code === 'ENOENT'; if ( ! is_not_found ) { console.error('Saved config exists but could not be read:', e); } } return null; } /** * Saves a dotfile beside the script so new configuration doesn't need to be * re-entered into Puter every time this script is used. * @param {*} peerId - The peer ID to save. * @param {*} webhookSecret - The webhook secret to save. * @param {*} nonce - The nonce to save. * @param {*} instanceUrl - The instance URL to save. */ function saveConfig (peerId, webhookSecret, nonce, instanceUrl) { const payload = { key: peerId, webhook_secret: webhookSecret, nonce, }; if ( typeof instanceUrl === 'string' && instanceUrl.trim() !== '' ) { payload.instance_url = instanceUrl.trim().replace(/\/+$/, ''); } fs.writeFileSync(CONFIG_PATH, JSON.stringify(payload, null, 2), 'utf8'); } /** * This wrapper around readline.question is used to promisify the interface * and remove whitespace from the input. * * @param {*} rl * @param {*} question * @param {*} defaultAnswer * @returns {Promise} - The trimmed answer. */ function ask (rl, question, defaultAnswer = '') { const prompt = defaultAnswer ? `${question} [${defaultAnswer}]: ` : `${question} `; return new Promise((resolve) => { rl.question(prompt, (answer) => { const trimmed = answer.trim(); resolve(trimmed !== '' ? trimmed : defaultAnswer); }); }); } async function main () { const rl = readline.createInterface({ input: process.stdin, output: process.stdout }); let peerId; let webhookSecret; let nonce; const existing = loadConfig(); if ( existing ) { const useExisting = await ask(rl, 'Existing key found. Use it? (y/n)', 'y'); const noAnswers = ['n', 'no']; if ( noAnswers.includes(useExisting.toLowerCase()) ) { peerId = `test-webhook-${randomHex(8)}`; webhookSecret = randomHex(32); nonce = 0; saveConfig(peerId, webhookSecret, nonce, existing.instance_url); console.log(''); console.log('New key generated.'); console.log(''); console.log('Add the following peer to your Puter instance config so it can accept'); console.log('webhooks from this test script. In your config file (e.g. config.json),'); console.log('under the "broadcast" section, add a "peers" array (if missing) and'); console.log('include this entry:'); console.log(''); console.log(JSON.stringify({ key: peerId, webhook_secret: webhookSecret, }, null, 2)); console.log(''); console.log('Example config structure:'); console.log(' "broadcast": {'); console.log(' "peers": ['); console.log(' { "key": "", "webhook_secret": "" }'); console.log(' ]'); console.log(' }'); console.log(''); console.log('Restart your Puter instance after updating the config.'); console.log(''); } else { peerId = existing.key; webhookSecret = existing.webhook_secret; nonce = existing.nonce; console.log(''); console.log('Using existing key:', peerId); console.log(''); } } else { peerId = `test-webhook-${randomHex(8)}`; webhookSecret = randomHex(32); nonce = 0; saveConfig(peerId, webhookSecret, nonce, undefined); console.log(''); console.log('Add the following peer to your Puter instance config so it can accept'); console.log('webhooks from this test script. In your config file (e.g. config.json),'); console.log('under the "broadcast" section, add a "peers" array (if missing) and'); console.log('include this entry:'); console.log(''); console.log(JSON.stringify({ key: peerId, webhook_secret: webhookSecret, }, null, 2)); console.log(''); console.log('Example config structure:'); console.log(' "broadcast": {'); console.log(' "peers": ['); console.log(' { "key": "", "webhook_secret": "" }'); console.log(' ]'); console.log(' }'); console.log(''); console.log('Restart your Puter instance after updating the config.'); console.log(''); } const defaultUrl = existing && existing.instance_url ? existing.instance_url : ''; const baseUrl = await ask(rl, 'Instance base URL (e.g. http://api.puter.localhost:4100)', defaultUrl); const url = baseUrl.trim().replace(/\/+$/, ''); if ( ! url ) { console.error('Please provide a URL.'); rl.close(); process.exit(1); } const webhookUrl = `${url}/broadcast/webhook`; const timestamp = Math.floor(Date.now() / 1000); const body = { key: 'test', data: { contents: 'I am a test message from test-webhook.js' }, meta: {}, }; const rawBody = JSON.stringify(body); const payloadToSign = `${timestamp}.${nonce}.${rawBody}`; const signature = crypto.createHmac('sha256', webhookSecret).update(payloadToSign).digest('hex'); try { const res = await fetch(webhookUrl, { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-Broadcast-Peer-Id': peerId, 'X-Broadcast-Timestamp': String(timestamp), 'X-Broadcast-Nonce': String(nonce), 'X-Broadcast-Signature': signature, }, body: rawBody, }); rl.close(); if ( res.ok ) { saveConfig(peerId, webhookSecret, nonce + 1, url); console.log(''); console.log('Test event sent successfully. Status:', res.status); const text = await res.text(); if ( text ) console.log('Response:', text); process.exit(0); } else { const text = await res.text(); console.error(''); console.error('Request failed. Status:', res.status, res.statusText); if ( text ) console.error('Response:', text); process.exit(1); } } catch ( err ) { rl.close(); console.error(''); console.error('Request failed:', err.message); process.exit(1); } } main();