diff --git a/REVIEW.md b/REVIEW.md index 0024ac1..05e10b5 100644 --- a/REVIEW.md +++ b/REVIEW.md @@ -590,10 +590,69 @@ Verified on a real Alpine 3.21 machine, driving the compiled binary through a PT Added to the release matrix, so Alpine-based OpenClaw containers get a working binary. -## Still open (not fixed) +## Resolved in the third pass -**Payments are received but never recorded.** `src/routes/payment.js` handles `order_created` +**Payments are received but never recorded** — the payment surface was removed entirely. Original finding: `src/routes/payment.js` handles `order_created` with `// TODO: Mark fix as paid in database`. With Lemon Squeezy configured a user can be charged $2 and nothing in the system records it or unlocks anything. The checkout path is otherwise sound. This needs a product decision — record and gate on payment, or remove the paid path until it is real — so it is flagged rather than guessed at. + + +--- + +# Third pass — payment removal and real break-fix scenarios + +## Payment surface removed + +`/api/checkout` created real Lemon Squeezy sessions while `order_created` was +`// TODO: Mark fix as paid in database`, so a configured store could charge $2 and record +nothing. Removed the checkout endpoint, the payment page, the payment webhook and the unused +Lemon Squeezy verifier. Svix verification stays — the Resend inbound-email webhook uses it. +`security-regressions` now asserts those routes stay 404. + +## Five break-fix scenarios on a real OpenClaw 2026.6.11 install + +Each scenario resets the box to a valid config, breaks one real thing, and scans. + +| # | Broken | ClawFix reported | Detected | +|---|---|---|---| +| 1 | Gateway process killed | `[critical] Gateway is not running` | **yes** | +| 2 | Port 18789 held by an unrelated process | `[critical] Port conflict detected` + `Gateway port 18789 is occupied by python3 (PID 726), but OpenClaw cannot reach it` | **yes** | +| 3 | `openclaw.json` corrupted | `[high] JSON5 parse failed: SyntaxError: JSON5: invalid character 'i' at 1:8` | **yes** | +| 4 | Gateway auth set to `none` on loopback | `[critical] Gateway auth missing on loopback` + `[medium] Gateway HTTP APIs are reachable without auth` | **yes** | +| 5 | `update.auto.enabled = true` | `[medium] Auto-update enabled (risk of restart loops)` | **yes** | + +**Detection: 5/5.** Scenario 2 is notably good — it names the squatting process and PID. + +## Repairing: the honest picture + +The guarded-repair pipeline ran end to end against the real install: plan built locally, +`invalid_token` rejected, single-use token enforced (`token_reused`), apply executed the real +`openclaw gateway restart`, verify re-checked the port, and the outcome was reported as +`verify_failed` — with a rescan agreeing the gateway was still down. + +That is the correct behaviour, and it is the B-24 fix working: before this pass the same run +would have claimed `applied`. + +But it is not a fix, and the reason matters: + +**ClawFix ships exactly one executable repair, and it cannot work in a container.** +`gateway-not-running` invokes `openclaw gateway restart`, which needs systemd or launchd. On +this Alpine host OpenClaw answered *"Gateway service disabled … systemd user services are +unavailable"*, so the repair could not succeed no matter how correct the plumbing is. Nothing in +the catalog can repair scenarios 2–5 either: the four other real problems ClawFix detects have +no repair at all. + +So today ClawFix is a strong **diagnostic** with one repair that only applies on service-managed +hosts. The site and README should say that plainly, and the catalog is the obvious place to +invest next — port-conflict and config-invalid are both detected precisely enough to repair. + +## Two quality issues seen only in real output + +- A finding surfaced as `[critical] timeout` with no other context — a native collector timing + out is being presented to the user as a critical OpenClaw problem + (`cli/core/diagnostics.js`, `text: finding.title || finding.message`). +- Raw parser output is used as a user-facing title: `JSON5 parse failed: SyntaxError: JSON5: + invalid character 'i' at 1:8`. Accurate, but it is a stack-trace fragment where a sentence + belongs. diff --git a/scripts/prove-remediation.mjs b/scripts/prove-remediation.mjs index 0ca2d60..4885935 100644 --- a/scripts/prove-remediation.mjs +++ b/scripts/prove-remediation.mjs @@ -53,15 +53,17 @@ await check('unknown Codex findings cannot inherit unrelated repairs', async () }]), []); }); -await check('results and payment routes validate fix IDs', async () => { - const [results, payment] = await Promise.all([ - text('src/routes/results.js'), - text('src/routes/payment.js'), - ]); +await check('results route validates fix IDs and the payment surface stays removed', async () => { + const results = await text('src/routes/results.js'); assert.match(results, /validateFixId\(req\.params\.fixId\)/); assert.match(results, /escapeHtml\(data\.analysis/); - assert.match(payment, /validateFixId\(req\.params\.fixId\)/); - assert.match(payment, /validateFixId\(req\.body\?\.fixId\)/); + + // Payments were removed: checkout created real Lemon Squeezy sessions while the + // order_created handler was a TODO, so a store could charge and record nothing. + const { existsSync } = await import('node:fs'); + assert.equal(existsSync(new URL('../src/routes/payment.js', import.meta.url)), false); + const server = await text('src/server.js'); + assert.doesNotMatch(server, /paymentRouter/); }); await check('CLI requires consent and recursively redacts every upload', async () => { diff --git a/src/landing.js b/src/landing.js index 963a8ad..cad7804 100644 --- a/src/landing.js +++ b/src/landing.js @@ -612,7 +612,7 @@ gateway-restart repair. Want me to?
You get a commented fix script. Read it, understand it, then run it. Nothing happens without your approval.
+You get a commented fix script. Read it, understand it, then run it. Nothing happens without your approval. One reviewed repair runs automatically today — restarting a stopped gateway on a host with systemd or launchd; everything else is diagnosis and guidance.
The CLI and server are MIT licensed. clawfix.dev is currently free to use.
The CLI and server are MIT licensed. clawfix.dev is free to use, with no paid tier and no payment path in the product.
Hosted limits may change. Self-hosting remains available under the MIT license.
diff --git a/src/routes/payment.js b/src/routes/payment.js deleted file mode 100644 index e4c14e3..0000000 --- a/src/routes/payment.js +++ /dev/null @@ -1,245 +0,0 @@ -import { Router } from 'express'; -import { validateFixId } from '../../cli/bin/security.js'; -import { verifyLemonSqueezySignature } from '../webhook-signatures.js'; - -export const paymentRouter = Router(); - -/** - * Payment integration — Lemon Squeezy for card payments, x402 for USDC on Base. - * - * Flow: - * 1. User gets a fix (free for pattern-matching, paid for AI analysis) - * 2. Results page shows payment button - * 3. Clicking opens Lemon Squeezy checkout overlay or x402 flow - * 4. Webhook confirms payment - * - * Environment variables: - * LEMONSQUEEZY_API_KEY — API key from Lemon Squeezy dashboard - * LEMONSQUEEZY_STORE_ID — Store ID - * LEMONSQUEEZY_VARIANT_ID — Product variant ID for "AI Fix" ($2) - * LEMONSQUEEZY_WEBHOOK_SECRET — Webhook signing secret - * PAYMENT_WALLET — Wallet address for USDC payments - */ - -const LS_CONFIG = { - apiKey: process.env.LEMONSQUEEZY_API_KEY, - storeId: process.env.LEMONSQUEEZY_STORE_ID, - variantId: process.env.LEMONSQUEEZY_VARIANT_ID, - webhookSecret: process.env.LEMONSQUEEZY_WEBHOOK_SECRET, -}; - -const configuredWallet = process.env.PAYMENT_WALLET || ''; -const WALLET = /^0x[a-fA-F0-9]{40}$/.test(configuredWallet) ? configuredWallet : ''; - -// Create a checkout session for a fix -paymentRouter.post('/checkout', async (req, res) => { - const fixId = validateFixId(req.body?.fixId); - - if (!fixId) { - return res.status(400).json({ error: 'A valid fixId is required' }); - } - - if (!LS_CONFIG.apiKey || !LS_CONFIG.storeId || !LS_CONFIG.variantId) { - return res.json({ - checkoutUrl: null, - message: 'Payment not yet configured. Fix is free during beta!', - walletAddress: WALLET, - chain: 'Base', - amount: '2 USDC', - }); - } - - try { - // Create Lemon Squeezy checkout - const response = await fetch('https://api.lemonsqueezy.com/v1/checkouts', { - method: 'POST', - headers: { - 'Authorization': `Bearer ${LS_CONFIG.apiKey}`, - 'Content-Type': 'application/vnd.api+json', - 'Accept': 'application/vnd.api+json', - }, - body: JSON.stringify({ - data: { - type: 'checkouts', - attributes: { - custom_price: null, // Use variant price - product_options: { - name: `ClawFix AI Diagnosis — ${fixId}`, - description: 'AI-powered diagnostic and fix script for your OpenClaw installation.', - }, - checkout_options: { - dark: true, - logo: false, - }, - checkout_data: { - custom: { - fix_id: fixId, - }, - }, - expires_at: new Date(Date.now() + 24 * 60 * 60 * 1000).toISOString(), // 24h expiry - }, - relationships: { - store: { data: { type: 'stores', id: LS_CONFIG.storeId } }, - variant: { data: { type: 'variants', id: LS_CONFIG.variantId } }, - }, - }, - }), - }); - - if (!response.ok) { - const err = await response.text(); - console.error('Lemon Squeezy error:', err); - throw new Error('Payment provider error'); - } - - const data = await response.json(); - const checkoutUrl = data.data.attributes.url; - - res.json({ - checkoutUrl, - fixId, - // Also offer crypto option - crypto: { - walletAddress: WALLET, - chain: 'Base', - amount: '2', - currency: 'USDC', - }, - }); - } catch (error) { - console.error('Checkout error:', error.message); - res.status(500).json({ - error: 'Failed to create checkout', - message: 'Try paying with USDC instead', - crypto: { - walletAddress: WALLET, - chain: 'Base', - amount: '2', - currency: 'USDC', - }, - }); - } -}); - -// Lemon Squeezy webhook handler -paymentRouter.post('/webhook/lemonsqueezy', async (req, res) => { - // Fail closed: an unsigned request is never a payment notification, and an unconfigured - // secret must not turn this into an open endpoint that logs whatever it is sent. - const verified = verifyLemonSqueezySignature({ - secret: LS_CONFIG.webhookSecret, - rawBody: req.rawBody, - signature: req.headers['x-signature'], - }); - if (!verified.ok) { - console.warn(`Rejected Lemon Squeezy webhook: ${verified.reason}`); - return res.status(401).json({ error: 'Invalid signature' }); - } - - const event = req.headers['x-event-name']; - const data = req.body; - - console.log(`Lemon Squeezy webhook: ${event}`); - - if (event === 'order_created') { - const fixId = data.meta?.custom_data?.fix_id; - const amount = data.data?.attributes?.total_formatted; - console.log(`💰 Payment received! Fix: ${fixId}, Amount: ${amount}`); - // TODO: Mark fix as paid in database - } - - res.json({ received: true }); -}); - -// Payment status page -paymentRouter.get('/pay/:fixId', (req, res) => { - const fixId = validateFixId(req.params.fixId); - if (!fixId) return res.status(400).type('text/plain').send('Invalid fix ID'); - const lsConfigured = !!(LS_CONFIG.apiKey && LS_CONFIG.storeId && LS_CONFIG.variantId); - - res.setHeader('Content-Type', 'text/html'); - res.send(paymentPage(fixId, lsConfigured)); -}); - -function paymentPage(fixId, lsConfigured) { - return ` - - - - -🎉 Free during beta!
All fixes are free while we're in beta. Enjoy!
Fix ID: ${fixId}
- ← View Your Fix - ` : ` -AI diagnosis + fix script for your OpenClaw
- - -— or —
- - - `} - - -This diagnosis is on us. Enjoy full AI analysis for free while we\\'re in early access.
';
+ html += '
ClawFix is MIT licensed and free to use.
';
html += 'Like ClawFix? Star us on GitHub — it helps a lot.