hush/internal/web/templates/reveal.html
jx12n ac52fbe0b9
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
CI is activated; point the docs at the token that works
The Woodpecker claim in DEPLOY.md and release.sh was wrong within an hour of
being written. Corrected at the source rather than annotated:

  * jordan/hush is active in Woodpecker (repo 139) with the Gitea webhook
    installed, so a push to main builds and deploys.
  * Activation takes the NUMERIC gitea repo id in forge_remote_id, not
    owner/name — worth recording, because passing the wrong shape and passing a
    dead token both come back 401 and look identical. GET /api/user separates
    them: it is auth-only, so 401 there is the token and 200 there means the
    request was the problem.
  * The credential is $THREE_SIX_WOODPECKER (and $THREE_SIX_GITEA), in the
    operator's environment.
  * The stale copy that caused the original 401 is fixed where it lives:
    k3sf-rdev-admin-key in GCP Secret Manager, property WOODPECKER_API_TOKEN,
    every other property preserved. ESO resynced and the token read out of
    rdev/rdev-credentials now answers 200. Documented alongside it: do not patch
    that k8s Secret directly, it is ESO-owned and a direct edit is reverted on
    the next refresh.

make release stays, with its reason updated — it is now the hotfix/rollback path
and the answer to "CI is down", rather than the only way to deploy.
2026-09-03 00:41:33 -06:00

116 lines
3.9 KiB
HTML

{{define "content"}}
<h1>hush<span>.</span></h1>
<p class="lede" id="lede">Someone sent you a secret. It opens once.</p>
<div id="gate">
<button id="go">reveal the secret</button>
<p class="note" id="msg">Nothing has been read yet. Opening it is this button,
so a link preview in a chat app cannot consume it.</p>
</div>
<div id="result" class="hide">
<div class="out" id="plain"></div>
<div class="row">
<button id="copy">copy</button>
</div>
<p class="note warn">Destroyed. Reloading will not show it again — copy it now.</p>
</div>
{{end}}
{{define "script"}}
<script nonce="{{.Nonce}}">
const $ = (id) => document.getElementById(id);
// The id comes from the path and the key from the fragment. Neither is
// interpolated by the server, so this page renders identically for every secret
// and reflects nothing.
const id = location.pathname.replace(/^\/s\//, "");
const key = location.hash.slice(1);
if (!key) {
$("go").disabled = true;
$("msg").className = "note err";
$("msg").textContent =
"This link is missing its key — the part after '#'. Chat apps and email " +
"clients sometimes truncate it. Ask the sender for the full link; the " +
"secret is intact and has not been opened.";
}
async function reveal() {
$("go").disabled = true;
$("msg").className = "note";
$("msg").textContent = "Opening…";
let res, body;
try {
res = await fetch("/api/secrets/" + encodeURIComponent(id) + "/reveal", { method: "POST" });
body = await res.json();
} catch (e) {
// The secret is very likely consumed at this point, so do not offer a retry
// that would report "gone" and read as a lie about what happened.
$("msg").className = "note err";
$("msg").textContent = "Could not reach hush: " + e.message +
". If the request left your browser, the secret is already destroyed.";
return;
}
if (res.status === 410) {
$("msg").className = "note err";
$("msg").textContent =
"Gone. This link was already opened, expired, or never existed. " +
"If you have not opened it yourself, assume someone else did and ask the " +
"sender to rotate the secret.";
return;
}
if (!res.ok) {
$("go").disabled = false;
$("msg").className = "note err";
$("msg").textContent = body && body.error ? body.error.message : "Server error (" + res.status + ").";
return;
}
let text;
try {
text = await open(body.ciphertext, key);
} catch {
// Decryption failed AFTER the server destroyed the ciphertext, so there is
// nothing to retry. Say so, because the alternative is a user reloading
// forever against a secret that no longer exists.
$("msg").className = "note err";
$("msg").textContent =
"The key in this link does not open this secret, and the ciphertext has " +
"now been destroyed. The link was probably altered in transit. Ask the " +
"sender to create a new one.";
return;
}
// "Someone sent you a secret. It opens once." is a description of the state
// before this line, not after it.
$("lede").textContent = "The secret.";
$("plain").textContent = text;
$("gate").classList.add("hide");
$("result").classList.remove("hide");
// Drop the key from the address bar so a screenshot, a shoulder-surfer or a
// later copy of the URL does not carry it. The secret is already destroyed
// server-side, so this only reduces incidental exposure.
history.replaceState(null, "", location.pathname);
$("copy").focus();
}
$("go").addEventListener("click", reveal);
$("copy").addEventListener("click", async () => {
try {
await navigator.clipboard.writeText($("plain").textContent);
$("copy").textContent = "copied";
setTimeout(() => ($("copy").textContent = "copy"), 1500);
} catch {
const r = document.createRange();
r.selectNodeContents($("plain"));
getSelection().removeAllRanges();
getSelection().addRange(r);
$("copy").textContent = "selected — press copy";
}
});
</script>
{{end}}