Files
time/ventures/up-to-date/app.js
T

64 lines
14 KiB
JavaScript
Raw Normal View History

'use strict';
const $=id=>document.getElementById(id), esc=s=>String(s??'').replace(/[&<>"']/g,c=>({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'}[c]));
const link=(url,label)=>`<a href="${esc(url)}" target="_blank" rel="noopener noreferrer">${esc(label)}</a>`;
let report=null, previous=null, previousRepo=null, timer=null;
async function json(url,options={}){const response=await fetch(url,{...options,signal:AbortSignal.timeout(15000)});if(!response.ok)throw new Error(`HTTP ${response.status}${response.status===403||response.status===429?' (API limit or denied access)':''}`);return response.json();}
function parseRepo(value){let s=value.trim().replace(/\/$/,'');if(s.startsWith('https://github.com/'))s=s.slice(19);if(!/^[\w.-]+\/[\w.-]+$/.test(s))throw new Error('Enter owner/repository or an https://github.com/owner/repository URL. Public GitHub only in v0.');return s.replace(/\.git$/,'');}
function parseFile(path,text,url,items,gaps){const add=(name,version,ecosystem,exact=false,coverage='Declared; installed version unknown')=>items.push({name,version,ecosystem,exact,path,url,coverage});
try{
if(path.endsWith('package.json')){const p=JSON.parse(text);for(const kind of ['dependencies','devDependencies','peerDependencies','optionalDependencies'])for(const [n,v]of Object.entries(p[kind]||{}))add(n,v,'npm',false,kind);for(const[n,v]of Object.entries(p.engines||{}))add(n,v,'runtime');}
else if(path.endsWith('package-lock.json')){const p=JSON.parse(text);if(p.packages){for(const[n,v]of Object.entries(p.packages)){if(n.startsWith('node_modules/')&&!n.slice(13).includes('/node_modules/')&&v.version)add(n.slice(13),v.version,'npm',true,'Resolved lockfile version; runtime not verified');}}else for(const[n,v]of Object.entries(p.dependencies||{}))if(v.version)add(n,v.version,'npm',true,'Resolved lockfile version');}
else if(/requirements[^/]*\.txt$/.test(path)){for(const line of text.split('\n')){const m=line.match(/^([\w.-]+)(?:\[[^\]]+\])?\s*(==|>=|~=|<=|>|<)?\s*([^;#\s]*)/);if(m)add(m[1],m[3]||'unversioned','PyPI',m[2]==='==',m[2]==='=='?'Exact manifest pin':'Range / installed version unknown');}}
else if(/Dockerfile[^/]*$/.test(path)){for(const m of text.matchAll(/^\s*FROM\s+(?:--platform=\S+\s+)?(\S+)/gmi))add(m[1].split(':')[0],m[1],'container');}
else if(path.includes('.github/workflows/')){for(const m of text.matchAll(/\buses:\s*['"]?([^\s'"#]+)/g))add(m[1].split('@')[0],m[1].split('@')[1]||'unversioned','action');}
else {add(path.split('/').pop(),'Detected','stack file');gaps.push(`${path}: detected but full parser unavailable (including yarn/pnpm/Python project lock formats).`);}
}catch(e){gaps.push(`${path}: parse failed: ${e.message}`);}}
function signal(title,detail,url,date,severity='info'){report.signals.push({title,detail,url,date,severity});}
async function scan(){if($('run').disabled)return;let repo;try{repo=parseRepo($('repo').value)}catch(e){$('status').textContent=e.message;return;}$('run').disabled=true;$('demo').disabled=true;$('export').disabled=true;$('results').hidden=true;$('onboarding').hidden=true;$('public-consent').checked=false;
report={repo,checkedAt:new Date().toISOString(),items:[],signals:[],gaps:[],files:[]};const gaps=report.gaps;try{
$('status').textContent='Reading repository metadata…';const api=`https://api.github.com/repos/${repo}`;const meta=await json(api);const branch=meta.default_branch;const commit=await json(`${api}/commits/${encodeURIComponent(branch)}`);report.commit=commit.sha;report.branch=branch;
const tree=await json(`${api}/git/trees/${commit.sha}?recursive=1`);if(tree.truncated)gaps.push('GitHub tree is truncated; inventory may omit files.');const paths=tree.tree.filter(f=>f.type==='blob'&&/(^|\/)(package\.json|package-lock\.json|yarn\.lock|pnpm-lock\.yaml|requirements[^/]*\.txt|pyproject\.toml|poetry\.lock|uv\.lock|go\.mod|Cargo\.toml|Dockerfile[^/]*|\.nvmrc|\.node-version)$|(^|\/)\.github\/workflows\/[^/]+\.ya?ml$/.test(f.path));if(paths.length>24)gaps.push(`${paths.length} relevant files found; first 24 scanned.`);
for(const f of paths.slice(0,24)){$('status').textContent=`Inspecting ${f.path}…`;const url=`https://github.com/${repo}/blob/${commit.sha}/${f.path}`;try{if(f.size>400000){gaps.push(`${f.path}: exceeds 400 KB limit.`);continue;}const blob=await json(`${api}/git/blobs/${f.sha}`);const bytes=Uint8Array.from(atob(blob.content.replace(/\s/g,'')),c=>c.charCodeAt(0));const text=new TextDecoder().decode(bytes);report.files.push({path:f.path,url});parseFile(f.path,text,url,report.items,gaps);}catch(e){gaps.push(`${f.path}: ${e.message}`);}}
if(!report.items.length)gaps.push('No supported stack declarations found. This is not evidence of a dependency-free repository.');
const unique=new Map();for(const i of report.items){const key=i.ecosystem+':'+i.name;const old=unique.get(key);if(!old||i.exact&&!old.exact)unique.set(key,i);}const dependencies=[...unique.values()].filter(i=>['npm','PyPI'].includes(i.ecosystem));if(dependencies.length>12)gaps.push(`${dependencies.length} dependencies inventoried; release/advisory checks limited to first 12 unique dependencies.`);
for(const i of dependencies.slice(0,12)){$('status').textContent=`Checking upstream signals for ${i.name}…`;if(i.ecosystem==='npm')try{const p=await json(`https://registry.npmjs.org/${encodeURIComponent(i.name)}`);const latest=p['dist-tags']?.latest, current=i.exact?p.versions?.[i.version]:null;signal(`${i.name} · latest ${latest||'unknown'}`,`Observed ${i.version}. ${i.exact&&latest===i.version?'Matches npm latest.': 'Review compatibility and release notes; a differing version/range is not proof an upgrade is required.'}`,`https://www.npmjs.com/package/${encodeURIComponent(i.name)}`,p.time?.[latest]||report.checkedAt);if(current?.deprecated)signal(`${i.name} · deprecated version`,current.deprecated,`https://www.npmjs.com/package/${encodeURIComponent(i.name)}/v/${encodeURIComponent(i.version)}`,report.checkedAt,'alert');else if(!i.exact)gaps.push(`${i.name}: installed version and version-specific deprecation unknown (range).`);}catch(e){gaps.push(`${i.name}: npm release/deprecation check unavailable: ${e.message}`);}
if(i.exact&&/^\d+\.\d+/.test(i.version))try{const result=await json('https://api.osv.dev/v1/query',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({package:{name:i.name,ecosystem:i.ecosystem},version:i.version})});const active=(result.vulns||[]).filter(v=>!v.withdrawn);if(!active.length)signal(`${i.name} ${i.version} · no OSV matches`,`OSV returned no published matches for this exact version at scan time. This is not a security certification.`, 'https://osv.dev/',report.checkedAt);for(const v of active)signal(`${i.name} ${i.version} · ${v.id}`,`${v.summary||'Published vulnerability'} — lockfile/pin matches; runtime reachability and exploitability require review.`,`https://osv.dev/vulnerability/${encodeURIComponent(v.id)}`,v.modified||v.published||report.checkedAt,'alert');}catch(e){gaps.push(`${i.name}: OSV check unavailable: ${e.message}`);}else gaps.push(`${i.name}: vulnerability status unknown; no supported exact version.`);}
try{const releases=await json(`${api}/releases?per_page=3`);for(const r of releases)signal(`Repository release · ${r.tag_name}`,`${r.name||r.tag_name}${r.prerelease?' (prerelease)':''}. Review release notes for changes affecting this stack.`,r.html_url,r.published_at);if(!releases.length)gaps.push('Repository has no GitHub releases; tags/changelog not evaluated.');}catch(e){gaps.push(`Repository releases unavailable: ${e.message}`);}
gaps.push('Docker and Action refs: inventory only; release, security and deprecation coverage unknown. Private repos, GitLab and runtime discovery are unsupported in this tab. Hosted pilot coverage is shown separately.');
const signature=JSON.stringify(report.items.map(i=>[i.name,i.version,i.path]));report.comparison=previous&&previousRepo===repo?(signature===previous?'No inventory changes since previous scan in this tab.':'Inventory changed since previous scan in this tab; review evidence.'):'First scan in this tab; no previous baseline.';previous=signature;previousRepo=repo;
$('status').textContent=`Scan complete · ${report.checkedAt} · ${repo}`;
}catch(e){gaps.push(`Scan incomplete: ${e.message}. Check repository visibility, connectivity and GitHub's unauthenticated rate limit (usually 60 requests/hour/IP). Retry later; no clean bill of health is implied.`);$('status').textContent='Scan incomplete — see unknowns below.';}
render();$('run').disabled=false;$('demo').disabled=false;$('export').disabled=false;}
function render(){$('results').hidden=false;$('onboarding').hidden=!report.files.length;$('summary').innerHTML=`<p><span class="badge">${report.files.length?'Live API evidence':'No repository evidence collected'}</span> ${report.items.length} observations · ${report.files.length} files · ${esc(report.checkedAt)}</p><p>${esc(report.comparison||'Incomplete scan; no baseline saved.')} ${report.commit?link(`https://github.com/${report.repo}/tree/${report.commit}`,`Source ${report.commit.slice(0,8)}`):''}</p>`;$('inventory').innerHTML=report.items.map(i=>`<tr><td>${esc(i.name)}<br><small>${esc(i.ecosystem)}</small></td><td>${esc(i.version)}</td><td>${link(i.url,i.path)}</td><td>${esc(i.coverage)}</td></tr>`).join('');$('signals').innerHTML=report.signals.map(s=>`<article class="${s.severity==='alert'?'alert':''}"><strong>${esc(s.title)}</strong><p>${esc(s.detail)}</p><small>${link(s.url,'Source')} · source/checked time: ${esc(s.date)}</small></article>`).join('')||'<p>No verified signals collected. See coverage gaps.</p>';$('gaps').innerHTML=report.gaps.map(g=>`<li>${esc(g)}</li>`).join('');}
$('scan').onsubmit=e=>{e.preventDefault();scan();};$('demo').onclick=()=>{$('repo').value='https://github.com/expressjs/express';scan();};$('interval').onchange=()=>{clearInterval(timer);if(+$('interval').value)timer=setInterval(scan,+$('interval').value);};$('export').onclick=()=>{const url=URL.createObjectURL(new Blob([JSON.stringify(report,null,2)],{type:'application/json'}));const a=document.createElement('a');a.href=url;a.download='up-to-date-evidence.json';a.click();setTimeout(()=>URL.revokeObjectURL(url),1000);};
function issueDraft(){
if(!report?.files.length)throw new Error('Scan a public repo before requesting monitoring.');
const repoUrl='https://github.com/'+report.repo;
const request=`UpToDate monitoring request\n\nRepository: ${repoUrl}\nRequested cadence: ${$('cadence').value}\nNotification preference: ${$('notification').value}\n\nFirst scan: ${report.checkedAt}\nSource commit: ${report.commit}\n\nI understand this is an early-access request. Hosted monitoring is NOT active until confirmed on this issue. No credentials or private details are included.`;
const query=new URLSearchParams({template:'early-access.yml',title:'Early access: UpToDate — '+report.repo,venture:'up-to-date',plan:'Free / hosted monitoring request',use:request});
return 'https://github.com/teahubxyz/time/issues/new?'+query;
}
$('request').onsubmit=e=>{e.preventDefault();if(!$('public-consent').checked)return;const url=issueDraft();$('request-status').textContent='Opening your draft. Submit it on GitHub to request monitoring; hosted checks remain inactive.';location.assign(url);};
document.querySelectorAll('a[href="#scan"]').forEach(a=>a.addEventListener('click',()=>setTimeout(()=>$('repo').focus({preventScroll:true}),300)));
async function hostedStatus(){
const el=document.getElementById('hosted-status');
try {
const r=await fetch('monitor-status.json',{cache:'no-store'});if(!r.ok)throw new Error('Status unavailable');
const st=await r.json();const date=n=>n?new Date(n*1000).toISOString():'Not yet';
el.replaceChildren();
for(const item of st.repos){
const article=document.createElement('article');
const title=document.createElement('strong');title.textContent=item.repo+' · '+item.state+' · '+item.scan_status;article.append(title);
for(const text of ['Last successful scan: '+date(item.last_success),'Next due: '+date(item.next_due),'Notification: '+item.notification,(item.error||''),'Coverage: '+(item.snapshot?.gaps||['No completed scan']).join(' ')]){const p=document.createElement('p');p.textContent=text;article.append(p);}
const p=document.createElement('p');p.textContent=item.diff.length?item.diff.length+' changes since prior successful snapshot. Review evidence below.':'No changes recorded / initial baseline.';article.append(p);
if(item.snapshot){const a=document.createElement('a');a.href='https://github.com/'+item.repo+'/tree/'+item.snapshot.commit;a.textContent='Review source '+item.snapshot.commit.slice(0,8);a.rel='noopener';article.append(a);}
for(const change of item.diff){const p=document.createElement('p');p.textContent=(change.component||change.kind)+': '+JSON.stringify(change.before)+' → '+JSON.stringify(change.after)+'. Review compatibility/advisory evidence before changing code.';article.append(p);if(change.after?.url&&/^https:\/\/(www\.npmjs\.com|osv\.dev|github\.com)\//.test(change.after.url)){const a=document.createElement('a');a.href=change.after.url;a.textContent='Review upstream evidence';article.append(a);}}
const d=document.createElement('details');const sum=document.createElement('summary');sum.textContent='Stored snapshot, diff and alert feed';const pre=document.createElement('pre');pre.style.whiteSpace='pre-wrap';pre.style.overflowWrap='anywhere';pre.textContent=JSON.stringify({snapshot:item.snapshot,diff:item.diff,alerts:item.alerts},null,2);d.append(sum,pre);article.append(d);el.append(article);
}
const p=document.createElement('p');p.className='meta';p.textContent='Status published '+st.generated_at+'. '+st.channel;el.append(p);
if(Date.now()-Date.parse(st.generated_at)>3600000){const p=document.createElement('p');p.textContent='Scheduler status is stale; current monitoring health is unknown.';el.prepend(p);}
} catch(e){el.textContent='Hosted status unavailable — monitoring health unknown. Please retry later.';}
}
hostedStatus();