APProjects's picture
site refresh (6 changed, 0 removed)
610be3c verified
Raw History Blame Contribute Delete
12.8 kB
<!doctype html><html lang="en"><head><meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>Your private incident feed — Vendor Status Watch</title>
<meta name="description" content="Hosted Vendor Status Watch buyers: derive your private RSS feed address from the email you checked out with and your generated access code. Nothing is sent anywhere; the maths happens in your browser.">
<meta name="robots" content="noindex">
<link rel="canonical" href="https://approjects-vendor-status-watch.static.hf.space/rt.html">
<style>
:root{--ink:#10151c;--muted:#5a6675;--line:#dfe5ec;--bg:#f7f9fc;--card:#fff;--accent:#0b62d6;--ok:#137a4b;--warn:#a4560a}
*{box-sizing:border-box}
body{margin:0;background:var(--bg);color:var(--ink);font:16px/1.6 -apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Helvetica,Arial,sans-serif}
main{max-width:720px;margin:0 auto;padding:40px 20px 72px}
h1{font-size:30px;line-height:1.2;margin:0 0 12px}
h2{font-size:20px;margin:40px 0 12px}
p{margin:0 0 16px;max-width:66ch}
.muted{color:var(--muted)}
.small{font-size:14px}
.card{background:var(--card);border:1px solid var(--line);border-radius:12px;padding:20px;margin:24px 0}
label{display:block;font-weight:600;margin:14px 0 6px}
input{width:100%;padding:13px 14px;font-size:16px;border:1px solid var(--line);border-radius:8px;background:#fff;color:var(--ink)}
button{margin-top:18px;width:100%;min-height:48px;padding:13px 20px;font-size:16px;font-weight:600;color:#fff;background:var(--accent);border:0;border-radius:8px;cursor:pointer}
button:disabled{background:#8fadd8;cursor:progress}
button.ghost{background:#fff;color:var(--accent);border:1px solid var(--accent);width:auto;min-height:44px;margin-top:10px}
code,.url{font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;font-size:13px}
.url{display:block;word-break:break-all;background:#f2f6fb;border:1px solid var(--line);border-radius:8px;padding:12px;margin:8px 0}
.ok{color:var(--ok);font-weight:600}
.warn{color:var(--warn);font-weight:600}
ol,ul{max-width:66ch;padding-left:22px}
li{margin:0 0 10px}
a{color:var(--accent)}
.brand{display:flex;align-items:center;gap:10px;margin-bottom:28px}
.brand svg{flex:0 0 auto}
.brand b{font-size:15px;letter-spacing:.02em}
hr{border:0;border-top:1px solid var(--line);margin:36px 0}
@media (max-width:560px){main{padding:24px 16px 56px}h1{font-size:25px}.card{padding:16px}}
</style></head><body><main>
<div class="brand"><svg width="30" height="30" viewBox="0 0 30 30" role="img" aria-label="Vendor Status Watch"><rect width="30" height="30" rx="7" fill="#0b62d6"/><path d="M7 19.5l4.2-6.4 3.4 3.9 3.1-6.2L23 19.5" fill="none" stroke="#fff" stroke-width="2.1" stroke-linecap="round" stroke-linejoin="round"/><circle cx="23" cy="9.6" r="2.4" fill="#7fd1a6"/></svg><b>VENDOR STATUS WATCH</b></div>
<h1>Your private incident feed</h1>
<p>Your feed address is worked out from two things: the email you check out with, and an
<b>access code</b>. Your browser does the maths; <b>neither box is ever sent to us</b> — there is no
server here to send it to.</p>
<h2>Before you buy — generate your access code</h2>
<p>Do not invent this code. A code a person could think up can also be guessed by someone who knows
your billing email, and guessing it would show them which vendors your company depends on. This
button draws 130 random bits from your own browser, which cannot be guessed.</p>
<div class="card">
<button id="gen" type="button">Generate my access code</button>
<div id="genout"></div>
</div>
<h2>After you buy — find your feed address</h2>
<form class="card" id="f">
<label for="email">The email you used at checkout</label>
<input id="email" type="email" autocomplete="email" placeholder="you@company.com" required>
<label for="key">Your access code</label>
<input id="key" type="text" autocomplete="off" placeholder="paste the code from your order" required>
<button id="go" type="submit">Show my feed address</button>
<div id="out"></div>
</form>
<h2>Point your channel at it</h2>
<p>Three platforms, three genuinely different mechanisms. Use the one that matches yours.</p>
<ul>
<li><b>Slack</b> — in the channel you want the alerts in, type
<code>/feed subscribe &lt;your feed address&gt;</code>. That command is built into Slack; nothing to install.</li>
<li><b>Microsoft Teams</b> — open the <b>Workflows</b> app, choose the template
<b>“Post to a channel when an RSS feed is published”</b>, and give it the address. The old Teams connector
route is gone: Microsoft retired Office 365 Connectors in Teams in May 2026.</li>
<li><b>Discord</b> — Discord has no built-in RSS subscription, so this is the one platform that needs a
third-party piece: any free RSS-to-Discord bot (MonitoRSS is the usual one) will post it.</li>
<li><b>Anything else</b> — it is plain RSS 2.0. Feedly, Inoreader, Thunderbird, NetNewsWire, or six lines
of your own code all read it.</li>
</ul>
<p>Not a buyer yet? <a href="https://api.apify.com/v2/key-value-stores/0zzrOgdX92RDhjipZ/records/sample.xml?signature=PenEtcEOLryDpKjrkWLS">Open the live sample feed</a> — real incidents, same format, rebuilt daily.</p>
<h2>What is in the feed, and what is not</h2>
<ul>
<li>One entry per <b>opened</b>, <b>updated</b> or <b>resolved</b> incident on the vendors you named, within
five minutes of the vendor publishing it.</li>
<li>One entry when a vendor's status page becomes unreadable — reported as <b>cannot see</b>, never as OK —
and one when it is readable again.</li>
<li>One <b>daily check-in</b> listing every watched vendor's current state, so a quiet feed is provably a
working feed.</li>
<li>No credential of yours, anywhere. We never asked for a webhook, so we cannot leak one.</li>
</ul>
<hr>
<p class="small muted">The address is derived with PBKDF2-HMAC-SHA256 at 600,000 rounds, in your browser,
over your email and your access code. Because the code carries 130 random bits, guessing it is not a
route in even for someone who knows your billing email. The resulting address is still a bearer URL —
anyone you forward it to can read it — so treat it like a calendar-sharing link. Lost it? Come back
here — the same two answers always produce the same address. <a href="https://approjects-vendor-status-watch.static.hf.space/">Vendor Status Watch</a> ·
<a href="https://approjects-vendor-status-watch.static.hf.space/privacy.html">Privacy</a> · <a href="https://approjects-vendor-status-watch.static.hf.space/terms.html">Terms and refunds</a></p>
</main>
<script>
const enc = new TextEncoder();
const DIR = "https://api.apify.com/v2/key-value-stores/0zzrOgdX92RDhjipZ/records/directory.json?signature=qu2elq0pzDANhrEL1NP7";
const normEmail = e => e.trim().split(/\s+/).join(' ').toLowerCase();
const normPass = k => k.replace(/\s+/g,'').toLowerCase();
const hex = b => [...new Uint8Array(b)].map(x=>x.toString(16).padStart(2,'0')).join('');
async function recordName(email, key){
const km = await crypto.subtle.importKey('raw', enc.encode('vsw|'+normEmail(email)+'|'+normPass(key)),
'PBKDF2', false, ['deriveBits']);
const bits = await crypto.subtle.deriveBits(
{name:'PBKDF2', hash:'SHA-256', salt: enc.encode('vsw-find-v1'), iterations: 600000}, km, 128);
return 'f-' + hex(bits) + '.xml';
}
async function unxor(secret, b64){
const kbits = await crypto.subtle.digest('SHA-256', enc.encode('vsw-url|' + secret));
const hk = await crypto.subtle.importKey('raw', kbits, {name:'HMAC', hash:'SHA-256'}, false, ['sign']);
const raw = Uint8Array.from(atob(b64), c => c.charCodeAt(0));
const out = new Uint8Array(raw.length);
let i = 0, done = 0;
while (done < raw.length) {
const blk = new Uint8Array(await crypto.subtle.sign('HMAC', hk, enc.encode(String(i))));
for (let j = 0; j < blk.length && done < raw.length; j++, done++) out[done] = raw[done] ^ blk[j];
i++;
}
return new TextDecoder().decode(out);
}
async function find(ev){
ev.preventDefault();
const btn = document.getElementById('go'), out = document.getElementById('out');
const email = document.getElementById('email').value, key = document.getElementById('key').value;
if (!email || !key) { out.innerHTML = '<p class="warn">Both boxes are needed.</p>'; return; }
btn.disabled = true; btn.textContent = 'Working — this takes a second or two';
out.innerHTML = '<p class="muted">Deriving your address in this browser (600,000 rounds on purpose) …</p>';
try {
const name = await recordName(email, key);
const res = await fetch(DIR, {cache:'no-store'});
if (!res.ok) throw new Error('directory HTTP ' + res.status);
const dir = await res.json();
const ent = (dir.entries || {})[name];
if (!ent) {
out.innerHTML = '<p class="warn">No feed for that email and access code.</p>' +
'<p class="small">Three usual causes: a typo in either box; the purchase is less than five minutes ' +
'old (the runner builds your feed on its next pass); or the order has expired. Reply to your Gumroad ' +
'receipt and a human-readable answer comes back — we can see which access code is on your order.</p>';
return;
}
const url = await unxor(name, ent.u);
out.innerHTML = '<p class="ok">Found it — this is your private feed address:</p>' +
'<code class="url" id="u">' + url.replace(/&/g,'&amp;').replace(/</g,'&lt;') + '</code>' +
'<button class="ghost" type="button" onclick="navigator.clipboard.writeText(document.getElementById(\'u\').textContent)">Copy the address</button>' +
'<p class="small muted">Last rebuilt ' + (ent.t || 'recently') + '. Keep it to yourself: anyone holding ' +
'the address can read which vendors you watch.</p>';
} catch (e) {
out.innerHTML = '<p class="warn">Something failed: ' + String(e) + '</p>' +
'<p class="small">Reply to your Gumroad receipt and we will send the address by hand.</p>';
} finally {
btn.disabled = false; btn.textContent = 'Show my feed address';
}
}
document.getElementById('f').addEventListener('submit', find);
// Access-code generator. The feed address is PBKDF2 over (email + access code), and the email half
// is not secret — a vendor's billing address is often public. So ALL of the unguessability has to
// live in the code. A human-chosen "8 characters or more, two words joined" key is worth maybe 20
// bits against a dictionary attack by someone who knows the email; 600,000 PBKDF2 rounds only buys
// about a second per guess, which is not enough. 130 bits from the browser's CSPRNG ends the
// attack outright. Crockford base32, lowercased to survive normPass().
function genCode(){
const A = 'abcdefghjkmnpqrstvwxyz0123456789'; // no i/l/o/u — misread when copied by hand
const r = new Uint8Array(26);
crypto.getRandomValues(r);
let s = '';
for (let i = 0; i < r.length; i++) {
if (i && i % 6 === 0) s += '-';
s += A[r[i] % A.length];
}
return s;
}
document.getElementById('gen').addEventListener('click', () => {
const code = genCode();
document.getElementById('genout').innerHTML =
'<p class="small muted" style="margin-bottom:6px">Your access code — copy it into the ' +
'<b>Access code</b> box at checkout, and keep a copy:</p>' +
'<p><code id="code" style="font-size:17px;word-break:break-all">' + code + '</code></p>' +
'<p class="small muted">We never see this code and cannot reset it. If you lose it, reply to your ' +
'Gumroad receipt and we will reissue your feed at a new address.</p>';
document.getElementById('key').value = code;
});
// Self-check. The recipe below is implemented twice — here in the browser and in Python in
// hosted/src/feed.py — and there is no JS runtime on the machine that builds this page, so the
// two could silently disagree and a buyer would be told "no feed for that email". These vectors
// were produced by the Python side; if the browser disagrees with them, say so loudly instead of
// blaming the buyer's typing.
(async () => {
try {
const name = await recordName('test@example.com', 'blue harbour');
const url = await unxor(name, 'L4ZggMBkGeYYl/1cQQQOt81/Fxp14yJQoFU=');
const ok = (name === 'f-9956578eaecb6a1b4f6e06707225eef6.xml') && (url === 'https://example.invalid/ok');
if (!ok) {
const b = document.createElement('p');
b.className = 'warn';
b.textContent = 'This page failed its own self-check, so the lookup below may be wrong. ' +
'Please reply to your Gumroad receipt and we will send your feed address by hand.';
document.getElementById('f').prepend(b);
}
console.log('vsw rt self-check', ok ? 'PASS' : 'FAIL', name);
} catch (e) { console.log('vsw rt self-check error', e); }
})();
</script>
</body></html>