generator share links; secure send copy message

This commit is contained in:
2026-08-28 11:46:56 -04:00
parent 8e02235a71
commit e60945e56e
5 changed files with 418 additions and 2 deletions

View File

@@ -94,8 +94,30 @@
</p>
<div class="link-row">
<input ref="linkInput" class="form-control mono" :value="link" readonly @focus="selectAll" />
<button class="btn btn-primary" @click="copyLink">{{ copied ? 'Copied' : 'Copy' }}</button>
<button class="btn btn-primary" @click="copyLink">{{ copied ? 'Copied' : 'Copy link' }}</button>
<!--
Copies a whole sentence, not the bare URL. Pasted into an EMPTY chat
box there is nothing selected for the link to attach itself to - which
is the failure this button exists to prevent. See copyMessage().
-->
<button class="btn btn-secondary" @click="copyMessage">
{{ messagecopied ? 'Copied' : 'Copy message' }}
</button>
</div>
<!--
The warning that matters, stated where the mistake gets made. The hint
below is about the key being unrecoverable, which is a different point
and does not stop anybody pasting the password itself.
-->
<div class="alert-send">
<strong>Send the link, not the secret.</strong>
If the password is already typed in a chat box, delete it before pasting
this. Pasting a link over selected text turns that text into the link's
caption, and the secret then travels in the message itself - the link
beside it protects nothing.
</div>
<p class="control-hint">
This is the only time the link is shown - it is not stored anywhere you can
look it up, and the key in it exists nowhere else.
@@ -142,6 +164,7 @@ const busy = ref(false)
const error = ref('')
const link = ref('')
const copied = ref(false)
const messagecopied = ref(false)
const linkInput = ref(null)
// Only nag once there is something to nag about: an empty box is not a mistake.
@@ -222,6 +245,35 @@ async function copyLink() {
}
}
/**
* The link with a sentence around it.
*
* A bare URL invites being pasted onto text that is already in the chat box,
* and a chat client turns the selection into the link's caption - so the secret
* travels as the visible message and the link protects nothing. It has happened
* in production: six LAPS passwords sent as the caption of a link whose whole
* job was to keep them out of the chat.
*
* A sentence has nowhere to attach itself. Pasted into an empty box it is a
* message; pasted over a selection it replaces it rather than decorating it.
* Plain text on purpose - a rich-text flavour is what would let a secret ride
* along as markup.
*/
async function copyMessage() {
const opens = views.value === 1
? 'It opens once and then destroys itself'
: `It opens ${views.value} times and then destroys itself`
const message =
`Secure link - ${opens}, and expires ${expiryLabel.value}: ${link.value}`
try {
await navigator.clipboard.writeText(message)
messagecopied.value = true
setTimeout(() => { messagecopied.value = false }, 2000)
} catch (err) {
selectAll()
}
}
function selectAll() {
linkInput.value?.select()
}
@@ -245,6 +297,17 @@ function reset() {
.counter.over { color: var(--warning); font-weight: 600; }
.control-note { color: var(--warning); font-size: 0.875rem; margin: 8px 0; }
.control-hint { color: var(--text-light); font-size: 0.8125rem; margin: 8px 0 12px; }
/* Loud enough to be read before the paste, quiet enough not to look like an
error on a page where nothing went wrong. */
.alert-send {
border-left: 3px solid var(--warning);
background: var(--bg-card-solid);
padding: 0.6rem 0.9rem;
margin: 0.75rem 0;
font-size: 0.875rem;
line-height: 1.5;
}
.alert-send strong { display: block; margin-bottom: 2px; }
.link-row { display: flex; gap: 8px; align-items: center; margin: 0.5rem 0; }
.link-row .form-control { flex: 1; }
.mono { font-family: 'SF Mono', 'Monaco', 'Consolas', monospace; font-size: 0.875rem; }