Opening the link opens it in THIS browser — already signed into whichever account you are trying to move away from. Copying it is how you authorise as somebody else: another browser profile, another browser, or your phone. This dialog exists for a machine with two Anthropic accounts on it, so that is the common case rather than the awkward one. Uses helpers/clipboard, which falls back to execCommand when navigator.clipboard is absent, and reports a failure rather than showing "Copied" over an empty clipboard. Corrected a claim in that helper's own comment while touching it: it said Officer is reached over the tailnet on plain http, as though that were the whole story. The owner browses over https and did when it was written. The http origins are the other ways in, and an install with no certificate has no other way in at all — which is why the fallback stays. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
83 lines
3.1 KiB
TypeScript
83 lines
3.1 KiB
TypeScript
/**
|
|
* Copy text, in a browser that may not be in a secure context.
|
|
*
|
|
* ── Why this exists ──
|
|
*
|
|
* `navigator.clipboard` is SECURE-CONTEXT ONLY. Over plain http on anything that
|
|
* is not localhost the whole object is undefined, so `navigator.clipboard.writeText`
|
|
* throws `TypeError: Cannot read properties of undefined`.
|
|
*
|
|
* Officer is reachable over plain http — `http://officer-dev:9000` on the tailnet —
|
|
* which is neither https nor localhost. The owner's own browsing is over https
|
|
* (`platform.officer.dev`) and was already https when this was written; the http
|
|
* origins are the other ways in, and an install with no certificate has no other
|
|
* way in at all. Twenty call sites were affected, in three states that all looked
|
|
* fine in review:
|
|
*
|
|
* bare `navigator.clipboard.writeText(x)` threw, killing the handler
|
|
* `navigator.clipboard?.writeText(x)` did nothing, silently
|
|
* one carrying the comment "Officer is always behind HTTPS"
|
|
*
|
|
* The middle one is the worst: a copy button that reports success and copies
|
|
* nothing is indistinguishable from a working one until you paste.
|
|
*
|
|
* ── The fallback ──
|
|
*
|
|
* `document.execCommand('copy')` over a temporary, off-screen textarea. It is
|
|
* deprecated and it works in every browser that runs this app, on any origin,
|
|
* because it predates the secure-context rule. Deprecated-and-working beats
|
|
* modern-and-absent.
|
|
*
|
|
* The textarea is positioned off-screen rather than hidden: `display:none` and
|
|
* `visibility:hidden` elements cannot be selected, so the copy silently fails.
|
|
* `readOnly` stops a mobile keyboard appearing for the instant it is focused.
|
|
*
|
|
* Returns whether it worked, so a caller can say "copied" only when it did.
|
|
* Reading the clipboard has no equivalent fallback — see `canReadClipboard`.
|
|
*/
|
|
export async function copyToClipboard(text: string): Promise<boolean> {
|
|
if (navigator.clipboard?.writeText) {
|
|
try {
|
|
await navigator.clipboard.writeText(text);
|
|
return true;
|
|
} catch {
|
|
// Permission refused, or a document that is not focused. Fall through.
|
|
}
|
|
}
|
|
|
|
if (typeof document === 'undefined') return false;
|
|
|
|
const area = document.createElement('textarea');
|
|
area.value = text;
|
|
area.setAttribute('readonly', '');
|
|
area.style.position = 'fixed';
|
|
area.style.top = '-9999px';
|
|
area.style.opacity = '0';
|
|
document.body.appendChild(area);
|
|
|
|
try {
|
|
area.select();
|
|
area.setSelectionRange(0, text.length);
|
|
return document.execCommand('copy');
|
|
} catch {
|
|
return false;
|
|
} finally {
|
|
document.body.removeChild(area);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Whether the clipboard can be READ on demand.
|
|
*
|
|
* There is no fallback for this one. `document.execCommand('paste')` was never
|
|
* permitted from script, so on an insecure origin the only way to get clipboard
|
|
* contents is a real paste event the user initiates — which is a different
|
|
* interaction, not a drop-in.
|
|
*
|
|
* Callers should use this to hide a "paste" affordance rather than offer one
|
|
* that cannot work.
|
|
*/
|
|
export function canReadClipboard(): boolean {
|
|
return typeof navigator !== 'undefined' && typeof navigator.clipboard?.read === 'function';
|
|
}
|