contact messages: browser-side PoW solving in the contact panel
The bridge routes {type: pow} frames to the panel. contact-main solves
each fresh challenge in the background with the backend-served easypow
solver module (imported by URL from a variable so Rollup leaves it
dynamic), aborting stale solves on replacement challenges. Sending
awaits the current solution and burns it with the message; a
'required' rejection re-solves and retries the message once.
Composer and ContactPanel need no changes: uploads stay ungated, and
the send path just awaits the in-flight solve.
This commit is contained in:
1 parent
ec35bc71e8
commit
0adcd0bf90
2 files changed
+58
-5
No files matched your search
@@ -31,6 +31,55 @@ function markSeen() {
|
||||
refreshBadges()
|
||||
}
|
||||
|
||||
// PoW (easypow, docs/messages.md): the server issues a fresh single-use
|
||||
// challenge with every registration/resync and after every accepted
|
||||
// message; one solution burns one message. The solver module is served
|
||||
// by the backend from the easypow package, so both sides always match.
|
||||
const SOLVER_URL = '/_contact/easypow.js' // backend-served; kept in a variable so Rollup leaves the import dynamic
|
||||
let bridge = null
|
||||
let token = ''
|
||||
let solverPromise = null
|
||||
let powSolution = null // Promise<string> solving the current challenge
|
||||
let powAborter = null
|
||||
let unsent = null // last message text, retried once if its solution was rejected
|
||||
|
||||
// The solution for the currently outstanding challenge; rides out a
|
||||
// challenge replacement (reconnect) by continuing with the new solve.
|
||||
async function currentSolution() {
|
||||
for (;;) {
|
||||
const p = powSolution
|
||||
if (!p) return ''
|
||||
try {
|
||||
return await p
|
||||
} catch {
|
||||
if (p === powSolution) return '' // failed without a replacement
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function sendMessage(md) {
|
||||
unsent = md
|
||||
const pow = await currentSolution()
|
||||
if (pow) bridge.send({ token, msg: md, pow })
|
||||
}
|
||||
|
||||
async function onPow(msg) {
|
||||
powAborter?.abort() // superseded challenge: stop the stale solve
|
||||
const ac = (powAborter = new AbortController())
|
||||
solverPromise ??= import(/* @vite-ignore */ SOLVER_URL)
|
||||
const { solve } = await solverPromise
|
||||
powSolution = solve(msg.challenge, ac.signal)
|
||||
powSolution.catch(() => {}) // aborts are expected
|
||||
// A rejected send (stale or already burned solution) is retried once
|
||||
// the fresh challenge is solved; a second rejection drops it.
|
||||
if (msg.required && unsent) {
|
||||
const md = unsent
|
||||
unsent = null
|
||||
const pow = await currentSolution()
|
||||
if (pow) bridge.send({ token, msg: md, pow })
|
||||
}
|
||||
}
|
||||
|
||||
// A "new reply" dot on every contact trigger button while the panel is
|
||||
// closed and the newest admin message is younger than the last look.
|
||||
// Called on thread pushes and (from pagerite.js) after navigations,
|
||||
@@ -49,13 +98,14 @@ function onThread(thread) {
|
||||
else refreshBadges()
|
||||
}
|
||||
|
||||
export function open(bridge) {
|
||||
export function open(br) {
|
||||
bridge = br
|
||||
if (!app) {
|
||||
const token = ensureToken()
|
||||
token = ensureToken()
|
||||
app = createApp(ContactPanel, {
|
||||
state,
|
||||
token,
|
||||
onSend: (md) => bridge.send({ token, msg: md }),
|
||||
onSend: (md) => sendMessage(md),
|
||||
onClose: () => {
|
||||
state.open = false
|
||||
refreshBadges()
|
||||
@@ -67,7 +117,7 @@ export function open(bridge) {
|
||||
app.mount(el)
|
||||
// Registration doubles as the resync: the server pushes the thread
|
||||
// state back, and pagerite.js re-registers on every socket reconnect.
|
||||
bridge.register(token, onThread)
|
||||
bridge.register(token, onThread, onPow)
|
||||
}
|
||||
state.open = true
|
||||
markSeen()
|
||||
|
||||
@@ -643,6 +643,7 @@ import { reconnectPolicy, socketSlot, watchConnecting } from "./reconnect";
|
||||
return;
|
||||
}
|
||||
if (msg?.type === "thread") contactListener(msg);
|
||||
else if (msg?.type === "pow") contactPowListener?.(msg);
|
||||
else if (msg?.type === "contact-error") console.warn("contact:", msg.detail);
|
||||
};
|
||||
ws.onclose = () => {
|
||||
@@ -893,10 +894,12 @@ import { reconnectPolicy, socketSlot, watchConnecting } from "./reconnect";
|
||||
let contactMod = null;
|
||||
let contactToken = "";
|
||||
let contactListener = null;
|
||||
let contactPowListener = null;
|
||||
const contactBridge = {
|
||||
register(token, onThread) {
|
||||
register(token, onThread, onPow) {
|
||||
contactToken = token;
|
||||
contactListener = onThread;
|
||||
contactPowListener = onPow;
|
||||
contactBridge.send({ token });
|
||||
},
|
||||
send(msg) {
|
||||
|
||||
Reference in new issue
Block a user