feat(dojobay): add DojoBay app (manifest, image, catalog, ports)
This commit is contained in:
@@ -0,0 +1,111 @@
|
||||
#!/usr/bin/env node
|
||||
// =============================================================================
|
||||
// The Dojo Bay — audit stored signed pairing blocks.
|
||||
//
|
||||
// READ-ONLY. Walks every record in the submission store and re-checks its
|
||||
// stored `signed` block with exactly the gate the submit endpoint uses
|
||||
// (verifySignedPayload over canonicalPairing(payload), against the notification
|
||||
// address of the record's own payment code). Nothing is written, no network is
|
||||
// touched, and the store is only ever read.
|
||||
//
|
||||
// Why this exists: records approved before the signed-message parser was fixed
|
||||
// were checked by a parser that excised the BIP47 tail before verifying, so the
|
||||
// verdict they received then is not the verdict they would receive now. This
|
||||
// tells you whether anything was left behind.
|
||||
//
|
||||
// Run on the box as the deploy user:
|
||||
// cd /var/www/dojobay/server && node audit-signed.mjs
|
||||
// SERVER_DATA_DIR defaults to ./data, the same path the server uses; set it
|
||||
// only if your store lives elsewhere.
|
||||
//
|
||||
// Buckets:
|
||||
// VERIFIED the stored signature is valid for one of the record's codes
|
||||
// FAILED a signature is present but verifies for none of them
|
||||
// UNSIGNED no signature stored (pre-gate migration, or a code-less record)
|
||||
// ERROR the record could not be evaluated at all
|
||||
// Exits non-zero if anything is FAILED, ERROR or UNSIGNED, so it can back a
|
||||
// cron check. UNSIGNED counted as a failure since the signature became a
|
||||
// structural requirement: the store refuses to write such a record and the
|
||||
// rebuild withholds it, so one showing up here is not awaiting a decision.
|
||||
// =============================================================================
|
||||
import { store } from "./store.ts";
|
||||
import { verifySignedPayload, notificationAddresses, canonicalPairing } from "./crypto.ts";
|
||||
|
||||
const networkOf = (rec) => (rec.network === "testnet" ? "testnet" : "bitcoin");
|
||||
|
||||
// Exported so the test suite can assert this reproduces the gate's verdict.
|
||||
// This MUST mirror server/index.mjs's signature gate exactly: same canonical
|
||||
// message, and the same set of acceptable signing addresses. An earlier version
|
||||
// derived the notification address for the record's own network, which meant
|
||||
// every testnet listing was reported as failing even though the gate accepted
|
||||
// it, because a PayNym signs from its mainnet address whatever the node is.
|
||||
export function auditRecord(rec) {
|
||||
const codes = Array.isArray(rec.paymentCodes) ? rec.paymentCodes : [];
|
||||
if (!rec.signed) {
|
||||
return { bucket: "UNSIGNED", detail: codes.length ? "record has a payment code but no signed block" : "no signed block and no payment code" };
|
||||
}
|
||||
const net = networkOf(rec);
|
||||
const expectedMessage = canonicalPairing(rec.payload);
|
||||
const tried = [];
|
||||
// A PayNym may have signed with either BIP47 variant, so every code on the
|
||||
// record is a legitimate candidate; the first that verifies wins.
|
||||
for (const code of codes) {
|
||||
const addrs = notificationAddresses(code);
|
||||
if (!addrs.length) { tried.push(`${code.slice(0, 12)}…: undecodable code`); continue; }
|
||||
const r = verifySignedPayload({ signedText: rec.signed, expectedMessage, expectedAddress: addrs, network: net });
|
||||
if (r.ok) return { bucket: "VERIFIED", detail: `${code.slice(0, 12)}… → ${addrs[0]}` };
|
||||
tried.push(`${code.slice(0, 12)}… (${addrs.join(" / ")}): ${r.error}`);
|
||||
}
|
||||
if (!codes.length) {
|
||||
const r = verifySignedPayload({ signedText: rec.signed, expectedMessage, network: net });
|
||||
return r.ok
|
||||
? { bucket: "FAILED", detail: "signature is internally valid but the record carries no payment code to bind it to" }
|
||||
: { bucket: "FAILED", detail: r.error };
|
||||
}
|
||||
return { bucket: "FAILED", detail: tried.join("\n ") };
|
||||
}
|
||||
|
||||
// ---- CLI ---------------------------------------------------------------
|
||||
// Only runs when executed directly, so tests can import auditRecord.
|
||||
const isMain = process.argv[1] && import.meta.url === new URL(`file://${process.argv[1]}`).href;
|
||||
if (!isMain) { /* imported for testing */ } else {
|
||||
|
||||
const recs = (await store.listSubmissions())
|
||||
.sort((a, b) => (a.network + a.name).localeCompare(b.network + b.name));
|
||||
|
||||
const buckets = { VERIFIED: [], FAILED: [], UNSIGNED: [], ERROR: [] };
|
||||
for (const rec of recs) {
|
||||
let res;
|
||||
try { res = auditRecord(rec); } catch (e) { res = { bucket: "ERROR", detail: e.message }; }
|
||||
buckets[res.bucket].push({ rec, detail: res.detail });
|
||||
}
|
||||
|
||||
console.log(`Audited ${recs.length} record(s) in the store.\n`);
|
||||
for (const b of ["FAILED", "ERROR", "UNSIGNED", "VERIFIED"]) {
|
||||
if (!buckets[b].length) continue;
|
||||
console.log(`${b}: ${buckets[b].length}`);
|
||||
for (const { rec, detail } of buckets[b]) {
|
||||
// Show the name as well as the id. Ids are immutable (reliability history
|
||||
// keys on them), so a record created before operator naming keeps its
|
||||
// payment-code-derived id even after its operator sets a name, and the id
|
||||
// alone is then unrecognisable.
|
||||
const label = rec.name && `${rec.network}-${rec.name}` !== rec.id ? `${rec.id} ("${rec.name}")` : rec.id;
|
||||
console.log(` [${b}] ${label} (${rec.status})${detail ? "\n " + detail : ""}`);
|
||||
}
|
||||
console.log("");
|
||||
}
|
||||
|
||||
// An UNSIGNED record is now a failure, not a decision. Until the signature rule
|
||||
// existed there was a legitimate answer to "this record predates the gate" and
|
||||
// the audit deliberately left the judgement to a maintainer. The store now
|
||||
// refuses to write such a record and the rebuild withholds it, so one appearing
|
||||
// here means something got in around those rules or predates them, and either
|
||||
// way it is not being published and needs dealing with.
|
||||
const bad = buckets.FAILED.length + buckets.ERROR.length + buckets.UNSIGNED.length;
|
||||
console.log(
|
||||
`Summary: ${buckets.VERIFIED.length} verified, ${buckets.FAILED.length} failed, ` +
|
||||
`${buckets.UNSIGNED.length} unsigned, ${buckets.ERROR.length} error.` +
|
||||
(buckets.UNSIGNED.length ? "\nUNSIGNED records are withheld from the public list. Ask the operator to sign their\npairing payload and resubmit, or remove the listing with server/remove-listing.ts." : "") +
|
||||
(bad ? `\nNON-ZERO EXIT: ${bad} record(s) need attention.` : "\nEvery record carries a signature and every signature verifies under the current gate."));
|
||||
process.exit(bad ? 1 : 0);
|
||||
}
|
||||
Reference in New Issue
Block a user