All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
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.
116 lines
3.9 KiB
HTML
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}}
|