From e934826b0a6f4c175e88935351d4eb64db7e9c09 Mon Sep 17 00:00:00 2001
From: Maghuro <1546139+maghuro@users.noreply.github.com>
Date: Wed, 14 Oct 2026 21:45:15 +0100
Subject: [PATCH 01/26] fix(bonanza): reconcile real chat HTML and timestamp
buckets
---
.../DarkPeers_BONanza_Giveaway.user.js | 116 ++++++++++++------
userscripts/giveaway/src/00-preamble.js | 5 +-
userscripts/giveaway/src/06-lifecycle.js | 111 ++++++++++++-----
3 files changed, 162 insertions(+), 70 deletions(-)
diff --git a/userscripts/giveaway/DarkPeers_BONanza_Giveaway.user.js b/userscripts/giveaway/DarkPeers_BONanza_Giveaway.user.js
index 863b14d..ea831d5 100644
--- a/userscripts/giveaway/DarkPeers_BONanza_Giveaway.user.js
+++ b/userscripts/giveaway/DarkPeers_BONanza_Giveaway.user.js
@@ -2,7 +2,7 @@
// @name DarkPeers BONanza Giveaway | Maghuro Fork
// @namespace https://darkpeers.org/users/maghuro
// @description BON giveaways on DarkPeers with an optional direct contribution to the BON Pool
-// @version 1.5.15
+// @version 1.5.16
// @author 🤖 T.R.A.V.I.S., Maghuro & M.A.E.S.T.R.O.
// @homepageURL https://darkpeers.org/users/maghuro
// @updateURL https://gist.githubusercontent.com/maghuro/da2dbfec94951990cbc54e75a9aee318/raw/DarkPeers_BONanza_Giveaway.meta.js
@@ -20,6 +20,9 @@
// DarkPeers-only fork of "Blutopia BON Giveaway" v6.2.2 by Nums (GPL-3.0-or-later).
// Changes in this fork:
+// - v1.5.16 reconciles real DarkPeers HTML/JoyPixels and second-resolution
+// timestamps after acknowledged opening POSTs. Ambiguous/duplicate
+// announcements remain blocked; sponsor boundary stays conservative.
// - All non-DarkPeers tracker support, the upload.cx extra commands and the
// openuserjs update check have been removed.
// - A host-selected percentage (0-50%, steps of 5) of the final pot is
@@ -4203,24 +4206,20 @@ body.host-panel-dragging * {
// An HTTP timeout is NOT evidence the server rejected the POST. Never
// release a pending opening snapshot merely because sendMessage returned
// false; an announced giveaway may already exist publicly.
+ // UNIT3D returns HTML (with JoyPixels images) for BBCode chat announcements.
+ // Its second-resolution timestamps cannot fit inside a millisecond POST.
async function verifyPendingGiveawayOpening(checkpoint) {
if (!checkpoint ||
(checkpoint.status !== "pending" && checkpoint.status !== "confirmed") ||
!checkpoint.preparedMessage || !Number.isFinite(Number(checkpoint.startedAt))) {
return null;
}
-
const afterId = optionalFiniteNumber(checkpoint.afterMessageId);
- // A missing pre-send cursor makes same-second identical announcements
- // indistinguishable. Never auto-confirm such a snapshot or payout.
if (!Number.isSafeInteger(afterId) || afterId <= 0) return null;
const startedAt = Number(checkpoint.startedAt);
const persistedHardEnd = optionalFiniteNumber(checkpoint.windowEndAt);
const completedAt = optionalFiniteNumber(checkpoint.sendCompletedAt);
const absoluteHardEnd = startedAt + OPENING_POST_MAX_PUBLICATION_WINDOW_MS;
- // An absent completion time means the tab may have crashed mid-POST:
- // the pre-persisted hard window is still authoritative. A recorded
- // completion can only NARROW the window, never extend it.
const windowEnd = Math.min(
absoluteHardEnd,
persistedHardEnd === null ? absoluteHardEnd : persistedHardEnd,
@@ -4229,13 +4228,50 @@ body.host-panel-dragging * {
if (!Number.isFinite(startedAt) || !Number.isFinite(windowEnd) ||
windowEnd <= startedAt) return null;
const senderId = Math.floor(Number(OT_USER_ID));
- if (!Number.isFinite(senderId) || senderId <= 0) return null;
+ if (!Number.isSafeInteger(senderId) || senderId <= 0) return null;
+ const decodeEntities = value => String(value).replace(
+ /&(#x[0-9a-f]+|#\d+|amp|lt|gt|quot|apos|nbsp);/gi,
+ (entity, token) => {
+ const t = token.toLowerCase();
+ if (t[0] === "#") {
+ const isHex = t[1] === "x";
+ const n = Number.parseInt(t.slice(isHex ? 2 : 1), isHex ? 16 : 10);
+ return Number.isInteger(n) && n >= 0 && n <= 0x10ffff &&
+ !(n >= 0xd800 && n <= 0xdfff)
+ ? String.fromCodePoint(n) : entity;
+ }
+ return ({amp: "&", lt: "<", gt: ">", quot: '"',
+ apos: "'", nbsp: " "})[t] || entity;
+ }
+ );
+ const canonical = (value, rendered) => {
+ let content = String(value ?? "");
+ if (rendered) {
+ content = content.replace(/
]*>/gi, tag => {
+ const alt = tag.match(/\balt\s*=\s*(["'])([\s\S]*?)\1/i);
+ return alt ? alt[2] : "\uFFFD";
+ }).replace(/
/gi, "\n")
+ .replace(/<\/(?:p|div)>/gi, "\n")
+ .replace(/<[^>]*>/g, "");
+ } else {
+ content = content.replace(
+ /\[(?:\/)?(?:b|i|u|s|color(?:=[^\]]*)?|size(?:=[^\]]*)?|url(?:=[^\]]*)?)\]/gi,
+ ""
+ );
+ }
+ return decodeEntities(content).replace(/[\s\u00a0]+/g, "");
+ };
+ const expected = canonical(checkpoint.preparedMessage, false);
+ if (!expected) return null;
+ const expectedHost = String(checkpoint.host ||
+ (typeof getLoggedInUsername === "function" ? getLoggedInUsername() : "")
+ ).trim().replace(/^@/, "").toLowerCase();
for (let attempt = 0; attempt < 3; attempt++) {
if (!ownsTabLock()) return null;
try {
- const url = new URL(`/api/chat/messages/${DARKPEERS_MAIN_CHATROOM_ID}`, location.origin);
- if (afterId !== null) url.searchParams.set("after_id", String(afterId));
+ const url = new URL("/api/chat/messages/" + DARKPEERS_MAIN_CHATROOM_ID, location.origin);
+ url.searchParams.set("after_id", String(afterId));
const response = await fetchWithTimeout(
url, { credentials: "include", cache: "no-store" }, 5000
);
@@ -4244,46 +4280,53 @@ body.host-panel-dragging * {
const payload = await response.json();
if (!ownsTabLock()) return null;
if (!Array.isArray(payload?.data)) throw new Error("Malformed Main Chat messages");
-
+ const matches = [];
for (const message of payload.data) {
const id = optionalFiniteNumber(message?.id);
if (!Number.isSafeInteger(id) || id <= afterId) continue;
- // A confirmed checkpoint must identify the SAME original
- // announcement after reload. Never replace it with a later
- // identical post, even inside the bounded send window.
if (checkpoint.status === "confirmed" &&
id !== optionalFiniteNumber(checkpoint.messageId)) continue;
- const authorId = Math.floor(Number(message?.user_id ?? message?.user?.id));
- if (authorId !== senderId) continue;
- if (String(message?.message ?? "") !== checkpoint.preparedMessage) continue;
-
+ const knownAuthorIds = [
+ message?.user_id, message?.user?.id,
+ message?.sender?.id, message?.author?.id
+ ].filter(v => v != null && v !== "").map(Number)
+ .filter(v => Number.isSafeInteger(v) && v > 0);
+ if (knownAuthorIds.length) {
+ if (knownAuthorIds.some(authorId => authorId !== senderId)) continue;
+ } else {
+ const serverName = String(
+ message?.user?.username ?? message?.user?.name ??
+ message?.author?.username ?? message?.username ?? ""
+ ).trim().replace(/^@/, "").toLowerCase();
+ if (!expectedHost || serverName !== expectedHost) continue;
+ }
+ if (canonical(message?.message, true) !== expected) continue;
const rawTs = String(message?.created_at || "");
const timestamp = Date.parse(rawTs);
const resolutionMs = unit3dTimestampResolutionMs(rawTs);
- // Require the ENTIRE server timestamp bucket to be inside
- // the original, bounded opening POST window. A bucket that
- // overlaps the pre-send boundary is ambiguous and must not
- // confirm an unrelated same-second message. An identical
- // message sent after this attempt cannot confirm it either.
if (!Number.isFinite(timestamp) ||
- !Number.isFinite(resolutionMs) || resolutionMs <= 0 ||
- timestamp < startedAt ||
- (timestamp + resolutionMs) > windowEnd) continue;
-
- return {
+ !Number.isFinite(resolutionMs) || resolutionMs <= 0) continue;
+ const whollyInside =
+ !(timestamp < startedAt || (timestamp + resolutionMs) > windowEnd);
+ const overlapsAttempt =
+ timestamp < windowEnd && timestamp + resolutionMs > startedAt;
+ // Only positive HTTP POST acknowledgement permits accepting
+ // a unique overlapping server-second bucket. On ambiguous
+ // delivery the stricter wholly-contained rule still applies.
+ if (!whollyInside &&
+ !(checkpoint.sendAcknowledged === true && overlapsAttempt)) continue;
+ matches.push({
messageId: id,
- // Exclude the entire potentially ambiguous publication
- // timestamp bucket from sponsorship. The 45-minute
- // deadline remains anchored to the earlier send start.
sponsorStartTs: Math.max(startedAt, timestamp + resolutionMs)
- };
+ });
}
+ // Never guess between duplicate equal announcements.
+ if (matches.length === 1) return matches[0];
+ if (matches.length > 1) return null;
} catch (error) {
console.warn("[BON Giveaway] Opening reconciliation deferred:", error);
}
- if (attempt < 2) {
- await new Promise(resolve => setTimeout(resolve, 700));
- }
+ if (attempt < 2) await new Promise(resolve => setTimeout(resolve, 700));
}
return null;
}
@@ -4646,6 +4689,8 @@ body.host-panel-dragging * {
giveawayData.__sponsorAccountingVerified = false;
giveawayData.openingCheckpoint = {
status: "pending",
+ host: giveawayData.host,
+ sendAcknowledged: false,
startedAt: openingSendStartedAt,
afterMessageId,
windowEndAt: openingSendStartedAt + OPENING_POST_MAX_PUBLICATION_WINDOW_MS,
@@ -4670,6 +4715,7 @@ body.host-panel-dragging * {
// this document survived the POST. If it did not, recovery uses
// the previously persisted bounded upper limit instead.
giveawayData.openingCheckpoint.sendCompletedAt = Date.now();
+ giveawayData.openingCheckpoint.sendAcknowledged = introSent === true;
if (!snapshotGiveaway({ force: true, verifyWrite: true })) {
logEvent("Opening delivery checkpoint not durable",
"Could not preserve the end of the opening attempt; recovery remains pending.");
diff --git a/userscripts/giveaway/src/00-preamble.js b/userscripts/giveaway/src/00-preamble.js
index 75f7149..ac26ae2 100644
--- a/userscripts/giveaway/src/00-preamble.js
+++ b/userscripts/giveaway/src/00-preamble.js
@@ -2,7 +2,7 @@
// @name DarkPeers BONanza Giveaway | Maghuro Fork
// @namespace https://darkpeers.org/users/maghuro
// @description BON giveaways on DarkPeers with an optional direct contribution to the BON Pool
-// @version 1.5.15
+// @version 1.5.16
// @author 🤖 T.R.A.V.I.S., Maghuro & M.A.E.S.T.R.O.
// @homepageURL https://darkpeers.org/users/maghuro
// @updateURL https://gist.githubusercontent.com/maghuro/da2dbfec94951990cbc54e75a9aee318/raw/DarkPeers_BONanza_Giveaway.meta.js
@@ -20,6 +20,9 @@
// DarkPeers-only fork of "Blutopia BON Giveaway" v6.2.2 by Nums (GPL-3.0-or-later).
// Changes in this fork:
+// - v1.5.16 reconciles real DarkPeers HTML/JoyPixels and second-resolution
+// timestamps after acknowledged opening POSTs. Ambiguous/duplicate
+// announcements remain blocked; sponsor boundary stays conservative.
// - All non-DarkPeers tracker support, the upload.cx extra commands and the
// openuserjs update check have been removed.
// - A host-selected percentage (0-50%, steps of 5) of the final pot is
diff --git a/userscripts/giveaway/src/06-lifecycle.js b/userscripts/giveaway/src/06-lifecycle.js
index bf273f9..4a72a4d 100644
--- a/userscripts/giveaway/src/06-lifecycle.js
+++ b/userscripts/giveaway/src/06-lifecycle.js
@@ -10,24 +10,20 @@
// An HTTP timeout is NOT evidence the server rejected the POST. Never
// release a pending opening snapshot merely because sendMessage returned
// false; an announced giveaway may already exist publicly.
+ // UNIT3D returns HTML (with JoyPixels images) for BBCode chat announcements.
+ // Its second-resolution timestamps cannot fit inside a millisecond POST.
async function verifyPendingGiveawayOpening(checkpoint) {
if (!checkpoint ||
(checkpoint.status !== "pending" && checkpoint.status !== "confirmed") ||
!checkpoint.preparedMessage || !Number.isFinite(Number(checkpoint.startedAt))) {
return null;
}
-
const afterId = optionalFiniteNumber(checkpoint.afterMessageId);
- // A missing pre-send cursor makes same-second identical announcements
- // indistinguishable. Never auto-confirm such a snapshot or payout.
if (!Number.isSafeInteger(afterId) || afterId <= 0) return null;
const startedAt = Number(checkpoint.startedAt);
const persistedHardEnd = optionalFiniteNumber(checkpoint.windowEndAt);
const completedAt = optionalFiniteNumber(checkpoint.sendCompletedAt);
const absoluteHardEnd = startedAt + OPENING_POST_MAX_PUBLICATION_WINDOW_MS;
- // An absent completion time means the tab may have crashed mid-POST:
- // the pre-persisted hard window is still authoritative. A recorded
- // completion can only NARROW the window, never extend it.
const windowEnd = Math.min(
absoluteHardEnd,
persistedHardEnd === null ? absoluteHardEnd : persistedHardEnd,
@@ -36,13 +32,50 @@
if (!Number.isFinite(startedAt) || !Number.isFinite(windowEnd) ||
windowEnd <= startedAt) return null;
const senderId = Math.floor(Number(OT_USER_ID));
- if (!Number.isFinite(senderId) || senderId <= 0) return null;
+ if (!Number.isSafeInteger(senderId) || senderId <= 0) return null;
+ const decodeEntities = value => String(value).replace(
+ /&(#x[0-9a-f]+|#\d+|amp|lt|gt|quot|apos|nbsp);/gi,
+ (entity, token) => {
+ const t = token.toLowerCase();
+ if (t[0] === "#") {
+ const isHex = t[1] === "x";
+ const n = Number.parseInt(t.slice(isHex ? 2 : 1), isHex ? 16 : 10);
+ return Number.isInteger(n) && n >= 0 && n <= 0x10ffff &&
+ !(n >= 0xd800 && n <= 0xdfff)
+ ? String.fromCodePoint(n) : entity;
+ }
+ return ({amp: "&", lt: "<", gt: ">", quot: '"',
+ apos: "'", nbsp: " "})[t] || entity;
+ }
+ );
+ const canonical = (value, rendered) => {
+ let content = String(value ?? "");
+ if (rendered) {
+ content = content.replace(/
]*>/gi, tag => {
+ const alt = tag.match(/\balt\s*=\s*(["'])([\s\S]*?)\1/i);
+ return alt ? alt[2] : "\uFFFD";
+ }).replace(/
/gi, "\n")
+ .replace(/<\/(?:p|div)>/gi, "\n")
+ .replace(/<[^>]*>/g, "");
+ } else {
+ content = content.replace(
+ /\[(?:\/)?(?:b|i|u|s|color(?:=[^\]]*)?|size(?:=[^\]]*)?|url(?:=[^\]]*)?)\]/gi,
+ ""
+ );
+ }
+ return decodeEntities(content).replace(/[\s\u00a0]+/g, "");
+ };
+ const expected = canonical(checkpoint.preparedMessage, false);
+ if (!expected) return null;
+ const expectedHost = String(checkpoint.host ||
+ (typeof getLoggedInUsername === "function" ? getLoggedInUsername() : "")
+ ).trim().replace(/^@/, "").toLowerCase();
for (let attempt = 0; attempt < 3; attempt++) {
if (!ownsTabLock()) return null;
try {
- const url = new URL(`/api/chat/messages/${DARKPEERS_MAIN_CHATROOM_ID}`, location.origin);
- if (afterId !== null) url.searchParams.set("after_id", String(afterId));
+ const url = new URL("/api/chat/messages/" + DARKPEERS_MAIN_CHATROOM_ID, location.origin);
+ url.searchParams.set("after_id", String(afterId));
const response = await fetchWithTimeout(
url, { credentials: "include", cache: "no-store" }, 5000
);
@@ -51,46 +84,53 @@
const payload = await response.json();
if (!ownsTabLock()) return null;
if (!Array.isArray(payload?.data)) throw new Error("Malformed Main Chat messages");
-
+ const matches = [];
for (const message of payload.data) {
const id = optionalFiniteNumber(message?.id);
if (!Number.isSafeInteger(id) || id <= afterId) continue;
- // A confirmed checkpoint must identify the SAME original
- // announcement after reload. Never replace it with a later
- // identical post, even inside the bounded send window.
if (checkpoint.status === "confirmed" &&
id !== optionalFiniteNumber(checkpoint.messageId)) continue;
- const authorId = Math.floor(Number(message?.user_id ?? message?.user?.id));
- if (authorId !== senderId) continue;
- if (String(message?.message ?? "") !== checkpoint.preparedMessage) continue;
-
+ const knownAuthorIds = [
+ message?.user_id, message?.user?.id,
+ message?.sender?.id, message?.author?.id
+ ].filter(v => v != null && v !== "").map(Number)
+ .filter(v => Number.isSafeInteger(v) && v > 0);
+ if (knownAuthorIds.length) {
+ if (knownAuthorIds.some(authorId => authorId !== senderId)) continue;
+ } else {
+ const serverName = String(
+ message?.user?.username ?? message?.user?.name ??
+ message?.author?.username ?? message?.username ?? ""
+ ).trim().replace(/^@/, "").toLowerCase();
+ if (!expectedHost || serverName !== expectedHost) continue;
+ }
+ if (canonical(message?.message, true) !== expected) continue;
const rawTs = String(message?.created_at || "");
const timestamp = Date.parse(rawTs);
const resolutionMs = unit3dTimestampResolutionMs(rawTs);
- // Require the ENTIRE server timestamp bucket to be inside
- // the original, bounded opening POST window. A bucket that
- // overlaps the pre-send boundary is ambiguous and must not
- // confirm an unrelated same-second message. An identical
- // message sent after this attempt cannot confirm it either.
if (!Number.isFinite(timestamp) ||
- !Number.isFinite(resolutionMs) || resolutionMs <= 0 ||
- timestamp < startedAt ||
- (timestamp + resolutionMs) > windowEnd) continue;
-
- return {
+ !Number.isFinite(resolutionMs) || resolutionMs <= 0) continue;
+ const whollyInside =
+ !(timestamp < startedAt || (timestamp + resolutionMs) > windowEnd);
+ const overlapsAttempt =
+ timestamp < windowEnd && timestamp + resolutionMs > startedAt;
+ // Only positive HTTP POST acknowledgement permits accepting
+ // a unique overlapping server-second bucket. On ambiguous
+ // delivery the stricter wholly-contained rule still applies.
+ if (!whollyInside &&
+ !(checkpoint.sendAcknowledged === true && overlapsAttempt)) continue;
+ matches.push({
messageId: id,
- // Exclude the entire potentially ambiguous publication
- // timestamp bucket from sponsorship. The 45-minute
- // deadline remains anchored to the earlier send start.
sponsorStartTs: Math.max(startedAt, timestamp + resolutionMs)
- };
+ });
}
+ // Never guess between duplicate equal announcements.
+ if (matches.length === 1) return matches[0];
+ if (matches.length > 1) return null;
} catch (error) {
console.warn("[BON Giveaway] Opening reconciliation deferred:", error);
}
- if (attempt < 2) {
- await new Promise(resolve => setTimeout(resolve, 700));
- }
+ if (attempt < 2) await new Promise(resolve => setTimeout(resolve, 700));
}
return null;
}
@@ -453,6 +493,8 @@
giveawayData.__sponsorAccountingVerified = false;
giveawayData.openingCheckpoint = {
status: "pending",
+ host: giveawayData.host,
+ sendAcknowledged: false,
startedAt: openingSendStartedAt,
afterMessageId,
windowEndAt: openingSendStartedAt + OPENING_POST_MAX_PUBLICATION_WINDOW_MS,
@@ -477,6 +519,7 @@
// this document survived the POST. If it did not, recovery uses
// the previously persisted bounded upper limit instead.
giveawayData.openingCheckpoint.sendCompletedAt = Date.now();
+ giveawayData.openingCheckpoint.sendAcknowledged = introSent === true;
if (!snapshotGiveaway({ force: true, verifyWrite: true })) {
logEvent("Opening delivery checkpoint not durable",
"Could not preserve the end of the opening attempt; recovery remains pending.");
From 5b73906438967fb7fc2a2c22cebcc2d203073e65 Mon Sep 17 00:00:00 2001
From: Maghuro <1546139+maghuro@users.noreply.github.com>
Date: Wed, 14 Oct 2026 21:45:47 +0100
Subject: [PATCH 02/26] test(bonanza): reproduce live DarkPeers opening failure
and fail-closed cases
---
tests/bonanza-opening-real-api.test.mjs | 111 ++++++++++++++++++++++++
1 file changed, 111 insertions(+)
create mode 100644 tests/bonanza-opening-real-api.test.mjs
diff --git a/tests/bonanza-opening-real-api.test.mjs b/tests/bonanza-opening-real-api.test.mjs
new file mode 100644
index 0000000..f52ff3c
--- /dev/null
+++ b/tests/bonanza-opening-real-api.test.mjs
@@ -0,0 +1,111 @@
+import assert from "node:assert/strict";
+import { readFileSync } from "node:fs";
+import test from "node:test";
+import vm from "node:vm";
+
+const lifecycle = readFileSync(
+ new URL("../userscripts/giveaway/src/06-lifecycle.js", import.meta.url), "utf8"
+);
+const first = lifecycle.indexOf(" async function verifyPendingGiveawayOpening(");
+const last = lifecycle.indexOf("\n async function startGiveaway()", first);
+assert.ok(first !== -1 && last > first);
+const source = lifecycle.slice(first, last);
+const startMs = Date.parse("2026-10-14T20:25:29.220Z");
+const endMs = Date.parse("2026-10-14T20:25:29.403Z");
+const preparedMessage = "🎁[b][i][u]\u2063[/u][/i][/b] 💙 [b][color=#4FAFFF]BON POOL CONTRIBUTION G\u200bIVEAWAY[/color][/b]\nI am hosting a giveaway for [b]101 042 BON[/b]";
+const renderedMessage = '
\u2063
BON POOL CONTRIBUTION G\u200bIVEAWAY
I am hosting a giveaway for 101 042 BON';
+
+const real = (id = 629180, override = {}) => ({
+ id, user: { id: 10, username: "maghuro" },
+ message: renderedMessage,
+ created_at: "2026-10-14T20:25:29+00:00",
+ ...override
+});
+
+async function verify(records, opts = {}) {
+ const checkpoint = {
+ status: opts.status || "pending", host: "maghuro",
+ startedAt: startMs, sendCompletedAt: endMs,
+ windowEndAt: startMs + 10000, afterMessageId: 629175,
+ preparedMessage, sendAcknowledged: opts.ack !== false,
+ ...(opts.checkpoint || {})
+ };
+ const ctx = vm.createContext({
+ URL, location: { origin: "https://darkpeers.org" },
+ OT_USER_ID: 10, DARKPEERS_MAIN_CHATROOM_ID: 1,
+ OPENING_POST_MAX_PUBLICATION_WINDOW_MS: 10000,
+ ownsTabLock: () => true,
+ optionalFiniteNumber: v => v == null ? null : Number(v),
+ unit3dTimestampResolutionMs: str => /\.\d+/.test(String(str)) ? 1 : 1000,
+ fetchWithTimeout: async () => ({ ok: true, json: async () => ({ data: records }) }),
+ setTimeout: fn => fn(), console: { warn() {} }
+ });
+ vm.runInContext(source, ctx);
+ return ctx.verifyPendingGiveawayOpening(checkpoint);
+}
+
+test("real DarkPeers HTML, JoyPixels alt and whole-second timestamp confirm acknowledged POST", async () => {
+ const r = await verify([real()]);
+ assert.equal(r.messageId, 629180);
+ assert.equal(r.sponsorStartTs, Date.parse("2026-10-14T20:25:30Z"));
+});
+
+test("unacknowledged POST cannot use a partially overlapping server-second bucket", async () => {
+ assert.equal(await verify([real()], { ack: false }), null);
+});
+
+test("earlier identical chat announcement cannot confirm a later attempt", async () => {
+ assert.equal(await verify([real(629177, {
+ created_at: "2026-10-14T20:25:23+00:00"
+ })]), null);
+});
+
+test("duplicate same-second announcements remain unverified despite HTTP 200", async () => {
+ assert.equal(await verify([real(), real(629181)]), null);
+});
+
+test("author ID, before-send cursor and actual content are mandatory", async () => {
+ assert.equal(await verify([
+ real(629170),
+ real(629181, { user: { id: 11, username: "other" } }),
+ real(629182, { message: renderedMessage.replace("101 042", "101 043") })
+ ]), null);
+});
+
+test("conflicting numeric user ID defeats matching username", async () => {
+ assert.equal(await verify([real(629180, { user_id: 99 })]), null);
+});
+
+test("no numeric or username author evidence cannot confirm an opening", async () => {
+ assert.equal(await verify([real(629180, { user: undefined })]), null);
+});
+
+test("authenticated username fallback supports missing numeric user IDs", async () => {
+ assert.equal((await verify([
+ real(629180, { user: { username: "maghuro" } })
+ ])).messageId, 629180);
+});
+
+test("confirmed checkpoints are pinned to the saved message ID", async () => {
+ assert.equal(await verify([real()], {
+ status: "confirmed", checkpoint: { messageId: 629181 }
+ }), null);
+});
+
+test("publication after HTTP completion cannot confirm", async () => {
+ assert.equal(await verify([real(629182, {
+ created_at: "2026-10-14T20:25:30+00:00"
+ })]), null);
+});
+
+test("opening acknowledgement is durably saved before reconciliation", () => {
+ const post = lifecycle.indexOf("const introSent = await sendMessage(");
+ const ack = lifecycle.indexOf(
+ "giveawayData.openingCheckpoint.sendAcknowledged = introSent === true;", post
+ );
+ const persist = lifecycle.indexOf(
+ "snapshotGiveaway({ force: true, verifyWrite: true })", ack
+ );
+ const verified = lifecycle.indexOf("await verifyPendingGiveawayOpening(", ack);
+ assert.ok(post > 0 && ack > post && persist > ack && verified > persist);
+});
From 8fdfef97cd4e00fcfe2da05b00b9a57e491f3fe8 Mon Sep 17 00:00:00 2001
From: Maghuro <1546139+maghuro@users.noreply.github.com>
Date: Wed, 14 Oct 2026 21:47:08 +0100
Subject: [PATCH 03/26] test(bonanza): align hardening version assertion with
1.5.16
---
tests/bonanza-review-hardening.test.mjs | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/tests/bonanza-review-hardening.test.mjs b/tests/bonanza-review-hardening.test.mjs
index 1a646bf..823e276 100644
--- a/tests/bonanza-review-hardening.test.mjs
+++ b/tests/bonanza-review-hardening.test.mjs
@@ -9,7 +9,7 @@ const sourcePath = new URL(
const source = readFileSync(sourcePath, "utf8");
test("review hardening invariants stay present", () => {
- assert.match(source, /^\/\/ @version\s+1\.5\.15$/m);
+ assert.match(source, /^\/\/ @version\s+1\.5\.16$/m);
assert.doesNotMatch(source, /pollChatFallback/);
assert.doesNotMatch(source, /onlyguardians/i);
assert.match(source, /async function getLatestMainChatReplayBoundary\(\)/);
From 1f1fca177986839a8beafe84a24039c7d5f5add9 Mon Sep 17 00:00:00 2001
From: Maghuro <1546139+maghuro@users.noreply.github.com>
Date: Wed, 14 Oct 2026 22:09:37 +0100
Subject: [PATCH 04/26] fix(bonanza): require per-attempt invisible proof to
confirm coarse timestamp
---
userscripts/giveaway/src/06-lifecycle.js | 36 ++++++++++++++++++++----
1 file changed, 31 insertions(+), 5 deletions(-)
diff --git a/userscripts/giveaway/src/06-lifecycle.js b/userscripts/giveaway/src/06-lifecycle.js
index 4a72a4d..c53216a 100644
--- a/userscripts/giveaway/src/06-lifecycle.js
+++ b/userscripts/giveaway/src/06-lifecycle.js
@@ -6,6 +6,20 @@
// sent minutes/hours later as this giveaway's opening.
const OPENING_POST_MAX_PUBLICATION_WINDOW_MS = 10000;
+ // Unique live-opening proof encoded as non-rendering Unicode code points
+ // already observed surviving DarkPeers chat HTML and bridge markers.
+ function createOpeningProofMarker() {
+ if (!window.crypto || typeof window.crypto.getRandomValues !== "function") return null;
+ const bytes = new Uint8Array(16);
+ window.crypto.getRandomValues(bytes);
+ const bits = Array.from(bytes, b =>
+ Array.from({ length: 8 }, (_, i) =>
+ (b & (1 << (7 - i))) ? "\u200B" : "\u2063"
+ ).join("")
+ ).join("");
+ return "\u2063\u200B\u2063\u200B" + bits + "\u200B\u2063\u200B\u2063";
+ }
+
// Reconcile an opening announcement against authoritative Main Chat history.
// An HTTP timeout is NOT evidence the server rejected the POST. Never
// release a pending opening snapshot merely because sendMessage returned
@@ -65,6 +79,10 @@
}
return decodeEntities(content).replace(/[\s\u00a0]+/g, "");
};
+ const proof = checkpoint.openingProof;
+ const hasUniqueProof = typeof proof === "string" &&
+ /^\u2063\u200B\u2063\u200B[\u2063\u200B]{128}\u200B\u2063\u200B\u2063$/.test(proof) &&
+ checkpoint.preparedMessage.endsWith(proof);
const expected = canonical(checkpoint.preparedMessage, false);
if (!expected) return null;
const expectedHost = String(checkpoint.host ||
@@ -114,11 +132,12 @@
!(timestamp < startedAt || (timestamp + resolutionMs) > windowEnd);
const overlapsAttempt =
timestamp < windowEnd && timestamp + resolutionMs > startedAt;
- // Only positive HTTP POST acknowledgement permits accepting
- // a unique overlapping server-second bucket. On ambiguous
- // delivery the stricter wholly-contained rule still applies.
+ // A positive HTTP status alone does not identify a returned
+ // same-second announcement. Require a unique random marker
+ // persisted before POST. Legacy openings remain fail-closed.
if (!whollyInside &&
- !(checkpoint.sendAcknowledged === true && overlapsAttempt)) continue;
+ !(hasUniqueProof && overlapsAttempt &&
+ canonical(message?.message, true).endsWith(proof))) continue;
matches.push({
messageId: id,
sponsorStartTs: Math.max(startedAt, timestamp + resolutionMs)
@@ -486,7 +505,13 @@
// Deadline: conservative PRE-SEND time. Sponsorship: independently
// verified PUBLICATION time (never the earlier request timestamp).
const openingSendStartedAt = Date.now();
- const preparedOpening = prepareOutgoingMessage(introMessage);
+ const openingProof = REHEARSAL_MODE ? null : createOpeningProofMarker();
+ if (!REHEARSAL_MODE && !openingProof) {
+ window.alert("GIVEAWAY ERROR: Secure opening proof unavailable. No message was posted.");
+ resetGiveaway();
+ return;
+ }
+ const preparedOpening = prepareOutgoingMessage(introMessage) + (openingProof || "");
giveawayStartTime = new Date(openingSendStartedAt);
giveawayData.openedAt = openingSendStartedAt;
giveawayData.endTs = openingSendStartedAt + totalTimeMs;
@@ -494,6 +519,7 @@
giveawayData.openingCheckpoint = {
status: "pending",
host: giveawayData.host,
+ openingProof,
sendAcknowledged: false,
startedAt: openingSendStartedAt,
afterMessageId,
From 3bc07bc20ee6698e74303b1d3a21a006696f2951 Mon Sep 17 00:00:00 2001
From: Maghuro <1546139+maghuro@users.noreply.github.com>
Date: Wed, 14 Oct 2026 22:10:10 +0100
Subject: [PATCH 05/26] build(bonanza): sync generated script with unique proof
---
.../DarkPeers_BONanza_Giveaway.user.js | 36 ++++++++++++++++---
1 file changed, 31 insertions(+), 5 deletions(-)
diff --git a/userscripts/giveaway/DarkPeers_BONanza_Giveaway.user.js b/userscripts/giveaway/DarkPeers_BONanza_Giveaway.user.js
index ea831d5..279d971 100644
--- a/userscripts/giveaway/DarkPeers_BONanza_Giveaway.user.js
+++ b/userscripts/giveaway/DarkPeers_BONanza_Giveaway.user.js
@@ -4202,6 +4202,20 @@ body.host-panel-dragging * {
// sent minutes/hours later as this giveaway's opening.
const OPENING_POST_MAX_PUBLICATION_WINDOW_MS = 10000;
+ // Unique live-opening proof encoded as non-rendering Unicode code points
+ // already observed surviving DarkPeers chat HTML and bridge markers.
+ function createOpeningProofMarker() {
+ if (!window.crypto || typeof window.crypto.getRandomValues !== "function") return null;
+ const bytes = new Uint8Array(16);
+ window.crypto.getRandomValues(bytes);
+ const bits = Array.from(bytes, b =>
+ Array.from({ length: 8 }, (_, i) =>
+ (b & (1 << (7 - i))) ? "\u200B" : "\u2063"
+ ).join("")
+ ).join("");
+ return "\u2063\u200B\u2063\u200B" + bits + "\u200B\u2063\u200B\u2063";
+ }
+
// Reconcile an opening announcement against authoritative Main Chat history.
// An HTTP timeout is NOT evidence the server rejected the POST. Never
// release a pending opening snapshot merely because sendMessage returned
@@ -4261,6 +4275,10 @@ body.host-panel-dragging * {
}
return decodeEntities(content).replace(/[\s\u00a0]+/g, "");
};
+ const proof = checkpoint.openingProof;
+ const hasUniqueProof = typeof proof === "string" &&
+ /^\u2063\u200B\u2063\u200B[\u2063\u200B]{128}\u200B\u2063\u200B\u2063$/.test(proof) &&
+ checkpoint.preparedMessage.endsWith(proof);
const expected = canonical(checkpoint.preparedMessage, false);
if (!expected) return null;
const expectedHost = String(checkpoint.host ||
@@ -4310,11 +4328,12 @@ body.host-panel-dragging * {
!(timestamp < startedAt || (timestamp + resolutionMs) > windowEnd);
const overlapsAttempt =
timestamp < windowEnd && timestamp + resolutionMs > startedAt;
- // Only positive HTTP POST acknowledgement permits accepting
- // a unique overlapping server-second bucket. On ambiguous
- // delivery the stricter wholly-contained rule still applies.
+ // A positive HTTP status alone does not identify a returned
+ // same-second announcement. Require a unique random marker
+ // persisted before POST. Legacy openings remain fail-closed.
if (!whollyInside &&
- !(checkpoint.sendAcknowledged === true && overlapsAttempt)) continue;
+ !(hasUniqueProof && overlapsAttempt &&
+ canonical(message?.message, true).endsWith(proof))) continue;
matches.push({
messageId: id,
sponsorStartTs: Math.max(startedAt, timestamp + resolutionMs)
@@ -4682,7 +4701,13 @@ body.host-panel-dragging * {
// Deadline: conservative PRE-SEND time. Sponsorship: independently
// verified PUBLICATION time (never the earlier request timestamp).
const openingSendStartedAt = Date.now();
- const preparedOpening = prepareOutgoingMessage(introMessage);
+ const openingProof = REHEARSAL_MODE ? null : createOpeningProofMarker();
+ if (!REHEARSAL_MODE && !openingProof) {
+ window.alert("GIVEAWAY ERROR: Secure opening proof unavailable. No message was posted.");
+ resetGiveaway();
+ return;
+ }
+ const preparedOpening = prepareOutgoingMessage(introMessage) + (openingProof || "");
giveawayStartTime = new Date(openingSendStartedAt);
giveawayData.openedAt = openingSendStartedAt;
giveawayData.endTs = openingSendStartedAt + totalTimeMs;
@@ -4690,6 +4715,7 @@ body.host-panel-dragging * {
giveawayData.openingCheckpoint = {
status: "pending",
host: giveawayData.host,
+ openingProof,
sendAcknowledged: false,
startedAt: openingSendStartedAt,
afterMessageId,
From 11749063611b3e97b4b91921ce0a5de7432dcc9d Mon Sep 17 00:00:00 2001
From: Maghuro <1546139+maghuro@users.noreply.github.com>
Date: Wed, 14 Oct 2026 22:10:13 +0100
Subject: [PATCH 06/26] test(bonanza): cover random opening proof and uncertain
POST recovery
---
tests/bonanza-opening-real-api.test.mjs | 31 +++++++++++++++++++++----
1 file changed, 26 insertions(+), 5 deletions(-)
diff --git a/tests/bonanza-opening-real-api.test.mjs b/tests/bonanza-opening-real-api.test.mjs
index f52ff3c..4e9e38d 100644
--- a/tests/bonanza-opening-real-api.test.mjs
+++ b/tests/bonanza-opening-real-api.test.mjs
@@ -12,8 +12,10 @@ assert.ok(first !== -1 && last > first);
const source = lifecycle.slice(first, last);
const startMs = Date.parse("2026-10-14T20:25:29.220Z");
const endMs = Date.parse("2026-10-14T20:25:29.403Z");
-const preparedMessage = "🎁[b][i][u]\u2063[/u][/i][/b] 💙 [b][color=#4FAFFF]BON POOL CONTRIBUTION G\u200bIVEAWAY[/color][/b]\nI am hosting a giveaway for [b]101 042 BON[/b]";
-const renderedMessage = '
\u2063
BON POOL CONTRIBUTION G\u200bIVEAWAY
I am hosting a giveaway for 101 042 BON';
+const openingProof = "\u2063\u200B\u2063\u200B" +
+ "\u200B\u2063".repeat(64) + "\u200B\u2063\u200B\u2063";
+const preparedMessage = "🎁[b][i][u]\u2063[/u][/i][/b] 💙 [b][color=#4FAFFF]BON POOL CONTRIBUTION G\u200bIVEAWAY[/color][/b]\nI am hosting a giveaway for [b]101 042 BON[/b]" + openingProof;
+const renderedMessage = '
\u2063
BON POOL CONTRIBUTION G\u200bIVEAWAY
I am hosting a giveaway for 101 042 BON' + openingProof;
const real = (id = 629180, override = {}) => ({
id, user: { id: 10, username: "maghuro" },
@@ -27,7 +29,7 @@ async function verify(records, opts = {}) {
status: opts.status || "pending", host: "maghuro",
startedAt: startMs, sendCompletedAt: endMs,
windowEndAt: startMs + 10000, afterMessageId: 629175,
- preparedMessage, sendAcknowledged: opts.ack !== false,
+ preparedMessage, openingProof, sendAcknowledged: opts.ack !== false,
...(opts.checkpoint || {})
};
const ctx = vm.createContext({
@@ -50,8 +52,27 @@ test("real DarkPeers HTML, JoyPixels alt and whole-second timestamp confirm ackn
assert.equal(r.sponsorStartTs, Date.parse("2026-10-14T20:25:30Z"));
});
-test("unacknowledged POST cannot use a partially overlapping server-second bucket", async () => {
- assert.equal(await verify([real()], { ack: false }), null);
+test("durable unique proof recovers a posted opening even after lost HTTP acknowledgement", async () => {
+ assert.equal((await verify([real()], { ack: false })).messageId, 629180);
+});
+
+test("old checkpoints without an opening proof still cannot match partial server-second buckets", async () => {
+ assert.equal(await verify([real()], { checkpoint: { openingProof: null } }), null);
+});
+
+test("mismatched or copied opening proof fails despite matching text", async () => {
+ const differentMarker = "\u2063\u200B\u2063\u200B" +
+ "\u2063\u200B".repeat(64) + "\u200B\u2063\u200B\u2063";
+ assert.equal(await verify([real(629180, {
+ message: renderedMessage.replace(openingProof, differentMarker)
+ })]), null);
+});
+
+test("the opening path generates a fresh 128-bit proof before checkpoint and POST", () => {
+ const marker = lifecycle.indexOf("const openingProof = REHEARSAL_MODE ? null : createOpeningProofMarker()");
+ const saved = lifecycle.indexOf("openingProof,", marker);
+ const posted = lifecycle.indexOf("const introSent = await sendMessage(", marker);
+ assert.ok(marker > 0 && saved > marker && posted > saved);
});
test("earlier identical chat announcement cannot confirm a later attempt", async () => {
From 123871799eda349454881d93805c9dfebc6f7fa7 Mon Sep 17 00:00:00 2001
From: Maghuro <1546139+maghuro@users.noreply.github.com>
Date: Wed, 14 Oct 2026 22:12:45 +0100
Subject: [PATCH 07/26] fix(bonanza): show safe non-modal cancellation for
pending openings
---
userscripts/giveaway/src/05-initialization.js | 105 ++++++++++++++++--
1 file changed, 96 insertions(+), 9 deletions(-)
diff --git a/userscripts/giveaway/src/05-initialization.js b/userscripts/giveaway/src/05-initialization.js
index 43cca3a..a3ca1bc 100644
--- a/userscripts/giveaway/src/05-initialization.js
+++ b/userscripts/giveaway/src/05-initialization.js
@@ -1762,6 +1762,100 @@
sponsorRecoveryInterval = setInterval(retry, SPONSOR_GIFT_HISTORY_POLL_MS);
}
+ // An unconfirmed opening must never trap the host in repeated modal alerts.
+ // Expose an explicit, guarded archive/cancel action instead. A cancelled
+ // opening is never settled automatically, and sponsors must be checked by
+ // the host against authenticated Gift History.
+ function showUnverifiedOpeningControls(savedSnapshot) {
+ const checkpoint = savedSnapshot?.giveawayData?.openingCheckpoint;
+ if (!checkpoint || document.getElementById("bonanzaOpeningRecoveryControls")) return;
+
+ const node = document.createElement("aside");
+ node.id = "bonanzaOpeningRecoveryControls";
+ node.setAttribute("role", "status");
+ Object.assign(node.style, {
+ position: "fixed", top: "76px", right: "16px",
+ maxWidth: "390px", zIndex: "2147483646",
+ background: "#1c2730", color: "#fff",
+ padding: "14px", border: "2px solid #ffbd52",
+ borderRadius: "9px", boxShadow: "0 3px 18px #0008"
+ });
+ const msg = document.createElement("p");
+ msg.textContent = "BONanza opening unverified. No entries, settlement or BON transfers will resume. Check the public chat and Gift History before cancelling.";
+ node.appendChild(msg);
+
+ if (checkpoint.status === "pending") {
+ const button = document.createElement("button");
+ button.type = "button";
+ button.textContent = "Cancel & archive pending giveaway";
+ button.style.cssText = "padding:8px;cursor:pointer;border-radius:5px;";
+ button.addEventListener("click", async () => {
+ if (!window.confirm(
+ "Cancel this pending opening? This only archives local recovery state. " +
+ "It does NOT remove the public announcement or refund sponsor gifts."
+ )) return;
+ if (!await acquireTabLock()) {
+ window.alert("Cannot cancel while another tab owns the giveaway. Close other DarkPeers tabs.");
+ return;
+ }
+
+ let cancelled = false;
+ try {
+ const host = getLoggedInUsername();
+ const current = readActiveGiveawayRawForHost(host, { migrateLegacy: true });
+ if (!ownsTabLock() || !current.readOk || !current.hasStoredState ||
+ !current.raw || !current.storageKey) {
+ throw new Error("Saved recovery state is missing or cannot be read safely.");
+ }
+ const saved = JSON.parse(current.raw);
+ const data = saved?.giveawayData;
+ const proof = data?.openingCheckpoint;
+ if (!data || normalizeUserKey(data.host) !== normalizeUserKey(host) ||
+ (data.rehearsalMode === true) !== REHEARSAL_MODE ||
+ proof?.status !== "pending" ||
+ Number(proof.startedAt) !== Number(checkpoint.startedAt)) {
+ throw new Error("Saved recovery state has changed; cancellation stopped.");
+ }
+ if (data.settlement || data.closingIntent || data.closingNoticeSent ||
+ data.closingNoticeProgress) {
+ throw new Error("Settlement evidence exists. Manual audit is required.");
+ }
+
+ const archiveKey = [
+ "bonanza-giveaway-cancelled", location.hostname,
+ encodeURIComponent(normalizeUserKey(host)),
+ String(proof.startedAt)
+ ].join("::");
+ const existing = localStorage.getItem(archiveKey);
+ if (existing !== null && existing !== current.raw) {
+ throw new Error("A different archive already exists for this opening.");
+ }
+ if (existing === null) localStorage.setItem(archiveKey, current.raw);
+ if (localStorage.getItem(archiveKey) !== current.raw) {
+ throw new Error("Archive write verification failed. Active state retained.");
+ }
+ if (localStorage.getItem(current.storageKey) !== current.raw) {
+ throw new Error("Active state changed before cancellation. No deletion performed.");
+ }
+ localStorage.removeItem(current.storageKey);
+ if (localStorage.getItem(current.storageKey) !== null) {
+ throw new Error("Active snapshot deletion failed.");
+ }
+ cancelled = true;
+ console.info("[BON Giveaway] Pending opening archived (not settled):", archiveKey);
+ } catch (error) {
+ console.error("[BON Giveaway] Pending cancellation deferred:", error);
+ window.alert("Cancellation not completed: " + String(error?.message || error));
+ } finally {
+ releaseTabLock();
+ }
+ if (cancelled) window.location.reload();
+ });
+ node.appendChild(button);
+ }
+ document.body.appendChild(node);
+ }
+
async function restoreGiveawayFromSnapshot(snap) {
// The earlier snapshot is only a discovery hint. Another tab may
// persist a newer settlement while we await the Web Lock. Classify
@@ -1888,10 +1982,7 @@
if (!verified) {
logEvent("Opening recovery deferred",
"Main Chat could not prove the uncertain opening; existing checkpoint preserved.");
- window.alert(
- "GIVEAWAY RECOVERY PAUSED: The public opening could not be verified. " +
- "No giveaway settlement or BON transfer will run. Retry from Main Chat later."
- );
+ showUnverifiedOpeningControls(snap);
releaseTabLock();
return false;
}
@@ -1902,11 +1993,7 @@
if (overdue > EXPIRED_SNAPSHOT_SETTLEMENT_GRACE_MS) {
logEvent("Opening recovery requires manual review",
"Opening verified but its automatic settlement grace has expired.");
- window.alert(
- "GIVEAWAY RECOVERY PAUSED: An old public opening was verified, " +
- "but automatic settlement is unsafe after the recovery grace. " +
- "Keep the saved recovery state for manual review."
- );
+ showUnverifiedOpeningControls(snap);
releaseTabLock();
return false;
}
From 739ca66f6ff79c212996950a783c5c4fa1f8f803 Mon Sep 17 00:00:00 2001
From: Maghuro <1546139+maghuro@users.noreply.github.com>
Date: Wed, 14 Oct 2026 22:13:29 +0100
Subject: [PATCH 08/26] docs/test/build(bonanza): cover recoverable proof and
cancellation
---
userscripts/giveaway/src/00-preamble.js | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/userscripts/giveaway/src/00-preamble.js b/userscripts/giveaway/src/00-preamble.js
index ac26ae2..393ca45 100644
--- a/userscripts/giveaway/src/00-preamble.js
+++ b/userscripts/giveaway/src/00-preamble.js
@@ -21,8 +21,8 @@
// DarkPeers-only fork of "Blutopia BON Giveaway" v6.2.2 by Nums (GPL-3.0-or-later).
// Changes in this fork:
// - v1.5.16 reconciles real DarkPeers HTML/JoyPixels and second-resolution
-// timestamps after acknowledged opening POSTs. Ambiguous/duplicate
-// announcements remain blocked; sponsor boundary stays conservative.
+// timestamps using per-opening 128-bit invisible proof markers. Ambiguous
+// announcements stay blocked; failed openings offer guarded archive/cancel.
// - All non-DarkPeers tracker support, the upload.cx extra commands and the
// openuserjs update check have been removed.
// - A host-selected percentage (0-50%, steps of 5) of the final pot is
From eef1315294833aeb83a3af46d27ba7c8bc9fc8d5 Mon Sep 17 00:00:00 2001
From: Maghuro <1546139+maghuro@users.noreply.github.com>
Date: Wed, 14 Oct 2026 22:13:33 +0100
Subject: [PATCH 09/26] docs/test/build(bonanza): cover recoverable proof and
cancellation
---
.../DarkPeers_BONanza_Giveaway.user.js | 109 ++++++++++++++++--
1 file changed, 98 insertions(+), 11 deletions(-)
diff --git a/userscripts/giveaway/DarkPeers_BONanza_Giveaway.user.js b/userscripts/giveaway/DarkPeers_BONanza_Giveaway.user.js
index 279d971..8a289f8 100644
--- a/userscripts/giveaway/DarkPeers_BONanza_Giveaway.user.js
+++ b/userscripts/giveaway/DarkPeers_BONanza_Giveaway.user.js
@@ -21,8 +21,8 @@
// DarkPeers-only fork of "Blutopia BON Giveaway" v6.2.2 by Nums (GPL-3.0-or-later).
// Changes in this fork:
// - v1.5.16 reconciles real DarkPeers HTML/JoyPixels and second-resolution
-// timestamps after acknowledged opening POSTs. Ambiguous/duplicate
-// announcements remain blocked; sponsor boundary stays conservative.
+// timestamps using per-opening 128-bit invisible proof markers. Ambiguous
+// announcements stay blocked; failed openings offer guarded archive/cancel.
// - All non-DarkPeers tracker support, the upload.cx extra commands and the
// openuserjs update check have been removed.
// - A host-selected percentage (0-50%, steps of 5) of the final pot is
@@ -3608,6 +3608,100 @@ body.host-panel-dragging * {
sponsorRecoveryInterval = setInterval(retry, SPONSOR_GIFT_HISTORY_POLL_MS);
}
+ // An unconfirmed opening must never trap the host in repeated modal alerts.
+ // Expose an explicit, guarded archive/cancel action instead. A cancelled
+ // opening is never settled automatically, and sponsors must be checked by
+ // the host against authenticated Gift History.
+ function showUnverifiedOpeningControls(savedSnapshot) {
+ const checkpoint = savedSnapshot?.giveawayData?.openingCheckpoint;
+ if (!checkpoint || document.getElementById("bonanzaOpeningRecoveryControls")) return;
+
+ const node = document.createElement("aside");
+ node.id = "bonanzaOpeningRecoveryControls";
+ node.setAttribute("role", "status");
+ Object.assign(node.style, {
+ position: "fixed", top: "76px", right: "16px",
+ maxWidth: "390px", zIndex: "2147483646",
+ background: "#1c2730", color: "#fff",
+ padding: "14px", border: "2px solid #ffbd52",
+ borderRadius: "9px", boxShadow: "0 3px 18px #0008"
+ });
+ const msg = document.createElement("p");
+ msg.textContent = "BONanza opening unverified. No entries, settlement or BON transfers will resume. Check the public chat and Gift History before cancelling.";
+ node.appendChild(msg);
+
+ if (checkpoint.status === "pending") {
+ const button = document.createElement("button");
+ button.type = "button";
+ button.textContent = "Cancel & archive pending giveaway";
+ button.style.cssText = "padding:8px;cursor:pointer;border-radius:5px;";
+ button.addEventListener("click", async () => {
+ if (!window.confirm(
+ "Cancel this pending opening? This only archives local recovery state. " +
+ "It does NOT remove the public announcement or refund sponsor gifts."
+ )) return;
+ if (!await acquireTabLock()) {
+ window.alert("Cannot cancel while another tab owns the giveaway. Close other DarkPeers tabs.");
+ return;
+ }
+
+ let cancelled = false;
+ try {
+ const host = getLoggedInUsername();
+ const current = readActiveGiveawayRawForHost(host, { migrateLegacy: true });
+ if (!ownsTabLock() || !current.readOk || !current.hasStoredState ||
+ !current.raw || !current.storageKey) {
+ throw new Error("Saved recovery state is missing or cannot be read safely.");
+ }
+ const saved = JSON.parse(current.raw);
+ const data = saved?.giveawayData;
+ const proof = data?.openingCheckpoint;
+ if (!data || normalizeUserKey(data.host) !== normalizeUserKey(host) ||
+ (data.rehearsalMode === true) !== REHEARSAL_MODE ||
+ proof?.status !== "pending" ||
+ Number(proof.startedAt) !== Number(checkpoint.startedAt)) {
+ throw new Error("Saved recovery state has changed; cancellation stopped.");
+ }
+ if (data.settlement || data.closingIntent || data.closingNoticeSent ||
+ data.closingNoticeProgress) {
+ throw new Error("Settlement evidence exists. Manual audit is required.");
+ }
+
+ const archiveKey = [
+ "bonanza-giveaway-cancelled", location.hostname,
+ encodeURIComponent(normalizeUserKey(host)),
+ String(proof.startedAt)
+ ].join("::");
+ const existing = localStorage.getItem(archiveKey);
+ if (existing !== null && existing !== current.raw) {
+ throw new Error("A different archive already exists for this opening.");
+ }
+ if (existing === null) localStorage.setItem(archiveKey, current.raw);
+ if (localStorage.getItem(archiveKey) !== current.raw) {
+ throw new Error("Archive write verification failed. Active state retained.");
+ }
+ if (localStorage.getItem(current.storageKey) !== current.raw) {
+ throw new Error("Active state changed before cancellation. No deletion performed.");
+ }
+ localStorage.removeItem(current.storageKey);
+ if (localStorage.getItem(current.storageKey) !== null) {
+ throw new Error("Active snapshot deletion failed.");
+ }
+ cancelled = true;
+ console.info("[BON Giveaway] Pending opening archived (not settled):", archiveKey);
+ } catch (error) {
+ console.error("[BON Giveaway] Pending cancellation deferred:", error);
+ window.alert("Cancellation not completed: " + String(error?.message || error));
+ } finally {
+ releaseTabLock();
+ }
+ if (cancelled) window.location.reload();
+ });
+ node.appendChild(button);
+ }
+ document.body.appendChild(node);
+ }
+
async function restoreGiveawayFromSnapshot(snap) {
// The earlier snapshot is only a discovery hint. Another tab may
// persist a newer settlement while we await the Web Lock. Classify
@@ -3734,10 +3828,7 @@ body.host-panel-dragging * {
if (!verified) {
logEvent("Opening recovery deferred",
"Main Chat could not prove the uncertain opening; existing checkpoint preserved.");
- window.alert(
- "GIVEAWAY RECOVERY PAUSED: The public opening could not be verified. " +
- "No giveaway settlement or BON transfer will run. Retry from Main Chat later."
- );
+ showUnverifiedOpeningControls(snap);
releaseTabLock();
return false;
}
@@ -3748,11 +3839,7 @@ body.host-panel-dragging * {
if (overdue > EXPIRED_SNAPSHOT_SETTLEMENT_GRACE_MS) {
logEvent("Opening recovery requires manual review",
"Opening verified but its automatic settlement grace has expired.");
- window.alert(
- "GIVEAWAY RECOVERY PAUSED: An old public opening was verified, " +
- "but automatic settlement is unsafe after the recovery grace. " +
- "Keep the saved recovery state for manual review."
- );
+ showUnverifiedOpeningControls(snap);
releaseTabLock();
return false;
}
From 4c0f708537725022bbf2165a9d2456b952622924 Mon Sep 17 00:00:00 2001
From: Maghuro <1546139+maghuro@users.noreply.github.com>
Date: Wed, 14 Oct 2026 22:13:36 +0100
Subject: [PATCH 10/26] docs/test/build(bonanza): cover recoverable proof and
cancellation
---
tests/bonanza-opening-real-api.test.mjs | 17 +++++++++++++++++
1 file changed, 17 insertions(+)
diff --git a/tests/bonanza-opening-real-api.test.mjs b/tests/bonanza-opening-real-api.test.mjs
index 4e9e38d..bd5fb73 100644
--- a/tests/bonanza-opening-real-api.test.mjs
+++ b/tests/bonanza-opening-real-api.test.mjs
@@ -130,3 +130,20 @@ test("opening acknowledgement is durably saved before reconciliation", () => {
const verified = lifecycle.indexOf("await verifyPendingGiveawayOpening(", ack);
assert.ok(post > 0 && ack > post && persist > ack && verified > persist);
});
+
+test("pending opening offers a user-controlled non-modal archive, never automatic clearing", () => {
+ const initialization = readFileSync(
+ new URL("../userscripts/giveaway/src/05-initialization.js", import.meta.url), "utf8"
+ );
+ assert.match(initialization, /function showUnverifiedOpeningControls\(/);
+ assert.match(initialization, /Cancel & archive pending giveaway/);
+ const start = initialization.indexOf("async function restoreGiveawayFromSnapshot(");
+ const end = initialization.indexOf("function showUnverifiedOpeningControls(snap)", start);
+ assert.ok(start >= 0);
+ const recovery = initialization.slice(start);
+ assert.match(recovery, /showUnverifiedOpeningControls\(snap\);/);
+ const createArchive = initialization.indexOf("localStorage.setItem(archiveKey, current.raw)");
+ const verifyArchive = initialization.indexOf("localStorage.getItem(archiveKey) !== current.raw", createArchive);
+ const remove = initialization.indexOf("localStorage.removeItem(current.storageKey)", verifyArchive);
+ assert.ok(createArchive > 0 && verifyArchive > createArchive && remove > verifyArchive);
+});
From 4a9a70f120e3f27ee604b37692eabdadfd4a4f3c Mon Sep 17 00:00:00 2001
From: Maghuro <1546139+maghuro@users.noreply.github.com>
Date: Wed, 14 Oct 2026 22:13:39 +0100
Subject: [PATCH 11/26] docs/test/build(bonanza): cover recoverable proof and
cancellation
---
docs/runbooks/bonanza-operations.md | 20 ++++++++++++++++++++
1 file changed, 20 insertions(+)
diff --git a/docs/runbooks/bonanza-operations.md b/docs/runbooks/bonanza-operations.md
index f3c5a68..3468406 100644
--- a/docs/runbooks/bonanza-operations.md
+++ b/docs/runbooks/bonanza-operations.md
@@ -92,3 +92,23 @@ location.reload();
```
Never run a real giveaway while the REHEARSAL MODE banner is visible.
+
+## Unverified opening in LIVE mode
+
+If Main Chat shows an announcement but it cannot be proved by the userscript, the
+checkpoint stays pending and **no giveaway settlement, sponsor processing, or
+BON transfer runs**. The recovery alert is now a non-modal notice.
+
+Use **Cancel & archive pending giveaway** only after checking the Main Chat and
+canonical Gift History. It requires a manual confirmation and an exclusive tab
+lock. The script re-reads the latest exact pending checkpoint, rejects any
+settlement evidence, writes a verified original-state archive, and only then
+removes the active key. It does **not** delete public chat messages or refund
+gifts received from sponsors. Those remain a manual follow-up with donors.
+If the archive cannot be persisted, the active snapshot remains untouched.
+
+New LIVE announcements carry a cryptographic invisible 128-bit per-opening
+proof to disambiguate whole-second API timestamps, and older snapshots cannot
+be silently upgraded to that proof. Rehearsal mode cannot establish that an
+invisible proof survives the real Main Chat rendering pipeline; validate this
+against the site before merging a new release.
From 27a19dbd5acc4c484192a4ce3a0af00bf11acba9 Mon Sep 17 00:00:00 2001
From: Maghuro <1546139+maghuro@users.noreply.github.com>
Date: Wed, 14 Oct 2026 22:14:53 +0100
Subject: [PATCH 12/26] docs(bonanza): encode proof-bound opening safety
invariant
---
AGENTS.md | 19 +++++++++++++------
1 file changed, 13 insertions(+), 6 deletions(-)
diff --git a/AGENTS.md b/AGENTS.md
index 9a167cd..1d841d1 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -53,12 +53,19 @@ multiple userscripts, not just BONanza.
identical old announcement might falsely verify the new opening).
- Before posting the public giveaway opening, persist and read back a
**pending opening checkpoint** with the exact prepared announcement,
- pre-send timestamp, Main Chat cursor and a bounded publication window.
- When the send resolves, persist its completion timestamp to further
- narrow that window. Reconciliation must prove the **entire** server-side
- timestamp-resolution bucket lies inside the original opening attempt:
- same-second overlap or later identical announcements are NOT sufficient.
- ANY opening API failure, including non-OK HTTP responses, must never
+ pre-send timestamp, Main Chat cursor, a cryptographically random
+ **per-opening proof marker** and a bounded publication window.
+ The proof marker must be part of the exact posted payload, survive real
+ DarkPeers HTML rendering, and be verified against the persisted checkpoint
+ with the authenticated author and after-send cursor. The server's timestamp
+ bucket may overlap the original send window ONLY when that distinct durable
+ per-opening marker matches uniquely. Without a valid marker, require the
+ **entire** server timestamp-resolution bucket to lie inside the original
+ opening attempt. An HTTP 200 alone is never sufficient to identify an
+ otherwise-identical same-second announcement. Once the send resolves, persist
+ its completion timestamp to narrow the window. A duplicated marker is
+ ambiguous and must not confirm. Old snapshots cannot acquire a marker
+ retroactively. ANY opening API failure, including non-OK HTTP responses, must never
trigger the chatbox fallback: the server may already have published
the opening, so reconcile the original checkpoint instead.
A timeout/ownership loss is ambiguous: do not erase the checkpoint,
From a382e32c81b7982d9a15f7ff7903e29693c9ad5e Mon Sep 17 00:00:00 2001
From: Maghuro <1546139+maghuro@users.noreply.github.com>
Date: Wed, 14 Oct 2026 22:16:28 +0100
Subject: [PATCH 13/26] fix(bonanza): reject unsupported custom BBCode before
public opening
---
userscripts/giveaway/src/06-lifecycle.js | 14 ++++++++++++++
1 file changed, 14 insertions(+)
diff --git a/userscripts/giveaway/src/06-lifecycle.js b/userscripts/giveaway/src/06-lifecycle.js
index c53216a..f73568b 100644
--- a/userscripts/giveaway/src/06-lifecycle.js
+++ b/userscripts/giveaway/src/06-lifecycle.js
@@ -177,6 +177,20 @@
return;
}
+ // DarkPeers renders richer BBCode (quote/code/img etc.) to HTML whose
+ // text cannot be compared 1:1 to the original markup. Reject those
+ // custom inputs BEFORE a LIVE public POST, never after announcing it.
+ const customStartText = String(customMessageInput?.value || "");
+ const inlineTags = new Set(["b", "i", "u", "s", "color", "size", "url"]);
+ const unsupportedTag = [...customStartText.matchAll(/\[\/?([a-z][a-z0-9]*)(?:=[^\]]*)?\]/gi)]
+ .find(match => !inlineTags.has(match[1].toLowerCase()));
+ if (unsupportedTag || /<\/?[a-z][^>]*>/i.test(customStartText)) {
+ window.alert("GIVEAWAY ERROR: Custom message contains unsupported BBCode/HTML. " +
+ "Remove the markup before starting. Nothing was posted.");
+ customMessageInput?.focus();
+ return;
+ }
+
// Never overwrite a recoverable opening whose public POST was ambiguous.
// The pending snapshot is the only path to reconcile entrants/sponsors
// after a reload. A second start could otherwise destroy that evidence.
From 8892414f1a4561795105b566c0d0a643bc4547ba Mon Sep 17 00:00:00 2001
From: Maghuro <1546139+maghuro@users.noreply.github.com>
Date: Wed, 14 Oct 2026 22:16:50 +0100
Subject: [PATCH 14/26] build(bonanza): sync custom BBCode preflight
---
.../giveaway/DarkPeers_BONanza_Giveaway.user.js | 14 ++++++++++++++
1 file changed, 14 insertions(+)
diff --git a/userscripts/giveaway/DarkPeers_BONanza_Giveaway.user.js b/userscripts/giveaway/DarkPeers_BONanza_Giveaway.user.js
index 8a289f8..7a28447 100644
--- a/userscripts/giveaway/DarkPeers_BONanza_Giveaway.user.js
+++ b/userscripts/giveaway/DarkPeers_BONanza_Giveaway.user.js
@@ -4460,6 +4460,20 @@ body.host-panel-dragging * {
return;
}
+ // DarkPeers renders richer BBCode (quote/code/img etc.) to HTML whose
+ // text cannot be compared 1:1 to the original markup. Reject those
+ // custom inputs BEFORE a LIVE public POST, never after announcing it.
+ const customStartText = String(customMessageInput?.value || "");
+ const inlineTags = new Set(["b", "i", "u", "s", "color", "size", "url"]);
+ const unsupportedTag = [...customStartText.matchAll(/\[\/?([a-z][a-z0-9]*)(?:=[^\]]*)?\]/gi)]
+ .find(match => !inlineTags.has(match[1].toLowerCase()));
+ if (unsupportedTag || /<\/?[a-z][^>]*>/i.test(customStartText)) {
+ window.alert("GIVEAWAY ERROR: Custom message contains unsupported BBCode/HTML. " +
+ "Remove the markup before starting. Nothing was posted.");
+ customMessageInput?.focus();
+ return;
+ }
+
// Never overwrite a recoverable opening whose public POST was ambiguous.
// The pending snapshot is the only path to reconcile entrants/sponsors
// after a reload. A second start could otherwise destroy that evidence.
From d70f2e8f38c7bcd4b01bc2167a3e3fe5a6f74621 Mon Sep 17 00:00:00 2001
From: Maghuro <1546139+maghuro@users.noreply.github.com>
Date: Wed, 14 Oct 2026 22:16:54 +0100
Subject: [PATCH 15/26] test(bonanza): regression for unsupported custom BBCode
---
tests/bonanza-opening-real-api.test.mjs | 11 +++++++++++
1 file changed, 11 insertions(+)
diff --git a/tests/bonanza-opening-real-api.test.mjs b/tests/bonanza-opening-real-api.test.mjs
index bd5fb73..7100e8f 100644
--- a/tests/bonanza-opening-real-api.test.mjs
+++ b/tests/bonanza-opening-real-api.test.mjs
@@ -147,3 +147,14 @@ test("pending opening offers a user-controlled non-modal archive, never automati
const remove = initialization.indexOf("localStorage.removeItem(current.storageKey)", verifyArchive);
assert.ok(createArchive > 0 && verifyArchive > createArchive && remove > verifyArchive);
});
+
+test("unsupported custom BBCode is rejected before public POST", () => {
+ const beginning = lifecycle.indexOf("async function startGiveaway()");
+ const block = lifecycle.indexOf("const unsupportedTag =", beginning);
+ const preflight = lifecycle.indexOf("const openingSendStartedAt = Date.now()", beginning);
+ assert.ok(beginning >= 0 && block > beginning && preflight > block);
+ const guard = lifecycle.slice(block, preflight);
+ assert.match(guard, /inlineTags\.has\(match\[1\]\.toLowerCase\(\)\)/);
+ assert.match(guard, /Custom message contains unsupported BBCode\/HTML/);
+ assert.match(guard, /return;/);
+});
From 48fb66260db8d8d614eeaf79eeb0a49d469299ea Mon Sep 17 00:00:00 2001
From: Maghuro <1546139+maghuro@users.noreply.github.com>
Date: Wed, 14 Oct 2026 22:38:21 +0100
Subject: [PATCH 16/26] fix(bonanza): preserve confirmed opening after UNIT3D
bounded chat history rolls over
---
userscripts/giveaway/src/06-lifecycle.js | 20 ++++++++++++++++++++
1 file changed, 20 insertions(+)
diff --git a/userscripts/giveaway/src/06-lifecycle.js b/userscripts/giveaway/src/06-lifecycle.js
index f73568b..a211dee 100644
--- a/userscripts/giveaway/src/06-lifecycle.js
+++ b/userscripts/giveaway/src/06-lifecycle.js
@@ -146,6 +146,26 @@
// Never guess between duplicate equal announcements.
if (matches.length === 1) return matches[0];
if (matches.length > 1) return null;
+
+ // UNIT3D's GET /api/chat/messages/{room} returns only the last
+ // configured chat.message_limit messages (currently 100) and
+ // does not implement pagination or the after_id query parameter.
+ // A previously CONFIRMED opening with a durable random proof
+ // becomes older than that window during busy chats. When a
+ // complete healthy latest-page has advanced beyond its saved
+ // message ID, retain that earlier confirmation. Never apply
+ // this shortcut to a pending, legacy or still-in-range opening.
+ const confirmedId = optionalFiniteNumber(checkpoint.messageId);
+ const pageIds = payload.data.map(message =>
+ optionalFiniteNumber(message?.id)
+ );
+ if (checkpoint.status === "confirmed" && hasUniqueProof &&
+ Number.isSafeInteger(confirmedId) && confirmedId > afterId &&
+ pageIds.length >= 100 && pageIds.every(id =>
+ Number.isSafeInteger(id) && id > 0
+ ) && Math.min(...pageIds) > confirmedId) {
+ return { messageId: confirmedId, historyEvicted: true };
+ }
} catch (error) {
console.warn("[BON Giveaway] Opening reconciliation deferred:", error);
}
From d81297bf5ea642c2c5fdaf9d4a2aa94b60f2e6f4 Mon Sep 17 00:00:00 2001
From: Maghuro <1546139+maghuro@users.noreply.github.com>
Date: Wed, 14 Oct 2026 22:38:34 +0100
Subject: [PATCH 17/26] fix(bonanza): safely restore proven openings after
bounded chat history expires
---
userscripts/giveaway/src/05-initialization.js | 24 ++++++++++++++++++-
1 file changed, 23 insertions(+), 1 deletion(-)
diff --git a/userscripts/giveaway/src/05-initialization.js b/userscripts/giveaway/src/05-initialization.js
index a3ca1bc..70147e5 100644
--- a/userscripts/giveaway/src/05-initialization.js
+++ b/userscripts/giveaway/src/05-initialization.js
@@ -1955,10 +1955,32 @@
const savedSponsorStart = optionalFiniteNumber(
snap.giveawayData.sponsorStartTs
);
+ // The latest-100 chat page may no longer include a previously
+ // confirmed announcement. Its persisted random proof and exact
+ // message ID can remain authoritative ONLY when the healthy
+ // bounded history has clearly advanced beyond that ID. There
+ // is no UNIT3D pagination endpoint to query older messages.
+ // Revalidate the persisted sponsor boundary conservatively;
+ // do not derive a new one from a later, unrelated chat message.
+ const historicalConfirmation = verified?.historyEvicted === true;
+ const historicalSponsorBoundaryOk = historicalConfirmation &&
+ savedSponsorStart !== null &&
+ Number(restoredCheckpoint.startedAt) ===
+ Number(snap.giveawayData.openedAt) &&
+ Number.isFinite(Number(restoredCheckpoint.sendCompletedAt)) &&
+ Number(restoredCheckpoint.sendCompletedAt) >=
+ Number(restoredCheckpoint.startedAt) &&
+ savedSponsorStart > Number(restoredCheckpoint.startedAt) &&
+ savedSponsorStart <= Math.min(
+ Number(restoredCheckpoint.windowEndAt),
+ Number(restoredCheckpoint.sendCompletedAt) + 1000
+ );
if (!verified ||
verified.messageId !== optionalFiniteNumber(restoredCheckpoint.messageId) ||
savedSponsorStart === null ||
- verified.sponsorStartTs !== savedSponsorStart) {
+ (historicalConfirmation
+ ? !historicalSponsorBoundaryOk
+ : verified.sponsorStartTs !== savedSponsorStart)) {
logEvent("Confirmed opening recovery deferred",
"Authoritative Main Chat could not reprove the exact saved opening and sponsor boundary.");
window.alert(
From a51dd5cb515c0a26f3bd6253aa6f0af4e706ab03 Mon Sep 17 00:00:00 2001
From: Maghuro <1546139+maghuro@users.noreply.github.com>
Date: Wed, 14 Oct 2026 22:38:57 +0100
Subject: [PATCH 18/26] test(bonanza): simulate 100-message UNIT3D history
rollover during restore
---
tests/bonanza-opening-real-api.test.mjs | 46 +++++++++++++++++++++++++
1 file changed, 46 insertions(+)
diff --git a/tests/bonanza-opening-real-api.test.mjs b/tests/bonanza-opening-real-api.test.mjs
index 7100e8f..6e2ef37 100644
--- a/tests/bonanza-opening-real-api.test.mjs
+++ b/tests/bonanza-opening-real-api.test.mjs
@@ -158,3 +158,49 @@ test("unsupported custom BBCode is rejected before public POST", () => {
assert.match(guard, /Custom message contains unsupported BBCode\/HTML/);
assert.match(guard, /return;/);
});
+
+const latest100 = Array.from({length:100}, (_, i) => ({
+ id: 629181 + i,
+ user: {id:11,username:"participant"},
+ message:"unrelated chat",
+ created_at:"2026-10-14T20:26:30+00:00"
+}));
+
+test("a proven confirmed opening survives when the recent 100 chat messages have advanced past its ID", async () => {
+ const r = await verify(latest100, {status:"confirmed",checkpoint:{messageId:629180}});
+ assert.equal(r.messageId,629180);
+ assert.equal(r.historyEvicted,true);
+});
+
+test("pending openings are NOT confirmed merely because history advanced", async () => {
+ assert.equal(await verify(latest100,{checkpoint:{messageId:629180}}),null);
+});
+
+test("incomplete chat history is insufficient to bypass publication proof", async () => {
+ assert.equal(await verify(latest100.slice(0,99), {
+ status:"confirmed",checkpoint:{messageId:629180}
+ }),null);
+});
+
+test("a missing confirmed opening still inside the returned ID range is not treated as evicted", async () => {
+ const mixed=[{...latest100[0],id:629178},...latest100.slice(1)];
+ assert.equal(await verify(mixed,{
+ status:"confirmed",checkpoint:{messageId:629180}
+ }),null);
+});
+
+test("legacy confirmed snapshots without the random proof cannot bypass authoritative chat", async () => {
+ assert.equal(await verify(latest100,{
+ status:"confirmed",checkpoint:{messageId:629180,openingProof:null}
+ }),null);
+});
+
+test("the history-expiry restore path retains the original sponsor boundary and fails closed for invalid dates", () => {
+ const init = readFileSync(
+ new URL("../userscripts/giveaway/src/05-initialization.js",import.meta.url),"utf8"
+ );
+ assert.match(init,/verified\?\.historyEvicted === true/);
+ assert.match(init,/savedSponsorStart > Number\(restoredCheckpoint\.startedAt\)/);
+ assert.match(init,/Number\(restoredCheckpoint\.sendCompletedAt\) \+ 1000/);
+ assert.match(init,/verified\.sponsorStartTs !== savedSponsorStart/);
+});
From 43452f4fd38a6d6a1c0dc968873cae2d40a97631 Mon Sep 17 00:00:00 2001
From: Maghuro <1546139+maghuro@users.noreply.github.com>
Date: Wed, 14 Oct 2026 22:39:12 +0100
Subject: [PATCH 19/26] build(bonanza): synchronize verified history-rollover
recovery
---
.../DarkPeers_BONanza_Giveaway.user.js | 44 ++++++++++++++++++-
1 file changed, 43 insertions(+), 1 deletion(-)
diff --git a/userscripts/giveaway/DarkPeers_BONanza_Giveaway.user.js b/userscripts/giveaway/DarkPeers_BONanza_Giveaway.user.js
index 7a28447..da2ff27 100644
--- a/userscripts/giveaway/DarkPeers_BONanza_Giveaway.user.js
+++ b/userscripts/giveaway/DarkPeers_BONanza_Giveaway.user.js
@@ -3801,10 +3801,32 @@ body.host-panel-dragging * {
const savedSponsorStart = optionalFiniteNumber(
snap.giveawayData.sponsorStartTs
);
+ // The latest-100 chat page may no longer include a previously
+ // confirmed announcement. Its persisted random proof and exact
+ // message ID can remain authoritative ONLY when the healthy
+ // bounded history has clearly advanced beyond that ID. There
+ // is no UNIT3D pagination endpoint to query older messages.
+ // Revalidate the persisted sponsor boundary conservatively;
+ // do not derive a new one from a later, unrelated chat message.
+ const historicalConfirmation = verified?.historyEvicted === true;
+ const historicalSponsorBoundaryOk = historicalConfirmation &&
+ savedSponsorStart !== null &&
+ Number(restoredCheckpoint.startedAt) ===
+ Number(snap.giveawayData.openedAt) &&
+ Number.isFinite(Number(restoredCheckpoint.sendCompletedAt)) &&
+ Number(restoredCheckpoint.sendCompletedAt) >=
+ Number(restoredCheckpoint.startedAt) &&
+ savedSponsorStart > Number(restoredCheckpoint.startedAt) &&
+ savedSponsorStart <= Math.min(
+ Number(restoredCheckpoint.windowEndAt),
+ Number(restoredCheckpoint.sendCompletedAt) + 1000
+ );
if (!verified ||
verified.messageId !== optionalFiniteNumber(restoredCheckpoint.messageId) ||
savedSponsorStart === null ||
- verified.sponsorStartTs !== savedSponsorStart) {
+ (historicalConfirmation
+ ? !historicalSponsorBoundaryOk
+ : verified.sponsorStartTs !== savedSponsorStart)) {
logEvent("Confirmed opening recovery deferred",
"Authoritative Main Chat could not reprove the exact saved opening and sponsor boundary.");
window.alert(
@@ -4429,6 +4451,26 @@ body.host-panel-dragging * {
// Never guess between duplicate equal announcements.
if (matches.length === 1) return matches[0];
if (matches.length > 1) return null;
+
+ // UNIT3D's GET /api/chat/messages/{room} returns only the last
+ // configured chat.message_limit messages (currently 100) and
+ // does not implement pagination or the after_id query parameter.
+ // A previously CONFIRMED opening with a durable random proof
+ // becomes older than that window during busy chats. When a
+ // complete healthy latest-page has advanced beyond its saved
+ // message ID, retain that earlier confirmation. Never apply
+ // this shortcut to a pending, legacy or still-in-range opening.
+ const confirmedId = optionalFiniteNumber(checkpoint.messageId);
+ const pageIds = payload.data.map(message =>
+ optionalFiniteNumber(message?.id)
+ );
+ if (checkpoint.status === "confirmed" && hasUniqueProof &&
+ Number.isSafeInteger(confirmedId) && confirmedId > afterId &&
+ pageIds.length >= 100 && pageIds.every(id =>
+ Number.isSafeInteger(id) && id > 0
+ ) && Math.min(...pageIds) > confirmedId) {
+ return { messageId: confirmedId, historyEvicted: true };
+ }
} catch (error) {
console.warn("[BON Giveaway] Opening reconciliation deferred:", error);
}
From 75159b15949e6c1b1c8832ca69feb22aa7a62143 Mon Sep 17 00:00:00 2001
From: Maghuro <1546139+maghuro@users.noreply.github.com>
Date: Wed, 14 Oct 2026 22:40:16 +0100
Subject: [PATCH 20/26] docs(bonanza): explain bounded UNIT3D chat restore
invariant
---
AGENTS.md | 8 ++++++++
1 file changed, 8 insertions(+)
diff --git a/AGENTS.md b/AGENTS.md
index 1d841d1..f92a952 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -72,6 +72,14 @@ multiple userscripts, not just BONanza.
re-post blindly, or settle BON.
Restore must verify the exact own-user announcement in authoritative
Main Chat history before enabling entries or any sponsor/payout action.
+ Exception for a **previously confirmed** opening: UNIT3D's chat GET
+ returns only the latest configured messages (observed: 100), ignores
+ `after_id`, and offers no pagination for this endpoint. When a healthy,
+ full latest page begins strictly after the durable confirmed message ID,
+ recovery may rely on the ORIGINAL recorded message ID and random proof,
+ with the unchanged, bounded sponsor-start timestamp revalidated. This
+ exception NEVER applies to a pending opening, a message still within the
+ returned ID range, a short/failed response, or a legacy proofless snapshot.
- The 45-minute absolute deadline is anchored to the conservative
**pre-send** time, so server response latency cannot extend the event.
**Sponsor eligibility must NOT use that time.** It begins strictly after
From 98db4dd23154a6fa5c69cf823693c6426dd504b6 Mon Sep 17 00:00:00 2001
From: Maghuro <1546139+maghuro@users.noreply.github.com>
Date: Wed, 14 Oct 2026 22:40:20 +0100
Subject: [PATCH 21/26] docs(bonanza): document real UNIT3D chat history window
---
docs/runbooks/bonanza-operations.md | 18 ++++++++++++++++++
1 file changed, 18 insertions(+)
diff --git a/docs/runbooks/bonanza-operations.md b/docs/runbooks/bonanza-operations.md
index 3468406..122109d 100644
--- a/docs/runbooks/bonanza-operations.md
+++ b/docs/runbooks/bonanza-operations.md
@@ -112,3 +112,21 @@ proof to disambiguate whole-second API timestamps, and older snapshots cannot
be silently upgraded to that proof. Rehearsal mode cannot establish that an
invisible proof survives the real Main Chat rendering pipeline; validate this
against the site before merging a new release.
+
+### Bounded Main Chat history
+
+The stock UNIT3D `GET /api/chat/messages/{room_id}` endpoint reads
+`ChatRepository::messages()`, returning the newest `chat.message_limit`
+messages (100 in the reviewed UNIT3D configuration). It does **not** support
+pagination or `after_id`; sending `?page=2` does not retrieve older messages.
+DarkPeers was independently observed returning precisely 100 messages.
+
+A LIVE opening previously confirmed with a durable random marker and message
+ID can outlive that bounded page. Restore uses the original persisted
+confirmation ONLY if a healthy full page has a minimum ID strictly beyond
+the saved confirmed message ID, and the persisted sponsor eligibility boundary
+is still valid. Pending or proofless openings and missing messages still
+within the API page remain fail-closed. This fallback cannot prove whether
+the original public message was subsequently deleted: it deliberately
+prioritizes a durably verified opening over rechecking evicted history.
+Never extend the 45-minute deadline or redetermine sponsors at reload.
From f4c92ea86d5b7d447787e27b3e38303520d2c128 Mon Sep 17 00:00:00 2001
From: Maghuro <1546139+maghuro@users.noreply.github.com>
Date: Wed, 14 Oct 2026 22:48:49 +0100
Subject: [PATCH 22/26] fix(bonanza): reject duplicate opening proofs even
outside send window
---
userscripts/giveaway/src/06-lifecycle.js | 12 +++++++++++-
1 file changed, 11 insertions(+), 1 deletion(-)
diff --git a/userscripts/giveaway/src/06-lifecycle.js b/userscripts/giveaway/src/06-lifecycle.js
index a211dee..72fd7d3 100644
--- a/userscripts/giveaway/src/06-lifecycle.js
+++ b/userscripts/giveaway/src/06-lifecycle.js
@@ -103,6 +103,11 @@
if (!ownsTabLock()) return null;
if (!Array.isArray(payload?.data)) throw new Error("Malformed Main Chat messages");
const matches = [];
+ // Track every matching announcement after the pre-send cursor,
+ // including posts OUTSIDE the permitted timestamp interval.
+ // Otherwise an out-of-window duplicate could be silently
+ // discarded, leaving a misleading single valid candidate.
+ const matchingAnnouncementIds = new Set();
for (const message of payload.data) {
const id = optionalFiniteNumber(message?.id);
if (!Number.isSafeInteger(id) || id <= afterId) continue;
@@ -123,6 +128,7 @@
if (!expectedHost || serverName !== expectedHost) continue;
}
if (canonical(message?.message, true) !== expected) continue;
+ matchingAnnouncementIds.add(id);
const rawTs = String(message?.created_at || "");
const timestamp = Date.parse(rawTs);
const resolutionMs = unit3dTimestampResolutionMs(rawTs);
@@ -143,7 +149,11 @@
sponsorStartTs: Math.max(startedAt, timestamp + resolutionMs)
});
}
- // Never guess between duplicate equal announcements.
+ // Reconcile only when there is ONE candidate across the
+ // entire returned Main Chat history, not just one within the
+ // timestamp window. This cannot establish uniqueness for an
+ // announcement already evicted by the fixed 100-message limit.
+ if (matchingAnnouncementIds.size > 1) return null;
if (matches.length === 1) return matches[0];
if (matches.length > 1) return null;
From b1c14896a185d4d9d8674414543b9c5fbbaebf78 Mon Sep 17 00:00:00 2001
From: Maghuro <1546139+maghuro@users.noreply.github.com>
Date: Wed, 14 Oct 2026 22:49:03 +0100
Subject: [PATCH 23/26] test(bonanza): reject repeated opening proof beyond
timestamp boundaries
---
tests/bonanza-opening-real-api.test.mjs | 27 +++++++++++++++++++++++++
1 file changed, 27 insertions(+)
diff --git a/tests/bonanza-opening-real-api.test.mjs b/tests/bonanza-opening-real-api.test.mjs
index 6e2ef37..360837a 100644
--- a/tests/bonanza-opening-real-api.test.mjs
+++ b/tests/bonanza-opening-real-api.test.mjs
@@ -204,3 +204,30 @@ test("the history-expiry restore path retains the original sponsor boundary and
assert.match(init,/Number\(restoredCheckpoint\.sendCompletedAt\) \+ 1000/);
assert.match(init,/verified\.sponsorStartTs !== savedSponsorStart/);
});
+
+test("a matching duplicate AFTER the opening POST window disqualifies a seemingly unique in-window announcement", async () => {
+ const duplicate = real(629181, {
+ created_at: "2026-10-14T20:25:40+00:00"
+ });
+ assert.equal(await verify([real(629180), duplicate]), null);
+});
+
+test("a matching duplicate BEFORE the attempt but after its message cursor also disqualifies opening", async () => {
+ const earlier = real(629177, {
+ created_at: "2026-10-14T20:25:23+00:00"
+ });
+ assert.equal(await verify([earlier, real(629180)]), null);
+});
+
+test("a duplicated row with the SAME message ID does not count as two announcements", async () => {
+ const record = real(629180);
+ const result = await verify([record, {...record}]);
+ assert.equal(result.messageId, 629180);
+});
+
+test("timestamp-invalid duplicate with the same proof also blocks ambiguous opening", async () => {
+ assert.equal(await verify([
+ real(629180),
+ real(629181, {created_at: "unknown"})
+ ]), null);
+});
From 51e257c756c8e4a8b5a3b813de039d8f4c550b92 Mon Sep 17 00:00:00 2001
From: Maghuro <1546139+maghuro@users.noreply.github.com>
Date: Wed, 14 Oct 2026 22:49:17 +0100
Subject: [PATCH 24/26] build(bonanza): sync duplicate-proof verification
safety guard
---
.../giveaway/DarkPeers_BONanza_Giveaway.user.js | 12 +++++++++++-
1 file changed, 11 insertions(+), 1 deletion(-)
diff --git a/userscripts/giveaway/DarkPeers_BONanza_Giveaway.user.js b/userscripts/giveaway/DarkPeers_BONanza_Giveaway.user.js
index da2ff27..abda017 100644
--- a/userscripts/giveaway/DarkPeers_BONanza_Giveaway.user.js
+++ b/userscripts/giveaway/DarkPeers_BONanza_Giveaway.user.js
@@ -4408,6 +4408,11 @@ body.host-panel-dragging * {
if (!ownsTabLock()) return null;
if (!Array.isArray(payload?.data)) throw new Error("Malformed Main Chat messages");
const matches = [];
+ // Track every matching announcement after the pre-send cursor,
+ // including posts OUTSIDE the permitted timestamp interval.
+ // Otherwise an out-of-window duplicate could be silently
+ // discarded, leaving a misleading single valid candidate.
+ const matchingAnnouncementIds = new Set();
for (const message of payload.data) {
const id = optionalFiniteNumber(message?.id);
if (!Number.isSafeInteger(id) || id <= afterId) continue;
@@ -4428,6 +4433,7 @@ body.host-panel-dragging * {
if (!expectedHost || serverName !== expectedHost) continue;
}
if (canonical(message?.message, true) !== expected) continue;
+ matchingAnnouncementIds.add(id);
const rawTs = String(message?.created_at || "");
const timestamp = Date.parse(rawTs);
const resolutionMs = unit3dTimestampResolutionMs(rawTs);
@@ -4448,7 +4454,11 @@ body.host-panel-dragging * {
sponsorStartTs: Math.max(startedAt, timestamp + resolutionMs)
});
}
- // Never guess between duplicate equal announcements.
+ // Reconcile only when there is ONE candidate across the
+ // entire returned Main Chat history, not just one within the
+ // timestamp window. This cannot establish uniqueness for an
+ // announcement already evicted by the fixed 100-message limit.
+ if (matchingAnnouncementIds.size > 1) return null;
if (matches.length === 1) return matches[0];
if (matches.length > 1) return null;
From 128b6c725104f9f7f897aa7b60c6a9f947c836b9 Mon Sep 17 00:00:00 2001
From: Maghuro <1546139+maghuro@users.noreply.github.com>
Date: Wed, 14 Oct 2026 22:50:07 +0100
Subject: [PATCH 25/26] fix(bonanza): deduplicate repeated API rows by message
ID without accepting distinct opening duplicates
---
userscripts/giveaway/src/06-lifecycle.js | 13 +++++++++----
1 file changed, 9 insertions(+), 4 deletions(-)
diff --git a/userscripts/giveaway/src/06-lifecycle.js b/userscripts/giveaway/src/06-lifecycle.js
index 72fd7d3..0a38972 100644
--- a/userscripts/giveaway/src/06-lifecycle.js
+++ b/userscripts/giveaway/src/06-lifecycle.js
@@ -144,10 +144,15 @@
if (!whollyInside &&
!(hasUniqueProof && overlapsAttempt &&
canonical(message?.message, true).endsWith(proof))) continue;
- matches.push({
- messageId: id,
- sponsorStartTs: Math.max(startedAt, timestamp + resolutionMs)
- });
+ // An API replay can contain the very same record twice.
+ // One message ID is one announcement; two distinct IDs
+ // with this proof are always ambiguous.
+ if (!matches.some(match => match.messageId === id)) {
+ matches.push({
+ messageId: id,
+ sponsorStartTs: Math.max(startedAt, timestamp + resolutionMs)
+ });
+ }
}
// Reconcile only when there is ONE candidate across the
// entire returned Main Chat history, not just one within the
From d188d45c3af654e969fcd7339a0271b9768b1fcd Mon Sep 17 00:00:00 2001
From: Maghuro <1546139+maghuro@users.noreply.github.com>
Date: Wed, 14 Oct 2026 22:50:19 +0100
Subject: [PATCH 26/26] build(bonanza): sync de-duplicated authoritative chat
candidate matching
---
.../giveaway/DarkPeers_BONanza_Giveaway.user.js | 13 +++++++++----
1 file changed, 9 insertions(+), 4 deletions(-)
diff --git a/userscripts/giveaway/DarkPeers_BONanza_Giveaway.user.js b/userscripts/giveaway/DarkPeers_BONanza_Giveaway.user.js
index abda017..3f90872 100644
--- a/userscripts/giveaway/DarkPeers_BONanza_Giveaway.user.js
+++ b/userscripts/giveaway/DarkPeers_BONanza_Giveaway.user.js
@@ -4449,10 +4449,15 @@ body.host-panel-dragging * {
if (!whollyInside &&
!(hasUniqueProof && overlapsAttempt &&
canonical(message?.message, true).endsWith(proof))) continue;
- matches.push({
- messageId: id,
- sponsorStartTs: Math.max(startedAt, timestamp + resolutionMs)
- });
+ // An API replay can contain the very same record twice.
+ // One message ID is one announcement; two distinct IDs
+ // with this proof are always ambiguous.
+ if (!matches.some(match => match.messageId === id)) {
+ matches.push({
+ messageId: id,
+ sponsorStartTs: Math.max(startedAt, timestamp + resolutionMs)
+ });
+ }
}
// Reconcile only when there is ONE candidate across the
// entire returned Main Chat history, not just one within the