feat(sql): the database helper and the TOTP helper, as subcommands

`crafter-review sql` hands the SQL to the driver's broker named in
REVIEW_GATES_DB; the broker runs it as one transaction, rolled back, and
streams the lines back with the exit code last. The client speaks protocol
3.0 over Crafter.Network's TCP client: cleartext, MD5 and SCRAM-SHA-256,
one Parse per statement, the transaction status as the backstop. The
splitter, the refusals, the table layout and every message work on code
points; connect and socket errors read "[Errno 111] Connection refused"
or "timed out". `crafter-review totp` prints "123456 (valid 17s)".

No sleep after the last connect attempt; TOTP reads the clock once; a
broker token that is no string gets the NO DATABASE answer and the exit
mark.

The goldens add the splitter, the comment stripping, the fingerprint, the
refusals, the first words, the headline, the cell, table, error and notice
layouts, the run's refusals and limits and 40 TOTP codes (2774 checks
now). tests/PgWire runs the helper over a Postgres stand-in and compares
its output with the goldens, line for line, plus the broker's protocol.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Jorijn van der Graaf 2026-10-31 07:20:00 +01:00
commit e8a58f893f
Signed by: jorijnvdgraaf
GPG key ID: 2937E59CDCC1BCFB
10 changed files with 3666 additions and 3 deletions

View file

@ -10,6 +10,8 @@ import :Process;
import :Inventory;
import :Config;
import :Freeze;
import :PgWire;
import :Totp;
import std;
namespace fs = std::filesystem;
@ -247,6 +249,10 @@ inventory unit, against a frozen commit range.)";
const std::string_view command = rest[at];
const std::span<const std::string_view> after = rest.subspan(at + 1);
try {
// The helpers a lens runs: they read neither the home nor the
// config, only their arguments, stdin and REVIEW_GATES_DB.
if (command == "sql") return PgWire::SqlMain(after);
if (command == "totp") return Totp::Main(after);
if (command == "inventory" && !configPath) return Inventory::Main(after);
if (command == "config") {
if (!after.empty()) return UsageError(Usage, "crafter-review", std::format("unrecognized arguments: {}", after.front()));

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,97 @@
// SPDX-License-Identifier: AGPL-3.0-only
// SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts®
module Crafter.Review:Totp_impl;
import :Totp;
import :Json;
import :Text;
import :Crypto;
import :Platform;
import std;
namespace Crafter::Review::Totp {
namespace {
constexpr std::string_view Usage = R"(A current TOTP code (RFC 6238, SHA-1, 30 s, 6 digits) for a login fixture - what the functional lens types into the
one-time-code field.
crafter-review totp <fixture.json> the fixture's `totpSecret` (base32), as the consumer's login seeder writes it
crafter-review totp --secret <base32>
Prints the code and the seconds left in its window; the lens regenerates when the window rolled over.)";
// Floor division and modulo of floats, for a positive step: fmod, then
// the quotient of what is left, floored.
double FloorDiv(double x, double y) {
const double mod = std::fmod(x, y);
double div = (x - mod) / y;
if (mod != 0.0 && ((y < 0) != (mod < 0))) div -= 1.0;
if (div == 0.0) return std::copysign(0.0, x / y);
double floored = std::floor(div);
if (div - floored > 0.5) floored += 1.0;
return floored;
}
double Mod(double x, double y) {
double mod = std::fmod(x, y);
if (mod != 0.0 && ((y < 0) != (mod < 0))) mod += y;
return mod;
}
}
Code At(std::string_view secret, double now, std::int32_t step, std::int32_t digits) {
// secret.strip().replace(' ', '').upper().rstrip('='), then padded.
const std::string s = Dyn::RStrip(Dyn::Upper(Dyn::Replace(Dyn::Strip(secret), " ", "")), "=");
const std::size_t length = Dyn::Length(s);
const std::string padded = s + std::string((8 - length % 8) % 8, '=');
if (!std::ranges::all_of(padded, [](char c) { return static_cast<std::uint8_t>(c) < 0x80; })) {
throw std::invalid_argument("string argument should contain only ASCII characters");
}
const std::string key = Base32Decode(padded);
const auto counter = static_cast<std::uint64_t>(FloorDiv(now, step));
std::string message(8, '\0');
for (std::int32_t i = 0; i < 8; ++i) message[static_cast<std::size_t>(i)] = static_cast<char>((counter >> (56 - 8 * i)) & 0xFF);
const std::string h = HmacSha1(key, message);
const std::size_t o = static_cast<std::uint8_t>(h.back()) & 0x0F;
std::uint32_t value = 0;
for (std::size_t i = 0; i < 4; ++i) value = (value << 8) | static_cast<std::uint8_t>(h[o + i]);
std::uint64_t modulus = 1;
for (std::int32_t i = 0; i < digits; ++i) modulus *= 10;
std::string code = std::to_string((value & 0x7FFFFFFF) % modulus);
if (code.size() < static_cast<std::size_t>(digits)) code.insert(0, static_cast<std::size_t>(digits) - code.size(), '0');
return {code, step - static_cast<std::int32_t>(Mod(now, step))};
}
std::int32_t Main(std::span<const std::string_view> args) {
std::string secret;
try {
if (args.size() == 2 && args[0] == "--secret") {
secret = std::string(args[1]);
} else if (args.size() == 1) {
const Json fixture = Dyn::LoadJsonFile(std::string(args[0]));
const Json* found = nullptr;
for (const std::string_view key : {"totpSecret", "totp_secret", "secret"}) {
const Json& value = Dyn::Get(fixture, key);
if (Dyn::Truthy(value)) {
found = &value;
break;
}
}
if (!found) {
std::println(std::cerr, "{}: no totpSecret in the fixture", args[0]);
return 1;
}
if (!found->is_string()) throw Dyn::Error("AttributeError", std::format("'{}' object has no attribute 'strip'", Dyn::TypeName(*found)));
secret = found->get<std::string>();
} else {
std::println(std::cerr, "{}", Usage);
return 1;
}
const Code code = At(secret, Platform::Now());
std::println("{} (valid {}s)", code.code, code.left);
return 0;
} catch (const std::exception& error) {
std::println(std::cerr, "totp: {}", error.what());
return 1;
}
}
}