<!DOCTYPE html><html lang="en"> <head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1"><link rel="icon" type="image/svg+xml" href="/favicon.svg"><title>A file was shared with you — dove</title><meta name="description" content="Encrypted end-to-end, decrypted in your browser. The key never touches a server."><meta name="theme-color" content="#fbfcff" media="(prefers-color-scheme: light)"><meta name="theme-color" content="#0b1220" media="(prefers-color-scheme: dark)"><meta property="og:title" content="A file was shared with you — dove"><meta property="og:description" content="Encrypted end-to-end, decrypted in your browser. The key never touches a server."><meta property="og:type" content="website"><meta property="og:image" content="__OGBASE__/og.png"><meta property="og:image:width" content="1200"><meta property="og:image:height" content="630"><meta name="twitter:card" content="summary_large_image"><meta name="twitter:image" content="__OGBASE__/og.png"><style>@import"https://fonts.googleapis.com/css2?family=DM+Mono:wght@400;500&family=Manrope:wght@400;500;600;700&display=swap";:root{color-scheme:light dark;--paper: #fbfcff;--paper-deep: #eef3f9;--ink: #111c2f;--muted: #58667a;--line: #d8e0ec;--panel: rgba(255, 255, 255, .88);--green: #2878c8;--green-strong: #164f8a;--green-soft: #e6f2ff;--yellow: #a8d8ff;--shadow: 0 24px 70px rgba(31, 75, 122, .12);--sans: "Manrope", system-ui, sans-serif;--mono: "DM Mono", ui-monospace, monospace}@media(prefers-color-scheme:dark){:root{--paper: #0b1220;--paper-deep: #121d2e;--ink: #f5f8fc;--muted: #aab7c9;--line: #29384d;--panel: rgba(18, 29, 46, .86);--green: #73b9f4;--green-strong: #9acdf7;--green-soft: #132c48;--yellow: #b8ddfb;--shadow: 0 22px 65px rgba(0, 0, 0, .34)}}*{box-sizing:border-box}html{scroll-behavior:smooth}body{margin:0;background:radial-gradient(circle at 15% -5%,color-mix(in srgb,var(--green-soft) 80%,transparent),transparent 36rem),var(--paper);color:var(--ink);font-family:var(--sans);font-size:16px;line-height:1.6;-webkit-font-smoothing:antialiased}a{color:inherit;text-underline-offset:.2em}button,input{font:inherit}button{color:inherit}::selection{background:var(--yellow);color:#111c2f}.shell{width:min(1120px,calc(100% - 40px));margin:0 auto}.site-header{display:flex;justify-content:space-between;align-items:center;padding:24px 0}.brand{display:inline-flex;align-items:center;gap:10px;font-weight:700;text-decoration:none;letter-spacing:-.03em}.mark{width:34px;height:34px;display:grid;place-items:center;border-radius:11px;background:linear-gradient(145deg,#3389d8,#164f8a);color:#fff;box-shadow:0 5px 14px #164f8a38}.mark svg{width:21px;height:21px;stroke-width:1.8}.nav{display:flex;align-items:center;gap:24px;color:var(--muted);font-size:.9rem}.nav a{text-decoration:none}.nav a:hover{color:var(--ink)}.button{display:inline-flex;justify-content:center;align-items:center;gap:9px;min-height:46px;padding:0 19px;border:1px solid var(--green-strong);border-radius:8px;background:var(--green-strong);color:var(--paper);text-decoration:none;font-weight:600;cursor:pointer}.button:hover{transform:translateY(-1px)}.button.secondary{background:transparent;color:var(--green-strong)}.eyebrow{display:inline-flex;align-items:center;gap:9px;font-family:var(--mono);color:var(--green);font-size:.75rem;letter-spacing:.08em;text-transform:uppercase}.eyebrow:before{content:"";width:22px;border-top:1px solid currentColor}.mono{font-family:var(--mono)}.site-footer{display:flex;justify-content:space-between;gap:24px;padding:34px 0 44px;border-top:1px solid var(--line);color:var(--muted);font-size:.85rem}.site-footer a{text-decoration:none}.sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border:0}:focus-visible{outline:3px solid #80c6ff;outline-offset:3px}@media(max-width:640px){.shell{width:min(100% - 28px,1120px)}.nav a:not(.button){display:none}.site-footer{flex-direction:column}}@media(prefers-reduced-motion:reduce){html{scroll-behavior:auto}*,*:before,*:after{transition-duration:.01ms!important;animation-duration:.01ms!important}}
[data-astro-cid-mnqtlyvs][hidden]{display:none!important}body{min-height:100vh;background:radial-gradient(circle at 50% -15%,var(--green-soft),transparent 42rem),var(--paper)}.share-shell[data-astro-cid-mnqtlyvs]{width:min(760px,calc(100% - 36px));margin:0 auto;min-height:100vh;display:flex;flex-direction:column}.secure[data-astro-cid-mnqtlyvs],.canonical-link[data-astro-cid-mnqtlyvs],.trust-note[data-astro-cid-mnqtlyvs] a[data-astro-cid-mnqtlyvs]{display:inline-flex;align-items:center;gap:7px}.secure[data-astro-cid-mnqtlyvs]{font:.72rem var(--mono);text-transform:uppercase;letter-spacing:.06em;color:var(--green)}.share-main[data-astro-cid-mnqtlyvs]{flex:1;padding:72px 0 80px}.file-card[data-astro-cid-mnqtlyvs]{border:1px solid var(--line);border-radius:16px;background:color-mix(in srgb,var(--paper) 88%,transparent);box-shadow:var(--shadow);padding:44px;backdrop-filter:blur(14px)}.file-top[data-astro-cid-mnqtlyvs]{display:flex;align-items:center;gap:24px}.file-glyph[data-astro-cid-mnqtlyvs]{width:66px;height:82px;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:7px;flex:0 0 auto;border:1px solid var(--line);border-radius:13px;color:var(--green-strong);background:var(--green-soft)}.file-glyph[data-astro-cid-mnqtlyvs] span[data-astro-cid-mnqtlyvs]{font:.62rem var(--mono)}.overline[data-astro-cid-mnqtlyvs]{margin:0 0 7px;color:var(--green);font:.7rem var(--mono);letter-spacing:.07em;text-transform:uppercase}.file-top[data-astro-cid-mnqtlyvs] h1[data-astro-cid-mnqtlyvs],.status-body[data-astro-cid-mnqtlyvs] h1[data-astro-cid-mnqtlyvs]{margin:0;font-size:clamp(1.65rem,5vw,2.4rem);line-height:1.2;letter-spacing:-.04em;overflow-wrap:anywhere}.meta[data-astro-cid-mnqtlyvs]{margin:6px 0 0;color:var(--muted)}.meta[data-astro-cid-mnqtlyvs] span[data-astro-cid-mnqtlyvs]{margin:0 7px}.trust-banner[data-astro-cid-mnqtlyvs]{display:flex;align-items:flex-start;gap:11px;margin-bottom:22px;padding:12px 15px;border:1px solid color-mix(in srgb,var(--green) 25%,var(--line));border-radius:10px;background:var(--green-soft)}.trust-mark[data-astro-cid-mnqtlyvs]{color:var(--green-strong);display:grid;place-items:center;margin-top:1px}.trust-banner[data-astro-cid-mnqtlyvs] strong[data-astro-cid-mnqtlyvs]{font-size:.9rem}.trust-banner[data-astro-cid-mnqtlyvs] p[data-astro-cid-mnqtlyvs]{margin:3px 0 0;color:var(--muted);font-size:.82rem;line-height:1.5}.policy[data-astro-cid-mnqtlyvs]{display:grid;grid-template-columns:1fr 1fr;gap:1px;background:var(--line);border:1px solid var(--line);border-radius:9px;overflow:hidden;margin:36px 0}.policy[data-astro-cid-mnqtlyvs]>div[data-astro-cid-mnqtlyvs]{background:var(--paper);padding:19px 18px 19px 52px;position:relative}.policy[data-astro-cid-mnqtlyvs] p[data-astro-cid-mnqtlyvs]{margin:0;color:var(--muted);font-size:.72rem}.policy[data-astro-cid-mnqtlyvs] strong[data-astro-cid-mnqtlyvs]{display:block;font-size:.9rem}.policy-icon[data-astro-cid-mnqtlyvs]{position:absolute;left:17px;top:25px;width:24px;height:24px;color:var(--green);display:grid;place-items:center}.download-button[data-astro-cid-mnqtlyvs]{width:100%;min-height:58px;display:flex;justify-content:space-between;align-items:center;padding:0 20px 0 24px;border:0;border-radius:8px;background:var(--green-strong);color:var(--paper);font-weight:650;cursor:pointer}.download-button[data-astro-cid-mnqtlyvs]:hover{filter:brightness(1.08)}.download-button[data-astro-cid-mnqtlyvs] b[data-astro-cid-mnqtlyvs]{width:27px;height:27px;display:grid;place-items:center;border:1px solid color-mix(in srgb,var(--paper) 40%,transparent);border-radius:50%}.action-note[data-astro-cid-mnqtlyvs]{color:var(--muted);font-size:.72rem;text-align:center;margin:12px 0 0}.pin-panel[data-astro-cid-mnqtlyvs]{margin-top:34px;padding-top:32px;border-top:1px solid var(--line)}.pin-glyph[data-astro-cid-mnqtlyvs]{width:44px;height:44px;display:grid;place-items:center;float:left;margin:0 15px 12px 0;border-radius:12px;color:var(--green-strong);background:var(--green-soft)}.pin-panel[data-astro-cid-mnqtlyvs] h2[data-astro-cid-mnqtlyvs]{margin:4px 0 8px;font-size:1.55rem;letter-spacing:-.035em}.pin-intro[data-astro-cid-mnqtlyvs]{color:var(--muted);margin:0 0 25px;font-size:.88rem}.pin-panel[data-astro-cid-mnqtlyvs] form[data-astro-cid-mnqtlyvs]{clear:both}.pin-panel[data-astro-cid-mnqtlyvs] label[data-astro-cid-mnqtlyvs]{display:block;margin-bottom:8px;font-size:.78rem;font-weight:650}.pin-row[data-astro-cid-mnqtlyvs]{display:grid;grid-template-columns:1fr auto;gap:10px}.pin-row[data-astro-cid-mnqtlyvs] input[data-astro-cid-mnqtlyvs]{min-width:0;height:56px;border:1px solid var(--line);border-radius:8px;background:var(--paper);color:var(--ink);font:1.45rem var(--mono);letter-spacing:.45em;padding:0 10px 0 24px}.pin-row[data-astro-cid-mnqtlyvs] input[data-astro-cid-mnqtlyvs]::placeholder{color:var(--line)}.pin-row[data-astro-cid-mnqtlyvs] input[data-astro-cid-mnqtlyvs][aria-invalid=true]{border-color:#bd5a58}.pin-row[data-astro-cid-mnqtlyvs] button[data-astro-cid-mnqtlyvs]{min-width:150px;display:flex;align-items:center;justify-content:center;gap:8px;border:0;border-radius:8px;background:var(--green-strong);color:var(--paper);font-weight:650;cursor:pointer}.pin-row[data-astro-cid-mnqtlyvs] button[data-astro-cid-mnqtlyvs]:disabled{cursor:wait;opacity:.8}.button-icon[data-astro-cid-mnqtlyvs]{display:grid;place-items:center}.button-icon[data-astro-cid-mnqtlyvs] .loader-icon,.pin-row[data-astro-cid-mnqtlyvs] button[data-astro-cid-mnqtlyvs].verifying .button-icon[data-astro-cid-mnqtlyvs] .lock-icon{display:none}.pin-row[data-astro-cid-mnqtlyvs] button[data-astro-cid-mnqtlyvs].verifying .button-icon[data-astro-cid-mnqtlyvs] .loader-icon{display:block;animation:spin .7s linear infinite}.pin-error[data-astro-cid-mnqtlyvs]{min-height:1.3em;margin:9px 0 0;color:#bd5a58;font-size:.76rem;font-weight:600}.pin-help[data-astro-cid-mnqtlyvs]{margin:2px 0 0;color:var(--muted);font-size:.7rem}.pin-separation[data-astro-cid-mnqtlyvs]{display:grid;grid-template-columns:1fr 1fr;gap:1px;margin-top:24px;overflow:hidden;border:1px solid var(--line);border-radius:9px;background:var(--line)}.pin-separation[data-astro-cid-mnqtlyvs]>div[data-astro-cid-mnqtlyvs]{padding:15px 17px;background:var(--paper)}.pin-separation[data-astro-cid-mnqtlyvs] strong[data-astro-cid-mnqtlyvs],.pin-separation[data-astro-cid-mnqtlyvs] span[data-astro-cid-mnqtlyvs]{display:block}.pin-separation[data-astro-cid-mnqtlyvs] strong[data-astro-cid-mnqtlyvs]{display:flex;align-items:center;gap:3px;color:var(--green-strong);font:.72rem var(--mono)}.pin-separation[data-astro-cid-mnqtlyvs] span[data-astro-cid-mnqtlyvs]{margin-top:5px;color:var(--muted);font-size:.68rem;line-height:1.5}.pin-pass[data-astro-cid-mnqtlyvs]{display:flex;align-items:center;gap:9px;margin:30px 0 20px;padding:13px 15px;border:1px solid color-mix(in srgb,var(--green) 30%,var(--line));border-radius:9px;color:var(--green-strong);background:var(--green-soft);font-size:.78rem}.pin-locked-inline[data-astro-cid-mnqtlyvs]{clear:both;padding:18px 0 2px;text-align:center}.pin-locked-inline[data-astro-cid-mnqtlyvs]>p[data-astro-cid-mnqtlyvs]:not(.overline){color:var(--muted)}.pin-locked-inline[data-astro-cid-mnqtlyvs] h2[data-astro-cid-mnqtlyvs]{font-size:1.8rem}.pin-locked-inline[data-astro-cid-mnqtlyvs] .status-glyph[data-astro-cid-mnqtlyvs]{float:none}.pin-locked-inline[data-astro-cid-mnqtlyvs] .next-step[data-astro-cid-mnqtlyvs]{max-width:480px;margin-inline:auto}@keyframes spin{to{transform:rotate(360deg)}}.working[data-astro-cid-mnqtlyvs]{padding:9px 0 0}.progress-head[data-astro-cid-mnqtlyvs]{display:flex;justify-content:space-between;font-size:.86rem;font-weight:600}.progress[data-astro-cid-mnqtlyvs]{height:7px;margin:13px 0;border-radius:10px;background:var(--paper-deep);overflow:hidden}.progress[data-astro-cid-mnqtlyvs] i[data-astro-cid-mnqtlyvs]{display:block;width:0;height:100%;background:var(--green);transition:width .1s linear}.working[data-astro-cid-mnqtlyvs]>p[data-astro-cid-mnqtlyvs]{color:var(--muted);font-size:.75rem}.complete{display:flex;align-items:center;gap:14px}.complete>span{width:38px;height:38px;border-radius:50%;display:grid;place-items:center;background:var(--green-soft);color:var(--green-strong)}.complete p{margin:2px 0 0;color:var(--muted);font-size:.8rem}.large-file[data-astro-cid-mnqtlyvs]{margin-top:34px;padding-top:30px;border-top:1px solid var(--line)}.large-file[data-astro-cid-mnqtlyvs] h2[data-astro-cid-mnqtlyvs]{font-size:1.7rem;letter-spacing:-.04em;margin:8px 0}.large-file[data-astro-cid-mnqtlyvs]>p[data-astro-cid-mnqtlyvs]:not(.overline,.action-note),.status-body[data-astro-cid-mnqtlyvs]>p[data-astro-cid-mnqtlyvs]:not(.overline,.technical){color:var(--muted)}.command[data-astro-cid-mnqtlyvs]{display:flex;align-items:center;gap:12px;background:#0d1727;color:#cce6fb;border-radius:7px;padding:13px 13px 13px 16px;margin:22px 0 16px}.command[data-astro-cid-mnqtlyvs] code[data-astro-cid-mnqtlyvs]{flex:1;overflow:hidden;text-overflow:ellipsis;font:.72rem var(--mono);white-space:nowrap}.command[data-astro-cid-mnqtlyvs] button[data-astro-cid-mnqtlyvs]{display:flex;align-items:center;gap:6px;background:#1d3049;border:1px solid #3b526e;color:#f5f8fc;border-radius:5px;padding:7px 10px;cursor:pointer}.canonical-link[data-astro-cid-mnqtlyvs]{color:var(--green-strong);font-weight:500}.status-card[data-astro-cid-mnqtlyvs]{min-height:420px;display:grid;align-items:center}.status-body[data-astro-cid-mnqtlyvs]{max-width:570px;margin:0 auto;text-align:center}.status-glyph[data-astro-cid-mnqtlyvs]{width:64px;height:64px;display:grid;place-items:center;margin:0 auto 25px;border-radius:18px;background:var(--green-soft);color:var(--green-strong)}.status-glyph[data-astro-cid-mnqtlyvs] svg{width:28px}.danger[data-astro-cid-mnqtlyvs],.danger-glyph[data-astro-cid-mnqtlyvs]{color:#bd5a58}.danger-glyph[data-astro-cid-mnqtlyvs]{background:color-mix(in srgb,#bd5a58 13%,var(--paper))}.status-body[data-astro-cid-mnqtlyvs]>p[data-astro-cid-mnqtlyvs]{line-height:1.7}.status-body[data-astro-cid-mnqtlyvs] code[data-astro-cid-mnqtlyvs]{font-family:var(--mono);color:var(--green-strong)}.status-fact[data-astro-cid-mnqtlyvs]{display:flex;justify-content:space-between;gap:18px;margin:28px 0 0;padding:16px 18px;border:1px solid var(--line);border-radius:9px;background:var(--paper);font-size:.82rem}.status-fact[data-astro-cid-mnqtlyvs] span[data-astro-cid-mnqtlyvs]{color:var(--muted)}.next-step[data-astro-cid-mnqtlyvs]{display:flex;flex-direction:column;gap:3px;margin-top:28px;padding:18px;border-radius:10px;background:var(--green-soft);font-size:.82rem}.next-step[data-astro-cid-mnqtlyvs] span[data-astro-cid-mnqtlyvs]{color:var(--muted)}.technical[data-astro-cid-mnqtlyvs]{color:var(--muted);font:.67rem var(--mono);margin-top:21px}.unsupported[data-astro-cid-mnqtlyvs] .command[data-astro-cid-mnqtlyvs]{text-align:left}.unsupported[data-astro-cid-mnqtlyvs] .canonical-link[data-astro-cid-mnqtlyvs]{justify-content:center}.trust-note[data-astro-cid-mnqtlyvs]{display:grid;grid-template-columns:58px 1fr;gap:24px;margin:26px 22px 0;padding:28px 8px 0;border-top:1px solid var(--line)}.key-mark[data-astro-cid-mnqtlyvs]{width:52px;height:52px;display:grid;place-items:center;border:1px solid var(--line);border-radius:14px;color:#fff;background:linear-gradient(145deg,#3389d8,#164f8a);box-shadow:0 8px 22px #164f8a2e}.trust-note[data-astro-cid-mnqtlyvs] h2[data-astro-cid-mnqtlyvs]{font-size:1.02rem;margin:0 0 7px}.trust-note[data-astro-cid-mnqtlyvs] p[data-astro-cid-mnqtlyvs]{color:var(--muted);font-size:.83rem;line-height:1.65;margin:0 0 8px}.trust-note[data-astro-cid-mnqtlyvs] code[data-astro-cid-mnqtlyvs]{color:var(--green)}.trust-note[data-astro-cid-mnqtlyvs] a[data-astro-cid-mnqtlyvs]{color:var(--green-strong);font-size:.8rem;font-weight:600}.share-footer[data-astro-cid-mnqtlyvs]{display:flex;justify-content:space-between;gap:20px;border-top:1px solid var(--line);padding:24px 0 32px;color:var(--muted);font-size:.7rem}.domain-notice[data-astro-cid-mnqtlyvs]{display:flex;align-items:flex-start;gap:12px;margin:28px 22px 0;padding:17px 18px;border:1px solid var(--line);border-radius:10px;background:var(--panel);color:var(--muted)}.domain-notice[data-astro-cid-mnqtlyvs]>svg{flex:0 0 auto;color:var(--green);margin-top:2px}.domain-notice[data-astro-cid-mnqtlyvs] p[data-astro-cid-mnqtlyvs]{margin:0;font-size:.74rem;line-height:1.6}.domain-notice[data-astro-cid-mnqtlyvs] code[data-astro-cid-mnqtlyvs]{color:var(--ink);font-family:var(--mono)}.domain-notice[data-astro-cid-mnqtlyvs] a[data-astro-cid-mnqtlyvs]{color:var(--green-strong);font-weight:600}@media(max-width:600px){.share-main[data-astro-cid-mnqtlyvs]{padding-top:40px}.file-card[data-astro-cid-mnqtlyvs]{padding:28px 20px}.file-top[data-astro-cid-mnqtlyvs]{align-items:flex-start;gap:16px}.file-glyph[data-astro-cid-mnqtlyvs]{width:48px;height:62px}.policy[data-astro-cid-mnqtlyvs],.pin-separation[data-astro-cid-mnqtlyvs],.pin-row[data-astro-cid-mnqtlyvs]{grid-template-columns:1fr}.pin-row[data-astro-cid-mnqtlyvs] button[data-astro-cid-mnqtlyvs]{min-height:52px}.status-card[data-astro-cid-mnqtlyvs]{min-height:390px}.status-fact[data-astro-cid-mnqtlyvs]{flex-direction:column;gap:2px}.trust-note[data-astro-cid-mnqtlyvs],.domain-notice[data-astro-cid-mnqtlyvs]{margin-inline:0}.trust-note[data-astro-cid-mnqtlyvs]{grid-template-columns:42px 1fr}.key-mark[data-astro-cid-mnqtlyvs]{width:40px;height:40px}.share-footer[data-astro-cid-mnqtlyvs]{flex-direction:column}.secure[data-astro-cid-mnqtlyvs]{font-size:.62rem}}
</style></head> <body> <div class="share-shell" data-state="ready" data-astro-cid-mnqtlyvs> <header class="site-header" data-astro-cid-mnqtlyvs><a class="brand" href="https://dove.sh" aria-label="dove home"> <span class="mark" aria-hidden="true"><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" class="lucide lucide-bird"> <path d="M16 7h.01"></path><path d="M3.4 18H12a8 8 0 0 0 8-8V7a4 4 0 0 0-7.28-2.3L2 20"></path><path d="m20 7 2 .5-2 .5"></path><path d="M10 18v3"></path><path d="M14 17.75V21"></path><path d="M7 18a6 6 0 0 0 3.84-10.61"></path> </svg></span> <span>dove</span> </a><div class="secure" data-astro-cid-mnqtlyvs><svg xmlns="http://www.w3.org/2000/svg" width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" data-astro-cid-mnqtlyvs="true" class="lucide lucide-shield-check"> <path d="M20 13c0 5-3.5 7.5-7.66 8.95a1 1 0 0 1-.67-.01C7.5 20.5 4 18 4 13V6a1 1 0 0 1 1-1c2 0 4.5-1.2 6.24-2.72a1.17 1.17 0 0 1 1.52 0C14.51 3.81 17 5 19 5a1 1 0 0 1 1 1z"></path><path d="m9 12 2 2 4-4"></path> </svg> Private transfer</div></header> <main class="share-main" data-astro-cid-mnqtlyvs> <section class="file-card" aria-labelledby="file-title" data-astro-cid-mnqtlyvs> <div id="trust-banner" class="trust-banner" hidden data-astro-cid-mnqtlyvs><span class="trust-mark" aria-hidden="true" data-astro-cid-mnqtlyvs><svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" data-astro-cid-mnqtlyvs="true" class="lucide lucide-shield-check"> <path d="M20 13c0 5-3.5 7.5-7.66 8.95a1 1 0 0 1-.67-.01C7.5 20.5 4 18 4 13V6a1 1 0 0 1 1-1c2 0 4.5-1.2 6.24-2.72a1.17 1.17 0 0 1 1.52 0C14.51 3.81 17 5 19 5a1 1 0 0 1 1 1z"></path><path d="m9 12 2 2 4-4"></path> </svg></span><div data-astro-cid-mnqtlyvs><strong data-astro-cid-mnqtlyvs>From <span id="trust-name" data-astro-cid-mnqtlyvs></span></strong><p id="trust-msg" hidden data-astro-cid-mnqtlyvs></p></div></div> <div class="file-top" data-astro-cid-mnqtlyvs><div class="file-glyph" aria-hidden="true" data-astro-cid-mnqtlyvs><svg xmlns="http://www.w3.org/2000/svg" width="29" height="29" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" data-astro-cid-mnqtlyvs="true" class="lucide lucide-file-archive"> <path d="M13.659 22H18a2 2 0 0 0 2-2V8a2.4 2.4 0 0 0-.706-1.706l-3.588-3.588A2.4 2.4 0 0 0 14 2H6a2 2 0 0 0-2 2v11.5"></path><path d="M14 2v5a1 1 0 0 0 1 1h5"></path><path d="M8 12v-1"></path><path d="M8 18v-2"></path><path d="M8 7V6"></path><circle cx="8" cy="20" r="2"></circle> </svg><span data-astro-cid-mnqtlyvs>ZIP</span></div><div data-astro-cid-mnqtlyvs><p class="overline" data-astro-cid-mnqtlyvs>A file was shared with you</p><h1 id="file-title" data-astro-cid-mnqtlyvs>project-archive.zip</h1><p class="meta" data-astro-cid-mnqtlyvs>24.8 MB · ZIP archive</p></div></div> <div class="policy" aria-label="Share status" data-astro-cid-mnqtlyvs><div data-astro-cid-mnqtlyvs><span class="policy-icon" aria-hidden="true" data-astro-cid-mnqtlyvs><svg xmlns="http://www.w3.org/2000/svg" width="17" height="17" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" data-astro-cid-mnqtlyvs="true" class="lucide lucide-clock-3"> <circle cx="12" cy="12" r="10"></circle><path d="M12 6v6h4"></path> </svg></span><p data-astro-cid-mnqtlyvs>Expires in</p><strong data-astro-cid-mnqtlyvs>1 day, 18 hours</strong></div><div data-astro-cid-mnqtlyvs><span class="policy-icon" aria-hidden="true" data-astro-cid-mnqtlyvs><svg xmlns="http://www.w3.org/2000/svg" width="17" height="17" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" data-astro-cid-mnqtlyvs="true" class="lucide lucide-download"> <path d="M12 15V3"></path><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"></path><path d="m7 10 5 5 5-5"></path> </svg></span><p data-astro-cid-mnqtlyvs>Downloads left</p><strong data-astro-cid-mnqtlyvs>2 of 3</strong></div></div> <div id="pin-pass" class="pin-pass" hidden data-astro-cid-mnqtlyvs><svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" data-astro-cid-mnqtlyvs="true" class="lucide lucide-shield-check"> <path d="M20 13c0 5-3.5 7.5-7.66 8.95a1 1 0 0 1-.67-.01C7.5 20.5 4 18 4 13V6a1 1 0 0 1 1-1c2 0 4.5-1.2 6.24-2.72a1.17 1.17 0 0 1 1.52 0C14.51 3.81 17 5 19 5a1 1 0 0 1 1 1z"></path><path d="m9 12 2 2 4-4"></path> </svg><span data-astro-cid-mnqtlyvs><strong data-astro-cid-mnqtlyvs>PIN accepted</strong> · Access approved; the decryption key is still local.</span></div> <div id="ready-state" data-astro-cid-mnqtlyvs><button class="download-button" id="download" type="button" data-astro-cid-mnqtlyvs><span data-astro-cid-mnqtlyvs>Decrypt & save file</span><b aria-hidden="true" data-astro-cid-mnqtlyvs><svg xmlns="http://www.w3.org/2000/svg" width="17" height="17" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" data-astro-cid-mnqtlyvs="true" class="lucide lucide-arrow-down-to-line"> <path d="M12 17V3"></path><path d="m6 11 6 6 6-6"></path><path d="M19 21H5"></path> </svg></b></button><p class="action-note" data-astro-cid-mnqtlyvs>The download starts only when you click. Opening this page does not use one.</p></div> <div id="working-state" class="working" hidden aria-live="polite" data-astro-cid-mnqtlyvs><div class="progress-head" data-astro-cid-mnqtlyvs><span data-astro-cid-mnqtlyvs>Decrypting on this device…</span><span id="percent" data-astro-cid-mnqtlyvs>0%</span></div><div class="progress" data-astro-cid-mnqtlyvs><i id="progress-bar" data-astro-cid-mnqtlyvs></i></div><p data-astro-cid-mnqtlyvs>The decryption key stays in this browser.</p></div> <div class="pin-panel" id="pin-panel" hidden data-astro-cid-mnqtlyvs> <div id="pin-entry-view" data-astro-cid-mnqtlyvs> <span class="pin-glyph" aria-hidden="true" data-astro-cid-mnqtlyvs><svg xmlns="http://www.w3.org/2000/svg" width="23" height="23" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" data-astro-cid-mnqtlyvs="true" class="lucide lucide-lock-keyhole"> <circle cx="12" cy="16" r="1"></circle><rect x="3" y="10" width="18" height="12" rx="2"></rect><path d="M7 10V7a5 5 0 0 1 10 0v3"></path> </svg></span> <p class="overline" data-astro-cid-mnqtlyvs>PIN required</p> <h2 data-astro-cid-mnqtlyvs>Enter the PIN the sender gave you.</h2> <p class="pin-intro" data-astro-cid-mnqtlyvs>The sender delivered it separately—by text or a call—so the link alone is not enough to open this file.</p> <form id="pin-form" novalidate data-astro-cid-mnqtlyvs> <label for="share-pin" data-astro-cid-mnqtlyvs>Share PIN</label> <div class="pin-row" data-astro-cid-mnqtlyvs><input id="share-pin" name="share-pin" type="password" inputmode="numeric" autocomplete="one-time-code" pattern="[0-9]{4,6}" minlength="4" maxlength="6" placeholder="••••••" required aria-describedby="pin-help pin-error" data-astro-cid-mnqtlyvs><button id="pin-submit" type="submit" data-astro-cid-mnqtlyvs><span class="button-icon" aria-hidden="true" data-astro-cid-mnqtlyvs><svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" data-astro-cid-mnqtlyvs="true" class="lucide lucide-lock-keyhole lock-icon"> <circle cx="12" cy="16" r="1"></circle><rect x="3" y="10" width="18" height="12" rx="2"></rect><path d="M7 10V7a5 5 0 0 1 10 0v3"></path> </svg><svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" data-astro-cid-mnqtlyvs="true" class="lucide lucide-loader-circle loader-icon"> <path d="M21 12a9 9 0 1 1-6.219-8.56"></path> </svg></span><span class="button-label" data-astro-cid-mnqtlyvs>Unlock file</span></button></div> <p id="pin-error" class="pin-error" role="alert" aria-live="assertive" data-astro-cid-mnqtlyvs></p> <p id="pin-help" class="pin-help" data-astro-cid-mnqtlyvs>4–6 digits · Checking the PIN does not use a download.</p> </form> <div class="pin-separation" data-astro-cid-mnqtlyvs><div data-astro-cid-mnqtlyvs><strong data-astro-cid-mnqtlyvs>PIN</strong><span data-astro-cid-mnqtlyvs>Checked by the gate<br data-astro-cid-mnqtlyvs>Rate-limited access</span></div><div data-astro-cid-mnqtlyvs><strong data-astro-cid-mnqtlyvs><svg xmlns="http://www.w3.org/2000/svg" width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" data-astro-cid-mnqtlyvs="true" class="lucide lucide-hash"> <line x1="4" x2="20" y1="9" y2="9"></line><line x1="4" x2="20" y1="15" y2="15"></line><line x1="10" x2="8" y1="3" y2="21"></line><line x1="16" x2="14" y1="3" y2="21"></line> </svg> fragment</strong><span data-astro-cid-mnqtlyvs>Stays on this device<br data-astro-cid-mnqtlyvs>Decrypts the file</span></div></div> </div> <div id="pin-locked-view" class="pin-locked-inline" hidden data-astro-cid-mnqtlyvs><span class="status-glyph danger-glyph" aria-hidden="true" data-astro-cid-mnqtlyvs><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" data-astro-cid-mnqtlyvs="true" class="lucide lucide-lock-keyhole"> <circle cx="12" cy="16" r="1"></circle><rect x="3" y="10" width="18" height="12" rx="2"></rect><path d="M7 10V7a5 5 0 0 1 10 0v3"></path> </svg></span><p class="overline danger" data-astro-cid-mnqtlyvs>Access locked</p><h2 id="pin-locked-title" tabindex="-1" data-astro-cid-mnqtlyvs>This share is locked for security.</h2><p data-astro-cid-mnqtlyvs>Too many PIN attempts were unsuccessful. The gate will not accept more guesses or release the encrypted file.</p><div class="next-step" data-astro-cid-mnqtlyvs><strong data-astro-cid-mnqtlyvs>Need the file?</strong><span data-astro-cid-mnqtlyvs>Ask the sender to revoke this share and create a new one.</span></div></div> </div> </section> <aside class="trust-note" data-astro-cid-mnqtlyvs><span class="key-mark" aria-hidden="true" data-astro-cid-mnqtlyvs><svg xmlns="http://www.w3.org/2000/svg" width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" data-astro-cid-mnqtlyvs="true" class="lucide lucide-hash"> <line x1="4" x2="20" y1="9" y2="9"></line><line x1="4" x2="20" y1="15" y2="15"></line><line x1="10" x2="8" y1="3" y2="21"></line><line x1="16" x2="14" y1="3" y2="21"></line> </svg></span><div data-astro-cid-mnqtlyvs><h2 data-astro-cid-mnqtlyvs>Decrypted here, not on a server.</h2><p data-astro-cid-mnqtlyvs>The key is in the part of this page’s address after <code data-astro-cid-mnqtlyvs>#</code>. That part is never sent over the network. dove uses it locally to unlock the file on this device.</p><a href="https://dove.sh/#security" data-astro-cid-mnqtlyvs>How dove protects your file <svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" data-astro-cid-mnqtlyvs="true" class="lucide lucide-external-link"> <path d="M15 3h6v6"></path><path d="M10 14 21 3"></path><path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6"></path> </svg></a></div></aside> <aside class="domain-notice" data-astro-cid-mnqtlyvs><svg xmlns="http://www.w3.org/2000/svg" width="19" height="19" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" data-astro-cid-mnqtlyvs="true" class="lucide lucide-earth"> <path d="M21.54 15H17a2 2 0 0 0-2 2v4.54"></path><path d="M7 3.34V5a3 3 0 0 0 3 3a2 2 0 0 1 2 2c0 1.1.9 2 2 2a2 2 0 0 0 2-2c0-1.1.9-2 2-2h3.17"></path><path d="M11 21.95V18a2 2 0 0 0-2-2a2 2 0 0 1-2-2v-1a2 2 0 0 0-2-2H2.05"></path><circle cx="12" cy="12" r="10"></circle> </svg><p data-astro-cid-mnqtlyvs>This share page is hosted by <code data-share-host data-astro-cid-mnqtlyvs>share.example</code>. If you need the CLI, install dove only from <a href="https://dove.sh/#install" data-astro-cid-mnqtlyvs>dove.sh</a> or the <a href="https://github.com/boomctl/dove" data-astro-cid-mnqtlyvs>official GitHub repository</a>.</p></aside> </main> <footer class="share-footer" data-astro-cid-mnqtlyvs><span data-astro-cid-mnqtlyvs>Shared securely with dove</span><span data-astro-cid-mnqtlyvs>The sender’s server holds encrypted data only.</span></footer> </div> <script>
const localhostNames = new Set(['localhost', '127.0.0.1', '::1']);
document.querySelectorAll('[data-share-host]').forEach((label) => { if (!localhostNames.has(location.hostname)) label.textContent = location.host; });
document.querySelectorAll('[data-get-command]').forEach((command) => command.textContent = `dove get ${location.href}`);
// ── The real decryptor. The key AND the encrypted metadata (filename, trust)
// ride the URL fragment (#…) — read locally, never sent to any server.
function b64urlToBytes(s) {
let b64 = s.replace(/-/g, '+').replace(/_/g, '/'); while (b64.length % 4) b64 += '=';
const bin = atob(b64); const u = new Uint8Array(bin.length);
for (let i = 0; i < bin.length; i++) u[i] = bin.charCodeAt(i);
return u;
}
// The fragment is just the secret now — the encrypted metadata lives on the
// server (so links stay short). Split on '.' only for backward compatibility.
const fragRaw = location.hash.replace(/^#/, '').trim();
const [secretB64] = fragRaw.split('.');
function keyBytesFromFragment() {
if (!secretB64) return null;
try { const u = b64urlToBytes(secretB64); return u.length === 32 ? u : null; } catch { return null; }
}
// Decrypt the metadata blob with SHA-256(secret ‖ "dove-meta-v1") — the exact
// key dove derives in Rust. Returns {name, from, msg} or null. The server holds
// this blob as opaque ciphertext; it's decrypted only here.
async function decryptMeta(metaB64) {
const secret = keyBytesFromFragment();
if (!secret || !metaB64) return null;
try {
const material = new Uint8Array([...secret, ...new TextEncoder().encode('dove-meta-v1')]);
const mkRaw = await crypto.subtle.digest('SHA-256', material);
const mk = await crypto.subtle.importKey('raw', mkRaw, 'AES-GCM', false, ['decrypt']);
const blob = b64urlToBytes(metaB64);
const pt = await crypto.subtle.decrypt({ name: 'AES-GCM', iv: blob.slice(0, 12) }, mk, blob.slice(12));
return JSON.parse(new TextDecoder().decode(pt));
} catch { return null; }
}
const pathParts = location.pathname.split('/').filter(Boolean);
const shareId = pathParts[1] || '';
let filename = 'download'; // set from the decrypted metadata on load
const downloadUrl = `${location.origin}/dl/${shareId}`;
const verifyUrl = `${location.origin}/verify/${shareId}`;
// Set once the gate confirms a PIN (two-step: verify for free, then the
// explicit Decrypt & save click spends the download). null for non-PIN shares.
let verifiedPin = null;
// Decrypt dove's chunked AES-256-GCM container (byte-for-byte the Rust format):
// header "DOVE"|ver|prefix(8)|chunkSize(4); chunks isLast|ctLen(4)|ct. nonce =
// prefix||counter(4 BE); AAD = counter(4 BE)||isLast. The terminal isLast=1
// chunk is required, so truncation fails authentication.
async function decrypt(buf, keyBytes, onProgress) {
const key = await crypto.subtle.importKey('raw', keyBytes, 'AES-GCM', false, ['decrypt']);
const dv = new DataView(buf);
if (String.fromCharCode(...new Uint8Array(buf, 0, 4)) !== 'DOVE') throw new Error('format');
if (dv.getUint8(4) !== 1) throw new Error('version');
const prefix = new Uint8Array(buf.slice(5, 13));
let off = 17;
const total = buf.byteLength;
const parts = [];
for (let counter = 0; ; counter++) {
if (off >= total) throw new Error('truncated');
const isLast = dv.getUint8(off); off += 1;
const ctLen = dv.getUint32(off, false); off += 4;
const ct = buf.slice(off, off + ctLen); off += ctLen;
const nonce = new Uint8Array(12); nonce.set(prefix, 0);
new DataView(nonce.buffer).setUint32(8, counter, false);
const aad = new Uint8Array(5);
new DataView(aad.buffer).setUint32(0, counter, false); aad[4] = isLast;
const pt = await crypto.subtle.decrypt({ name: 'AES-GCM', iv: nonce, additionalData: aad }, key, ct);
parts.push(new Uint8Array(pt));
onProgress(off / total);
if (isLast === 1) break;
}
return new Blob(parts);
}
// PIN-locked shares: the fragment carries only a secret; the real key is
// PBKDF2(PIN, secret) — byte-identical to dove's Rust derivation. The PIN
// (out of band) is also verified at the gate, so it's a genuine second factor.
async function deriveKey(pin, secretBytes) {
const base = await crypto.subtle.importKey('raw', new TextEncoder().encode(pin), 'PBKDF2', false, ['deriveBits']);
const bits = await crypto.subtle.deriveBits({ name: 'PBKDF2', salt: secretBytes, iterations: 600000, hash: 'SHA-256' }, base, 256);
return new Uint8Array(bits);
}
function setPct(f) {
const p = Math.round(Math.max(0, Math.min(1, f)) * 100);
const el = document.querySelector('#percent'); const bar = document.querySelector('#progress-bar');
if (el) el.textContent = `${p}%`; if (bar) bar.style.width = `${p}%`;
}
function saveBlob(blob, name) {
const a = document.createElement('a');
a.href = URL.createObjectURL(blob); a.download = name; a.click();
setTimeout(() => URL.revokeObjectURL(a.href), 4000);
}
function workDone() {
const working = document.querySelector('#working-state');
if (working) working.innerHTML = '<div class="complete"><span>✓</span><div><strong>File ready</strong><p>Your browser saved the decrypted file.</p></div></div>';
}
function workFail(msg) {
const working = document.querySelector('#working-state');
if (working) working.innerHTML = `<div class="complete"><span>✕</span><div><strong>Couldn’t open the file</strong><p>${msg}</p></div></div>`;
}
const downloadBtn = document.querySelector('#download');
downloadBtn && downloadBtn.addEventListener('click', async () => {
const secret = keyBytesFromFragment();
const ready = document.querySelector('#ready-state');
const working = document.querySelector('#working-state');
if (!secret) { alert('This link is missing its key (nothing after #).'); return; }
if (ready) ready.hidden = true; if (working) working.hidden = false;
try {
// PIN shares were already verified for free; the key is PBKDF2(pin, secret)
// and the PIN rides the download request. Non-PIN: the fragment IS the key.
const key = verifiedPin ? await deriveKey(verifiedPin, secret) : secret;
const url = verifiedPin ? `${downloadUrl}?pin=${encodeURIComponent(verifiedPin)}` : downloadUrl;
const resp = await fetch(url);
if (resp.status === 410) return workFail('This share has expired or reached its download limit.');
if (!resp.ok) return workFail(`The gate returned ${resp.status}.`);
const buf = await resp.arrayBuffer();
const blob = await decrypt(buf, key, setPct);
saveBlob(blob, filename);
workDone();
} catch (e) {
// AES-GCM auth failure, truncation, or a bad key — never a plaintext write.
workFail('The file couldn’t be verified — the data may be incomplete or the key wrong.');
}
});
document.querySelectorAll('[data-copy-command]').forEach((button) => button.addEventListener('click', async () => { const command = button.parentElement && button.parentElement.querySelector('code'); await navigator.clipboard.writeText((command && command.textContent) || ''); const label = button.querySelector('span'); if (label) label.textContent = 'Copied'; setTimeout(() => { if (label) label.textContent = 'Copy'; }, 1500); }));
// ── PIN-locked shares. The gate verifies the PIN (rate-limited, so brute force
// is prevented), and it's folded into the key via PBKDF2. The real PIN panel
// (the design's markup) is rendered into the page hidden; we reveal it when
// /meta reports the share is PIN-locked — no injected approximation.
function showPinLocked() {
const entry = document.querySelector('#pin-entry-view');
const locked = document.querySelector('#pin-locked-view');
if (entry) entry.hidden = true;
if (locked) { locked.hidden = false; const t = locked.querySelector('#pin-locked-title'); if (t) t.focus(); }
}
function setupPin() {
const ready = document.querySelector('#ready-state');
if (ready) ready.hidden = true;
const panel = document.querySelector('#pin-panel');
if (panel) panel.hidden = false;
const overline = document.querySelector('.file-top .overline');
if (overline) overline.textContent = 'A PIN-locked file was shared with you';
const input = document.querySelector('#share-pin');
const error = document.querySelector('#pin-error');
const submit = document.querySelector('#pin-submit');
input && input.addEventListener('input', () => { input.value = input.value.replace(/\D/g, '').slice(0, 6); input.removeAttribute('aria-invalid'); if (error) error.textContent = ''; });
const form = document.querySelector('#pin-form');
form && form.addEventListener('submit', async (e) => {
e.preventDefault();
const secret = keyBytesFromFragment();
const pin = ((input && input.value) || '').replace(/\D/g, '');
if (!secret) { if (error) error.textContent = 'This link is missing its key (nothing after #).'; return; }
if (pin.length < 4) { if (input) input.setAttribute('aria-invalid', 'true'); if (error) error.textContent = 'Enter the 4–6 digit PIN the sender sent you.'; if (input) input.focus(); return; }
const label = submit && submit.querySelector('.button-label');
if (submit) { submit.disabled = true; submit.classList.add('verifying'); } if (label) label.textContent = 'Checking PIN…';
try {
// Verify only — this does NOT spend a download. The download happens when
// the recipient clicks Decrypt & save on the next step.
const resp = await fetch(`${verifyUrl}?pin=${encodeURIComponent(pin)}`);
if (resp.status === 423) { showPinLocked(); return; }
if (resp.status === 401) {
let body = {}; try { body = await resp.json(); } catch {}
if (body.attempts_remaining === 0) { showPinLocked(); return; }
const n = body.attempts_remaining;
if (error) error.textContent = (n != null) ? `That PIN didn’t match — ${n} attempt${n === 1 ? '' : 's'} left.` : 'That PIN didn’t match.';
if (input) { input.setAttribute('aria-invalid', 'true'); input.select(); }
if (submit) { submit.disabled = false; submit.classList.remove('verifying'); } if (label) label.textContent = 'Try again';
return;
}
if (!resp.ok) {
if (error) error.textContent = resp.status === 410 ? 'This share has expired or reached its download limit.' : `The gate returned ${resp.status}.`;
if (submit) { submit.disabled = false; submit.classList.remove('verifying'); } if (label) label.textContent = 'Unlock file';
return;
}
// Verified — hold the PIN and move to the explicit Decrypt & save step.
// No auto-download; the click below is what spends the download.
verifiedPin = pin;
const panel = document.querySelector('#pin-panel'); if (panel) panel.hidden = true;
const pass = document.querySelector('#pin-pass'); if (pass) pass.hidden = false;
const ready = document.querySelector('#ready-state'); if (ready) ready.hidden = false;
const dl = document.querySelector('#download'); if (dl) dl.focus();
} catch (err) {
if (error) error.textContent = 'Couldn’t reach the gate — try again.';
if (submit) { submit.disabled = false; submit.classList.remove('verifying'); } if (label) label.textContent = 'Unlock file';
}
});
}
// ── Populate the page from the gate's free /meta endpoint (no decrement).
const LARGE_BYTES = 500 * 1024 * 1024; // over this → hand off to the CLI
function humanSize(n) {
const u = ['B', 'KB', 'MB', 'GB', 'TB']; let b = n, i = 0;
while (b >= 1024 && i < u.length - 1) { b /= 1024; i++; }
return (i >= 3 ? b.toFixed(1) : Math.round(b)) + ' ' + u[i];
}
function humanLeft(secs) {
if (secs <= 0) return 'expired';
const d = Math.floor(secs / 86400), h = Math.floor((secs % 86400) / 3600), m = Math.floor((secs % 3600) / 60);
if (d > 0) return `${d} day${d > 1 ? 's' : ''}${h ? `, ${h} hour${h > 1 ? 's' : ''}` : ''}`;
if (h > 0) return `${h} hour${h > 1 ? 's' : ''}${m ? `, ${m} min` : ''}`;
return `${m} min`;
}
function showGone(title, msg) {
const card = document.querySelector('.file-card');
if (card) card.innerHTML = `<div class="status-body"><p class="overline danger">Share unavailable</p><h1 id="file-title">${title}</h1><p>${msg}</p><div class="next-step"><strong>Need the file?</strong><span>Ask the sender to create a new share.</span></div></div>`;
}
(async () => {
if (!shareId) return;
let meta;
try {
const r = await fetch(`${location.origin}/meta/${shareId}`);
if (r.status === 404) return showGone('This share is unavailable.', 'It may have been revoked, or it never existed.');
if (!r.ok) return; // leave the page as-is on a transient error
meta = await r.json();
} catch { return; }
// Decrypt the metadata (filename + trust) from the server's opaque blob —
// the key is in the fragment, so this happens only here, client-side.
const fm = await decryptMeta(meta.meta);
if (fm) {
if (fm.name) filename = fm.name;
const title = document.querySelector('#file-title'); if (title) title.textContent = filename;
const ext0 = (filename.split('.').pop() || '').toUpperCase().slice(0, 4);
const glyph = document.querySelector('.file-glyph span'); if (glyph && ext0) glyph.textContent = ext0;
if (fm.from) {
const nameEl = document.querySelector('#trust-name'); if (nameEl) nameEl.textContent = fm.from;
const msgEl = document.querySelector('#trust-msg'); if (msgEl) { msgEl.textContent = fm.msg || ''; msgEl.hidden = !fm.msg; }
const banner = document.querySelector('#trust-banner'); if (banner) banner.hidden = false;
}
}
const now = Math.floor(Date.now() / 1000);
const size = meta.size || 0;
const ext = (filename.split('.').pop() || '').toUpperCase().slice(0, 4);
const metaEl = document.querySelector('.meta'); if (metaEl && size) metaEl.textContent = `${humanSize(size)}${ext ? ` · ${ext} file` : ''}`;
const strongs = document.querySelectorAll('.policy strong');
if (strongs[0]) strongs[0].textContent = humanLeft(meta.expires_at - now);
if (strongs[1]) strongs[1].textContent = meta.downloads_total ? `${meta.downloads_remaining} of ${meta.downloads_total}` : `${meta.downloads_remaining}`;
if (meta.expires_at <= now) return showGone('This share has expired.', 'dove stopped serving the file when its window ended.');
if (meta.downloads_remaining <= 0) return showGone('No downloads remain.', 'This share reached the limit its sender set.');
// Already locked out on PIN — show the locked view, nothing to try here.
if (meta.locked) { setupPin(); showPinLocked(); return; }
// Too big to decrypt in a tab → hand off to the CLI (with --pin if locked).
if (size > LARGE_BYTES) {
const ready = document.querySelector('#ready-state');
if (ready) {
const cmd = `dove get ${location.href}${meta.pin_required ? ' --pin <PIN>' : ''}`;
const note = meta.pin_required ? '<p class="action-note">This share is PIN-locked — use the PIN the sender sent you.</p>' : '';
ready.innerHTML = `<div class="large-file"><p class="overline">Large file · ${humanSize(size)}</p><h2>Use dove to save this file.</h2><p>This file is too large to decrypt reliably in a browser. dove streams it to disk without holding it all in memory.</p><div class="command"><code>${cmd}</code><button type="button"><span>Copy</span></button></div><a class="canonical-link" href="https://dove.sh/#install">Get dove at <strong>dove.sh</strong></a>${note}</div>`;
const btn = ready.querySelector('button'); const code = ready.querySelector('code');
btn && btn.addEventListener('click', async () => { await navigator.clipboard.writeText(code.textContent); const s = btn.querySelector('span'); if (s) { s.textContent = 'Copied'; setTimeout(() => { s.textContent = 'Copy'; }, 1500); } });
}
return;
}
// Small PIN-locked file → in-browser PIN entry, then decrypt with the key.
if (meta.pin_required) { setupPin(); return; }
})();
</script> </body></html>