Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
e934826
fix(bonanza): reconcile real chat HTML and timestamp buckets
maghuro Oct 14, 2026
5b73906
test(bonanza): reproduce live DarkPeers opening failure and fail-clos…
maghuro Oct 14, 2026
8fdfef9
test(bonanza): align hardening version assertion with 1.5.16
maghuro Oct 14, 2026
1f1fca1
fix(bonanza): require per-attempt invisible proof to confirm coarse t…
maghuro Oct 14, 2026
3bc07bc
build(bonanza): sync generated script with unique proof
maghuro Oct 14, 2026
1174906
test(bonanza): cover random opening proof and uncertain POST recovery
maghuro Oct 14, 2026
1238717
fix(bonanza): show safe non-modal cancellation for pending openings
maghuro Oct 14, 2026
739ca66
docs/test/build(bonanza): cover recoverable proof and cancellation
maghuro Oct 14, 2026
eef1315
docs/test/build(bonanza): cover recoverable proof and cancellation
maghuro Oct 14, 2026
4c0f708
docs/test/build(bonanza): cover recoverable proof and cancellation
maghuro Oct 14, 2026
4a9a70f
docs/test/build(bonanza): cover recoverable proof and cancellation
maghuro Oct 14, 2026
27a19db
docs(bonanza): encode proof-bound opening safety invariant
maghuro Oct 14, 2026
a382e32
fix(bonanza): reject unsupported custom BBCode before public opening
maghuro Oct 14, 2026
8892414
build(bonanza): sync custom BBCode preflight
maghuro Oct 14, 2026
d70f2e8
test(bonanza): regression for unsupported custom BBCode
maghuro Oct 14, 2026
48fb662
fix(bonanza): preserve confirmed opening after UNIT3D bounded chat hi…
maghuro Oct 14, 2026
d81297b
fix(bonanza): safely restore proven openings after bounded chat histo…
maghuro Oct 14, 2026
a51dd5c
test(bonanza): simulate 100-message UNIT3D history rollover during re…
maghuro Oct 14, 2026
43452f4
build(bonanza): synchronize verified history-rollover recovery
maghuro Oct 14, 2026
75159b1
docs(bonanza): explain bounded UNIT3D chat restore invariant
maghuro Oct 14, 2026
98db4dd
docs(bonanza): document real UNIT3D chat history window
maghuro Oct 14, 2026
f4c92ea
fix(bonanza): reject duplicate opening proofs even outside send window
maghuro Oct 14, 2026
b1c1489
test(bonanza): reject repeated opening proof beyond timestamp boundaries
maghuro Oct 14, 2026
51e257c
build(bonanza): sync duplicate-proof verification safety guard
maghuro Oct 14, 2026
128b6c7
fix(bonanza): deduplicate repeated API rows by message ID without acc…
maghuro Oct 14, 2026
d188d45
build(bonanza): sync de-duplicated authoritative chat candidate matching
maghuro Oct 14, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 21 additions & 6 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -53,18 +53,33 @@ 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,
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
Expand Down
38 changes: 38 additions & 0 deletions docs/runbooks/bonanza-operations.md
Original file line number Diff line number Diff line change
Expand Up @@ -92,3 +92,41 @@ 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.

### 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.
233 changes: 233 additions & 0 deletions tests/bonanza-opening-real-api.test.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,233 @@
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 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 = '<img class="joypixels" alt="&#x1f381;"/> <b><i><u>\u2063</u></i></b> <img class="joypixels" alt="&#x1f499;"/> <b><span style="color: #4FAFFF">BON POOL CONTRIBUTION G\u200bIVEAWAY</span></b><br>I am hosting a giveaway for <b>101 042 BON</b>' + openingProof;

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, openingProof, 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("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 () => {
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);
});

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);
});

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;/);
});

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/);
});

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);
});
2 changes: 1 addition & 1 deletion tests/bonanza-review-hardening.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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\(\)/);
Expand Down
Loading
Loading