Spaces:
Running
Running
Download rt.html from APProjects/vendor-status-watch: direct link, hf CLI and curl.
- Browser
- Download file 12.8 kB
-
https://huggingface.co/spaces/APProjects/vendor-status-watch/resolve/main/rt.html
- Command line
-
hf download hf://spaces/APProjects/vendor-status-watch/rt.html
-
curl -L -o rt.html https://huggingface.co/spaces/APProjects/vendor-status-watch/resolve/main/rt.html
12.8 kB
| <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 <your feed address></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,'&').replace(/</g,'<') + '</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> |