Webhook signing, usage-event compaction, timelocked emergency withdrawal, discount codes - #719
Open
frienzy514-png wants to merge 3 commits into
Open
Conversation
Every webhook registered via POST /api/webhooks/low-balance now gets a per-provider signing secret (generated automatically if not supplied, returned exactly once in the registration response). Outbound deliveries from the webhook registry (fireWebhook / retry queue / shutdown drain) and the legacy PROVIDER_WEBHOOK_URL path now carry an `X-Signature-256: sha256=<hmac>` header, computed as HMAC-SHA256(secret, rawBody), with a global WEBHOOK_SECRET env var as fallback. Also fixes a bug in checkAndNotifyLowBalance() where registry-fired webhooks silently never fired unless the legacy PROVIDER_WEBHOOK_URL env var happened to also be set. Adds backend/src/lib/webhookSignature.ts with the shared sign/verify helpers, documents verification steps in backend/API.md, and repairs the mqtt-webhook.integration.test.ts webhook-registry tests (wrong call arity, non-functional cross-test cleanup) so they actually exercise the signing path. Closes Dev-AdeTutu#688
…n job usage_events grows ~1KB/event and reaches multiple GB within months at fleet scale. Adds compactUsageEvents() (Closes Dev-AdeTutu#685): - Keeps detailed 'submitted' rows for USAGE_DETAIL_RETENTION_DAYS (90) - Rolls older rows into a new usage_summary table, aggregated per (date, meter_id) — total_units, total_cost, event_count — via a single SQL upsert so repeated runs accumulate correctly instead of overwriting - Archives rows older than USAGE_ARCHIVE_RETENTION_DAYS (365) to a gzipped JSONL file under USAGE_ARCHIVE_DIR before deleting them, so raw records survive even though they leave the live DB (point the dir at an s3fs/gcsfuse mount to treat it as real cold storage) - Leaves 'pending'/'failed' events alone regardless of age so retry/replay keeps working - Runs VACUUM after deleting rows to reclaim disk space - Scheduled daily at 02:00 UTC via startUsageCompactionWorker(), wired into server startup alongside the existing retry worker Also adds POST /api/usage-events/compact (run on demand) and GET /api/usage-events/summary (query the rollup), both admin-authed, plus two Prometheus counters and a real-SQLite integration test suite. Closes Dev-AdeTutu#685
Closes Dev-AdeTutu#686 — emergency admin fund withdrawal function: - Reworks emergency_withdraw into a two-step, timelocked flow: `emergency_withdraw(amount, recipient)` first call announces the intent and starts a 48h timelock (emits withdrawal_announced); calling again with the same amount/recipient after the timelock elapses executes the transfer, capped at the contract's current token balance (emits emergency_withdrawal). Still requires the contract to be frozen and the caller to be admin, like the previous immediate-drain version. - Caps the withdrawable amount at TOTAL_REVENUE, a new cumulative (never-decremented) counter of gross revenue ever collected via make_payment / make_payment_with_discount — so a compromised admin key can't drain more than customers have actually paid in, regardless of the contract's raw token balance. - Adds cancel_emergency_withdrawal (clears a pending announcement) and get_pending_emergency_withdrawal (view). - Replaces the previous immediate full-drain emergency_withdraw(to) — that version had no cap and no timelock, which is exactly the "immediate withdrawal: security risk if admin key compromised" scenario Dev-AdeTutu#686 calls out avoiding. Closes Dev-AdeTutu#687 — promotional discount codes: - New DiscountCode type + DataKey::DiscountCode(code) storage, and admin_create_discount / admin_revoke_discount / get_discount / is_discount_valid. - make_payment_with_discount(meter_id, payer, amount, plan, code) validates the code (exists, active, not expired, under its usage limit), computes final_cost = amount * (100 - discount_pct) / 100, increments the usage counter, and charges final_cost through the same payment path as make_payment (extracted into a shared execute_payment helper). Emits discount_applied and returns the final charged amount. New ContractError variants: TimelockNotElapsed, AmountExceedsRevenue, NoWithdrawalAnnounced, DiscountCodeNotFound/AlreadyExists/Inactive/Expired/ Exhausted, InvalidDiscountPercent. README.md documents the new event topics. Verification note: the crate's `#[cfg(test)] mod tests` block does not currently compile on main (191 pre-existing errors — mostly Symbol-vs- String meter_id mismatches, plus a ContractEvents::iter API drift), unrelated to this change and out of scope to fix here. Verified instead via `cargo check` (library-only, clean) and `cargo build --target wasm32v1-none --release` (clean); new tests for both features were confirmed to add zero new compiler errors by diffing `cargo check --tests` output before/after (191 errors either way) and cross-referencing error line numbers against the new code's line ranges.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Implements the 4 issues assigned to me on this repo:
#688 — Webhook signing (
backend/)POST /api/webhooks/low-balancenow gets a per-provider HMAC signing secret (auto-generated if not supplied, returned once in the response).fireWebhook/retry queue/shutdown drain, and the legacyPROVIDER_WEBHOOK_URLpath) now carryX-Signature-256: sha256=<hmac>, with a globalWEBHOOK_SECRETenv var as fallback.PROVIDER_WEBHOOK_URLenv var also happened to be set.backend/src/lib/webhookSignature.ts; verification steps documented inbackend/API.md.#685 — Usage-event retention/compaction (
backend/)compactUsageEvents(): rollssubmittedevents older than 90 days into a newusage_summarytable (per(date, meter_id)), archives events older than 365 days to a gzipped JSONL file before deleting them, leavespending/failedevents untouched, and runsVACUUM.startUsageCompactionWorker); also exposed viaPOST /api/usage-events/compact(run on demand) andGET /api/usage-events/summary(query the rollup).#686 — Timelocked emergency withdrawal (
contracts/)emergency_withdraw(amount, recipient)is now a two-step, timelocked flow: first call announces and starts a 48h timelock; calling again with the same args after the timelock elapses executes the transfer (capped at the contract's balance).TOTAL_REVENUE), so a compromised admin key can't drain more than customers actually paid in.cancel_emergency_withdrawalandget_pending_emergency_withdrawal.#687 — Promotional discount codes (
contracts/)admin_create_discount/admin_revoke_discount/get_discount/is_discount_valid.make_payment_with_discount(meter_id, payer, amount, plan, code)validates the code and chargesamount * (100 - discount_pct) / 100through the same payment path asmake_payment.Testing
backend/tests/usage-event-compaction.integration.test.ts, updatedbackend/tests/mqtt-webhook.integration.test.ts) — all pass undernpm test. Also repaired pre-existing wrong-arityregisterWebhookcalls in the webhook integration suite so it actually exercises the signing path (was silently broken before this PR).cargo check(library) andcargo build --target wasm32v1-none --releaseare clean. Note:contracts/solar_grid's#[cfg(test)] mod testsblock does not currently compile onmain— 191 pre-existing errors (mostlySymbolpassed wheremeter_id: Stringis now expected, plus aContractEvents::iterAPI drift), unrelated to this PR. New tests for both contract features were added and confirmed to introduce zero new compiler errors (191 before and after, cross-referenced by line range againstcargo check --testsoutput), but can't be run end-to-end until that pre-existing breakage is fixed separately.