perf: avoid needless downloads and track request latency (#876)

* perf: avoid needless downloads and track request latency

* fix: correct request timing and monitoring revalidation
This commit is contained in:
Thomas Durieux authored and GitHub committed 2026-10-08 10:38:33 +01:00
1 parent db966356ec
commit a9760eab39
28 files changed
+1208 -73

No files matched your search

+10
View File
@@ -1,5 +1,10 @@
services:
anonymous_github:
logging:
driver: json-file
options:
max-size: "25m"
max-file: "4"
build: .
restart: always
image: tdurieux/anonymous_github:v2
@@ -36,6 +41,11 @@ services:
condition: service_healthy
streamer:
logging:
driver: json-file
options:
max-size: "25m"
max-file: "4"
build: .
restart: always
image: tdurieux/anonymous_github:v2
+90
View File
@@ -0,0 +1,90 @@
# Request performance monitoring
The admin overview shows API and streamer request latency, interrupted responses,
first-byte latency, work stages and process resources. It uses the existing Redis
service. `/api/admin/performance?minutes=15` returns the same data after the existing
admin authentication and authorization checks. The other supported window is 60
minutes. Responses are private and never HTTP-cached.
Request measurement starts before body parsing, authentication and rate limiting.
Each response is counted once on completion or premature close. A response that
sent HTTP 200 and then disconnected is reported as interrupted. Completion means
Node handed the response to the operating system; it does not prove client receipt.
First-byte timing measures the first response headers, including explicit header
flushes. Byte counts measure encoded response body writes and exclude HTTP headers.
## Latency and diagnosis
The latency histograms use millisecond boundaries of 10, 50, 100, 250, 500, 1000,
2500, 5000, 10000, 30000 and 60000, plus an overflow bucket. Displayed p95 and p99
values are bucket upper bounds, not exact percentiles. No samples or a percentile
above 60 seconds produces `null`; the UI distinguishes these cases using count.
Downloads include body transfer time, so compare their first-byte and total latency
before treating a long transfer as slow processing.
Slow requests produce structured warning logs above one second for ordinary API
calls and above ten seconds for file, website and streamer transfers. Interrupted
requests also produce warnings. These thresholds identify requests for diagnosis;
they do not automatically page anyone. Healthcheck timings are counted separately
and successful healthchecks do not produce access logs.
Logs contain a generated request ID, a fixed route label, method, status, terminal
outcome, duration, first-byte time, body bytes and work stages. API calls forward
the request ID to streamers. Repository IDs, source names, file paths, bodies and
query values are absent from these new request records and metric labels.
Stages include repository lookup, authorization, GitHub requests, source-cache
lookup and fill, worker admission, worker anonymization and streamer response
headers. Source and transformed-cache hits and misses are counted separately.
Source download timing includes waiting for response headers and failures before
streaming begins.
Stages can overlap and must not be summed to reconstruct total latency. An archive
upstream span also includes downstream backpressure; it is not pure network time.
Work outside HTTP requests, such as background download jobs, stays in the existing
queue monitoring rather than these request histograms.
## Resources, retention and overhead
Each process batches at most 512 metric groups every 15 seconds. Minute counters
expire after two hours and survive application rollouts. Read windows are restricted
to 15 or 60 minutes, with a ten-second cache and at most 32 live instances. Redis
commands have a two-second monitoring deadline and offline command queuing is
disabled. Monitoring never awaits Redis from an HTTP request handler, except the
admin report itself. Failures discard a bounded batch and show unavailable reports
or dropped-batch counters instead of silently implying healthy coverage.
Samples include process RSS, heap, external buffers, CPU usage, cgroup memory
limit, event-loop p95 and maximum delay, active requests, PID-owned sockets and
CLOSE_WAIT states, and worker activity, queue and byte reservations. Samples expire
after 90 seconds. The UI highlights samples older than 45 seconds, dropped metrics,
CLOSE_WAIT, event-loop p95 above 100 ms and RSS above 80 percent of its limit.
CPU usage is relative to one CPU and can exceed 100 percent across multiple cores.
RSS is process memory, not Docker's accounting of the entire cgroup.
A bounded runtime history keeps 240 API samples and 960 streamer samples, enough
for roughly one hour at one API and four streamers. The response includes this
history and the UI shows each live process's peak observed RSS in the selected
window. Additional replicas or rollout overlap shorten the history. Retired
processes disappear from the live table while retained request counters and
resource history remain available. Missing samples do not establish health.
Application Docker logs rotate at 25 MB with four files per container. The admin
overview's disk check runs asynchronously and caches its result for 30 seconds.
These settings avoid making monitoring itself block the Node event loop or fill
the host disk with unbounded access logs.
## Performance changes
ZIP HEAD requests check repository access and download permission, then return
headers without requesting a token, counting a view or generating an archive.
File and website HEAD requests validate metadata and content gates before ending
without content download or transformation. File validators accept weak and
multiple If-None-Match values using Express freshness handling.
Website responses use private, revalidated ETags covering content SHA, commit,
tree generation, cache revision, file path, rendering version and current options.
Validation follows repository lifecycle, page settings and content checks. Matching
validators avoid fetching and rendering content again; changed terms or content
invalidate them.
Raw Markdown served after a rendering failure has a separate validator, so later
requests retry rendering when the transient failure clears.
+1 -1
View File
@@ -1,6 +1,6 @@
{
"core.min.js": "core.3b07189932.min.js",
"vendor.min.js": "vendor.a1e4517da7.min.js",
"vendor.min.js": "vendor.ae4f635979.min.js",
"mermaid.min.js": "mermaid.f848a72d16.min.js",
"all.min.css": "all.947ab87f1c.min.css",
"markdown.min.js": "markdown.44f2e93820.min.js",
+48
View File
@@ -17,6 +17,54 @@
<div class="admin-empty" v-if="loading">Loading overview&hellip;</div>
<div class="alert alert-danger" style="margin:12px 0" v-if="error"><svg class="ui-icon" aria-hidden="true"><use href="#ui-icon-warning"></use></svg> {{ error }}</div>
<section class="ov-panel-card" style="margin:16px 0">
<div class="ov-panel-head">
<span class="ov-panel-title">Request performance</span>
<select v-model.number="performanceMinutes" @change="loadPerformance()" aria-label="Performance time range">
<option :value="15">Last 15 minutes</option><option :value="60">Last hour</option>
</select>
</div>
<p v-if="performanceError" class="alert alert-warning">{{ performanceError }}</p>
<p v-if="!performance &amp;&amp; !performanceError">Loading performance monitoring&hellip;</p>
<div v-if="performance?.available">
<p>Latency includes upstream waits and response transfer. Percentiles show histogram bucket upper bounds. Samples refresh every 30 seconds.</p>
<div class="table-responsive">
<table class="table table-sm">
<thead><tr><th>Service</th><th>Route</th><th>Requests</th><th>Average</th><th>First byte p95</th><th>p95</th><th>p99</th><th>Interrupted</th><th>5xx</th><th>Slow</th></tr></thead>
<tbody><tr v-for="row in performance.routes" :key="row.service + row.method + row.route">
<td>{{ row.service }}</td><td>{{ row.method }} {{ row.route }}</td><td>{{ row.count }}</td><td>{{ row.avgMs }} ms</td>
<td>{{ firstByteBound(row) }}</td><td>{{ latencyBound(row, 'p95UpperMs') }}</td><td>{{ latencyBound(row, 'p99UpperMs') }}</td>
<td>{{ row.aborted }}</td><td>{{ row.errors }}</td><td>{{ row.slow }}</td>
</tr></tbody>
</table>
</div>
<p v-if="!performance.routes?.length">No recorded requests in this window yet.</p>
<h3>API and streamer processes</h3>
<p v-if="!performance.instances?.length" class="alert alert-warning">No recent process samples. Monitoring coverage is incomplete.</p>
<div class="table-responsive">
<table class="table table-sm">
<thead><tr><th>Process</th><th>RSS / limit</th><th>Peak RSS in window</th><th>CPU</th><th>Event loop p95</th><th>Active requests</th><th>Sockets / CLOSE_WAIT</th><th>Workers / waiting</th><th>Sample age</th><th>Dropped batches / metrics</th></tr></thead>
<tbody><tr v-for="sample in performance.instances" :key="sample.instance" :class="{'table-warning': performanceWarning(sample)}">
<td>{{ sample.instance }}</td><td>{{ humanBytes(sample.memory.rss) }} / {{ humanBytes(sample.memoryLimitBytes) }}</td>
<td>{{ humanBytes(peakRss(sample)) }}</td><td>{{ sample.cpuPercent.toFixed(1) }}%</td><td>{{ sample.eventLoop.p95Ms.toFixed(1) }} ms</td>
<td>{{ sample.activeRequests }}</td><td>{{ sample.sockets?.descriptors ?? 'Unavailable' }} / {{ sample.sockets?.closeWait ?? 'Unavailable' }}</td>
<td>{{ sample.workers?.running ?? 0 }} / {{ sample.workers?.waiting ?? 0 }}</td><td>{{ sampleAge(sample) }} s</td>
<td>{{ sample.droppedBatches }} / {{ sample.droppedMetrics }}</td>
</tr></tbody>
</table>
</div>
<h3>Work stages and cache results</h3>
<div class="table-responsive">
<table class="table table-sm">
<thead><tr><th>Service</th><th>Stage</th><th>Samples</th><th>Average</th><th>p95</th></tr></thead>
<tbody><tr v-for="row in performance.stages.filter(row =&gt; row.metric !== 'first_byte')" :key="row.service + row.metric">
<td>{{ row.service }}</td><td>{{ row.metric }}</td><td>{{ row.count }}</td><td>{{ row.avgMs }} ms</td><td>{{ latencyBound(row, 'p95UpperMs') }}</td>
</tr></tbody>
</table>
</div>
</div>
</section>
<div v-if="data">
<!-- ── Top KPI row ──────────────────────────────────────────── -->
+39
View File
@@ -1734,6 +1734,44 @@ export const overviewAdminController = function (state, http, location, interval
state.data = null;
state.loading = true;
state.error = null;
state.performance = null;
state.performanceMinutes = 15;
state.performanceError = null;
state.latencyBound = function (row, field) {
if (!row.count) return "No samples";
return row[field] == null ? "> 60 s" : "\u2264 " + row[field] + " ms";
};
state.firstByteBound = function (row) {
return state.latencyBound({ count: row.firstByteCount, p95UpperMs: row.firstByteP95UpperMs }, "p95UpperMs");
};
state.sampleAge = function (sample) { return Math.max(0, Math.round((Date.now() - sample.sampledAt) / 1000)); };
state.peakRss = function (sample) {
return (state.performance.runtimeSeries || []).filter(function (point) { return point.instance === sample.instance; })
.reduce(function (peak, point) { return Math.max(peak, point.rss); }, sample.memory.rss);
};
state.performanceWarning = function (sample) {
return state.sampleAge(sample) > 45 || sample.droppedBatches || sample.droppedMetrics ||
sample.sockets?.closeWait > 0 || sample.eventLoop.p95Ms > 100 ||
sample.memoryLimitBytes && sample.memory.rss > sample.memoryLimitBytes * 0.8;
};
var performanceLoading = false;
state.loadPerformance = function () {
var minutes = state.performanceMinutes;
if (state.performance?.windowMinutes && state.performance.windowMinutes !== minutes) state.performance = null;
if (performanceLoading) return;
performanceLoading = true;
http.get("/api/admin/performance", { params: { minutes: minutes } }).then(function (r) {
if (minutes !== state.performanceMinutes) return;
state.performance = r.data;
state.performanceError = r.data.available ? null : "Performance monitoring is unavailable. Request history may be incomplete.";
}, function () {
if (minutes !== state.performanceMinutes) return;
state.performanceError = "Could not refresh performance monitoring. Displayed samples may be stale.";
}).finally(function () {
performanceLoading = false;
if (minutes !== state.performanceMinutes) state.loadPerformance();
});
};
function humanBytes(b) {
if (b == null) return "—";
@@ -1834,6 +1872,7 @@ export const overviewAdminController = function (state, http, location, interval
};
function load() {
state.loadPerformance();
http.get("/api/admin/overview").then(function (r) {
r.data.history = computeDailyHistory(r.data.history);
r.data.daily = {
+22 -22
View File
@@ -1,30 +1,30 @@
"use strict";(()=>{var xl=Object.defineProperty;var ed=(e,t)=>{for(var n in t)xl(e,n,{get:t[n],enumerable:!0})};var me=["abap","abc","actionscript","ada","alda","apache_conf","apex","applescript","aql","asciidoc","asl","assembly_x86","autohotkey","batchfile","c9search","c_cpp","cirru","clojure","cobol","coffee","coldfusion","crystal","csharp","csound_document","csound_orchestra","csound_score","csp","css","curly","d","dart","diff","django","dockerfile","dot","drools","edifact","eiffel","ejs","elixir","elm","erlang","forth","fortran","fsharp","fsl","ftl","gcode","gherkin","gitignore","glsl","gobstones","golang","graphqlschema","groovy","haml","handlebars","haskell","haskell_cabal","haxe","hjson","html","html_elixir","html_ruby","ini","io","jack","jade","java","javascript","json","json5","jsoniq","jsp","jssm","jsx","julia","kotlin","latex","less","liquid","lisp","livescript","logiql","logtalk","lsl","lua","luapage","lucene","makefile","markdown","mask","matlab","maze","mediawiki","mel","mixal","mushcode","mysql","nginx","nim","nix","nsis","nunjucks","objectivec","ocaml","pascal","perl","perl6","pgsql","php","php_laravel_blade","pig","plain_text","powershell","praat","prisma","prolog","properties","protobuf","puppet","python","qml","r","razor","rdoc","red","redshift","rhtml","rst","ruby","rust","sass","scad","scala","scheme","scss","sh","sjs","slim","smarty","snippets","soy_template","space","sparql","sql","sqlserver","stylus","svg","swift","tcl","terraform","tex","text","textile","toml","tsx","turtle","twig","typescript","vala","vbscript","velocity","verilog","vhdl","visualforce","wollok","xml","xquery","yaml","zeek"];var ve={notebook:"/script/notebook.c3144c18ae.min.js",org:"/script/org.f61f262915.min.js",markdown:"/script/markdown.44f2e93820.min.js",editor:"/script/editor.4f142d3adc.min.js",pdf:"/script/pdf.4d9cc6ff49.min.js"};var us=new Map,sd=ve;function Jt(e){if(!us.has(e)){let t=document.createElement("script");t.src=sd[e];let n=new Promise((r,s)=>{t.onload=r,t.onerror=()=>{us.delete(e),t.remove(),s(new Error(`Unable to load ${e}. Please retry.`))}});us.set(e,n),document.head.appendChild(t)}return us.get(e)}async function lr(){await Jt("editor"),ace.config.set("basePath","/script/external/ace/")}function po(e){let t=Object.create(null);for(let n of e.split(","))t[n]=1;return n=>n in t}var Pe={},Qt=[],at=()=>{},sn=()=>!1,fo=e=>e.charCodeAt(0)===111&&e.charCodeAt(1)===110&&(e.charCodeAt(2)>122||e.charCodeAt(2)<97),ho=e=>e.startsWith("onUpdate:"),Ge=Object.assign,Oo=(e,t)=>{let n=e.indexOf(t);n>-1&&e.splice(n,1)},nd=Object.prototype.hasOwnProperty,De=(e,t)=>nd.call(e,t),be=Array.isArray,$t=e=>Io(e)==="[object Map]",co=e=>Io(e)==="[object Set]",dr=e=>Io(e)==="[object Date]";var we=e=>typeof e=="function",ze=e=>typeof e=="string",ht=e=>typeof e=="symbol",Te=e=>e!==null&&typeof e=="object",nn=e=>(Te(e)||we(e))&&we(e.then)&&we(e.catch),pr=Object.prototype.toString,Io=e=>pr.call(e),rn=e=>Io(e).slice(8,-1),cs=e=>Io(e)==="[object Object]",ps=e=>ze(e)&&e!=="NaN"&&e[0]!=="-"&&""+parseInt(e,10)===e,mo=po(",key,ref,ref_for,ref_key,onVnodeBeforeMount,onVnodeMounted,onVnodeBeforeUpdate,onVnodeUpdated,onVnodeBeforeUnmount,onVnodeUnmounted");var fs=e=>{let t=Object.create(null);return(n=>t[n]||(t[n]=e(n)))},rd=/-\w/g,Je=fs(e=>e.replace(rd,t=>t.slice(1).toUpperCase())),id=/\B([A-Z])/g,St=fs(e=>e.replace(id,"-$1").toLowerCase()),Xt=fs(e=>e.charAt(0).toUpperCase()+e.slice(1)),Po=fs(e=>e?`on${Xt(e)}`:""),gt=(e,t)=>!Object.is(e,t),vo=(e,...t)=>{for(let n=0;n<e.length;n++)e[n](...t)},hs=(e,t,n,r=!1)=>{Object.defineProperty(e,t,{configurable:!0,enumerable:!1,writable:r,value:n})},Vo=e=>{let t=parseFloat(e);return isNaN(t)?e:t};var ur,qo=()=>ur||(ur=typeof globalThis<"u"?globalThis:typeof self<"u"?self:typeof window<"u"?window:typeof global<"u"?global:{});function Me(e){if(be(e)){let t={};for(let n=0;n<e.length;n++){let r=e[n],s=ze(r)?ud(r):Me(r);if(s)for(let i in s)t[i]=s[i]}return t}else if(ze(e)||Te(e))return e}var ad=/;(?![^(]*\))/g,ld=/:([^]+)/,dd=/\/\*[^]*?\*\//g;function ud(e){let t={};return e.replace(dd,"").split(ad).forEach(n=>{if(n){let r=n.split(ld);r.length>1&&(t[r[0].trim()]=r[1].trim())}}),t}function A(e){let t="";if(ze(e))t=e;else if(be(e))for(let n=0;n<e.length;n++){let r=A(e[n]);r&&(t+=r+" ")}else if(Te(e))for(let n in e)e[n]&&(t+=n+" ");return t.trim()}var fr="itemscope,allowfullscreen,formnovalidate,ismap,nomodule,novalidate,readonly",hr=po(fr),cd=po(fr+",async,autofocus,autoplay,controls,default,defer,disabled,inert,loop,open,required,reversed,scoped,seamless,checked,muted,multiple,selected");function ms(e){return!!e||e===""}function pd(e,t){if(e.length!==t.length)return!1;let n=!0;for(let r=0;n&&r<e.length;r++)n=go(e[r],t[r]);return n}function cr(e,t){if(e.size!==t.size)return!1;let n=Array.from(t),r=new Uint8Array(n.length);for(let s of e){let i=-1;for(let a=0;a<n.length;a++)if(!r[a]&&go(s,n[a])){i=a;break}if(i<0)return!1;r[i]=1}return!0}function go(e,t){if(e===t)return!0;let n=dr(e),r=dr(t);if(n||r)returLine truncated
`))),e[js]=Bi(s);let i=r||s.props&&s.props.type==="number";So(e,t?"change":"input",a=>{a.target.composing||e[js](Fn(e.value,n,i))}),(n||i)&&So(e,"change",()=>{e.value=Fn(e.value,n,i)}),t||(So(e,"compositionstart",oc),So(e,"compositionend",Gi),So(e,"change",Gi))},mounted(e,{value:t,modifiers:{trim:n,number:r}}){let s=t??"",i=e[Bs];delete e[Bs],i!==void 0&&(e.type==="text"||e.type==="textarea")&&e.value!==i?e[js](Fn(e.value,n,r)):e.value=s},beforeUpdate(e,{value:t,oldValue:n,modifiers:{lazy:r,trim:s,number:i}},a){if(e[js]=Bi(a),e.composing)return;let u=(i||e.type==="number")&&!/^0\d/.test(e.value)?Vo(e.value):e.value,f=t??"";if(u===f)return;let m=e.getRootNode();(m instanceof Document||m instanceof ShadowRoot)&&m.activeElement===e&&e.type!=="range"&&(r&&t===n||s&&e.value.trim()===f)||(e.value=f)}};var sc=["ctrl","shift","alt","meta"],nc={stop:e=>e.stopPropagation(),prevent:e=>e.preventDefault(),self:e=>e.target!==e.currentTarget,ctrl:e=>!e.ctrlKey,shift:e=>!e.shiftKey,alt:e=>!e.altKey,meta:e=>!e.metaKey,left:e=>"button"in e&&e.button!==0,middle:e=>"button"in e&&e.button!==1,right:e=>"button"in e&&e.button!==2,exact:(e,t)=>sc.some(n=>e[`${n}Key`]&&!t.includes(n))},he=(e,t)=>{if(!e)return e;let n=e._withMods||(e._withMods={}),r=t.join(".");return n[r]||(n[r]=((s,...i)=>{for(let a=0;a<t.length;a++){let u=nc[t[a]];if(u&&u(s,t))return}return e(s,...i)}))},rc={esc:"escape",space:" ",up:"arrow-up",left:"arrow-left",right:"arrow-right",down:"arrow-down",delete:"backspace"},No=(e,t)=>{let n=e._withKeys||(e._withKeys={}),r=t.join(".");return n[r]||(n[r]=(s=>{if(!("key"in s))return;let i=St(s.key);if(t.some(a=>a===i||rc[a]===i))return e(s)}))},ic=Ge({patchProp:xu},$u),Wi;function ac(){return Wi||(Wi=Ni(ic))}var Qi=((...e)=>{let t=ac().createApp(...e),{mount:n}=t;return t.mount=r=>{let s=dc(r);if(!s)return;let i=t._component;!we(i)&&!i.render&&!i.template&&(i.template=s.innerHTML),s.nodeType===1&&(s.textContent="");let a=n(s,!1,lc(s));return s instanceof Element&&(s.removeAttribute("v-cloak"),s.setAttribute("data-v-app","")),a},t});function lc(e){if(e instanceof SVGElement)return"svg";if(typeof MathMLElement=="function"&&e instanceof MathMLElement)return"mathml"}function dc(e){return ze(e)?document.querySelector(e):e}var lo=typeof document<"u";function Zi(e){return typeof e=="object"||"displayName"in e||"props"in e||"__vccOpts"in e}function uc(e){return e.__esModule||e[Symbol.toStringTag]==="Module"||e.default&&Zi(e.default)}var Oe=Object.assign;function Ws(e,t){let n={};for(let r in t){let s=t[r];n[r]=wt(s)?s.map(e):e(s)}return n}var Ro=()=>{},wt=Array.isArray;function Bn(e,t){let n={};for(let r in e)n[r]=r in t?t[r]:e[r];return n}var xi=/#/g,cc=/&/g,pc=/\//g,fc=/=/g,hc=/\?/g,ea=/\+/g,mc=/%5B/g,vc=/%5D/g,ta=/%5E/g,gc=/%60/g,oa=/%7B/g,yc=/%7C/g,sa=/%7D/g,bc=/%20/g;function Gn(e){return e==null?"":encodeURI(""+e).replace(yc,"|").replace(mc,"[").replace(vc,"]")}function na(e){return Gn(e).replace(oa,"{").replace(sa,"}").replace(ta,"^")}function Hn(e){return Gn(e).replace(ea,"%2B").replace(bc,"+").replace(xi,"%23").replace(cc,"%26").replace(gc,"`").replace(oa,"{").replace(sa,"}").replace(ta,"^")}function wc(e){return Hn(e).replace(fc,"%3D")}function kc(e){return Gn(e).replace(xi,"%23").replace(hc,"%3F")}function ra(e){return kc(e).replace(pc,"%2F")}function Do(e){if(e==null)return null;try{return decodeURIComponent(""+e)}catch{}return""+e}var Ec=/\/$/,Cc=e=>e.replace(Ec,"");function Ks(e,t,n="/"){let r,s={},i="",a="",u=t.indexOf("#"),f=t.indexOf("?");return f=u>=0&&f>u?-1:f,f>=0&&(r=t.slice(0,f),i=t.slice(f,u>0?u:t.length),s=e(i.slice(1))),u>=0&&(r=r||t.slice(0,u),a=t.slice(u,t.length)),r=Sc(r??t,n),{fullPath:r+i+a,path:r,query:s,hash:Do(a)}}function ia(e,t){let n=t.query?e(t.query):"";return t.path+(n&&"?")+n+(t.hash||"")}function Wn(e,t){return!t||!e.toLowerCase().startsWith(t.toLowerCase())?e:e.slice(t.length)||"/"}function aa(e,t,n){let r=t.matched.length-1,s=n.matched.length-1;return r>-1&&r===s&&io(t.matched[r],n.matched[s])&&Kn(t.params,n.params)&&e(t.query)===e(n.query)&&t.hash===n.hash}function io(e,t){return(e.aliasOf||e)===(t.aliasOf||t)}function Kn(e,t){if(Object.keys(e).length!==Object.keys(t).length)return!1;for(var n in e)if(!_c(e[n],t[n]))return!1;return!0}function _c(e,t){return wt(e)?Xi(e,t):wt(t)?Xi(t,e):e?.valueOf()===t?.valueOf()}function Xi(e,t){return wt(t)?e.length===t.length&&e.every((n,r)=>n===t[r]):e.length===1&&e[0]===t}function Sc(e,t){if(e.startsWith("/"))return e;if(!e)return t;let n=t.split("/"),r=e.split("/"),s=r[r.length-1];(s===".."||s===".")&&r.push("");let i=n.length-1,a,u;for(a=0;a<r.length;a++)if(u=r[a],u!==".")if(u==="..")i>1&&i--;else break;return n.slice(0,i).join("/")+"/"+r.slice(a).join("/")}var Wt={path:"/",name:void 0,params:{},query:{},hash:"",fullPath:"/",matched:[],meta:{},redirectedFrom:void 0},Ys=(function(e){return e.pop="pop",e.push="push",e})({}),Js=(function(e){return e.back="back",e.forward="forward",e.unknown="",e})({});function la(e){if(!e)if(lo){let t=document.querLine truncated
"use strict";(()=>{var sd=Object.defineProperty;var nd=(e,t)=>{for(var s in t)sd(e,s,{get:t[s],enumerable:!0})};var he=["abap","abc","actionscript","ada","alda","apache_conf","apex","applescript","aql","asciidoc","asl","assembly_x86","autohotkey","batchfile","c9search","c_cpp","cirru","clojure","cobol","coffee","coldfusion","crystal","csharp","csound_document","csound_orchestra","csound_score","csp","css","curly","d","dart","diff","django","dockerfile","dot","drools","edifact","eiffel","ejs","elixir","elm","erlang","forth","fortran","fsharp","fsl","ftl","gcode","gherkin","gitignore","glsl","gobstones","golang","graphqlschema","groovy","haml","handlebars","haskell","haskell_cabal","haxe","hjson","html","html_elixir","html_ruby","ini","io","jack","jade","java","javascript","json","json5","jsoniq","jsp","jssm","jsx","julia","kotlin","latex","less","liquid","lisp","livescript","logiql","logtalk","lsl","lua","luapage","lucene","makefile","markdown","mask","matlab","maze","mediawiki","mel","mixal","mushcode","mysql","nginx","nim","nix","nsis","nunjucks","objectivec","ocaml","pascal","perl","perl6","pgsql","php","php_laravel_blade","pig","plain_text","powershell","praat","prisma","prolog","properties","protobuf","puppet","python","qml","r","razor","rdoc","red","redshift","rhtml","rst","ruby","rust","sass","scad","scala","scheme","scss","sh","sjs","slim","smarty","snippets","soy_template","space","sparql","sql","sqlserver","stylus","svg","swift","tcl","terraform","tex","text","textile","toml","tsx","turtle","twig","typescript","vala","vbscript","velocity","verilog","vhdl","visualforce","wollok","xml","xquery","yaml","zeek"];var ve={notebook:"/script/notebook.c3144c18ae.min.js",org:"/script/org.f61f262915.min.js",markdown:"/script/markdown.44f2e93820.min.js",editor:"/script/editor.4f142d3adc.min.js",pdf:"/script/pdf.4d9cc6ff49.min.js"};var cs=new Map,ad=ve;function Xt(e){if(!cs.has(e)){let t=document.createElement("script");t.src=ad[e];let s=new Promise((r,n)=>{t.onload=r,t.onerror=()=>{cs.delete(e),t.remove(),n(new Error(`Unable to load ${e}. Please retry.`))}});cs.set(e,s),document.head.appendChild(t)}return cs.get(e)}async function ur(){await Xt("editor"),ace.config.set("basePath","/script/external/ace/")}function ho(e){let t=Object.create(null);for(let s of e.split(","))t[s]=1;return s=>s in t}var Pe={},Zt=[],at=()=>{},rn=()=>!1,vo=e=>e.charCodeAt(0)===111&&e.charCodeAt(1)===110&&(e.charCodeAt(2)>122||e.charCodeAt(2)<97),go=e=>e.startsWith("onUpdate:"),Ge=Object.assign,Po=(e,t)=>{let s=e.indexOf(t);s>-1&&e.splice(s,1)},ld=Object.prototype.hasOwnProperty,De=(e,t)=>ld.call(e,t),ye=Array.isArray,$t=e=>Vo(e)==="[object Map]",Ut=e=>Vo(e)==="[object Set]",cr=e=>Vo(e)==="[object Date]";var we=e=>typeof e=="function",ze=e=>typeof e=="string",mt=e=>typeof e=="symbol",Te=e=>e!==null&&typeof e=="object",an=e=>(Te(e)||we(e))&&we(e.then)&&we(e.catch),mr=Object.prototype.toString,Vo=e=>mr.call(e),ln=e=>Vo(e).slice(8,-1),ps=e=>Vo(e)==="[object Object]",fs=e=>ze(e)&&e!=="NaN"&&e[0]!=="-"&&""+parseInt(e,10)===e,yo=ho(",key,ref,ref_for,ref_key,onVnodeBeforeMount,onVnodeMounted,onVnodeBeforeUpdate,onVnodeUpdated,onVnodeBeforeUnmount,onVnodeUnmounted");var ms=e=>{let t=Object.create(null);return(s=>t[s]||(t[s]=e(s)))},dd=/-\w/g,Je=ms(e=>e.replace(dd,t=>t.slice(1).toUpperCase())),ud=/\B([A-Z])/g,St=ms(e=>e.replace(ud,"-$1").toLowerCase()),xt=ms(e=>e.charAt(0).toUpperCase()+e.slice(1)),qo=ms(e=>e?`on${xt(e)}`:""),gt=(e,t)=>!Object.is(e,t),bo=(e,...t)=>{for(let s=0;s<e.length;s++)e[s](...t)},hs=(e,t,s,r=!1)=>{Object.defineProperty(e,t,{configurable:!0,enumerable:!1,writable:r,value:s})},wo=e=>{let t=parseFloat(e);return isNaN(t)?e:t};var pr,Mo=()=>pr||(pr=typeof globalThis<"u"?globalThis:typeof self<"u"?self:typeof window<"u"?window:typeof global<"u"?global:{});function Me(e){if(ye(e)){let t={};for(let s=0;s<e.length;s++){let r=e[s],n=ze(r)?md(r):Me(r);if(n)for(let i in n)t[i]=n[i]}return t}else if(ze(e)||Te(e))return e}var cd=/;(?![^(]*\))/g,pd=/:([^]+)/,fd=/\/\*[^]*?\*\//g;function md(e){let t={};return e.replace(fd,"").split(cd).forEach(s=>{if(s){let r=s.split(pd);r.length>1&&(t[r[0].trim()]=r[1].trim())}}),t}function A(e){let t="";if(ze(e))t=e;else if(ye(e))for(let s=0;s<e.length;s++){let r=A(e[s]);r&&(t+=r+" ")}else if(Te(e))for(let s in e)e[s]&&(t+=s+" ");return t.trim()}var hr="itemscope,allowfullscreen,formnovalidate,ismap,nomodule,novalidate,readonly",vr=ho(hr),hd=ho(hr+",async,autofocus,autoplay,controls,default,defer,disabled,inert,loop,open,required,reversed,scoped,seamless,checked,muted,multiple,selected");function vs(e){return!!e||e===""}function vd(e,t){if(e.length!==t.length)return!1;let s=!0;for(let r=0;s&&r<e.length;r++)s=Nt(e[r],t[r]);return s}function fr(e,t){if(e.size!==t.size)return!1;let s=Array.from(t),r=new Uint8Array(s.length);for(let n of e){let i=-1;for(let a=0;a<s.length;a++)if(!r[a]&&Nt(n,s[a])){i=a;break}if(i<0)return!1;r[i]=1}return!0}function Nt(e,t){if(e===t)return!0;let s=cr(e),r=cr(t);if(s||r)returLine truncated
`))),e[uo]=Ws(n);let i=r||n.props&&n.props.type==="number";lo(e,t?"change":"input",a=>{a.target.composing||e[uo](Hn(e.value,s,i))}),(s||i)&&lo(e,"change",()=>{e.value=Hn(e.value,s,i)}),t||(lo(e,"compositionstart",ic),lo(e,"compositionend",Ki),lo(e,"change",Ki))},mounted(e,{value:t,modifiers:{trim:s,number:r}}){let n=t??"",i=e[js];delete e[js],i!==void 0&&(e.type==="text"||e.type==="textarea")&&e.value!==i?e[uo](Hn(e.value,s,r)):e.value=n},beforeUpdate(e,{value:t,oldValue:s,modifiers:{lazy:r,trim:n,number:i}},a){if(e[uo]=Ws(a),e.composing)return;let u=(i||e.type==="number")&&!/^0\d/.test(e.value)?wo(e.value):e.value,f=t??"";if(u===f)return;let h=e.getRootNode();(h instanceof Document||h instanceof ShadowRoot)&&h.activeElement===e&&e.type!=="range"&&(r&&t===s||n&&e.value.trim()===f)||(e.value=f)}};var xi={deep:!0,created(e,{value:t,modifiers:{number:s}},r){e._modelValue=t,lo(e,"change",()=>{let n=Array.prototype.filter.call(e.options,f=>f.selected).map(f=>s?wo(Ks(f)):Ks(f)),i=e.multiple,a=i?Ut(e._modelValue)?new Set(n):n:n[0],u=e._pendingValue=[i,i?ye(a)?n.slice():n:a];try{e[uo](a)}finally{st(()=>{e._pendingValue===u&&(e._pendingValue=void 0)})}}),e[uo]=Ws(r)},mounted(e,{value:t}){Yi(e,t)},beforeUpdate(e,{value:t},s){e._modelValue=t,e[uo]=Ws(s)},updated(e,{value:t}){let s=e._pendingValue;e._pendingValue=void 0,(!s||s[0]!==e.multiple||!ac(t,s[1],s[0]))&&Yi(e,t)}};function ac(e,t,s){if(!s)return Nt(e,t);if(ye(e))return Nt(e,t);if(Ut(e)){if(e.size!==t.length)return!1;for(let r of t)if(!e.has(r))return!1;return!0}return!1}function Yi(e,t){let s=e.multiple,r=ye(t);if(!(s&&!r&&!Ut(t))){for(let n=0,i=e.options.length;n<i;n++){let a=e.options[n],u=Ks(a);if(s)if(r){let f=typeof u;f==="string"||f==="number"?a.selected=t.some(h=>String(h)===String(u)):a.selected=gr(t,u)>-1}else a.selected=t.has(u);else if(Nt(Ks(a),t)){e.selectedIndex!==n&&(e.selectedIndex=n);return}}!s&&e.selectedIndex!==-1&&(e.selectedIndex=-1)}}function Ks(e){return"_value"in e?e._value:e.value}var lc=["ctrl","shift","alt","meta"],dc={stop:e=>e.stopPropagation(),prevent:e=>e.preventDefault(),self:e=>e.target!==e.currentTarget,ctrl:e=>!e.ctrlKey,shift:e=>!e.shiftKey,alt:e=>!e.altKey,meta:e=>!e.metaKey,left:e=>"button"in e&&e.button!==0,middle:e=>"button"in e&&e.button!==1,right:e=>"button"in e&&e.button!==2,exact:(e,t)=>lc.some(s=>e[`${s}Key`]&&!t.includes(s))},me=(e,t)=>{if(!e)return e;let s=e._withMods||(e._withMods={}),r=t.join(".");return s[r]||(s[r]=((n,...i)=>{for(let a=0;a<t.length;a++){let u=dc[t[a]];if(u&&u(n,t))return}return e(n,...i)}))},uc={esc:"escape",space:" ",up:"arrow-up",left:"arrow-left",right:"arrow-right",down:"arrow-down",delete:"backspace"},Ro=(e,t)=>{let s=e._withKeys||(e._withKeys={}),r=t.join(".");return s[r]||(s[r]=(n=>{if(!("key"in n))return;let i=St(n.key);if(t.some(a=>a===i||uc[a]===i))return e(n)}))},cc=Ge({patchProp:sc},zu),Ji;function pc(){return Ji||(Ji=Ti(cc))}var ea=((...e)=>{let t=pc().createApp(...e),{mount:s}=t;return t.mount=r=>{let n=mc(r);if(!n)return;let i=t._component;!we(i)&&!i.render&&!i.template&&(i.template=n.innerHTML),n.nodeType===1&&(n.textContent="");let a=s(n,!1,fc(n));return n instanceof Element&&(n.removeAttribute("v-cloak"),n.setAttribute("data-v-app","")),a},t});function fc(e){if(e instanceof SVGElement)return"svg";if(typeof MathMLElement=="function"&&e instanceof MathMLElement)return"mathml"}function mc(e){return ze(e)?document.querySelector(e):e}var fo=typeof document<"u";function oa(e){return typeof e=="object"||"displayName"in e||"props"in e||"__vccOpts"in e}function hc(e){return e.__esModule||e[Symbol.toStringTag]==="Module"||e.default&&oa(e.default)}var Oe=Object.assign;function Ys(e,t){let s={};for(let r in t){let n=t[r];s[r]=wt(n)?n.map(e):e(n)}return s}var Ao=()=>{},wt=Array.isArray;function Wn(e,t){let s={};for(let r in e)s[r]=r in t?t[r]:e[r];return s}var sa=/#/g,vc=/&/g,gc=/\//g,yc=/=/g,bc=/\?/g,na=/\+/g,wc=/%5B/g,kc=/%5D/g,ra=/%5E/g,Ec=/%60/g,ia=/%7B/g,Cc=/%7C/g,aa=/%7D/g,_c=/%20/g;function Kn(e){return e==null?"":encodeURI(""+e).replace(Cc,"|").replace(wc,"[").replace(kc,"]")}function la(e){return Kn(e).replace(ia,"{").replace(aa,"}").replace(ra,"^")}function jn(e){return Kn(e).replace(na,"%2B").replace(_c,"+").replace(sa,"%23").replace(vc,"%26").replace(Ec,"`").replace(ia,"{").replace(aa,"}").replace(ra,"^")}function Sc(e){return jn(e).replace(yc,"%3D")}function Nc(e){return Kn(e).replace(sa,"%23").replace(bc,"%3F")}function da(e){return Nc(e).replace(gc,"%2F")}function To(e){if(e==null)return null;try{return decodeURIComponent(""+e)}catch{}return""+e}var Dc=/\/$/,Rc=e=>e.replace(Dc,"");function Js(e,t,s="/"){let r,n={},i="",a="",u=t.indexOf("#"),f=t.indexOf("?");return f=u>=0&&f>u?-1:f,f>=0&&(r=t.slice(0,f),i=t.slice(f,u>0?u:t.length),n=e(i.slice(1))),u>=0&&(r=r||t.slice(0,u),a=t.slice(u,t.length)),r=Ac(r??t,s),{fullPath:r+i+a,path:r,query:n,hash:To(a)}}function ua(e,t){let s=t.query?e(t.query):"";return t.path+(s&&"?")+s+(t.hash||"")}function Yn(e,t){return!t||!e.toLowerCase().sLine truncated
${JSON.stringify(e)}${t?`
while being at
`+JSON.stringify(t):""}`},[Be.NAVIGATION_GUARD_REDIRECT]({from:e,to:t}){return`Redirected from "${e.fullPath}" to "${Ac(t)}" via a navigation guard.`},[Be.NAVIGATION_ABORTED]({from:e,to:t}){return`Navigation aborted from "${e.fullPath}" to "${t.fullPath}" via a navigation guard.`},[Be.NAVIGATION_CANCELLED]({from:e,to:t}){return`Navigation cancelled from "${e.fullPath}" to "${t.fullPath}" with a new navigation.`},[Be.NAVIGATION_DUPLICATED]({from:e,to:t}){return`Avoided redundant navigation to current location: "${e.fullPath}".`}};function ao(e,t){return Oe(new Error,{type:e,[fa]:!0},t)}function Mt(e,t){return e instanceof Error&&fa in e&&(t==null||!!(e.type&t))}var Tc=["params","query","hash"];function Ac(e){if(typeof e=="string")return e;if(e.path!=null)return e.path;let t={};for(let n of Tc)n in e&&(t[n]=e[n]);return JSON.stringify(t,null,2)}function ha(e){let t={};if(e===""||e==="?")return t;let n=(e[0]==="?"?e.slice(1):e).split("&");for(let r=0;r<n.length;++r){let s=n[r].replace(ea," "),i=s.indexOf("="),a=Do(i<0?s:s.slice(0,i)),u=i<0?null:Do(s.slice(i+1));if(a in t){let f=t[a];wt(f)||(f=t[a]=[f]),f.push(u)}else t[a]=u}return t}function Qn(e){let t="";for(let n in e){let r=e[n];if(n=wc(n),r==null){r!==void 0&&(t+=(t.length?"&":"")+n);continue}(wt(r)?r.map(s=>s&&Hn(s)):[r&&Hn(r)]).forEach(s=>{s!==void 0&&(t+=(t.length?"&":"")+n,s!=null&&(t+="="+s))})}return t}function ma(e){let t={};for(let n in e){let r=e[n];r!==void 0&&(t[n]=wt(r)?r.map(s=>s==null?null:""+s):r==null?r:""+r)}return t}var va=Symbol(""),Xn=Symbol(""),Qs=Symbol(""),Xs=Symbol(""),Zs=Symbol("");function To(){let e=[];function t(r){return e.push(r),()=>{let s=e.indexOf(r);s>-1&&e.splice(s,1)}}function n(){e=[]}return{add:t,list:()=>e.slice(),reset:n}}function Gt(e,t,n,r,s,i=a=>a()){let a=r&&(r.enterCallbacks[s]=r.enterCallbacks[s]||[]);return()=>new Promise((u,f)=>{let m=h=>{h===!1?f(ao(Be.NAVIGATION_ABORTED,{from:n,to:t})):h instanceof Error?f(h):Rc(h)?f(ao(Be.NAVIGATION_GUARD_REDIRECT,{from:t,to:h})):(a&&r.enterCallbacks[s]===a&&typeof h=="function"&&a.push(h),u())},g=i(()=>e.call(r&&r.instances[s],t,n,m)),w=Promise.resolve(g);e.length<3&&(w=w.then(m)),w.catch(h=>f(h))})}function xs(e,t,n,r,s=i=>i()){let i=[];for(let a of e)for(let u in a.components){let f=a.components[u];if(!(t!=="beforeRouteEnter"&&!a.instances[u]))if(Zi(f)){let m=(f.__vccOpts||f)[t];m&&i.push(Gt(m,n,r,a,u,s))}else{let m=f();i.push(()=>m.then(g=>{if(!g)throw new Error(`Couldn't resolve component "${u}" at "${a.path}"`);let w=uc(g)?g.default:g;a.mods[u]=g,a.components[u]=w;let h=(w.__vccOpts||w)[t];return h&&Gt(h,n,r,a,u,s)()}))}}return i}function ga(e,t){let n=[],r=[],s=[],i=Math.max(t.matched.length,e.matched.length);for(let a=0;a<i;a++){let u=t.matched[a];u&&(e.matched.find(m=>io(m,u))?r.push(u):n.push(u));let f=e.matched[a];f&&(t.matched.find(m=>io(m,f))||s.push(f))}return[n,r,s]}var Oc=()=>location.protocol+"//"+location.host;function Ra(e,t){let{pathname:n,search:r,hash:s}=t,i=e.indexOf("#");if(i>-1){let a=s.includes(e.slice(i))?e.slice(i).length:1,u=s.slice(a);return u[0]!=="/"&&(u="/"+u),Wn(u,"")}return Wn(n,e)+r+s}function Ic(e,t,n,r){let s=[],i=[],a=null,u=({state:h})=>{let b=Ra(e,location),E=n.value,k=t.value,R=0;if(h){if(n.value=b,t.value=h,a&&a===E){a=null;return}R=k?h.position-k.position:0}else r(b);s.forEach(D=>{D(n.value,E,{delta:R,type:Ys.pop,direction:R?R>0?Js.forward:Js.back:Js.unknown})})};function f(){a=n.value}function m(h){s.push(h);let b=()=>{let E=s.indexOf(h);E>-1&&s.splice(E,1)};return i.push(b),b}function g(){if(document.visibilityState==="hidden"){let{history:h}=window;if(!h.state)return;h.replaceState(Oe({},h.state,{scroll:ds()}),"")}}function w(){for(let h of i)h();i=[],window.removeEventListener("popstate",u),window.removeEventListener("pagehide",g),document.removeEventListener("visibilitychange",g)}return window.addEventListener("popstate",u),window.addEventListener("pagehide",g),document.addEventListener("visibilitychange",g),{pauseListeners:f,listen:m,destroy:w}}function ya(e,t,n,r=!1,s=!1){return{back:e,current:t,forward:n,replaced:r,position:window.history.length,scroll:s?ds():null}}function Pc(e){let{history:t,location:n}=window,r={value:Ra(e,n)},s={value:t.state};s.value||i(r.value,{back:null,current:r.value,forward:null,position:t.length-1,replaced:!0,scroll:null},!0);function i(f,m,g){let w=e.indexOf("#"),h=w>-1?(n.host&&document.querySelector("base")?e:e.slice(w))+f:Oc()+e+f;try{t[g?"replaceState":"pushState"](m,"",h),s.value=m}catch(b){console.error(b),n[g?"replace":"assign"](h)}}function a(f,m){i(f,Oe({},t.state,ya(s.value.back,f,s.value.forward,!0),m,{position:s.value.position}),!0),r.value=f}function u(f,m){let g=Oe({},s.value,t.state,{forward:f,scroll:ds()});i(g.current,g,!0),i(f,Oe({},ya(r.value,f,null),{position:g.position+1},m),!1),r.value=f}return{location:r,state:s,push:u,replace:a}}function Ta(e){e=la(e);let t=Pc(e),n=Ic(e,t.state,t.location,t.replace);function r(i,a=!0){a||n.pauseListeners(),history.go(i)}let s=Oe({locatLine truncated
`)),e.options=Object.assign({},e.options,m.options),i.restore()})}a();let u=null;e.saveDefault=f=>{if(f&&f.preventDefault&&f.preventDefault(),e.saving)return;let m=i.checkpoint(),g={terms:e.terms.split(`
`).map(w=>w.trim()).filter(w=>w.length>0),options:e.options};e.saving=!0,e.error=null,t.post("/api/user/default",g).then(()=>{i.accept(m),e.saving=!1,e.message=e.unsavedChanges?null:"Saved",u&&r.cancel(u),u=r(()=>{e.message=null},2500)},w=>{e.saving=!1;let b="ERRORS."+(w&&w.data&&w.data.error);n(b).then(E=>{e.error=E&&E!==b?E:"Unable to save your defaults. Please try again."},()=>{e.error="Unable to save your defaults. Please try again."})})},e.deleteAccount=()=>{confirm("Delete your account? All your anonymized repositories, gists, and pull requests will be removed, and your personal data will be erased. This cannot be undone.")&&(e.deletingAccount=!0,t.delete("/api/user").then(()=>{window.location.href="/"},()=>{e.deletingAccount=!1,e.deleteError="Unable to delete the account. Please try again."}))}},La=function(e,t,n){e.repoId=null,e.repoUrl=null,e.claim=()=>{t.post("/api/repo/claim",{repoId:e.repoId,repoUrl:e.repoUrl}).then(r=>{n.url("/dashboard")},r=>{e.error=r.data,e.claimForm.repoUrl.setValidity("not_found",!1),e.claimForm.repoId.setValidity("not_found",!1)})}},Ua=function(e,t,n,r,s){e.user&&!e.user.status&&n.url("/dashboard"),e.watch("user.status",()=>{e.user&&!e.user.status&&n.url("/dashboard")}),e.features=[{key:"anonymize",num:"01",eyebrow:"Anonymize",title:"Double-anonymous,",accent:"your rules.",text:"Choose what reviewers may see: links, images, PDFs, notebooks, GitHub Pages. Add your own terms, with regex if you need it, and pick the expiration date.",cta:"Start an anonymization",href:"/anonymize",url:"anonymous.4open.science/anonymize",img:"/imgs/anonymize.png",alt:"The anonymize form: source repository, terms to redact, options and a live README preview"},{key:"review",num:"02",eyebrow:"Review",title:"Reviewers browse",accent:"the real thing.",text:"Highlighted source code, rendered PDFs, images, and notebooks, in a familiar file explorer. GitHub Pages is also supported.",cta:"Open the example",href:"https://anonymous.4open.science/r/840c8c57-3c32-451e-bf12-0e20be300389/",target:"_self",url:"anonymous.4open.science/r/840c8c57-\u2026",img:"/imgs/explorer.png",alt:"The repository explorer with a file tree and a rendered README"},{key:"manage",num:"03",eyebrow:"Manage",title:"One dashboard,",accent:"until the decision.",text:"Monitor views, edit configuration, remove or update your repository. Program chairs can group submissions under a conference with one shared expiry.",cta:"Open the dashboard",href:"/dashboard",needsUser:!0,url:"anonymous.4open.science/dashboard",img:"/imgs/dashboard.png",alt:"The dashboard listing anonymized repositories with status, views and expiry"}],e.feature=e.features[0].key,e.selectFeature=function(u){e.feature=u},e.featureHref=function(u){return u.needsUser&&!e.user?"/signin":u.href},e.featureTarget=function(u){return u.needsUser&&!e.user?"_self":u.target||void 0},e.featureKeydown=function(u,f){let m={ArrowDown:1,ArrowRight:1,ArrowUp:-1,ArrowLeft:-1,Home:"first",End:"last"}[u.key];if(m===void 0)return;u.preventDefault();let g=e.features.length,w=m==="first"?0:m==="last"?g-1:(f+m+g)%g;e.feature=e.features[w].key,s(()=>{let h=r.document.getElementById("feature-tab-"+e.feature);h&&h.focus()})},e.cards=[{key:"repositories",total:0,label:"repositories anonymized"},{key:"users",total:0,label:"researchers"},{key:"pageViews",total:0,label:"page views"},{key:"pullRequests",total:0,label:"pull requests"}];function i(){t.get("/api/stat/").then(u=>{e.stat=u.data,e.cards[0].total=u.data.nbRepositories,e.cards[1].total=u.data.nbUsers,e.cards[2].total=u.data.nbPageViews,e.cards[3].total=u.data.nbPullRequests})}i();function a(u){let f={series:u,bars:[],viewW:100,deltaToday:0,pctChange:0,pctAbs:0,isUp:!0};if(!u||u.length<2)return f;let m=new Array(u.length-1);for(let k=1;k<u.length;k++)m[k-1]=u[k]-u[k-1];let g=m.length,w=Math.max.apply(null,m),h=Math.min.apply(null,m),b=Math.min(0,h),E=w-b||1;f.viewW=g*2,f.bars=new Array(g);for(let k=0;k<g;k++){let R=(m[k]-b)/E,D=Math.max(1.5,R*34);f.bars[k]={x:(k*2+.25).toFixed(2),y:(36-D).toFixed(2),w:"1.5",h:D.toFixed(2)}}if(f.deltaToday=m[g-1],g>=2){let k=m[g-2];k&&(f.pctChange=(f.deltaToday-k)/k*100)}return f.pctAbs=Math.round(Math.abs(f.pctChange)),f.isUp=f.pctChange>=0,f}e.history={repositories:a([]),users:a([]),pageViews:a([]),pullRequests:a([])},t.get("/api/stat/history?days=60").then(u=>{let f=u.data||[];e.history={repositories:a(f.map(m=>m.nbRepositories||0)),users:a(f.map(m=>m.nbUsers||0)),pageViews:a(f.map(m=>m.nbPageViews||0)),pullRequests:a(f.map(m=>m.nbPullRequests||0))}})},Fa=function(e,t,n,r,s,i){let a=ct();e.on("routeLeave",function(){$('[data-toggle="tooltip"]').tooltip("dispose")}),e.watch("user.status",()=>{e.user==null&&n.url("/")}),e.user==null&&n.url("/"),a.timeout(()=>{$('[data-toggle="tooltip"]').tooltip()},250),e.quotaExpanded=!1,e.closeQuota=()=>{e.quotaExpanded=!1,rt(()=>document.querySelector(".dashboard-quota > summary")?.focus())},Kt()(document,"click",C=>{e.quotaExpanded&&!C.target.closest?.(".dashboard-quota")&& Line truncated
`)),e.options=Object.assign({},e.options,j.options),e.options.expirationDate=j.options&&j.options.expirationDate?new Date(j.options.expirationDate):T(),!r.repoId&&!r.pullRequestId&&!r.gistId&&!e.anonymize?.terms?.dirty&&(e.terms=e.defaultTerms),_&&_()})}function U(_,I,j){e.anonymize&&e.anonymize[_]&&e.anonymize[_].setValidity(I,j)}function ie(_){try{let I=parseGithubUrl(_);if(I&&I.owner&&I.repo)return I.owner+"/"+I.repo}catch{}return null}e.reconnectingSource=!1,e.reconnectRepositoryId=void 0,e.reconnectSource=async()=>{if(confirm("Reconnect to the repository currently at this GitHub URL? Review its branch and commit before saving. Saving will replace the published snapshot.")){e.reconnectingSource=!0,e.reconnectRepositoryId=void 0,e.source.commit="",e.source.branch="",e.readme="",e.branches=[];try{await G()}catch{}}};function re(){return e.reconnectingSource||!e.isUpdate||!e._originalRepositoryID?void 0:ie(e.sourceUrl)===e._originalFullName?e._originalRepositoryID:void 0}async function C(){e._preservingDraft=!0;try{await e.urlSelected(!0)}finally{e._preservingDraft=!1}}async function O(){let _;try{_=JSON.parse(sessionStorage.getItem("github-access-draft")||"null")}catch{return!1}if(!_||_.path!==s.path()||Date.now()-_.savedAt>=30*6e4)return R.restore()?(e.sourceUrl&&await C(),!0):!1;R.restore();for(let I of["sourceUrl","terms","repoId","pullRequestId","gistId","source","options","conference","githubConnection"])Object.prototype.hasOwnProperty.call(_.draft,I)&&(e[I]=_.draft[I]);return e.options.expirationDate&&(e.options.expirationDate=new Date(e.options.expirationDate)),await C(),sessionStorage.removeItem("github-access-draft"),!0}q(()=>{!r.repoId&&!r.pullRequestId&&!r.gistId&&a(O,0),r.repoId&&r.repoId!=""&&(e.isUpdate=!0,e.detectedType="repo",e.repoId=r.repoId,t.get("/api/repo/"+e.repoId).then(async _=>{e.githubConnection=_.data.connection||"oauth",e.sourceUrl="https://github.com/"+_.data.source.fullName,e._originalFullName=_.data.source.fullName,e.terms=_.data.options.terms.filter(I=>I).join(`
`),e.source=_.data.source,e.role=_.data.role||"owner",e.coauthors=_.data.coauthors||[],e._originalBranch=_.data.source.branch,e.options=Object.assign({},e.options,_.data.options),e.conference=_.data.conference,e._originalConference=_.data.conference,e.repositoryID=_.data.source.repositoryID,e._originalRepositoryID=_.data.source.repositoryID,_.data.options.expirationDate&&(e.options.expirationDate=new Date(_.data.options.expirationDate)),E=k(parseGithubUrl(e.sourceUrl)),!await O()&&(await Promise.all([G(),J()]),ue())},()=>{s.url("/404")})),r.pullRequestId&&r.pullRequestId!=""&&(e.isUpdate=!0,e.detectedType="pr",e.pullRequestId=r.pullRequestId,t.get("/api/pr/"+e.pullRequestId).then(async _=>{if(e.githubConnection=_.data.connection||"oauth",e.sourceUrl="https://github.com/"+_.data.source.repositoryFullName+"/pull/"+_.data.source.pullRequestId,e.terms=_.data.options.terms.filter(I=>I).join(`
`),e.source=_.data.source,e.options=Object.assign({},e.options,_.data.options),e.conference=_.data.conference,e._originalConference=_.data.conference,_.data.options.expirationDate&&(e.options.expirationDate=new Date(_.data.options.expirationDate)),E=k(parseGithubUrl(e.sourceUrl)),!await O())try{await pe()}catch{}},()=>{s.url("/404")})),r.gistId&&r.gistId!=""&&(e.isUpdate=!0,e.detectedType="gist",e.gistId=r.gistId,t.get("/api/gist/"+e.gistId).then(async _=>{e.sourceUrl="https://gist.github.com/"+_.data.source.gistId,e.terms=_.data.options.terms.filter(I=>I).join(`
`),e.source=_.data.source,e.options=Object.assign({},e.options,_.data.options),e.conference=_.data.conference,e._originalConference=_.data.conference,_.data.options.expirationDate&&(e.options.expirationDate=new Date(_.data.options.expirationDate)),E=k(parseGithubUrl(e.sourceUrl)),!await O()&&await Ae()},()=>{s.url("/404")}))}),t.get("/github/connections").then(_=>{e.githubConnections=_.data,!r.repoId&&!r.pullRequestId&&!r.gistId&&e.githubConnection===void 0&&_.data.appEnabled&&_.data.appConnected&&(e.githubConnection="github-app",e.sourceUrl&&e.workspaceOpen&&C())}).catch(()=>{}),e.urlSelected=async(_=!1)=>{let I=++f,j;try{j=parseGithubUrl(e.sourceUrl.trim())}catch{e.sourceLoading=!1,oe.cancel(),_e.cancel(),Se.cancel(),e.previewStatus="idle",e.sourceUrlError=e.sourceUrl?"Please provide a valid GitHub URL.":"",U("sourceUrl","github",!1);return}let M=k(j),v=_||E===M;E=M,e.sourceUrlError="",U("sourceUrl","github",!0);let S=document.activeElement?.id==="sourceUrl-landing";e.workspaceOpen=!0,S&&a(()=>document.getElementById("sourceUrl")?.focus(),0),e.reconnectingSource=!1,e.reconnectRepositoryId=void 0,!v&&!e.isUpdate&&(e.repoId="",e.pullRequestId="",e.gistId=""),e.details=null,e.branches=[],v||(e.source={type:"GitHubStream",branch:"",commit:""}),oe.cancel(),_e.cancel(),Se.cancel(),le.clear(),Ee.clear(),e.previewStatus="idle",e.anonymize_readme="",e.readme="",e.html_readme="",e.detectedType=null,e.gistOAuthRequired=!1,e.gistAppRequired=!1,e._preservingDraft=v;try{j.gistId&&!j.repo?(e.detectedType="gist",e.source={gistId:j.gistId},await Ae()):j.pullRequestId?(e.detectedType="pr",e.source={repositoryFullName:j.owner+"/"+j.repo,pullRequestId:j.pullRequestId},await pe()):(e.detectedType="repo",await Promise.all([G(),J()]),ue())}catch{}finally{I===f&&(e._preservingDraft=!1)}$('[data-toggle="tooltip"]').tooltip()},$('[data-toggle="tooltip"]').tooltip(),e.watch("source.branch",async()=>{if(e.detectedType!=="repo")return;let _=e.branches.filter(j=>j.name==e.source.branch)[0];if(!_)return;!(!e.reconnectingSource&&e.isUpdate&&e._originalBranch===e.source.branch&&!!e.source.commit)&&!(e._preservingDraft&&e.source.commit)&&(e.source.commit=_.commit),e.readme=_.readme,await J(),ue()}),e.getBranches=async _=>{let I=parseGithubUrl(e.sourceUrl),j=h();try{let M=await t.get(`/api/repo/${I.owner}/${I.repo}/branches`,{params:{reconnect:e.reconnectingSource?"1":void 0,anonymizedRepoId:e.isUpdate&&r.repoId&&ie(e.sourceUrl)===e._originalFullName?r.repoId:void 0,connection:e.githubConnection,force:_===!0||e.reconnectingSource?"1":"0",repositoryID:re()}});if(!w(j))return;e.branches=M.data,e.sourceUnreachable=!1,e.source.branch||(e.source.branch=e.details.defaultBranch);let v=e.branches.filter(S=>S.name==e.source.branch);v.length>0&&(!(!e.reconnectingSource&&!_&&e.isUpdate&&!e.options.update&&e._originalBranch===e.source.branch&&e.source.commit)&&!(e._preservingDraft&&e.source.commit)&&(e.source.commit=v[0].commit),e.readme=v[0].readme,await J(_))}catch(M){if(!w(j))return;e.branches=[],e.sourceUnreachable=M&&(M.status===404||M.data&&M.data.error==="repo_not_found");let v=M&&M.data&&M.data.error||(M&&M.status===404?"repo_not_found":"unknown_error");i("ERRORS."+v).then(S=>{e.toasts=e.toasts||[],e.addToast({title:"Error",date:new Date,body:S}),e.error=S},console.error),typeof U=="function"&&U("sourceUrl","missing",!1)}finally{b(j)}};async function G(){let _=parseGithubUrl(e.sourceUrl),I=h();try{z();let j=await t.get(`/api/repo/${_.owner}/${_.repo}/`,{params:{reconnect:e.reconnectingSource?"1":void 0,anonymizedRepoId:e.isUpdate&&r.repoId&&ie(e.sourceUrl)===e._originalFullName?r.repoId:void 0,connection:e.githubConnection,repositoryID:re(),force:"1"}});if(!w(I))return;e.details=j.data,e.reconnectingSource&&(e.reconnectRepositoryId=e.details.externalId),e.details&&e.details.id&&(e.repositoryID=e.details.id),e.repoId||(e.repoId=e.details.repo+"-"+generateRandomId(4)),await e.getBranches(e.reconnectingSource)}catch(j){if(!w(I))return;throw j.data&&(i("ERRORS."+j.data.error).then(M=>{e.addToast({title:"Error",date:new Date,body:M}),e.error=M},console.error),te(j.data.error)),U("sourceUrl","missing",!1),j}finally{b(I)}}async function J(_){if(e.readme&&!_)return e.readme;let I=parseGithubUrl(e.sourceUrl),j=h();try{let M=await t.get(`/api/repo/${I.owner}/${I.repo}/readme`,{params:{reconnect:e.reconnectingSource?"1":void 0,anonymizedRepoId:e.isUpdate&&r.repoId&&ie(e.sourceUrl)===e._originalFullName?r.repoId:void 0,connection:e.githubConnection,force:_===!0?"1":"0",branch:e.source.branch,repositoryID:re()}});w(j,!0)&&(e.readme=M.data)}catch{w(j,!0)&&(e.readme="")}finally{b(j)}}function ee(){let _={terms:e.terms?e.terms.split(`
`):[],image:!!e.options.image,link:!!e.options.link,repoId:e.repoId};e.source&&e.source.branch&&(_.branchName=e.source.branch);try{let I=parseGithubUrl(e.sourceUrl);_.repoName=`${I.owner}/${I.repo}`}catch{}return _}function Q(_,I,j){let M=null,v=0,S=()=>{if(e.detectedType!==_)return;M&&a.cancel(M);let V=++v;e.previewStatus="loading",M=a(()=>{M=null;let F=I();if(!F){e.previewStatus="idle";return}t.post("/api/anonymize-preview",F).then(B=>{V===v&&(j(B.data,F),e.previewStatus="ready")}).catch(()=>{V===v&&(e.previewStatus="error")})},200)};return S.cancel=()=>{v++,M&&a.cancel(M),M=null},e.on("dispose",S.cancel),S}e.retryPreview=()=>{e.detectedType==="repo"&&oe(),e.detectedType==="pr"&&_e(),e.detectedType==="gist"&&Se()};let oe=Q("repo",()=>e.readme?{content:e.readme,options:ee()}:null,_=>{if(typeof _?.content!="string")throw Error("Invalid preview response");e.anonymize_readme=_.content;let I="";try{let M=parseGithubUrl(e.sourceUrl),v=e.source.branch||e.details&&e.details.defaultBranch||"main";I=`https://github.com/${M.owner}/${M.repo}/raw/${v}/`}catch{}let j=renderMD(e.anonymize_readme,I);e.html_readme=j,a(Prism.highlightAll,150)});function ue(){!e.anonymize||!e.anonymize.terms||(e.termsRegexWarning=!!e.terms&&!!e.terms.match(/[-[\]{}()*+?.,\\^$|#]/g),oe())}async function pe(){let _=parseGithubUrl(e.sourceUrl),I=h();try{z();let j=await t.get(`/api/pr/${_.owner}/${_.repo}/${_.pullRequestId}`,{params:{connection:e.githubConnection}});if(!w(I))return;e.details=j.data,e.pullRequestId||(e.pullRequestId=_.repo+"-PR"+_.pullRequestId+"-"+generateRandomId(4))}catch(j){if(!w(I))return;throw j.data&&(i("ERRORS."+j.data.error).then(M=>{e.addToast({title:"Error",date:new Date,body:M}),e.error=M},console.error),te(j.data.error)),U("sourceUrl","missing",!1),j}finally{b(I)}}let le=He(new Map);function Ie(){let _=new Set,I=e.details&&e.details.pullRequest;if(!I)return _;typeof I.title=="string"&&_.add(I.title),typeof I.body=="string"&&_.add(I.body),typeof I.diff=="string"&&_.add(I.diff);let j=I.comments||[];for(let M of j)typeof M.author=="string"&&_.add(M.author),typeof M.body=="string"&&_.add(M.body);return _}let _e=Q("pr",()=>{let _=Ie(),I=Array.from(_);return I.length===0?null:{contents:I,options:ee()}},(_,I)=>{if(!Array.isArray(_?.contents)||_.contents.length!==I.contents.length||_.contents.some(v=>typeof v!="string"))throw Error("Invalid preview response");let j=I.contents,M=new Map;for(let v=0;v<j.length&&v<_.contents.length;v++)M.set(j[v],_.contents[v]);le.clear(),M.forEach((v,S)=>le.set(S,v))});e.anonymizePrContent=function(_){return _&&(le.has(_)?le.get(_):"")};async function Ae(){let _=parseGithubUrl(e.sourceUrl),I=h();try{z(),e.gistOAuthRequired=!1,e.gistAppRequired=!1;let j=await t.get(`/api/gist/source/${_.gistId}`);if(!w(I))return;e.details=j.data,e.gistId||(e.gistId="gist-"+_.gistId.substring(0,6)+"-"+generateRandomId(4))}catch(j){if(!w(I))return;if(j.data?.error==="github_app_reconnect_required"){e.gistAppRequired=!0,U("sourceUrl","missing",!1);return}if(j.data?.error==="github_oauth_required"){e.gistOAuthRequired=!0,U("sourceUrl","missing",!1);return}throw j.data&&(i("ERRORS."+j.data.error).then(M=>{e.addToast({title:"Error",date:new Date,body:M}),e.error=M},console.error),te(j.data.error)),U("sourceUrl","missing",!1),j}finally{b(I)}}let Ee=He(new Map);function Ve(){let _=new Set,I=e.details&&e.details.gist;if(!I)return _;typeof I.description=="string"&&_.add(I.description),typeof I.ownerLogin=="string"&&_.add(I.ownerLogin);let j=I.files||[];for(let v of j)typeof v.filename=="string"&&_.add(v.filename),typeof v.content=="string"&&_.add(v.content);let M=I.comments||[];for(let v of M)typeof v.author=="string"&&_.add(v.author),typeof v.body=="string"&&_.add(v.body);return _}let Se=Q("gist",()=>{let _=Ve(),I=Array.from(_);return I.length===0?null:{contents:I,options:ee()}},(_,I)=>{if(!Array.isArray(_?.contents)||_.contents.length!==I.contents.length||_.contents.some(v=>typeof v!="string"))throw Error("Invalid preview response");let j=I.contents,M=new Map;for(let v=0;v<j.length&&v<_.contents.length;v++)M.set(j[v],_.contents[v]);Ee.clear(),M.forEach((v,S)=>Ee.set(S,v)),Le()});e.anonymizeGistContent=function(_){return _&&(Ee.has(_)?Ee.get(_):"")},e.previewGistFiles=[];function Le(){let _=e.details&&e.details.gist&&e.details.gist.files||[];e.previewGistFiles=_.map(I=>({filename:e.anonymizeGistContent(I.filename),content:e.anonymizeGistContent(I.content),language:I.language}))}e.watch("details.gist.files",Le,!0),e.watch("terms",Le);function Ue(){let _=e.conference;if(e.conference_data=null,!_)return;let I=e.isUpdate&&_===e._originalConference;t.get("/api/conferences/"+_).then(j=>{e.conference===_&&(e.conference_data=j.data,e.conference_data.startDate=new Date(e.conference_data.startDate),e.conference_data.endDate=new Date(e.conference_data.endDate),!I&&(e.options.expirationDate=new Date(e.conference_data.endDate),e.options.expirationMode="remove",e.options.update=e.conference_data.options.update,e.options.image=e.con Line truncated
`).filter(S=>S),connection:e.githubConnection,fullName:`${j.owner}/${j.repo}`,repository:e.sourceUrl,reconnectRepositoryId:e.reconnectRepositoryId,options:e.options,source:e.source,conference:e.conference};e.details&&(M.options.pageSource=e.details.pageSource),z();let v=e.isUpdate?"/api/repo/"+e.repoId:"/api/repo/";t.post(v,M,{headers:{"Content-Type":"application/json"}}).then(()=>{R.clear(),window.location.href="/status/"+e.repoId},S=>{S.data&&(i("ERRORS."+S.data.error).then(V=>{e.error=V},console.error),te(S.data.error))}).finally(()=>{I.disabled=!1})},e.anonymizeGist=_=>{if(e.sourceLoading){e.error="Wait for the source to finish loading before saving.";return}if(ne())return;if(e.anonymize?.invalid||e.sourceUrlError){e.error="Please correct the highlighted fields before saving.";return}let I=_.currentTarget||_.target.closest?.("button")||_.target;I.disabled=!0;let j=parseGithubUrl(e.sourceUrl),M={gistId:e.gistId,terms:e.terms.trim().split(`
`).filter(S=>S),source:{gistId:j.gistId},options:e.options,conference:e.conference};z();let v=e.isUpdate?"/api/gist/"+e.gistId:"/api/gist/";t.post(v,M,{headers:{"Content-Type":"application/json"}}).then(()=>{R.clear(),window.location.href="/gist/"+e.gistId},S=>{S.data&&(i("ERRORS."+S.data.error).then(V=>{e.error=V},console.error),te(S.data.error))}).finally(()=>{I.disabled=!1})},e.anonymizePullRequest=_=>{if(e.sourceLoading){e.error="Wait for the source to finish loading before saving.";return}if(ne())return;if(e.anonymize?.invalid||e.sourceUrlError){e.error="Please correct the highlighted fields before saving.";return}let I=_.currentTarget||_.target.closest?.("button")||_.target;I.disabled=!0;let j=parseGithubUrl(e.sourceUrl),M={pullRequestId:e.pullRequestId,connection:e.githubConnection,terms:e.terms.trim().split(`
`).filter(S=>S),source:{repositoryFullName:`${j.owner}/${j.repo}`,pullRequestId:j.pullRequestId},options:e.options,conference:e.conference};z();let v=e.isUpdate?"/api/pr/"+e.pullRequestId:"/api/pr/";t.post(v,M,{headers:{"Content-Type":"application/json"}}).then(()=>{R.clear(),window.location.href="/pr/"+e.pullRequestId},S=>{S.data&&(i("ERRORS."+S.data.error).then(V=>{e.error=V},console.error),te(S.data.error))}).finally(()=>{I.disabled=!1})},e.watch("conference",()=>{Ue()}),e.watch("terms",()=>{e.detectedType==="repo"&&ue(),e.detectedType==="pr"&&_e(),e.detectedType==="gist"&&Se()}),e.watch("options.image",()=>{e.detectedType==="repo"&&ue(),e.detectedType==="pr"&&_e(),e.detectedType==="gist"&&Se()}),e.watch("options.link",()=>{e.detectedType==="repo"&&ue(),e.detectedType==="pr"&&_e(),e.detectedType==="gist"&&Se()}),e.watch("details",()=>{e.detectedType==="pr"&&_e(),e.detectedType==="gist"&&Se()},!0)},tr=function(e,t,n,r,s,i){let a=ct(),u=Kt();e.on("dark-mode",(O,G)=>{e.aceOption&&(G?e.aceOption.theme="nord_dark":e.aceOption.theme="chrome")});let f=0,m=!1;e.on("dispose",()=>{m=!0,f++,g&&g.resolve()}),e.files=[],e.isMac=/Mac|iPhone|iPad|iPod/.test(navigator.platform||navigator.userAgent),e.fileSearchQuery="",e.fileSearchResults=null,e.fileSearchLoading=!1,u(document,"keydown",function(O){if((O.metaKey||O.ctrlKey)&&O.key==="k"){O.preventDefault();var G=document.querySelector(".tree-search-input");G&&(G.focus(),G.select())}});var g=null;e.onFileSearchChange=function(){g&&(g.resolve(),g=null);let O=e.fileSearchQuery;if(!O||O.length<2){e.fileSearchResults=null,e.fileSearchLoading=!1;return}e.fileSearchLoading=!0;let G=i.defer();g=G,t.get(`/api/repo/${e.repoId}/files/search?q=${encodeURIComponent(O)}`,{timeout:G.promise}).then(function(J){if(!(m||g!==G)){g=null,e.fileSearchLoading=!1;var ee={};e.files.forEach(function(z){ee[(z.path||"")+"/"+z.name]=!0});for(var Q=[],oe={},ue=0;ue<J.data.length;ue++){var pe=J.data[ue];if(pe.path)for(var le=pe.path.split("/"),Ie="",_e=0;_e<le.length;_e++){var Ae=Ie;Ie=Ie?Ie+"/"+le[_e]:le[_e];var Ee=Ae+"/"+le[_e];!ee[Ee]&&!oe[Ee]&&(oe[Ee]=!0,Q.push({name:le[_e],path:Ae}))}}Q.sort(function(z,ne){return(z.path||"").split("/").length-(ne.path||"").split("/").length}),Q.length>0&&e.files.push.apply(e.files,Q);for(var Ve=[],Se=0;Se<J.data.length;Se++){var Le=J.data[Se],Ue=(Le.path||"")+"/"+Le.name;!ee[Ue]&&Le.size!=null&&(Ve.push(Le),ee[Ue]=!0)}Ve.length>0&&e.files.push.apply(e.files,Ve),e.fileSearchResults=J.data}},function(){!m&&g===G&&(g=null,e.fileSearchLoading=!1,e.fileSearchResults=[])})};let w={yml:"yaml",txt:"text",py:"python",js:"javascript",ts:"typescript"},h=["license","txt"],b=["png","jpg","jpeg","gif","svg","ico","bmp","tiff","tif","webp","avif","heif","heic"],E=["wav","mp3","ogg","wma","flac","aac","m4a"],k=["mp4","avi","webm","mov","mpg","mpeg","mkv","flv","wmv","3gp","3g2","m4v","f4v","f4p","f4a","f4b"];e.on("routeUpdate",function(O,G){if(e.repoId!=r.repoId)return C();if((r.path||"")!=e.filePath){e.filePath=r.path||"",e.paths=e.filePath.split("/").filter(J=>J&&J.trim().length>0),re();for(let J=0;J<e.paths.length-1;J++){let ee=J>0?e.paths.slice(0,J).join("/"):"";e.files.some(oe=>oe.path===ee)||e.getFiles(ee)}}});function R(){if(e.paths[0]!="")return;let O=["readme.md","readme.txt","readme.org","readme.1st","readme"],G={};for(let ee of e.files)ee.name.toLowerCase().indexOf("readme")>-1&&(G[ee.name.toLowerCase()]=ee.name);let J=null;for(let ee of O)if(G[ee]){J=ee;break}if(!J&&Object.keys(G).length>0&&(J=Object.keys(G)[0]),J){let ee=n.url();ee[ee.length-1]!="/"&&(ee+="/"),n.url(ee+encodePathForUrl(G[J]))}}e.fileCounts=null,e.getFiles=function(O){let G=e.repoId;return t.get(`/api/repo/${e.repoId}/files/?path=${encodeURIComponent(O)}&v=${e.options.lastUpdateDate}`).then(function(J){if(m||G!==e.repoId)return[];let ee=O||"";return e.files=e.files.filter(Q=>Q.path!==ee),e.files.push(...J.data),J.data},function(J){if(m||G!==e.repoId)return[];e.type="error",e.content=J&&J.data&&J.data.error||"unknown_error",e.files=[]})};function D(){let O=e.repoId;t.get(`/api/repo/${e.repoId}/files/counts`).then(function(G){m||O!==e.repoId||(e.fileCounts=G.data)},function(){e.fileCounts={}})}function T(){return e.files.filter(O=>O.name==e.paths[e.paths.length-1]&&O.path==e.paths.slice(0,e.paths.length-1).join("/"))[0]}var P=null;e.on("dispose",function(){P&&a.interval.cancel(P)});function H(O){if(m)return;let G=e.repoId;t.get(`/api/repo/${e.repoId}/options`).then(J=>{if(!(m||G!==e.repoId)){if(e.options=J.data,e.options.url){window.location=e.options.url;return}O&&O(J.data)}},J=>{if(!(m||G!==e.repoId)){var ee=J.data||{};if(ee.error==="rate_limited"&&ee.resetAt){let oe=function(){var ue=Math.max(0,Math.ceil((e.rateLimitResetAt-Date.now())/1e3));if(ue<=0)e.rateLimitCountdown="",e.rateLimitResetAt=0,P&&(a.interval.cancel(P),P=null),H(O);else{var pe=Math.floor(ue/60),le=ue%60;e.rateLimitCountdown=pe>0?pe+"m "+le+"s":le+"s"}};var Q=oe;e.type="rate_limited",e.rateLimitResetAt=ee.resetAt,P&&a.interval.canc Line truncated
`+k.join(`
`)],{type:"text/csv"}),D=document.createElement("a");D.href=URL.createObjectURL(R),D.download=r.username+"-repositories.csv",D.click()},e.showStatusMessage=h=>{let b=h.statusMessage||"(no message)";window.prompt(`Status message for ${h.repoId} (${h.status}):`,b)},e.fetchGithubInfo=h=>{let b=window.open("","_blank");b&&b.document.write("<pre>Loading GitHub info for "+h.repoId+"...</pre>"),t.get("/api/admin/repos/"+h.repoId+"/github").then(E=>{b&&(b.document.open(),b.document.write('<pre style="font:13px monospace;padding:16px;white-space:pre-wrap">'+JSON.stringify(E.data,null,2).replace(/[<>]/g,k=>k==="<"?"&lt;":"&gt;")+"</pre>"),b.document.close())},E=>{let k=E&&E.data?JSON.stringify(E.data,null,2):String(E);b&&(b.document.body.innerHTML='<pre style="color:#B42318;padding:16px">'+k+"</pre>")})};function f(h){t.get("/api/admin/users/"+h+"/repos",{}).then(b=>{e.repositories=b.data},b=>{console.error(b)})}function m(h){t.get("/api/admin/users/"+h,{}).then(b=>{e.userInfo=b.data},b=>{console.error(b)})}m(r.username),f(r.username),e.banUser=()=>{confirm(`Ban user ${r.username}?`)&&t.post(`/api/admin/users/${r.username}/ban`).then(()=>m(r.username),h=>console.error(h))},e.activateUser=()=>{t.post(`/api/admin/users/${r.username}/activate`).then(()=>m(r.username),h=>console.error(h))},e.promoteUser=()=>{confirm(`Promote ${r.username} to admin?`)&&t.post(`/api/admin/users/${r.username}/promote`).then(()=>m(r.username),h=>console.error(h))},e.demoteUser=()=>{confirm(`Remove admin privileges from ${r.username}?`)&&t.post(`/api/admin/users/${r.username}/demote`).then(()=>m(r.username),h=>console.error(h))},e.tokens=[],e.tokenForm={name:"",plaintext:null};function g(){t.get("/api/admin/tokens").then(h=>{e.tokens=h.data||[]},h=>{h.status!==401&&h.status!==403&&console.error(h)})}g(),e.createToken=()=>{e.tokenForm.name&&t.post("/api/admin/tokens",{name:e.tokenForm.name}).then(h=>{e.tokenForm.plaintext=h.data.token,e.tokenForm.name="",g()},h=>console.error(h))},e.revokeToken=h=>{confirm(`Revoke token "${h.name}"?`)&&t.delete("/api/admin/tokens/"+h.id).then(()=>g(),b=>console.error(b))},e.removeCache=h=>{confirm("Remove cached files for "+h.repoId+"?")&&t.delete("/api/admin/repos/"+h.repoId).then(()=>f(r.username),b=>console.error(b))},e.removeRepository=h=>{confirm("Remove repository "+h.repoId+"?")&&t.delete("/api/repo/"+h.repoId+"/").then(()=>f(r.username),b=>console.error(b))},e.updateRepository=h=>{let b=He({title:`Refreshing ${h.repoId}...`,date:new Date,body:`The repository ${h.repoId} is going to be refreshed.`});e.toasts.push(b),t.post(`/api/repo/${h.repoId}/refresh`).then(E=>{E.data.status=="ready"?b.title=`${h.repoId} is refreshed.`:b.title=`Refreshing of ${h.repoId}.`},E=>{b.title=`Error during the refresh of ${h.repoId}.`,b.body=E.body})},e.getGitHubRepositories=h=>{t.get(`/api/user/${e.userInfo.username}/all_repositories`,{params:{force:"1"}}).then(b=>{e.userInfo.repositories=b.data})};let w=null;e.watch("query",()=>{s.timeout.cancel(w),w=s.timeout(()=>{f(r.username)},500)},!0)},Xa=function(e,t,n){let r=ct(),s=Kt();e.Math=Math,e.watch("user.status",()=>{e.user==null&&n.url("/")}),e.user==null&&n.url("/"),e.conferences=[],e.total=-1,e.totalPage=0,e.statusCounts=[];let i=E=>{if(E.key==="/"&&!["INPUT","TEXTAREA","SELECT"].includes(document.activeElement?.tagName)){E.preventDefault();let k=document.querySelector('.admin-filter-toolbar input[type="search"]');k&&k.focus()}};s(document,"keydown",i),e.on("dispose",()=>document.removeEventListener("keydown",i)),e.clearFilter=E=>{E==="dateRange"?(e.query.dateFrom="",e.query.dateTo=""):e.query[E]="",e.query.page=1},e.chips=[];let a=()=>{let E=[];(e.query.dateFrom||e.query.dateTo)&&E.push({key:"dateRange",label:"Date",value:(e.query.dateFrom||"\u2026")+" \u2013 "+(e.query.dateTo||"\u2026")}),e.chips=E};e.statusCountFor=E=>{let k=(e.statusCounts||[]).find(R=>R._id===E);return k?k.count:0},e.toggleSortDirection=()=>{e.query.direction=e.query.direction==="asc"?"desc":"asc"},e.sortBy=E=>{e.query.sort===E?e.query.direction=e.query.direction==="asc"?"desc":"asc":(e.query.sort=E,e.query.direction="desc"),e.query.page=1},e.sortIcon=E=>e.query.sort===E?e.query.direction==="asc"?"fa-arrow-up":"fa-arrow-down":"";let u="admin.conferences.filterPrefs",f={page:1,limit:25,sort:"name",direction:"asc",search:"",dateFrom:"",dateTo:"",ready:!1,expired:!1,removed:!1,error:!0,preparing:!0},m=loadFilterPrefs(u)||{};e.query=Object.assign({},f,m,{page:1,search:""});let g=n.search();g.search&&(e.query.search=g.search);let w="admin.conferences.presets";e.presets=JSON.parse(localStorage.getItem(w)||"[]"),e.savePreset=()=>{let E=window.prompt("Preset name:");if(!E)return;let k=Object.assign({},e.query);delete k.page,e.presets=(e.presets||[]).filter(R=>R.name!==E),e.presets.push({name:E,query:k}),localStorage.setItem(w,JSON.stringify(e.presets))},e.applyPreset=E=>{Object.assign(e.query,E.query,{page:1})},e.deletePreset=E=>{e.presets=(e.presets||[]).filter(k=>k.name!==E.name),localStorage.setItem(w,JS Line truncated
`+JSON.stringify(t):""}`},[je.NAVIGATION_GUARD_REDIRECT]({from:e,to:t}){return`Redirected from "${e.fullPath}" to "${qc(t)}" via a navigation guard.`},[je.NAVIGATION_ABORTED]({from:e,to:t}){return`Navigation aborted from "${e.fullPath}" to "${t.fullPath}" via a navigation guard.`},[je.NAVIGATION_CANCELLED]({from:e,to:t}){return`Navigation cancelled from "${e.fullPath}" to "${t.fullPath}" with a new navigation.`},[je.NAVIGATION_DUPLICATED]({from:e,to:t}){return`Avoided redundant navigation to current location: "${e.fullPath}".`}};function po(e,t){return Oe(new Error,{type:e,[ga]:!0},t)}function Lt(e,t){return e instanceof Error&&ga in e&&(t==null||!!(e.type&t))}var Vc=["params","query","hash"];function qc(e){if(typeof e=="string")return e;if(e.path!=null)return e.path;let t={};for(let s of Vc)s in e&&(t[s]=e[s]);return JSON.stringify(t,null,2)}function ya(e){let t={};if(e===""||e==="?")return t;let s=(e[0]==="?"?e.slice(1):e).split("&");for(let r=0;r<s.length;++r){let n=s[r].replace(na," "),i=n.indexOf("="),a=To(i<0?n:n.slice(0,i)),u=i<0?null:To(n.slice(i+1));if(a in t){let f=t[a];wt(f)||(f=t[a]=[f]),f.push(u)}else t[a]=u}return t}function Zn(e){let t="";for(let s in e){let r=e[s];if(s=Sc(s),r==null){r!==void 0&&(t+=(t.length?"&":"")+s);continue}(wt(r)?r.map(n=>n&&jn(n)):[r&&jn(r)]).forEach(n=>{n!==void 0&&(t+=(t.length?"&":"")+s,n!=null&&(t+="="+n))})}return t}function ba(e){let t={};for(let s in e){let r=e[s];r!==void 0&&(t[s]=wt(r)?r.map(n=>n==null?null:""+n):r==null?r:""+r)}return t}var wa=Symbol(""),xn=Symbol(""),Zs=Symbol(""),xs=Symbol(""),en=Symbol("");function Oo(){let e=[];function t(r){return e.push(r),()=>{let n=e.indexOf(r);n>-1&&e.splice(n,1)}}function s(){e=[]}return{add:t,list:()=>e.slice(),reset:s}}function Kt(e,t,s,r,n,i=a=>a()){let a=r&&(r.enterCallbacks[n]=r.enterCallbacks[n]||[]);return()=>new Promise((u,f)=>{let h=m=>{m===!1?f(po(je.NAVIGATION_ABORTED,{from:s,to:t})):m instanceof Error?f(m):Pc(m)?f(po(je.NAVIGATION_GUARD_REDIRECT,{from:t,to:m})):(a&&r.enterCallbacks[n]===a&&typeof m=="function"&&a.push(m),u())},v=i(()=>e.call(r&&r.instances[n],t,s,h)),k=Promise.resolve(v);e.length<3&&(k=k.then(h)),k.catch(m=>f(m))})}function tn(e,t,s,r,n=i=>i()){let i=[];for(let a of e)for(let u in a.components){let f=a.components[u];if(!(t!=="beforeRouteEnter"&&!a.instances[u]))if(oa(f)){let h=(f.__vccOpts||f)[t];h&&i.push(Kt(h,s,r,a,u,n))}else{let h=f();i.push(()=>h.then(v=>{if(!v)throw new Error(`Couldn't resolve component "${u}" at "${a.path}"`);let k=hc(v)?v.default:v;a.mods[u]=v,a.components[u]=k;let m=(k.__vccOpts||k)[t];return m&&Kt(m,s,r,a,u,n)()}))}}return i}function ka(e,t){let s=[],r=[],n=[],i=Math.max(t.matched.length,e.matched.length);for(let a=0;a<i;a++){let u=t.matched[a];u&&(e.matched.find(h=>co(h,u))?r.push(u):s.push(u));let f=e.matched[a];f&&(t.matched.find(h=>co(h,f))||n.push(f))}return[s,r,n]}var Mc=()=>location.protocol+"//"+location.host;function Ia(e,t){let{pathname:s,search:r,hash:n}=t,i=e.indexOf("#");if(i>-1){let a=n.includes(e.slice(i))?e.slice(i).length:1,u=n.slice(a);return u[0]!=="/"&&(u="/"+u),Yn(u,"")}return Yn(s,e)+r+n}function Lc(e,t,s,r){let n=[],i=[],a=null,u=({state:m})=>{let y=Ia(e,location),E=s.value,w=t.value,R=0;if(m){if(s.value=y,t.value=m,a&&a===E){a=null;return}R=w?m.position-w.position:0}else r(y);n.forEach(D=>{D(s.value,E,{delta:R,type:Qs.pop,direction:R?R>0?Xs.forward:Xs.back:Xs.unknown})})};function f(){a=s.value}function h(m){n.push(m);let y=()=>{let E=n.indexOf(m);E>-1&&n.splice(E,1)};return i.push(y),y}function v(){if(document.visibilityState==="hidden"){let{history:m}=window;if(!m.state)return;m.replaceState(Oe({},m.state,{scroll:us()}),"")}}function k(){for(let m of i)m();i=[],window.removeEventListener("popstate",u),window.removeEventListener("pagehide",v),document.removeEventListener("visibilitychange",v)}return window.addEventListener("popstate",u),window.addEventListener("pagehide",v),document.addEventListener("visibilitychange",v),{pauseListeners:f,listen:h,destroy:k}}function Ea(e,t,s,r=!1,n=!1){return{back:e,current:t,forward:s,replaced:r,position:window.history.length,scroll:n?us():null}}function $c(e){let{history:t,location:s}=window,r={value:Ia(e,s)},n={value:t.state};n.value||i(r.value,{back:null,current:r.value,forward:null,position:t.length-1,replaced:!0,scroll:null},!0);function i(f,h,v){let k=e.indexOf("#"),m=k>-1?(s.host&&document.querySelector("base")?e:e.slice(k))+f:Mc()+e+f;try{t[v?"replaceState":"pushState"](h,"",m),n.value=h}catch(y){console.error(y),s[v?"replace":"assign"](m)}}function a(f,h){i(f,Oe({},t.state,Ea(n.value.back,f,n.value.forward,!0),h,{position:n.value.position}),!0),r.value=f}function u(f,h){let v=Oe({},n.value,t.state,{forward:f,scroll:us()});i(v.current,v,!0),i(f,Oe({},Ea(r.value,f,null),{position:v.position+1},h),!1),r.value=f}return{location:r,state:n,push:u,replace:a}}function Pa(e){e=pa(e);let t=$c(e),s=Lc(e,t.state,t.location,t.replace);function r(i,a=!0){a||s.pauseListeners(),history.go(i)}let n=Oe({locatLine truncated
`)),e.options=Object.assign({},e.options,h.options),i.restore()})}a();let u=null;e.saveDefault=f=>{if(f&&f.preventDefault&&f.preventDefault(),e.saving)return;let h=i.checkpoint(),v={terms:e.terms.split(`
`).map(k=>k.trim()).filter(k=>k.length>0),options:e.options};e.saving=!0,e.error=null,t.post("/api/user/default",v).then(()=>{i.accept(h),e.saving=!1,e.message=e.unsavedChanges?null:"Saved",u&&r.cancel(u),u=r(()=>{e.message=null},2500)},k=>{e.saving=!1;let y="ERRORS."+(k&&k.data&&k.data.error);s(y).then(E=>{e.error=E&&E!==y?E:"Unable to save your defaults. Please try again."},()=>{e.error="Unable to save your defaults. Please try again."})})},e.deleteAccount=()=>{confirm("Delete your account? All your anonymized repositories, gists, and pull requests will be removed, and your personal data will be erased. This cannot be undone.")&&(e.deletingAccount=!0,t.delete("/api/user").then(()=>{window.location.href="/"},()=>{e.deletingAccount=!1,e.deleteError="Unable to delete the account. Please try again."}))}},Ha=function(e,t,s){e.repoId=null,e.repoUrl=null,e.claim=()=>{t.post("/api/repo/claim",{repoId:e.repoId,repoUrl:e.repoUrl}).then(r=>{s.url("/dashboard")},r=>{e.error=r.data,e.claimForm.repoUrl.setValidity("not_found",!1),e.claimForm.repoId.setValidity("not_found",!1)})}},Ba=function(e,t,s,r,n){e.user&&!e.user.status&&s.url("/dashboard"),e.watch("user.status",()=>{e.user&&!e.user.status&&s.url("/dashboard")}),e.features=[{key:"anonymize",num:"01",eyebrow:"Anonymize",title:"Double-anonymous,",accent:"your rules.",text:"Choose what reviewers may see: links, images, PDFs, notebooks, GitHub Pages. Add your own terms, with regex if you need it, and pick the expiration date.",cta:"Start an anonymization",href:"/anonymize",url:"anonymous.4open.science/anonymize",img:"/imgs/anonymize.png",alt:"The anonymize form: source repository, terms to redact, options and a live README preview"},{key:"review",num:"02",eyebrow:"Review",title:"Reviewers browse",accent:"the real thing.",text:"Highlighted source code, rendered PDFs, images, and notebooks, in a familiar file explorer. GitHub Pages is also supported.",cta:"Open the example",href:"https://anonymous.4open.science/r/840c8c57-3c32-451e-bf12-0e20be300389/",target:"_self",url:"anonymous.4open.science/r/840c8c57-\u2026",img:"/imgs/explorer.png",alt:"The repository explorer with a file tree and a rendered README"},{key:"manage",num:"03",eyebrow:"Manage",title:"One dashboard,",accent:"until the decision.",text:"Monitor views, edit configuration, remove or update your repository. Program chairs can group submissions under a conference with one shared expiry.",cta:"Open the dashboard",href:"/dashboard",needsUser:!0,url:"anonymous.4open.science/dashboard",img:"/imgs/dashboard.png",alt:"The dashboard listing anonymized repositories with status, views and expiry"}],e.feature=e.features[0].key,e.selectFeature=function(u){e.feature=u},e.featureHref=function(u){return u.needsUser&&!e.user?"/signin":u.href},e.featureTarget=function(u){return u.needsUser&&!e.user?"_self":u.target||void 0},e.featureKeydown=function(u,f){let h={ArrowDown:1,ArrowRight:1,ArrowUp:-1,ArrowLeft:-1,Home:"first",End:"last"}[u.key];if(h===void 0)return;u.preventDefault();let v=e.features.length,k=h==="first"?0:h==="last"?v-1:(f+h+v)%v;e.feature=e.features[k].key,n(()=>{let m=r.document.getElementById("feature-tab-"+e.feature);m&&m.focus()})},e.cards=[{key:"repositories",total:0,label:"repositories anonymized"},{key:"users",total:0,label:"researchers"},{key:"pageViews",total:0,label:"page views"},{key:"pullRequests",total:0,label:"pull requests"}];function i(){t.get("/api/stat/").then(u=>{e.stat=u.data,e.cards[0].total=u.data.nbRepositories,e.cards[1].total=u.data.nbUsers,e.cards[2].total=u.data.nbPageViews,e.cards[3].total=u.data.nbPullRequests})}i();function a(u){let f={series:u,bars:[],viewW:100,deltaToday:0,pctChange:0,pctAbs:0,isUp:!0};if(!u||u.length<2)return f;let h=new Array(u.length-1);for(let w=1;w<u.length;w++)h[w-1]=u[w]-u[w-1];let v=h.length,k=Math.max.apply(null,h),m=Math.min.apply(null,h),y=Math.min(0,m),E=k-y||1;f.viewW=v*2,f.bars=new Array(v);for(let w=0;w<v;w++){let R=(h[w]-y)/E,D=Math.max(1.5,R*34);f.bars[w]={x:(w*2+.25).toFixed(2),y:(36-D).toFixed(2),w:"1.5",h:D.toFixed(2)}}if(f.deltaToday=h[v-1],v>=2){let w=h[v-2];w&&(f.pctChange=(f.deltaToday-w)/w*100)}return f.pctAbs=Math.round(Math.abs(f.pctChange)),f.isUp=f.pctChange>=0,f}e.history={repositories:a([]),users:a([]),pageViews:a([]),pullRequests:a([])},t.get("/api/stat/history?days=60").then(u=>{let f=u.data||[];e.history={repositories:a(f.map(h=>h.nbRepositories||0)),users:a(f.map(h=>h.nbUsers||0)),pageViews:a(f.map(h=>h.nbPageViews||0)),pullRequests:a(f.map(h=>h.nbPullRequests||0))}})},ja=function(e,t,s,r,n,i){let a=ct();e.on("routeLeave",function(){$('[data-toggle="tooltip"]').tooltip("dispose")}),e.watch("user.status",()=>{e.user==null&&s.url("/")}),e.user==null&&s.url("/"),a.timeout(()=>{$('[data-toggle="tooltip"]').tooltip()},250),e.quotaExpanded=!1,e.closeQuota=()=>{e.quotaExpanded=!1,st(()=>document.querySelector(".dashboard-quota > summary")?.focus())},Jt()(document,"click",C=>{e.quotaExpanded&&!C.target.closest?.(".dashboard-quota")&& Line truncated
`)),e.options=Object.assign({},e.options,B.options),e.options.expirationDate=B.options&&B.options.expirationDate?new Date(B.options.expirationDate):T(),!r.repoId&&!r.pullRequestId&&!r.gistId&&!e.anonymize?.terms?.dirty&&(e.terms=e.defaultTerms),_&&_()})}function U(_,I,B){e.anonymize&&e.anonymize[_]&&e.anonymize[_].setValidity(I,B)}function ae(_){try{let I=parseGithubUrl(_);if(I&&I.owner&&I.repo)return I.owner+"/"+I.repo}catch{}return null}e.reconnectingSource=!1,e.reconnectRepositoryId=void 0,e.reconnectSource=async()=>{if(confirm("Reconnect to the repository currently at this GitHub URL? Review its branch and commit before saving. Saving will replace the published snapshot.")){e.reconnectingSource=!0,e.reconnectRepositoryId=void 0,e.source.commit="",e.source.branch="",e.readme="",e.branches=[];try{await G()}catch{}}};function ie(){return e.reconnectingSource||!e.isUpdate||!e._originalRepositoryID?void 0:ae(e.sourceUrl)===e._originalFullName?e._originalRepositoryID:void 0}async function C(){e._preservingDraft=!0;try{await e.urlSelected(!0)}finally{e._preservingDraft=!1}}async function O(){let _;try{_=JSON.parse(sessionStorage.getItem("github-access-draft")||"null")}catch{return!1}if(!_||_.path!==n.path()||Date.now()-_.savedAt>=30*6e4)return R.restore()?(e.sourceUrl&&await C(),!0):!1;R.restore();for(let I of["sourceUrl","terms","repoId","pullRequestId","gistId","source","options","conference","githubConnection"])Object.prototype.hasOwnProperty.call(_.draft,I)&&(e[I]=_.draft[I]);return e.options.expirationDate&&(e.options.expirationDate=new Date(e.options.expirationDate)),await C(),sessionStorage.removeItem("github-access-draft"),!0}q(()=>{!r.repoId&&!r.pullRequestId&&!r.gistId&&a(O,0),r.repoId&&r.repoId!=""&&(e.isUpdate=!0,e.detectedType="repo",e.repoId=r.repoId,t.get("/api/repo/"+e.repoId).then(async _=>{e.githubConnection=_.data.connection||"oauth",e.sourceUrl="https://github.com/"+_.data.source.fullName,e._originalFullName=_.data.source.fullName,e.terms=_.data.options.terms.filter(I=>I).join(`
`),e.source=_.data.source,e.role=_.data.role||"owner",e.coauthors=_.data.coauthors||[],e._originalBranch=_.data.source.branch,e.options=Object.assign({},e.options,_.data.options),e.conference=_.data.conference,e._originalConference=_.data.conference,e.repositoryID=_.data.source.repositoryID,e._originalRepositoryID=_.data.source.repositoryID,_.data.options.expirationDate&&(e.options.expirationDate=new Date(_.data.options.expirationDate)),E=w(parseGithubUrl(e.sourceUrl)),!await O()&&(await Promise.all([G(),J()]),ue())},()=>{n.url("/404")})),r.pullRequestId&&r.pullRequestId!=""&&(e.isUpdate=!0,e.detectedType="pr",e.pullRequestId=r.pullRequestId,t.get("/api/pr/"+e.pullRequestId).then(async _=>{if(e.githubConnection=_.data.connection||"oauth",e.sourceUrl="https://github.com/"+_.data.source.repositoryFullName+"/pull/"+_.data.source.pullRequestId,e.terms=_.data.options.terms.filter(I=>I).join(`
`),e.source=_.data.source,e.options=Object.assign({},e.options,_.data.options),e.conference=_.data.conference,e._originalConference=_.data.conference,_.data.options.expirationDate&&(e.options.expirationDate=new Date(_.data.options.expirationDate)),E=w(parseGithubUrl(e.sourceUrl)),!await O())try{await pe()}catch{}},()=>{n.url("/404")})),r.gistId&&r.gistId!=""&&(e.isUpdate=!0,e.detectedType="gist",e.gistId=r.gistId,t.get("/api/gist/"+e.gistId).then(async _=>{e.sourceUrl="https://gist.github.com/"+_.data.source.gistId,e.terms=_.data.options.terms.filter(I=>I).join(`
`),e.source=_.data.source,e.options=Object.assign({},e.options,_.data.options),e.conference=_.data.conference,e._originalConference=_.data.conference,_.data.options.expirationDate&&(e.options.expirationDate=new Date(_.data.options.expirationDate)),E=w(parseGithubUrl(e.sourceUrl)),!await O()&&await Ae()},()=>{n.url("/404")}))}),t.get("/github/connections").then(_=>{e.githubConnections=_.data,!r.repoId&&!r.pullRequestId&&!r.gistId&&e.githubConnection===void 0&&_.data.appEnabled&&_.data.appConnected&&(e.githubConnection="github-app",e.sourceUrl&&e.workspaceOpen&&C())}).catch(()=>{}),e.urlSelected=async(_=!1)=>{let I=++f,B;try{B=parseGithubUrl(e.sourceUrl.trim())}catch{e.sourceLoading=!1,oe.cancel(),_e.cancel(),Se.cancel(),e.previewStatus="idle",e.sourceUrlError=e.sourceUrl?"Please provide a valid GitHub URL.":"",U("sourceUrl","github",!1);return}let M=w(B),g=_||E===M;E=M,e.sourceUrlError="",U("sourceUrl","github",!0);let S=document.activeElement?.id==="sourceUrl-landing";e.workspaceOpen=!0,S&&a(()=>document.getElementById("sourceUrl")?.focus(),0),e.reconnectingSource=!1,e.reconnectRepositoryId=void 0,!g&&!e.isUpdate&&(e.repoId="",e.pullRequestId="",e.gistId=""),e.details=null,e.branches=[],g||(e.source={type:"GitHubStream",branch:"",commit:""}),oe.cancel(),_e.cancel(),Se.cancel(),le.clear(),Ee.clear(),e.previewStatus="idle",e.anonymize_readme="",e.readme="",e.html_readme="",e.detectedType=null,e.gistOAuthRequired=!1,e.gistAppRequired=!1,e._preservingDraft=g;try{B.gistId&&!B.repo?(e.detectedType="gist",e.source={gistId:B.gistId},await Ae()):B.pullRequestId?(e.detectedType="pr",e.source={repositoryFullName:B.owner+"/"+B.repo,pullRequestId:B.pullRequestId},await pe()):(e.detectedType="repo",await Promise.all([G(),J()]),ue())}catch{}finally{I===f&&(e._preservingDraft=!1)}$('[data-toggle="tooltip"]').tooltip()},$('[data-toggle="tooltip"]').tooltip(),e.watch("source.branch",async()=>{if(e.detectedType!=="repo")return;let _=e.branches.filter(B=>B.name==e.source.branch)[0];if(!_)return;!(!e.reconnectingSource&&e.isUpdate&&e._originalBranch===e.source.branch&&!!e.source.commit)&&!(e._preservingDraft&&e.source.commit)&&(e.source.commit=_.commit),e.readme=_.readme,await J(),ue()}),e.getBranches=async _=>{let I=parseGithubUrl(e.sourceUrl),B=m();try{let M=await t.get(`/api/repo/${I.owner}/${I.repo}/branches`,{params:{reconnect:e.reconnectingSource?"1":void 0,anonymizedRepoId:e.isUpdate&&r.repoId&&ae(e.sourceUrl)===e._originalFullName?r.repoId:void 0,connection:e.githubConnection,force:_===!0||e.reconnectingSource?"1":"0",repositoryID:ie()}});if(!k(B))return;e.branches=M.data,e.sourceUnreachable=!1,e.source.branch||(e.source.branch=e.details.defaultBranch);let g=e.branches.filter(S=>S.name==e.source.branch);g.length>0&&(!(!e.reconnectingSource&&!_&&e.isUpdate&&!e.options.update&&e._originalBranch===e.source.branch&&e.source.commit)&&!(e._preservingDraft&&e.source.commit)&&(e.source.commit=g[0].commit),e.readme=g[0].readme,await J(_))}catch(M){if(!k(B))return;e.branches=[],e.sourceUnreachable=M&&(M.status===404||M.data&&M.data.error==="repo_not_found");let g=M&&M.data&&M.data.error||(M&&M.status===404?"repo_not_found":"unknown_error");i("ERRORS."+g).then(S=>{e.toasts=e.toasts||[],e.addToast({title:"Error",date:new Date,body:S}),e.error=S},console.error),typeof U=="function"&&U("sourceUrl","missing",!1)}finally{y(B)}};async function G(){let _=parseGithubUrl(e.sourceUrl),I=m();try{z();let B=await t.get(`/api/repo/${_.owner}/${_.repo}/`,{params:{reconnect:e.reconnectingSource?"1":void 0,anonymizedRepoId:e.isUpdate&&r.repoId&&ae(e.sourceUrl)===e._originalFullName?r.repoId:void 0,connection:e.githubConnection,repositoryID:ie(),force:"1"}});if(!k(I))return;e.details=B.data,e.reconnectingSource&&(e.reconnectRepositoryId=e.details.externalId),e.details&&e.details.id&&(e.repositoryID=e.details.id),e.repoId||(e.repoId=e.details.repo+"-"+generateRandomId(4)),await e.getBranches(e.reconnectingSource)}catch(B){if(!k(I))return;throw B.data&&(i("ERRORS."+B.data.error).then(M=>{e.addToast({title:"Error",date:new Date,body:M}),e.error=M},console.error),te(B.data.error)),U("sourceUrl","missing",!1),B}finally{y(I)}}async function J(_){if(e.readme&&!_)return e.readme;let I=parseGithubUrl(e.sourceUrl),B=m();try{let M=await t.get(`/api/repo/${I.owner}/${I.repo}/readme`,{params:{reconnect:e.reconnectingSource?"1":void 0,anonymizedRepoId:e.isUpdate&&r.repoId&&ae(e.sourceUrl)===e._originalFullName?r.repoId:void 0,connection:e.githubConnection,force:_===!0?"1":"0",branch:e.source.branch,repositoryID:ie()}});k(B,!0)&&(e.readme=M.data)}catch{k(B,!0)&&(e.readme="")}finally{y(B)}}function ee(){let _={terms:e.terms?e.terms.split(`
`):[],image:!!e.options.image,link:!!e.options.link,repoId:e.repoId};e.source&&e.source.branch&&(_.branchName=e.source.branch);try{let I=parseGithubUrl(e.sourceUrl);_.repoName=`${I.owner}/${I.repo}`}catch{}return _}function Q(_,I,B){let M=null,g=0,S=()=>{if(e.detectedType!==_)return;M&&a.cancel(M);let V=++g;e.previewStatus="loading",M=a(()=>{M=null;let F=I();if(!F){e.previewStatus="idle";return}t.post("/api/anonymize-preview",F).then(j=>{V===g&&(B(j.data,F),e.previewStatus="ready")}).catch(()=>{V===g&&(e.previewStatus="error")})},200)};return S.cancel=()=>{g++,M&&a.cancel(M),M=null},e.on("dispose",S.cancel),S}e.retryPreview=()=>{e.detectedType==="repo"&&oe(),e.detectedType==="pr"&&_e(),e.detectedType==="gist"&&Se()};let oe=Q("repo",()=>e.readme?{content:e.readme,options:ee()}:null,_=>{if(typeof _?.content!="string")throw Error("Invalid preview response");e.anonymize_readme=_.content;let I="";try{let M=parseGithubUrl(e.sourceUrl),g=e.source.branch||e.details&&e.details.defaultBranch||"main";I=`https://github.com/${M.owner}/${M.repo}/raw/${g}/`}catch{}let B=renderMD(e.anonymize_readme,I);e.html_readme=B,a(Prism.highlightAll,150)});function ue(){!e.anonymize||!e.anonymize.terms||(e.termsRegexWarning=!!e.terms&&!!e.terms.match(/[-[\]{}()*+?.,\\^$|#]/g),oe())}async function pe(){let _=parseGithubUrl(e.sourceUrl),I=m();try{z();let B=await t.get(`/api/pr/${_.owner}/${_.repo}/${_.pullRequestId}`,{params:{connection:e.githubConnection}});if(!k(I))return;e.details=B.data,e.pullRequestId||(e.pullRequestId=_.repo+"-PR"+_.pullRequestId+"-"+generateRandomId(4))}catch(B){if(!k(I))return;throw B.data&&(i("ERRORS."+B.data.error).then(M=>{e.addToast({title:"Error",date:new Date,body:M}),e.error=M},console.error),te(B.data.error)),U("sourceUrl","missing",!1),B}finally{y(I)}}let le=He(new Map);function Ie(){let _=new Set,I=e.details&&e.details.pullRequest;if(!I)return _;typeof I.title=="string"&&_.add(I.title),typeof I.body=="string"&&_.add(I.body),typeof I.diff=="string"&&_.add(I.diff);let B=I.comments||[];for(let M of B)typeof M.author=="string"&&_.add(M.author),typeof M.body=="string"&&_.add(M.body);return _}let _e=Q("pr",()=>{let _=Ie(),I=Array.from(_);return I.length===0?null:{contents:I,options:ee()}},(_,I)=>{if(!Array.isArray(_?.contents)||_.contents.length!==I.contents.length||_.contents.some(g=>typeof g!="string"))throw Error("Invalid preview response");let B=I.contents,M=new Map;for(let g=0;g<B.length&&g<_.contents.length;g++)M.set(B[g],_.contents[g]);le.clear(),M.forEach((g,S)=>le.set(S,g))});e.anonymizePrContent=function(_){return _&&(le.has(_)?le.get(_):"")};async function Ae(){let _=parseGithubUrl(e.sourceUrl),I=m();try{z(),e.gistOAuthRequired=!1,e.gistAppRequired=!1;let B=await t.get(`/api/gist/source/${_.gistId}`);if(!k(I))return;e.details=B.data,e.gistId||(e.gistId="gist-"+_.gistId.substring(0,6)+"-"+generateRandomId(4))}catch(B){if(!k(I))return;if(B.data?.error==="github_app_reconnect_required"){e.gistAppRequired=!0,U("sourceUrl","missing",!1);return}if(B.data?.error==="github_oauth_required"){e.gistOAuthRequired=!0,U("sourceUrl","missing",!1);return}throw B.data&&(i("ERRORS."+B.data.error).then(M=>{e.addToast({title:"Error",date:new Date,body:M}),e.error=M},console.error),te(B.data.error)),U("sourceUrl","missing",!1),B}finally{y(I)}}let Ee=He(new Map);function Ve(){let _=new Set,I=e.details&&e.details.gist;if(!I)return _;typeof I.description=="string"&&_.add(I.description),typeof I.ownerLogin=="string"&&_.add(I.ownerLogin);let B=I.files||[];for(let g of B)typeof g.filename=="string"&&_.add(g.filename),typeof g.content=="string"&&_.add(g.content);let M=I.comments||[];for(let g of M)typeof g.author=="string"&&_.add(g.author),typeof g.body=="string"&&_.add(g.body);return _}let Se=Q("gist",()=>{let _=Ve(),I=Array.from(_);return I.length===0?null:{contents:I,options:ee()}},(_,I)=>{if(!Array.isArray(_?.contents)||_.contents.length!==I.contents.length||_.contents.some(g=>typeof g!="string"))throw Error("Invalid preview response");let B=I.contents,M=new Map;for(let g=0;g<B.length&&g<_.contents.length;g++)M.set(B[g],_.contents[g]);Ee.clear(),M.forEach((g,S)=>Ee.set(S,g)),$e()});e.anonymizeGistContent=function(_){return _&&(Ee.has(_)?Ee.get(_):"")},e.previewGistFiles=[];function $e(){let _=e.details&&e.details.gist&&e.details.gist.files||[];e.previewGistFiles=_.map(I=>({filename:e.anonymizeGistContent(I.filename),content:e.anonymizeGistContent(I.content),language:I.language}))}e.watch("details.gist.files",$e,!0),e.watch("terms",$e);function Ue(){let _=e.conference;if(e.conference_data=null,!_)return;let I=e.isUpdate&&_===e._originalConference;t.get("/api/conferences/"+_).then(B=>{e.conference===_&&(e.conference_data=B.data,e.conference_data.startDate=new Date(e.conference_data.startDate),e.conference_data.endDate=new Date(e.conference_data.endDate),!I&&(e.options.expirationDate=new Date(e.conference_data.endDate),e.options.expirationMode="remove",e.options.update=e.conference_data.options.update,e.options.image=e.con Line truncated
`).filter(S=>S),connection:e.githubConnection,fullName:`${B.owner}/${B.repo}`,repository:e.sourceUrl,reconnectRepositoryId:e.reconnectRepositoryId,options:e.options,source:e.source,conference:e.conference};e.details&&(M.options.pageSource=e.details.pageSource),z();let g=e.isUpdate?"/api/repo/"+e.repoId:"/api/repo/";t.post(g,M,{headers:{"Content-Type":"application/json"}}).then(()=>{R.clear(),window.location.href="/status/"+e.repoId},S=>{S.data&&(i("ERRORS."+S.data.error).then(V=>{e.error=V},console.error),te(S.data.error))}).finally(()=>{I.disabled=!1})},e.anonymizeGist=_=>{if(e.sourceLoading){e.error="Wait for the source to finish loading before saving.";return}if(ne())return;if(e.anonymize?.invalid||e.sourceUrlError){e.error="Please correct the highlighted fields before saving.";return}let I=_.currentTarget||_.target.closest?.("button")||_.target;I.disabled=!0;let B=parseGithubUrl(e.sourceUrl),M={gistId:e.gistId,terms:e.terms.trim().split(`
`).filter(S=>S),source:{gistId:B.gistId},options:e.options,conference:e.conference};z();let g=e.isUpdate?"/api/gist/"+e.gistId:"/api/gist/";t.post(g,M,{headers:{"Content-Type":"application/json"}}).then(()=>{R.clear(),window.location.href="/gist/"+e.gistId},S=>{S.data&&(i("ERRORS."+S.data.error).then(V=>{e.error=V},console.error),te(S.data.error))}).finally(()=>{I.disabled=!1})},e.anonymizePullRequest=_=>{if(e.sourceLoading){e.error="Wait for the source to finish loading before saving.";return}if(ne())return;if(e.anonymize?.invalid||e.sourceUrlError){e.error="Please correct the highlighted fields before saving.";return}let I=_.currentTarget||_.target.closest?.("button")||_.target;I.disabled=!0;let B=parseGithubUrl(e.sourceUrl),M={pullRequestId:e.pullRequestId,connection:e.githubConnection,terms:e.terms.trim().split(`
`).filter(S=>S),source:{repositoryFullName:`${B.owner}/${B.repo}`,pullRequestId:B.pullRequestId},options:e.options,conference:e.conference};z();let g=e.isUpdate?"/api/pr/"+e.pullRequestId:"/api/pr/";t.post(g,M,{headers:{"Content-Type":"application/json"}}).then(()=>{R.clear(),window.location.href="/pr/"+e.pullRequestId},S=>{S.data&&(i("ERRORS."+S.data.error).then(V=>{e.error=V},console.error),te(S.data.error))}).finally(()=>{I.disabled=!1})},e.watch("conference",()=>{Ue()}),e.watch("terms",()=>{e.detectedType==="repo"&&ue(),e.detectedType==="pr"&&_e(),e.detectedType==="gist"&&Se()}),e.watch("options.image",()=>{e.detectedType==="repo"&&ue(),e.detectedType==="pr"&&_e(),e.detectedType==="gist"&&Se()}),e.watch("options.link",()=>{e.detectedType==="repo"&&ue(),e.detectedType==="pr"&&_e(),e.detectedType==="gist"&&Se()}),e.watch("details",()=>{e.detectedType==="pr"&&_e(),e.detectedType==="gist"&&Se()},!0)},sr=function(e,t,s,r,n,i){let a=ct(),u=Jt();e.on("dark-mode",(O,G)=>{e.aceOption&&(G?e.aceOption.theme="nord_dark":e.aceOption.theme="chrome")});let f=0,h=!1;e.on("dispose",()=>{h=!0,f++,v&&v.resolve()}),e.files=[],e.isMac=/Mac|iPhone|iPad|iPod/.test(navigator.platform||navigator.userAgent),e.fileSearchQuery="",e.fileSearchResults=null,e.fileSearchLoading=!1,u(document,"keydown",function(O){if((O.metaKey||O.ctrlKey)&&O.key==="k"){O.preventDefault();var G=document.querySelector(".tree-search-input");G&&(G.focus(),G.select())}});var v=null;e.onFileSearchChange=function(){v&&(v.resolve(),v=null);let O=e.fileSearchQuery;if(!O||O.length<2){e.fileSearchResults=null,e.fileSearchLoading=!1;return}e.fileSearchLoading=!0;let G=i.defer();v=G,t.get(`/api/repo/${e.repoId}/files/search?q=${encodeURIComponent(O)}`,{timeout:G.promise}).then(function(J){if(!(h||v!==G)){v=null,e.fileSearchLoading=!1;var ee={};e.files.forEach(function(z){ee[(z.path||"")+"/"+z.name]=!0});for(var Q=[],oe={},ue=0;ue<J.data.length;ue++){var pe=J.data[ue];if(pe.path)for(var le=pe.path.split("/"),Ie="",_e=0;_e<le.length;_e++){var Ae=Ie;Ie=Ie?Ie+"/"+le[_e]:le[_e];var Ee=Ae+"/"+le[_e];!ee[Ee]&&!oe[Ee]&&(oe[Ee]=!0,Q.push({name:le[_e],path:Ae}))}}Q.sort(function(z,ne){return(z.path||"").split("/").length-(ne.path||"").split("/").length}),Q.length>0&&e.files.push.apply(e.files,Q);for(var Ve=[],Se=0;Se<J.data.length;Se++){var $e=J.data[Se],Ue=($e.path||"")+"/"+$e.name;!ee[Ue]&&$e.size!=null&&(Ve.push($e),ee[Ue]=!0)}Ve.length>0&&e.files.push.apply(e.files,Ve),e.fileSearchResults=J.data}},function(){!h&&v===G&&(v=null,e.fileSearchLoading=!1,e.fileSearchResults=[])})};let k={yml:"yaml",txt:"text",py:"python",js:"javascript",ts:"typescript"},m=["license","txt"],y=["png","jpg","jpeg","gif","svg","ico","bmp","tiff","tif","webp","avif","heif","heic"],E=["wav","mp3","ogg","wma","flac","aac","m4a"],w=["mp4","avi","webm","mov","mpg","mpeg","mkv","flv","wmv","3gp","3g2","m4v","f4v","f4p","f4a","f4b"];e.on("routeUpdate",function(O,G){if(e.repoId!=r.repoId)return C();if((r.path||"")!=e.filePath){e.filePath=r.path||"",e.paths=e.filePath.split("/").filter(J=>J&&J.trim().length>0),ie();for(let J=0;J<e.paths.length-1;J++){let ee=J>0?e.paths.slice(0,J).join("/"):"";e.files.some(oe=>oe.path===ee)||e.getFiles(ee)}}});function R(){if(e.paths[0]!="")return;let O=["readme.md","readme.txt","readme.org","readme.1st","readme"],G={};for(let ee of e.files)ee.name.toLowerCase().indexOf("readme")>-1&&(G[ee.name.toLowerCase()]=ee.name);let J=null;for(let ee of O)if(G[ee]){J=ee;break}if(!J&&Object.keys(G).length>0&&(J=Object.keys(G)[0]),J){let ee=s.url();ee[ee.length-1]!="/"&&(ee+="/"),s.url(ee+encodePathForUrl(G[J]))}}e.fileCounts=null,e.getFiles=function(O){let G=e.repoId;return t.get(`/api/repo/${e.repoId}/files/?path=${encodeURIComponent(O)}&v=${e.options.lastUpdateDate}`).then(function(J){if(h||G!==e.repoId)return[];let ee=O||"";return e.files=e.files.filter(Q=>Q.path!==ee),e.files.push(...J.data),J.data},function(J){if(h||G!==e.repoId)return[];e.type="error",e.content=J&&J.data&&J.data.error||"unknown_error",e.files=[]})};function D(){let O=e.repoId;t.get(`/api/repo/${e.repoId}/files/counts`).then(function(G){h||O!==e.repoId||(e.fileCounts=G.data)},function(){e.fileCounts={}})}function T(){return e.files.filter(O=>O.name==e.paths[e.paths.length-1]&&O.path==e.paths.slice(0,e.paths.length-1).join("/"))[0]}var P=null;e.on("dispose",function(){P&&a.interval.cancel(P)});function H(O){if(h)return;let G=e.repoId;t.get(`/api/repo/${e.repoId}/options`).then(J=>{if(!(h||G!==e.repoId)){if(e.options=J.data,e.options.url){window.location=e.options.url;return}O&&O(J.data)}},J=>{if(!(h||G!==e.repoId)){var ee=J.data||{};if(ee.error==="rate_limited"&&ee.resetAt){let oe=function(){var ue=Math.max(0,Math.ceil((e.rateLimitResetAt-Date.now())/1e3));if(ue<=0)e.rateLimitCountdown="",e.rateLimitResetAt=0,P&&(a.interval.cancel(P),P=null),H(O);else{var pe=Math.floor(ue/60),le=ue%60;e.rateLimitCountdown=pe>0?pe+"m "+le+"s":le+"s"}};var Q=oe;e.type="rate_limited",e.rateLimitResetAt=ee.resetAt,P&&a.interval.canc Line truncated
`+w.join(`
`)],{type:"text/csv"}),D=document.createElement("a");D.href=URL.createObjectURL(R),D.download=r.username+"-repositories.csv",D.click()},e.showStatusMessage=m=>{let y=m.statusMessage||"(no message)";window.prompt(`Status message for ${m.repoId} (${m.status}):`,y)},e.fetchGithubInfo=m=>{let y=window.open("","_blank");y&&y.document.write("<pre>Loading GitHub info for "+m.repoId+"...</pre>"),t.get("/api/admin/repos/"+m.repoId+"/github").then(E=>{y&&(y.document.open(),y.document.write('<pre style="font:13px monospace;padding:16px;white-space:pre-wrap">'+JSON.stringify(E.data,null,2).replace(/[<>]/g,w=>w==="<"?"&lt;":"&gt;")+"</pre>"),y.document.close())},E=>{let w=E&&E.data?JSON.stringify(E.data,null,2):String(E);y&&(y.document.body.innerHTML='<pre style="color:#B42318;padding:16px">'+w+"</pre>")})};function f(m){t.get("/api/admin/users/"+m+"/repos",{}).then(y=>{e.repositories=y.data},y=>{console.error(y)})}function h(m){t.get("/api/admin/users/"+m,{}).then(y=>{e.userInfo=y.data},y=>{console.error(y)})}h(r.username),f(r.username),e.banUser=()=>{confirm(`Ban user ${r.username}?`)&&t.post(`/api/admin/users/${r.username}/ban`).then(()=>h(r.username),m=>console.error(m))},e.activateUser=()=>{t.post(`/api/admin/users/${r.username}/activate`).then(()=>h(r.username),m=>console.error(m))},e.promoteUser=()=>{confirm(`Promote ${r.username} to admin?`)&&t.post(`/api/admin/users/${r.username}/promote`).then(()=>h(r.username),m=>console.error(m))},e.demoteUser=()=>{confirm(`Remove admin privileges from ${r.username}?`)&&t.post(`/api/admin/users/${r.username}/demote`).then(()=>h(r.username),m=>console.error(m))},e.tokens=[],e.tokenForm={name:"",plaintext:null};function v(){t.get("/api/admin/tokens").then(m=>{e.tokens=m.data||[]},m=>{m.status!==401&&m.status!==403&&console.error(m)})}v(),e.createToken=()=>{e.tokenForm.name&&t.post("/api/admin/tokens",{name:e.tokenForm.name}).then(m=>{e.tokenForm.plaintext=m.data.token,e.tokenForm.name="",v()},m=>console.error(m))},e.revokeToken=m=>{confirm(`Revoke token "${m.name}"?`)&&t.delete("/api/admin/tokens/"+m.id).then(()=>v(),y=>console.error(y))},e.removeCache=m=>{confirm("Remove cached files for "+m.repoId+"?")&&t.delete("/api/admin/repos/"+m.repoId).then(()=>f(r.username),y=>console.error(y))},e.removeRepository=m=>{confirm("Remove repository "+m.repoId+"?")&&t.delete("/api/repo/"+m.repoId+"/").then(()=>f(r.username),y=>console.error(y))},e.updateRepository=m=>{let y=He({title:`Refreshing ${m.repoId}...`,date:new Date,body:`The repository ${m.repoId} is going to be refreshed.`});e.toasts.push(y),t.post(`/api/repo/${m.repoId}/refresh`).then(E=>{E.data.status=="ready"?y.title=`${m.repoId} is refreshed.`:y.title=`Refreshing of ${m.repoId}.`},E=>{y.title=`Error during the refresh of ${m.repoId}.`,y.body=E.body})},e.getGitHubRepositories=m=>{t.get(`/api/user/${e.userInfo.username}/all_repositories`,{params:{force:"1"}}).then(y=>{e.userInfo.repositories=y.data})};let k=null;e.watch("query",()=>{n.timeout.cancel(k),k=n.timeout(()=>{f(r.username)},500)},!0)},tl=function(e,t,s){let r=ct(),n=Jt();e.Math=Math,e.watch("user.status",()=>{e.user==null&&s.url("/")}),e.user==null&&s.url("/"),e.conferences=[],e.total=-1,e.totalPage=0,e.statusCounts=[];let i=E=>{if(E.key==="/"&&!["INPUT","TEXTAREA","SELECT"].includes(document.activeElement?.tagName)){E.preventDefault();let w=document.querySelector('.admin-filter-toolbar input[type="search"]');w&&w.focus()}};n(document,"keydown",i),e.on("dispose",()=>document.removeEventListener("keydown",i)),e.clearFilter=E=>{E==="dateRange"?(e.query.dateFrom="",e.query.dateTo=""):e.query[E]="",e.query.page=1},e.chips=[];let a=()=>{let E=[];(e.query.dateFrom||e.query.dateTo)&&E.push({key:"dateRange",label:"Date",value:(e.query.dateFrom||"\u2026")+" \u2013 "+(e.query.dateTo||"\u2026")}),e.chips=E};e.statusCountFor=E=>{let w=(e.statusCounts||[]).find(R=>R._id===E);return w?w.count:0},e.toggleSortDirection=()=>{e.query.direction=e.query.direction==="asc"?"desc":"asc"},e.sortBy=E=>{e.query.sort===E?e.query.direction=e.query.direction==="asc"?"desc":"asc":(e.query.sort=E,e.query.direction="desc"),e.query.page=1},e.sortIcon=E=>e.query.sort===E?e.query.direction==="asc"?"fa-arrow-up":"fa-arrow-down":"";let u="admin.conferences.filterPrefs",f={page:1,limit:25,sort:"name",direction:"asc",search:"",dateFrom:"",dateTo:"",ready:!1,expired:!1,removed:!1,error:!0,preparing:!0},h=loadFilterPrefs(u)||{};e.query=Object.assign({},f,h,{page:1,search:""});let v=s.search();v.search&&(e.query.search=v.search);let k="admin.conferences.presets";e.presets=JSON.parse(localStorage.getItem(k)||"[]"),e.savePreset=()=>{let E=window.prompt("Preset name:");if(!E)return;let w=Object.assign({},e.query);delete w.page,e.presets=(e.presets||[]).filter(R=>R.name!==E),e.presets.push({name:E,query:w}),localStorage.setItem(k,JSON.stringify(e.presets))},e.applyPreset=E=>{Object.assign(e.query,E.query,{page:1})},e.deletePreset=E=>{e.presets=(e.presets||[]).filter(w=>w.name!==E.name),localStorage.setItem(k,JS Line truncated
`).map((ue,pe)=>pe===0?ue:oe+ue).join(`
`)}else typeof C=="number"||typeof C=="boolean"?Q=String(C):Q=JSON.stringify(C);ie.push(`${J}${Q}${ee}`)}),ie.push("}"),ie.join(`
`)}function m(D){let T=[],P="",H=/(\w+):(>=|<=|!=|>|<|=)?([^\s]+)/g,q=0,U;for(;U=H.exec(D);)P+=D.slice(q,U.index),q=H.lastIndex,T.push({key:U[1],op:U[2]||"=",val:U[3]});return P+=D.slice(q),{filters:T,free:P.trim().toLowerCase()}}function g(D,T){for(let P of T.filters){let H=(U,ie,re)=>{let C=parseFloat(U),O=parseFloat(ie);return re==="="?String(U)===String(ie):re==="!="?String(U)!==String(ie):re===">="?C>=O:re==="<="?C<=O:re===">"?C>O:re==="<"?C<O:!0},q;if(P.key==="code")q=D.displayMessage;else if(P.key==="module")q=D.module;else if(P.key==="status")q=D._status;else if(P.key==="url")q=D._url;else if(P.key==="repo")q=D._repoId;else if(P.key==="level")q=D.level;else continue;if(q==null||!H(q,P.val,P.op))return!1}return!(T.free&&((D.displayMessage||"")+" "+(D.module||"")+" "+(D._url||"")+" "+JSON.stringify(D.raw||[])).toLowerCase().indexOf(T.free)===-1)}function w(){let D=m(e.query.search||"");e.parsedFilterCount=D.filters.length;let T=e.query.bucket,P=e.entries.filter(q=>T&&q._bucket!==T?!1:g(q,D)),H=e.query.group;if(H){let q=re=>H==="module"?re.module:re.displayMessage||re.message||"_",U=new Map;for(let re of P){let C=q(re);if(U.has(C)){let O=U.get(C);O.count++,O._related.push(re),new Date(re.ts)>new Date(O.ts)&&(O.ts=re.ts,O._url=re._url,O._status=re._status),new Date(re.ts)<new Date(O._firstSeen)&&(O._firstSeen=re.ts)}else{let O=Object.assign({},re);O._key=`${H}:${C}`,O._related=[re],O._firstSeen=re.ts,O._lastHourCount=0,O.count=1,U.set(C,O)}}let ie=Date.now()-3600*1e3;for(let re of U.values())re._lastHourCount=re._related.filter(C=>new Date(C.ts).getTime()>=ie).length;P=Array.from(U.values())}else P=P.map((q,U)=>(q._key="row:"+U+":"+q.ts,q._related=[q],q._firstSeen=q.ts,q._lastHourCount=0,q.count=1,q));e.query.sort==="count"?P.sort((q,U)=>U.count-q.count||new Date(U.ts)-new Date(q.ts)):P.sort((q,U)=>new Date(U.ts)-new Date(q.ts)),e.visible=P}function h(D){let T=D?e.entries.length:0,P=D?e.pageSize:Math.max(e.pageSize,e.entries.length||e.pageSize);t.get("/api/admin/errors",{params:{offset:T,limit:P}}).then(H=>{let q=(H.data.entries||[]).map(u);e.entries=D?e.entries.concat(q):q,e.available=!!H.data.available,e.cap=H.data.max||e.cap,e.total=H.data.total||e.entries.length,w()},H=>console.error(H))}e.loadMore=()=>h(!0),e.canLoadMore=()=>e.entries.length<e.total;function b(){t.get("/api/admin/errors/stats").then(D=>{let T=D.data||{},P=T.prev24h?Math.round((T.last24h-T.prev24h)/T.prev24h*100):0;e.stats={last24h:T.last24h||0,prev24h:T.prev24h||0,delta:P,severity:T.severity||{error:0,warn:0,info:0},unique:T.unique||{error:0,warn:0,info:0},buckets:T.buckets||[],dropped:T.dropped||0}},D=>console.error(D))}function E(){h(),b()}e.barPx=(D,T)=>{let P=e.stats.buckets||[],H=0;for(let re of P)H=Math.max(H,(re.error||0)+(re.warn||0)+(re.info||0));if(!H)return 0;let q=(D.error||0)+(D.warn||0)+(D.info||0);if(!q)return 0;let U=Math.round(q/H*60),ie=D[T]||0;return Math.round(ie/q*U)},e.bucketTitle=D=>`${new Date(D.hour).toLocaleTimeString([],{hour:"2-digit",minute:"2-digit"})} \xB7 ${D.error||0} err \xB7 ${D.warn||0} warn \xB7 ${D.info||0} info`,e.toggle=D=>{e.expanded[D._key]=!e.expanded[D._key]},e.setBucket=D=>{e.query.bucket=D},e.refreshNow=E,e.clearAll=()=>{confirm("Clear all captured errors?")&&t.delete("/api/admin/errors").then(E,D=>console.error(D))},e.exportCsv=()=>{let D=["ts","level","module","displayMessage","_status","_url","_repoId"],T=[D.join(",")];for(let U of e.visible)T.push(D.map(ie=>{let re=U[ie]==null?"":String(U[ie]);return/[",\n]/.test(re)?`"${re.replace(/"/g,'""')}"`:re}).join(","));let P=new Blob([T.join(`
`)],{type:"text/csv;charset=utf-8"}),H=URL.createObjectURL(P),q=document.createElement("a");q.href=H,q.download=`errors-${new Date().toISOString().slice(0,19)}.csv`,document.body.appendChild(q),q.click(),document.body.removeChild(q),URL.revokeObjectURL(H)};function k(D){e.copyHint=`${D} copied`,s.timeout(()=>{e.copyHint=""},1500)}e.copyJson=D=>{navigator.clipboard.writeText(D._detailJson).then(()=>k("JSON"))},e.copyCurl=D=>{if(!D._url)return;let P=`curl -X ${D._method||"GET"} '${window.location.origin}${D._url}'`;navigator.clipboard.writeText(P).then(()=>k("curl"))},E();let R=r(()=>{e.query.autoRefresh&&E()},15e3);e.on("dispose",()=>r.cancel(R)),e.watch("query.search",w),e.watch("query.bucket",w),e.watch("query.sort",w),e.watch("query.group",w)},el=function(e,t,n,r){if(e.Math=Math,e.watch("user.status",()=>{e.user==null&&n.url("/")}),e.user==null){n.url("/");return}e.data=null,e.loading=!0,e.error=null;function s(h){if(h==null)return"\u2014";for(var b=["B","KB","MB","GB","TB"],E=0,k=h;k>=1024&&E<b.length-1;)k/=1024,E++;return k.toFixed(E>0?1:0)+" "+b[E]}e.humanBytes=s;function i(h){if(!h)return"\u2014";var b=Math.floor(h/86400),E=Math.floor(h%86400/3600),k=Math.floor(h%3600/60);return b>0?b+"d "+(E<10?"0":"")+E+"h":E>0?E+"h "+(k<10?"0":"")+k+"m":k+"m"}e.humanDuration=i;function a(h){return h==null?"\u2014":h>=1e6?(h/1e6).toFixed(1)+"M":h>=1e3?(h/1e3).toFixed(1)+"K":String(h)}e.humanNum=a,e.queueTotal=function(h){return h?(h.waiting||0)+(h.active||0)+(h.delayed||0)+(h.failed||0):0},e.statusCount=function(h){if(!e.data||!e.data.repos)return 0;for(var b=e.data.repos.statusBreakdown||[],E=0;E<b.length;E++)if(b[E]._id===h)return b[E].count;return 0},e.barPct=function(h){var b=e.data&&e.data.repos?e.data.repos.total:0;if(!b)return 0;var E=[h];h==="expired"&&E.push("expiring"),h==="removed"&&E.push("removing"),h==="preparing"&&E.push("download");var k=0;return E.forEach(function(R){k+=e.statusCount(R)}),Math.max(.4,k/b*100)},e.errPct=function(h){if(!e.data||!e.data.errors)return 0;var b=Math.max(e.data.errors.severity.error,e.data.errors.severity.warn,e.data.errors.severity.info,1);return e.data.errors.severity[h]/b*100};function u(h){var b=h||[];return b.map(function(E,k){var R=b[k-1]||{},D=Object.assign({},E);return D.dailyRepositories=k?Math.max(0,(E.nbRepositories||0)-(R.nbRepositories||0)):0,D.dailyUsers=k?Math.max(0,(E.nbUsers||0)-(R.nbUsers||0)):0,D.dailyPageViews=k?Math.max(0,(E.nbPageViews||0)-(R.nbPageViews||0)):0,D})}function f(h){var b=h&&h.length?h[h.length-1]:{};return{repositories:b.dailyRepositories||0,users:b.dailyUsers||0,pageViews:b.dailyPageViews||0}}var m={};e.historyBarH=function(h,b){return!h||!m[b]?0:Math.max(1,Math.round(h[b]/m[b]*140))},e.historyLabel=function(h){if(!h||!h.date)return"";var b=new Date(h.date);return b.getUTCMonth()+1+"/"+b.getUTCDate()};function g(){t.get("/api/admin/overview").then(function(h){h.data.history=u(h.data.history),h.data.daily={today:f(h.data.history)},e.data=h.data,e.loading=!1,e.error=null,m={},(h.data.history||[]).forEach(function(b){["dailyPageViews","dailyRepositories","dailyUsers","nbUsers"].forEach(function(E){(!m[E]||b[E]>m[E])&&(m[E]=b[E])})})},function(h){e.loading=!1,e.error=h.data&&h.data.error||"Failed to load overview"})}g();var w=r(g,3e4);e.on("dispose",function(){r.cancel(w)})};var tl=[{path:"/connections",template:"partials/connections.htm",title:"GitHub connections \u2013 Anonymous GitHub",preserveExplorer:!1,setup:(e,t)=>Wa(e,t.http)},{path:"/signin",template:"partials/signin.htm",title:"Sign in \u2013 Anonymous GitHub",preserveExplorer:!1,setup:(e,t)=>Ka(e,t.http)},{path:"/",template:"partials/home.htm",title:"Anonymous GitHub \u2013 Share the code, not the author",preserveExplorer:!1,setup:(e,t)=>Ua(e,t.http,t.location,t.window,t.timeout)},{path:"/dashboard",template:"partials/dashboard.htm",title:"Your anonymizations \u2013 Anonymous GitHub",preserveExplorer:!1,setup:(e,t)=>Fa(e,t.http,t.location,t.promises,t.window,t.quotaService)},{path:"/pr-dashboard",redirect:"/dashboard"},{path:"/anonymize/:repoId?",template:"partials/anonymize.htm",title:"New anonymization \u2013 Anonymous GitHub",preserveExplorer:!1,setup:(e,t)=>en(e,t.http,t.html,t.params,t.location,t.translate,t.timeout)},{path:"/pull-request-anonymize/:pullRequestId?",template:"partials/anonymize.htm",title:"Anonymize a pull request \u2013 Anonymous GitHub",preserveExplorer:!1,setup:(e,t)=>en(e,t.http,t.html,t.params,t.location,t.translate,t.timeout)},{path:"/gist-anonymize/:gistId?",template:"partials/anonymize.htm",title:"Anonymize a gist \u2013 Anonymous GitHub",preserveExplorer:!1,setup:(e,t)=>en(e,t.http,t.html,t.params,t.location,t.translate,t.timeout)},{path:"/status/:repoId",template:"partials/status.htm",title:"Repository status \u2013 Anonymous GitHub",preserveExplorer:!1,setup:(e,t)=>za(e,t.http,t.params)},{path:"/conferences",template:"partials/conferences.htm",title:"Your conferences \u2013 Anonymous GitHub",preserveExplorer:!1,setup:(e,t)=>Ba(e,t.http,t.location) Line truncated
anonymous_github`)])])])])]),p(" Limitations "),o("h4",{class:"faq-cat"},[o("svg",{class:"ui-icon mr-2","aria-hidden":"true"},[o("use",{href:"#ui-icon-warning"})]),y("Limitations ")]),o("div",{class:"panel-group",id:"faq-limitations",role:"tablist","aria-multiselectable":"true"},[o("div",{class:"panel panel-default mb-3"},[o("div",{class:"panel-heading p-3",role:"tab",id:"headingLimitation"},[o("h3",{class:"panel-title"},[o("a",{class:"collapsed",role:"button","data-toggle":"collapse","data-parent":"#faq-limitations",href:"#limitation","aria-expanded":"false","aria-controls":"limitation"}," What are the limitations of Anonymous GitHub? ")])]),o("div",{id:"limitation",class:"panel-collapse collapse",role:"tabpanel","aria-labelledby":"headingLimitation"},[o("div",{class:"panel-body p-3"},[o("ul",null,[o("li",null," Anonymous GitHub only anonymizes text-based files. Binary files (compiled executables, archives, etc.) are served as-is without anonymization. "),o("li",null," Files larger than 8 MB are not supported. "),o("li",null," Static site generators (such as Jekyll) used with GitHub Pages are not fully supported, although Markdown files are converted to HTML. "),o("li",null," The anonymization of terms within source code may change the behavior of the program (e.g., if a replaced term appears in a string literal or identifier). ")])])])])]),p(" Privacy & Security "),o("h4",{class:"faq-cat"},[o("svg",{class:"ui-icon mr-2","aria-hidden":"true"},[o("use",{href:"#ui-icon-shield"})]),y("Privacy & Security ")]),o("div",{class:"panel-group",id:"faq-privacy",role:"tablist","aria-multiselectable":"true"},[o("div",{class:"panel panel-default mb-3"},[o("div",{class:"panel-heading p-3",role:"tab",id:"headingData"},[o("h3",{class:"panel-title"},[o("a",{class:"collapsed",role:"button","data-toggle":"collapse","data-parent":"#faq-privacy",href:"#data","aria-expanded":"false","aria-controls":"data"}," How is my data handled? ")])]),o("div",{id:"data",class:"panel-collapse collapse",role:"tabpanel","aria-labelledby":"headingData"},[o("div",{class:"panel-body p-3"},[o("p",null," Data stored on Anonymous GitHub is never shared or used for any purpose beyond providing the anonymization service. When a repository is removed or expires, only its configuration is retained \u2014 this makes it easy to restore the repository if needed and ensures that no future repository reuses the same ID. ")])])]),o("div",{class:"panel panel-default mb-3"},[o("div",{class:"panel-heading p-3",role:"tab",id:"headingPermissions"},[o("h3",{class:"panel-title"},[o("a",{class:"collapsed",role:"button","data-toggle":"collapse","data-parent":"#faq-privacy",href:"#permissions","aria-expanded":"false","aria-controls":"permissions"}," Why does GitHub say Anonymous GitHub asks for write access? ")])]),o("div",{id:"permissions",class:"panel-collapse collapse",role:"tabpanel","aria-labelledby":"headingPermissions"},[o("div",{class:"panel-body p-3"},[o("p",null,[y("The GitHub App has read-only access to the repositories you choose. You can change repository access on your "),o("a",{href:"/connections"},"GitHub connections"),y(" page.")]),o("p",null,"Legacy GitHub OAuth includes write access to private repositories because GitHub's OAuth scopes do not offer a read-only option for them. Existing OAuth anonymizations and gists can still use this connection."),o("p",null,"Anonymous GitHub reads repository content. It never pushes, modifies, or deletes files on GitHub.")])])]),o("div",{class:"panel panel-default mb-3"},[o("div",{class:"panel-heading p-3",role:"tab",id:"headingViewer"},[o("h3",{class:"panel-title"},[o("a",{class:"collapsed",role:"button","data-toggle":"collapse","data-parent":"#faq-privacy",href:"#viewer","aria-expanded":"false","aria-controls":"viewer"}," Can repository owners see who viewed their anonymized repository? ")])]),o("div",{id:"viewer",class:"panel-collapse collapse",role:"tabpanel","aria-labelledby":"headingViewer"},[o("div",{class:"panel-body p-3"},[o("p",null," No. Only the total number of views is tracked as an incremental counter. It is not possible for the repository owner or for Anonymous GitHub to identify individual viewers. Reviewer anonymity is fully preserved. ")])])])]),p(" Self-Hosting "),o("h4",{class:"faq-cat"},[o("svg",{class:"ui-icon mr-2","aria-hidden":"true"},[o("use",{href:"#project-icon-storage"})]),y("Self-Hosting ")]),o("div",{class:"panel-group",id:"faq-hosting",role:"tablist","aria-multiselectable":"true"},[o("div",{class:"panel panel-default mb-3"},[o("div",{class:"panel-heading p-3",role:"tab",id:"headingDeploy"},[o("h3",{class:"panel-title"},[o("a",{class:"collapsed",role:"button","data-toggle":"collapse","data-parent":"#faq-hosting",href:"#deploy","aria-expanded":"false","aria-controls":"deploy"}," Can I deploy my own instance? ")])]),o("div",{id:"deploy",class:"panel-collapse collapse",role:"tabpanel","aria-labelledby":"headingDeploy"},[o("div",{class:"panel-body p-3"},[o("p",null,[y(" Yes. Anonymous Git Line truncated
`)}else typeof C=="number"||typeof C=="boolean"?Q=String(C):Q=JSON.stringify(C);ae.push(`${J}${Q}${ee}`)}),ae.push("}"),ae.join(`
`)}function h(D){let T=[],P="",H=/(\w+):(>=|<=|!=|>|<|=)?([^\s]+)/g,q=0,U;for(;U=H.exec(D);)P+=D.slice(q,U.index),q=H.lastIndex,T.push({key:U[1],op:U[2]||"=",val:U[3]});return P+=D.slice(q),{filters:T,free:P.trim().toLowerCase()}}function v(D,T){for(let P of T.filters){let H=(U,ae,ie)=>{let C=parseFloat(U),O=parseFloat(ae);return ie==="="?String(U)===String(ae):ie==="!="?String(U)!==String(ae):ie===">="?C>=O:ie==="<="?C<=O:ie===">"?C>O:ie==="<"?C<O:!0},q;if(P.key==="code")q=D.displayMessage;else if(P.key==="module")q=D.module;else if(P.key==="status")q=D._status;else if(P.key==="url")q=D._url;else if(P.key==="repo")q=D._repoId;else if(P.key==="level")q=D.level;else continue;if(q==null||!H(q,P.val,P.op))return!1}return!(T.free&&((D.displayMessage||"")+" "+(D.module||"")+" "+(D._url||"")+" "+JSON.stringify(D.raw||[])).toLowerCase().indexOf(T.free)===-1)}function k(){let D=h(e.query.search||"");e.parsedFilterCount=D.filters.length;let T=e.query.bucket,P=e.entries.filter(q=>T&&q._bucket!==T?!1:v(q,D)),H=e.query.group;if(H){let q=ie=>H==="module"?ie.module:ie.displayMessage||ie.message||"_",U=new Map;for(let ie of P){let C=q(ie);if(U.has(C)){let O=U.get(C);O.count++,O._related.push(ie),new Date(ie.ts)>new Date(O.ts)&&(O.ts=ie.ts,O._url=ie._url,O._status=ie._status),new Date(ie.ts)<new Date(O._firstSeen)&&(O._firstSeen=ie.ts)}else{let O=Object.assign({},ie);O._key=`${H}:${C}`,O._related=[ie],O._firstSeen=ie.ts,O._lastHourCount=0,O.count=1,U.set(C,O)}}let ae=Date.now()-3600*1e3;for(let ie of U.values())ie._lastHourCount=ie._related.filter(C=>new Date(C.ts).getTime()>=ae).length;P=Array.from(U.values())}else P=P.map((q,U)=>(q._key="row:"+U+":"+q.ts,q._related=[q],q._firstSeen=q.ts,q._lastHourCount=0,q.count=1,q));e.query.sort==="count"?P.sort((q,U)=>U.count-q.count||new Date(U.ts)-new Date(q.ts)):P.sort((q,U)=>new Date(U.ts)-new Date(q.ts)),e.visible=P}function m(D){let T=D?e.entries.length:0,P=D?e.pageSize:Math.max(e.pageSize,e.entries.length||e.pageSize);t.get("/api/admin/errors",{params:{offset:T,limit:P}}).then(H=>{let q=(H.data.entries||[]).map(u);e.entries=D?e.entries.concat(q):q,e.available=!!H.data.available,e.cap=H.data.max||e.cap,e.total=H.data.total||e.entries.length,k()},H=>console.error(H))}e.loadMore=()=>m(!0),e.canLoadMore=()=>e.entries.length<e.total;function y(){t.get("/api/admin/errors/stats").then(D=>{let T=D.data||{},P=T.prev24h?Math.round((T.last24h-T.prev24h)/T.prev24h*100):0;e.stats={last24h:T.last24h||0,prev24h:T.prev24h||0,delta:P,severity:T.severity||{error:0,warn:0,info:0},unique:T.unique||{error:0,warn:0,info:0},buckets:T.buckets||[],dropped:T.dropped||0}},D=>console.error(D))}function E(){m(),y()}e.barPx=(D,T)=>{let P=e.stats.buckets||[],H=0;for(let ie of P)H=Math.max(H,(ie.error||0)+(ie.warn||0)+(ie.info||0));if(!H)return 0;let q=(D.error||0)+(D.warn||0)+(D.info||0);if(!q)return 0;let U=Math.round(q/H*60),ae=D[T]||0;return Math.round(ae/q*U)},e.bucketTitle=D=>`${new Date(D.hour).toLocaleTimeString([],{hour:"2-digit",minute:"2-digit"})} \xB7 ${D.error||0} err \xB7 ${D.warn||0} warn \xB7 ${D.info||0} info`,e.toggle=D=>{e.expanded[D._key]=!e.expanded[D._key]},e.setBucket=D=>{e.query.bucket=D},e.refreshNow=E,e.clearAll=()=>{confirm("Clear all captured errors?")&&t.delete("/api/admin/errors").then(E,D=>console.error(D))},e.exportCsv=()=>{let D=["ts","level","module","displayMessage","_status","_url","_repoId"],T=[D.join(",")];for(let U of e.visible)T.push(D.map(ae=>{let ie=U[ae]==null?"":String(U[ae]);return/[",\n]/.test(ie)?`"${ie.replace(/"/g,'""')}"`:ie}).join(","));let P=new Blob([T.join(`
`)],{type:"text/csv;charset=utf-8"}),H=URL.createObjectURL(P),q=document.createElement("a");q.href=H,q.download=`errors-${new Date().toISOString().slice(0,19)}.csv`,document.body.appendChild(q),q.click(),document.body.removeChild(q),URL.revokeObjectURL(H)};function w(D){e.copyHint=`${D} copied`,n.timeout(()=>{e.copyHint=""},1500)}e.copyJson=D=>{navigator.clipboard.writeText(D._detailJson).then(()=>w("JSON"))},e.copyCurl=D=>{if(!D._url)return;let P=`curl -X ${D._method||"GET"} '${window.location.origin}${D._url}'`;navigator.clipboard.writeText(P).then(()=>w("curl"))},E();let R=r(()=>{e.query.autoRefresh&&E()},15e3);e.on("dispose",()=>r.cancel(R)),e.watch("query.search",k),e.watch("query.bucket",k),e.watch("query.sort",k),e.watch("query.group",k)},nl=function(e,t,s,r){if(e.Math=Math,e.watch("user.status",()=>{e.user==null&&s.url("/")}),e.user==null){s.url("/");return}e.data=null,e.loading=!0,e.error=null,e.performance=null,e.performanceMinutes=15,e.performanceError=null,e.latencyBound=function(y,E){return y.count?y[E]==null?"> 60 s":"\u2264 "+y[E]+" ms":"No samples"},e.firstByteBound=function(y){return e.latencyBound({count:y.firstByteCount,p95UpperMs:y.firstByteP95UpperMs},"p95UpperMs")},e.sampleAge=function(y){return Math.max(0,Math.round((Date.now()-y.sampledAt)/1e3))},e.peakRss=function(y){return(e.performance.runtimeSeries||[]).filter(function(E){return E.instance===y.instance}).reduce(function(E,w){return Math.max(E,w.rss)},y.memory.rss)},e.performanceWarning=function(y){return e.sampleAge(y)>45||y.droppedBatches||y.droppedMetrics||y.sockets?.closeWait>0||y.eventLoop.p95Ms>100||y.memoryLimitBytes&&y.memory.rss>y.memoryLimitBytes*.8};var n=!1;e.loadPerformance=function(){var y=e.performanceMinutes;e.performance?.windowMinutes&&e.performance.windowMinutes!==y&&(e.performance=null),!n&&(n=!0,t.get("/api/admin/performance",{params:{minutes:y}}).then(function(E){y===e.performanceMinutes&&(e.performance=E.data,e.performanceError=E.data.available?null:"Performance monitoring is unavailable. Request history may be incomplete.")},function(){y===e.performanceMinutes&&(e.performanceError="Could not refresh performance monitoring. Displayed samples may be stale.")}).finally(function(){n=!1,y!==e.performanceMinutes&&e.loadPerformance()}))};function i(y){if(y==null)return"\u2014";for(var E=["B","KB","MB","GB","TB"],w=0,R=y;R>=1024&&w<E.length-1;)R/=1024,w++;return R.toFixed(w>0?1:0)+" "+E[w]}e.humanBytes=i;function a(y){if(!y)return"\u2014";var E=Math.floor(y/86400),w=Math.floor(y%86400/3600),R=Math.floor(y%3600/60);return E>0?E+"d "+(w<10?"0":"")+w+"h":w>0?w+"h "+(R<10?"0":"")+R+"m":R+"m"}e.humanDuration=a;function u(y){return y==null?"\u2014":y>=1e6?(y/1e6).toFixed(1)+"M":y>=1e3?(y/1e3).toFixed(1)+"K":String(y)}e.humanNum=u,e.queueTotal=function(y){return y?(y.waiting||0)+(y.active||0)+(y.delayed||0)+(y.failed||0):0},e.statusCount=function(y){if(!e.data||!e.data.repos)return 0;for(var E=e.data.repos.statusBreakdown||[],w=0;w<E.length;w++)if(E[w]._id===y)return E[w].count;return 0},e.barPct=function(y){var E=e.data&&e.data.repos?e.data.repos.total:0;if(!E)return 0;var w=[y];y==="expired"&&w.push("expiring"),y==="removed"&&w.push("removing"),y==="preparing"&&w.push("download");var R=0;return w.forEach(function(D){R+=e.statusCount(D)}),Math.max(.4,R/E*100)},e.errPct=function(y){if(!e.data||!e.data.errors)return 0;var E=Math.max(e.data.errors.severity.error,e.data.errors.severity.warn,e.data.errors.severity.info,1);return e.data.errors.severity[y]/E*100};function f(y){var E=y||[];return E.map(function(w,R){var D=E[R-1]||{},T=Object.assign({},w);return T.dailyRepositories=R?Math.max(0,(w.nbRepositories||0)-(D.nbRepositories||0)):0,T.dailyUsers=R?Math.max(0,(w.nbUsers||0)-(D.nbUsers||0)):0,T.dailyPageViews=R?Math.max(0,(w.nbPageViews||0)-(D.nbPageViews||0)):0,T})}function h(y){var E=y&&y.length?y[y.length-1]:{};return{repositories:E.dailyRepositories||0,users:E.dailyUsers||0,pageViews:E.dailyPageViews||0}}var v={};e.historyBarH=function(y,E){return!y||!v[E]?0:Math.max(1,Math.round(y[E]/v[E]*140))},e.historyLabel=function(y){if(!y||!y.date)return"";var E=new Date(y.date);return E.getUTCMonth()+1+"/"+E.getUTCDate()};function k(){e.loadPerformance(),t.get("/api/admin/overview").then(function(y){y.data.history=f(y.data.history),y.data.daily={today:h(y.data.history)},e.data=y.data,e.loading=!1,e.error=null,v={},(y.data.history||[]).forEach(function(E){["dailyPageViews","dailyRepositories","dailyUsers","nbUsers"].forEach(function(w){(!v[w]||E[w]>v[w])&&(v[w]=E[w])})})},function(y){e.loading=!1,e.error=y.data&&y.data.error||"Failed to load overview"})}k();var m=r(k,3e4);e.on("dispose",function(){r.cancel(m)})};var rl=[{path:"/connections",template:"partials/connections.htm",title:"GitHub connections \u2013 Anonymous GitHub",preserveExplorer:!1,setup:(e,t)=>Qa(e,t.http)},{path:"/signin",template:"partials/signin.htm",title:"Sign in \u2013 Anonymous GitHub",preserveExplorer:!1,setup:(e,t)=>Xa(e,t.http)},{path:"/",template:"parti Line truncated
anonymous_github`)])])])])]),p(" Limitations "),o("h4",{class:"faq-cat"},[o("svg",{class:"ui-icon mr-2","aria-hidden":"true"},[o("use",{href:"#ui-icon-warning"})]),b("Limitations ")]),o("div",{class:"panel-group",id:"faq-limitations",role:"tablist","aria-multiselectable":"true"},[o("div",{class:"panel panel-default mb-3"},[o("div",{class:"panel-heading p-3",role:"tab",id:"headingLimitation"},[o("h3",{class:"panel-title"},[o("a",{class:"collapsed",role:"button","data-toggle":"collapse","data-parent":"#faq-limitations",href:"#limitation","aria-expanded":"false","aria-controls":"limitation"}," What are the limitations of Anonymous GitHub? ")])]),o("div",{id:"limitation",class:"panel-collapse collapse",role:"tabpanel","aria-labelledby":"headingLimitation"},[o("div",{class:"panel-body p-3"},[o("ul",null,[o("li",null," Anonymous GitHub only anonymizes text-based files. Binary files (compiled executables, archives, etc.) are served as-is without anonymization. "),o("li",null," Files larger than 8 MB are not supported. "),o("li",null," Static site generators (such as Jekyll) used with GitHub Pages are not fully supported, although Markdown files are converted to HTML. "),o("li",null," The anonymization of terms within source code may change the behavior of the program (e.g., if a replaced term appears in a string literal or identifier). ")])])])])]),p(" Privacy & Security "),o("h4",{class:"faq-cat"},[o("svg",{class:"ui-icon mr-2","aria-hidden":"true"},[o("use",{href:"#ui-icon-shield"})]),b("Privacy & Security ")]),o("div",{class:"panel-group",id:"faq-privacy",role:"tablist","aria-multiselectable":"true"},[o("div",{class:"panel panel-default mb-3"},[o("div",{class:"panel-heading p-3",role:"tab",id:"headingData"},[o("h3",{class:"panel-title"},[o("a",{class:"collapsed",role:"button","data-toggle":"collapse","data-parent":"#faq-privacy",href:"#data","aria-expanded":"false","aria-controls":"data"}," How is my data handled? ")])]),o("div",{id:"data",class:"panel-collapse collapse",role:"tabpanel","aria-labelledby":"headingData"},[o("div",{class:"panel-body p-3"},[o("p",null," Data stored on Anonymous GitHub is never shared or used for any purpose beyond providing the anonymization service. When a repository is removed or expires, only its configuration is retained \u2014 this makes it easy to restore the repository if needed and ensures that no future repository reuses the same ID. ")])])]),o("div",{class:"panel panel-default mb-3"},[o("div",{class:"panel-heading p-3",role:"tab",id:"headingPermissions"},[o("h3",{class:"panel-title"},[o("a",{class:"collapsed",role:"button","data-toggle":"collapse","data-parent":"#faq-privacy",href:"#permissions","aria-expanded":"false","aria-controls":"permissions"}," Why does GitHub say Anonymous GitHub asks for write access? ")])]),o("div",{id:"permissions",class:"panel-collapse collapse",role:"tabpanel","aria-labelledby":"headingPermissions"},[o("div",{class:"panel-body p-3"},[o("p",null,[b("The GitHub App has read-only access to the repositories you choose. You can change repository access on your "),o("a",{href:"/connections"},"GitHub connections"),b(" page.")]),o("p",null,"Legacy GitHub OAuth includes write access to private repositories because GitHub's OAuth scopes do not offer a read-only option for them. Existing OAuth anonymizations and gists can still use this connection."),o("p",null,"Anonymous GitHub reads repository content. It never pushes, modifies, or deletes files on GitHub.")])])]),o("div",{class:"panel panel-default mb-3"},[o("div",{class:"panel-heading p-3",role:"tab",id:"headingViewer"},[o("h3",{class:"panel-title"},[o("a",{class:"collapsed",role:"button","data-toggle":"collapse","data-parent":"#faq-privacy",href:"#viewer","aria-expanded":"false","aria-controls":"viewer"}," Can repository owners see who viewed their anonymized repository? ")])]),o("div",{id:"viewer",class:"panel-collapse collapse",role:"tabpanel","aria-labelledby":"headingViewer"},[o("div",{class:"panel-body p-3"},[o("p",null," No. Only the total number of views is tracked as an incremental counter. It is not possible for the repository owner or for Anonymous GitHub to identify individual viewers. Reviewer anonymity is fully preserved. ")])])])]),p(" Self-Hosting "),o("h4",{class:"faq-cat"},[o("svg",{class:"ui-icon mr-2","aria-hidden":"true"},[o("use",{href:"#project-icon-storage"})]),b("Self-Hosting ")]),o("div",{class:"panel-group",id:"faq-hosting",role:"tablist","aria-multiselectable":"true"},[o("div",{class:"panel panel-default mb-3"},[o("div",{class:"panel-heading p-3",role:"tab",id:"headingDeploy"},[o("h3",{class:"panel-title"},[o("a",{class:"collapsed",role:"button","data-toggle":"collapse","data-parent":"#faq-hosting",href:"#deploy","aria-expanded":"false","aria-controls":"deploy"}," Can I deploy my own instance? ")])]),o("div",{id:"deploy",class:"panel-collapse collapse",role:"tabpanel","aria-labelledby":"headingDeploy"},[o("div",{class:"panel-body p-3"},[o("p",null,[b(" Yes. Anonymous Git Line truncated
anonymized repository often think it is about the paper, so the
label names the service and the tooltip says who to contact. `),t[12]||(t[12]=o("li",{class:"nav-item d-none d-lg-block"},[o("a",{class:"nav-link",target:"_blank",rel:"noopener",href:"https://github.com/tdurieux/anonymous_github/issues/new?template=issue_report.yml",title:"Report a problem with the Anonymous GitHub service. Questions about an anonymized repository or paper go to its authors.","data-offset":"30"},[o("svg",{class:"ui-icon d-lg-none mr-1","aria-hidden":"true"},[o("use",{href:"#ui-icon-bug"})]),y("Report a bug ")])],-1)),e.isDarkMode?p("v-if",!0):(l(),d("li",e4,[o("a",{class:"nav-link nav-icon",href:"#",title:"Switch to dark mode","aria-label":"Switch to dark mode",role:"button",onClick:t[0]||(t[0]=he(n=>{e.darkMode(!0)},["prevent"]))},[...t[7]||(t[7]=[o("svg",{class:"ui-icon","aria-hidden":"true"},[o("use",{href:"#ui-icon-moon"})],-1)])])])),e.isDarkMode?(l(),d("li",t4,[o("a",{class:"nav-link nav-icon",href:"#",title:"Switch to light mode","aria-label":"Switch to light mode",role:"button",onClick:t[1]||(t[1]=he(n=>{e.darkMode(!1)},["prevent"]))},[...t[8]||(t[8]=[o("svg",{class:"ui-icon","aria-hidden":"true"},[o("use",{href:"#ui-icon-sun"})],-1)])])])):p("v-if",!0),e.user?p("v-if",!0):(l(),d("li",o4,[...t[9]||(t[9]=[o("a",{class:"nav-link btn-signin",target:"_self",href:"/signin","data-offset":"30"},[o("i",{class:"fab fa-github mr-1"}),y(" Sign in ")],-1)])])),e.user?(l(),d("li",s4,[o("a",n4,[e.user?.photo?(l(),d("img",{key:0,width:"22",height:"22",class:"rounded-circle",src:e.safeUrl(e.user?.photo)},null,8,r4)):p("v-if",!0),o("span",i4,c(e.user?.username),1)]),o("div",a4,[o("h6",l4,"Signed in as @"+c(e.user?.username),1),t[10]||(t[10]=ce('<a class="dropdown-item" href="/profile"><svg class="ui-icon" aria-hidden="true"><use href="#project-icon-filter"></use></svg><span>Settings<small>Defaults, quotas, account</small></span></a><a class="dropdown-item" href="/connections"><i class="fab fa-github" aria-hidden="true"></i><span>GitHub connections<small>Repository access and OAuth</small></span></a><a class="dropdown-item" href="/claim"><svg class="ui-icon" aria-hidden="true"><use href="#ui-icon-link"></use></svg><span>Claim an anonymization<small>Attach an existing mirror to your account</small></span></a><div class="dropdown-divider"></div><a class="dropdown-item" href="https://github.com/tdurieux/anonymous_github/issues/new?template=issue_report.yml" target="_blank" rel="noopener"><svg class="ui-icon" aria-hidden="true"><use href="#ui-icon-bug"></use></svg><span>Report a bug<small>About this service, not a paper</small></span></a><a class="dropdown-item" href="https://github.com/tdurieux/anonymous_github/" target="_blank" rel="noopener"><i class="fab fa-github" aria-hidden="true"></i><span>Source code</span></a><a class="dropdown-item" href="https://ko-fi.com/tdurieux" target="_blank" rel="noopener"><svg class="ui-icon" aria-hidden="true"><use href="#ui-icon-heart"></use></svg><span>Support the project</span></a><div class="dropdown-divider"></div><a class="dropdown-item" href="/api/user/logout" target="_self"><svg class="ui-icon" aria-hidden="true"><use href="#ui-icon-logout"></use></svg><span>Sign out</span></a>',9))])])):p("v-if",!0)])])],2),e.generalMessage?(l(),d("div",{key:0,class:"navbar shadow generalMessage",innerHTML:e.sanitize(e.generalMessage)},null,8,d4)):p("v-if",!0)],64)}var u4={class:"paper-landing"},c4={class:"paper-hero"},p4={class:"paper-hero-copy"},f4={class:"paper-cta-row"},h4={key:0,href:"/signin",target:"_self",class:"btn-hero"},m4={key:1,href:"/anonymize",class:"btn-hero"},v4={class:"paper-stats paper-wrap",id:"metrics","aria-label":"Usage over the last 60 days"},g4={class:"paper-stats-grid"},y4={class:"paper-stat-value"},b4={class:"paper-stat-label"},w4=["viewBox"],k4=["x","y","width","height"],E4={key:1,class:"paper-stat-delta"},C4={class:"paper-features paper-wrap","aria-label":"Product features"},_4={class:"paper-features-list",role:"tablist","aria-label":"Features","aria-orientation":"vertical"},S4=["onClick","onKeydown","id","aria-controls","aria-selected","tabindex"],N4={class:"paper-eyebrow"},D4={class:"paper-feature-title"},R4={class:"paper-feature-more"},T4={class:"paper-feature-title paper-feature-title-m","aria-hidden":"true"},A4=["href","target"],O4={class:"paper-features-stage"},I4=["id","aria-labelledby"],P4={class:"paper-frame"},V4={class:"paper-frame-bar","aria-hidden":"true"},q4={class:"url"},M4=["src","alt"],$4={class:"paper-footer"},L4={class:"paper-footer-inner"},U4={class:"paper-footer-col"},F4={key:0,href:"/dashboard"},z4={key:1,href:"/conferences"},H4={class:"paper-footer-foot"};function El(e,t){return l(),d("div",u4,[p(" Hero: copy on the left, the product on the right "),o("section",c4,[o("div",p4,[t[2]||(t[2]=o("div",{class:"paper-eyebrow"},"For double-blind peer review",-1)),t[3]||(t[3]=o("h1",{class:"hero-title"},[y(" Share the code,"),o("br"),o("span",{class:"accent"},"not the author.")],-1)),t[4]||(t[4]=o("p",{class:"hero-subtitle Line truncated
label names the service and the tooltip says who to contact. `),t[12]||(t[12]=o("li",{class:"nav-item d-none d-lg-block"},[o("a",{class:"nav-link",target:"_blank",rel:"noopener",href:"https://github.com/tdurieux/anonymous_github/issues/new?template=issue_report.yml",title:"Report a problem with the Anonymous GitHub service. Questions about an anonymized repository or paper go to its authors.","data-offset":"30"},[o("svg",{class:"ui-icon d-lg-none mr-1","aria-hidden":"true"},[o("use",{href:"#ui-icon-bug"})]),b("Report a bug ")])],-1)),e.isDarkMode?p("v-if",!0):(l(),d("li",y4,[o("a",{class:"nav-link nav-icon",href:"#",title:"Switch to dark mode","aria-label":"Switch to dark mode",role:"button",onClick:t[0]||(t[0]=me(s=>{e.darkMode(!0)},["prevent"]))},[...t[7]||(t[7]=[o("svg",{class:"ui-icon","aria-hidden":"true"},[o("use",{href:"#ui-icon-moon"})],-1)])])])),e.isDarkMode?(l(),d("li",b4,[o("a",{class:"nav-link nav-icon",href:"#",title:"Switch to light mode","aria-label":"Switch to light mode",role:"button",onClick:t[1]||(t[1]=me(s=>{e.darkMode(!1)},["prevent"]))},[...t[8]||(t[8]=[o("svg",{class:"ui-icon","aria-hidden":"true"},[o("use",{href:"#ui-icon-sun"})],-1)])])])):p("v-if",!0),e.user?p("v-if",!0):(l(),d("li",w4,[...t[9]||(t[9]=[o("a",{class:"nav-link btn-signin",target:"_self",href:"/signin","data-offset":"30"},[o("i",{class:"fab fa-github mr-1"}),b(" Sign in ")],-1)])])),e.user?(l(),d("li",k4,[o("a",E4,[e.user?.photo?(l(),d("img",{key:0,width:"22",height:"22",class:"rounded-circle",src:e.safeUrl(e.user?.photo)},null,8,C4)):p("v-if",!0),o("span",_4,c(e.user?.username),1)]),o("div",S4,[o("h6",N4,"Signed in as @"+c(e.user?.username),1),t[10]||(t[10]=ce('<a class="dropdown-item" href="/profile"><svg class="ui-icon" aria-hidden="true"><use href="#project-icon-filter"></use></svg><span>Settings<small>Defaults, quotas, account</small></span></a><a class="dropdown-item" href="/connections"><i class="fab fa-github" aria-hidden="true"></i><span>GitHub connections<small>Repository access and OAuth</small></span></a><a class="dropdown-item" href="/claim"><svg class="ui-icon" aria-hidden="true"><use href="#ui-icon-link"></use></svg><span>Claim an anonymization<small>Attach an existing mirror to your account</small></span></a><div class="dropdown-divider"></div><a class="dropdown-item" href="https://github.com/tdurieux/anonymous_github/issues/new?template=issue_report.yml" target="_blank" rel="noopener"><svg class="ui-icon" aria-hidden="true"><use href="#ui-icon-bug"></use></svg><span>Report a bug<small>About this service, not a paper</small></span></a><a class="dropdown-item" href="https://github.com/tdurieux/anonymous_github/" target="_blank" rel="noopener"><i class="fab fa-github" aria-hidden="true"></i><span>Source code</span></a><a class="dropdown-item" href="https://ko-fi.com/tdurieux" target="_blank" rel="noopener"><svg class="ui-icon" aria-hidden="true"><use href="#ui-icon-heart"></use></svg><span>Support the project</span></a><div class="dropdown-divider"></div><a class="dropdown-item" href="/api/user/logout" target="_self"><svg class="ui-icon" aria-hidden="true"><use href="#ui-icon-logout"></use></svg><span>Sign out</span></a>',9))])])):p("v-if",!0)])])],2),e.generalMessage?(l(),d("div",{key:0,class:"navbar shadow generalMessage",innerHTML:e.sanitize(e.generalMessage)},null,8,D4)):p("v-if",!0)],64)}var R4={class:"paper-landing"},T4={class:"paper-hero"},A4={class:"paper-hero-copy"},O4={class:"paper-cta-row"},I4={key:0,href:"/signin",target:"_self",class:"btn-hero"},P4={key:1,href:"/anonymize",class:"btn-hero"},V4={class:"paper-stats paper-wrap",id:"metrics","aria-label":"Usage over the last 60 days"},q4={class:"paper-stats-grid"},M4={class:"paper-stat-value"},L4={class:"paper-stat-label"},$4=["viewBox"],U4=["x","y","width","height"],F4={key:1,class:"paper-stat-delta"},z4={class:"paper-features paper-wrap","aria-label":"Product features"},H4={class:"paper-features-list",role:"tablist","aria-label":"Features","aria-orientation":"vertical"},B4=["onClick","onKeydown","id","aria-controls","aria-selected","tabindex"],j4={class:"paper-eyebrow"},G4={class:"paper-feature-title"},W4={class:"paper-feature-more"},K4={class:"paper-feature-title paper-feature-title-m","aria-hidden":"true"},Y4=["href","target"],J4={class:"paper-features-stage"},Q4=["id","aria-labelledby"],X4={class:"paper-frame"},Z4={class:"paper-frame-bar","aria-hidden":"true"},x4={class:"url"},e3=["src","alt"],t3={class:"paper-footer"},o3={class:"paper-footer-inner"},s3={class:"paper-footer-col"},n3={key:0,href:"/dashboard"},r3={key:1,href:"/conferences"},i3={class:"paper-footer-foot"};function Nl(e,t){return l(),d("div",R4,[p(" Hero: copy on the left, the product on the right "),o("section",T4,[o("div",A4,[t[2]||(t[2]=o("div",{class:"paper-eyebrow"},"For double-blind peer review",-1)),t[3]||(t[3]=o("h1",{class:"hero-title"},[b(" Share the code,"),o("br"),o("span",{class:"accent"},"not the author.")],-1)),t[4]||(t[4]=o("p",{class:"hero-subtitle Line truncated
MIT CSAIL
jane.smith@example.org`},IV={id:"termsHelp",class:"form-text text-muted"},PV={class:"invalid-feedback"},VV={id:"settings-display",class:"paper-settings-section"},qV={class:"form-group paper-check-list"},MV={class:"form-check"},$V={class:"form-check-input",type:"checkbox",id:"link",name:"link"},LV={class:"form-check"},UV={class:"form-check-input",type:"checkbox",id:"image",name:"image"},FV={class:"form-check"},zV={class:"form-check-input",type:"checkbox",id:"pdf",name:"pdf"},HV={class:"form-check"},jV={class:"form-check-input",type:"checkbox",id:"notebook",name:"notebook"},BV={id:"settings-features",class:"paper-settings-section"},GV={class:"form-group paper-check-list"},WV={class:"form-check"},KV={class:"form-check-input",type:"checkbox",id:"page",name:"page"},YV={class:"form-check"},JV={class:"form-check-input",type:"checkbox",id:"loc",name:"loc"},QV={class:"form-check"},XV={class:"form-check-input",type:"checkbox",id:"update",name:"update"},ZV={class:"form-group"},xV={class:"form-control",id:"mode",name:"mode"},e5={class:"form-text text-muted"},t5={id:"settings-expiration",class:"paper-settings-section"},o5={class:"form-group"},s5={class:"form-control",id:"expiration",name:"expiration"},n5={key:0,class:"draft-status",role:"status"},r5={key:1,class:"draft-status"},i5={class:"paper-settings-footer"},a5={key:0,class:"paper-inline-alert paper-inline-alert-error",role:"alert"},l5=["textContent"],d5={class:"paper-settings-actions"},u5=["disabled"],c5={key:0,class:"ui-icon mr-1","aria-hidden":"true"},p5={id:"settings-account",class:"paper-settings-section"},f5={class:"paper-account-card"},h5=["src"],m5={class:"paper-account-identity"},v5={class:"paper-account-name"},g5={class:"paper-danger-zone"},y5={key:0,class:"paper-inline-alert paper-inline-alert-error",role:"alert"},b5=["textContent"],w5=["disabled"];function Dl(e,t){let n=We("quota-usage"),r=ye("paper-scrollspy"),s=ye("field"),i=ye("form");return l(),d("div",EV,[t[52]||(t[52]=ce('<div class="paper-crumbs"><a href="/dashboard">My work</a> \xA0/\xA0 <span class="here">Settings</span></div><h1 class="paper-page-title">Your <em>settings</em></h1><p class="paper-page-lede">Defaults applied to every new anonymization, your quotas, and your account.</p>',3)),o("div",CV,[N((l(),d("aside",_V,[...t[4]||(t[4]=[ce('<div class="paper-settings-toc-head">On this page</div><nav><a href="#settings-quota">Quota</a><a href="#settings-terms">Terms to redact</a><a href="#settings-display">Display</a><a href="#settings-features">Features</a><a href="#settings-expiration">Expiration</a><a href="#settings-account">Account</a></nav>',2)])])),[[r]]),o("div",SV,[p(" Account usage shares the dashboard component. "),o("section",NV,[t[6]||(t[6]=o("div",{class:"paper-section-eyebrow"},"Quota",-1)),t[7]||(t[7]=o("p",{class:"paper-section-copy"},"What you are using across all your anonymizations. Conferences can lift these limits during a review window.",-1)),e.quota?(l(),qt(n,{key:0,quota:e.quota,fmt:e.fmt},null,8,["quota","fmt"])):p("v-if",!0),e.quota?p("v-if",!0):(l(),d("div",DV,[(l(),d(X,null,ae([1,2,3],(a,u)=>o("div",RV,[...t[5]||(t[5]=[o("div",{class:"quota-header"},[o("span",{class:"skeleton skeleton-line",style:{width:"40%"}}),o("span",{class:"skeleton skeleton-line",style:{width:"25%"}})],-1),o("div",{class:"quota-track"},null,-1)])])),64))]))]),N((l(),d("form",{class:"form needs-validation",name:"defaultsForm",novalidate:"",onSubmit:t[2]||(t[2]=he(a=>e.submitForm(a,()=>{e.saveDefault(a)}),["prevent"]))},[o("section",TV,[t[13]||(t[13]=o("div",{class:"paper-section-eyebrow"},"Terms to redact",-1)),t[14]||(t[14]=o("p",{class:"paper-section-copy"},"Pre-filled in every new anonymization. You can still edit the list per anonymization.",-1)),o("div",AV,[t[12]||(t[12]=o("label",{class:"paper-field-label",for:"terms"},"Default terms",-1)),N(o("textarea",OV,null,512),[[s,{state:e.viewState,set:a=>{e.terms=a},value:e.terms,form:"defaultsForm",options:{debounce:250}}]]),o("small",IV,[t[8]||(t[8]=y(" One term per line (regex allowed). Each match is replaced by ",-1)),o("code",null,c(e.site_options?.ANONYMIZATION_MASK||"XXX")+"-[N]",1),t[9]||(t[9]=y(", or use ",-1)),t[10]||(t[10]=o("code",null,"term=>replacement",-1)),t[11]||(t[11]=y(" to pick your own. ",-1))]),N(o("div",PV," Terms are in an invalid format ",512),[[Y,e.defaultsForm?.terms?.errors?.format]])])]),o("section",VV,[t[23]||(t[23]=o("div",{class:"paper-section-eyebrow"},"Display",-1)),t[24]||(t[24]=o("p",{class:"paper-section-copy"},"What reviewers can see inside an anonymized repository.",-1)),o("div",qV,[o("div",MV,[N(o("input",$V,null,512),[[s,{state:e.viewState,set:a=>{e.options.link=a},value:e.options?.link,form:"defaultsForm",options:{}}]]),t[15]||(t[15]=o("label",{class:"form-check-label",for:"link"},"Keep links",-1)),t[16]||(t[16]=o("small",{class:"form-text text-muted"},"Unchecked, every link in text files is removed.",-1))]),o("div",LV,[N(o("input",UV,null,512),[[s,{state:e.viewState,set:a=>{e.options.image=a},value:e.op Line truncated
`);for(let w=0;w<g.length;w++){let h=g[w];if(h.startsWith("diff --git")){m(),f();continue}if(h.startsWith("--- ")){i&&i.lines.length&&m(),f().oldPath=h.replace(/^--- (a\/)?/,"").trim();continue}if(h.startsWith("+++ ")){f().newPath=h.replace(/^\+\+\+ (b\/)?/,"").trim();continue}if(!(h.startsWith("index ")||h.startsWith("similarity index")||h.startsWith("rename ")||h.startsWith("new file mode")||h.startsWith("deleted file mode")||h.startsWith("Binary files"))){if(h.startsWith("@@")){let b=h.match(/@@\s+-(\d+)(?:,\d+)?\s+\+(\d+)(?:,\d+)?\s+@@/);b&&(a=parseInt(b[1],10),u=parseInt(b[2],10)),f().lines.push({kind:"hunk",oldNo:"",newNo:"",text:h});continue}i&&(h.startsWith("+")?(i.lines.push({kind:"add",oldNo:"",newNo:u,text:h.slice(1)}),u++):h.startsWith("-")?(i.lines.push({kind:"remove",oldNo:a,newNo:"",text:h.slice(1)}),a++):(i.lines.push({kind:"ctx",oldNo:a,newNo:u,text:h.startsWith(" ")?h.slice(1):h}),a++,u++))}}return n(s,i),s.join("")}})({trustAsHtml:e=>e});function qq(e,t){return e==null?"":Number(e).toLocaleString(void 0,t==null?{}:{minimumFractionDigits:t,maximumFractionDigits:t})}function Mq(e){return String(e??"").toUpperCase()}function $q(e,t,n=0){return(e||[]).slice(n,n+t)}function Lq(e,t){return Array.isArray(e)?typeof t=="function"?e.filter(t):e:[]}function Uq(e,t){if(!t)return e||[];let n=Array.isArray(t)?t:[t];return[...e||[]].sort((r,s)=>{for(let i of n){let a=i[0]==="-"?-1:1;i=i.replace(/^[+-]/,"");let u=i.split(".").reduce((m,g)=>m?.[g],r),f=i.split(".").reduce((m,g)=>m?.[g],s);if(typeof u=="string"&&(u=u.toLowerCase()),typeof f=="string"&&(f=f.toLowerCase()),u!==f)return(u==null?-1:f==null?1:u<f?-1:1)*a}return 0})}function Fq(e,t){if(!e)return"";let n=new Date(e);if(isNaN(n))return String(e);if(!t||t==="mediumDate")return n.toLocaleDateString(void 0,{year:"numeric",month:"short",day:"numeric"});let r={yyyy:n.getFullYear(),MMM:n.toLocaleDateString(void 0,{month:"short"}),MM:String(n.getMonth()+1).padStart(2,"0"),dd:String(n.getDate()).padStart(2,"0"),d:n.getDate(),HH:String(n.getHours()).padStart(2,"0"),mm:String(n.getMinutes()).padStart(2,"0"),ss:String(n.getSeconds()).padStart(2,"0")};return t.replace(/yyyy|MMM|MM|dd|HH|mm|ss|d/g,s=>r[s])}function zq(e,t){return(t?.[e]??t?.[e===1?"one":"other"]??"").replace(/\{\}/g,e??0)}var ql={ERRORS:{cache_busy:"Downloads are busy. Please try again shortly.",invalid_sort:"The selected sort order is invalid.",invalid_cursor:"This project page is no longer valid. Reload the list.",repository_changed:"The repository changed while processing. Please retry.",repository_job_cancelled:"The repository changed or was removed while processing.",storage_delete_failed:"Unable to remove cached files. Please try again later.",unknown_error:"Unknown error, contact the admin.",unreachable:"Anonymous GitHub is unreachable, contact the admin.",request_error:"Unable to download the file, check your connection or contact the admin.",not_found:"The requested resource could not be found.",not_connected:"You must be logged in to perform this action.",not_authorized:"You do not have permission to perform this action.",unable_to_connect_user:"Unable to connect your account. Please try again later.",user_not_found:"The requested user could not be found.",user_banned:"Your account has been banned. Contact the admin for more information.",repo_access_limited:"GitHub blocked access because the repository's organization restricts third-party OAuth apps. Ask an org owner to approve Anonymous GitHub under Settings \u2192 Third-party Access \u2192 OAuth app policy, or anonymize a personal fork instead.",repo_saml_enforcement:"The repository's organization enforces SAML single sign-on. Authorize your token for that organization (GitHub \u2192 Settings \u2192 Applications, or re-run the org's SSO sign-in), then retry. Alternatively, anonymize a personal fork.",repo_not_found:"The repository was not found on GitHub. Check the URL and spelling, make sure you are signed in to the account that can see it, and confirm the repo isn't hidden under an org that restricts third-party app access.",repo_empty:"The selected branch has no commits on GitHub. Push at least one commit, or pick a different branch, then retry.",repo_not_accessible:"Anonymous GitHub cannot access this repository. Verify the repository exists and that Anonymous GitHub has been authorized for the owning organization.",repository_archived:"This repository has been archived and its files are no longer available.",repository_expired:"The repository is expired.",invalid_status:"This action cannot be performed while the resource is in its current state.",repository_not_ready:"Anonymous GitHub is still processing the repository, it can take several minutes.",repository_not_accessible:"This repository is currently not accessible.",repo_is_updating:"Anonymous GitHub is still processing the repository, it can take several minutes.",invalid_repo:"The provided repository is not valid.",invalid_source_repository:"The repository source name Line truncated
jane.smith@example.org`},QV={id:"termsHelp",class:"form-text text-muted"},XV={class:"invalid-feedback"},ZV={id:"settings-display",class:"paper-settings-section"},xV={class:"form-group paper-check-list"},e5={class:"form-check"},t5={class:"form-check-input",type:"checkbox",id:"link",name:"link"},o5={class:"form-check"},s5={class:"form-check-input",type:"checkbox",id:"image",name:"image"},n5={class:"form-check"},r5={class:"form-check-input",type:"checkbox",id:"pdf",name:"pdf"},i5={class:"form-check"},a5={class:"form-check-input",type:"checkbox",id:"notebook",name:"notebook"},l5={id:"settings-features",class:"paper-settings-section"},d5={class:"form-group paper-check-list"},u5={class:"form-check"},c5={class:"form-check-input",type:"checkbox",id:"page",name:"page"},p5={class:"form-check"},f5={class:"form-check-input",type:"checkbox",id:"loc",name:"loc"},m5={class:"form-check"},h5={class:"form-check-input",type:"checkbox",id:"update",name:"update"},v5={class:"form-group"},g5={class:"form-control",id:"mode",name:"mode"},y5={class:"form-text text-muted"},b5={id:"settings-expiration",class:"paper-settings-section"},w5={class:"form-group"},k5={class:"form-control",id:"expiration",name:"expiration"},E5={key:0,class:"draft-status",role:"status"},C5={key:1,class:"draft-status"},_5={class:"paper-settings-footer"},S5={key:0,class:"paper-inline-alert paper-inline-alert-error",role:"alert"},N5=["textContent"],D5={class:"paper-settings-actions"},R5=["disabled"],T5={key:0,class:"ui-icon mr-1","aria-hidden":"true"},A5={id:"settings-account",class:"paper-settings-section"},O5={class:"paper-account-card"},I5=["src"],P5={class:"paper-account-identity"},V5={class:"paper-account-name"},q5={class:"paper-danger-zone"},M5={key:0,class:"paper-inline-alert paper-inline-alert-error",role:"alert"},L5=["textContent"],$5=["disabled"];function Ol(e,t){let s=We("quota-usage"),r=be("paper-scrollspy"),n=be("field"),i=be("form");return l(),d("div",FV,[t[52]||(t[52]=ce('<div class="paper-crumbs"><a href="/dashboard">My work</a> \xA0/\xA0 <span class="here">Settings</span></div><h1 class="paper-page-title">Your <em>settings</em></h1><p class="paper-page-lede">Defaults applied to every new anonymization, your quotas, and your account.</p>',3)),o("div",zV,[N((l(),d("aside",HV,[...t[4]||(t[4]=[ce('<div class="paper-settings-toc-head">On this page</div><nav><a href="#settings-quota">Quota</a><a href="#settings-terms">Terms to redact</a><a href="#settings-display">Display</a><a href="#settings-features">Features</a><a href="#settings-expiration">Expiration</a><a href="#settings-account">Account</a></nav>',2)])])),[[r]]),o("div",BV,[p(" Account usage shares the dashboard component. "),o("section",jV,[t[6]||(t[6]=o("div",{class:"paper-section-eyebrow"},"Quota",-1)),t[7]||(t[7]=o("p",{class:"paper-section-copy"},"What you are using across all your anonymizations. Conferences can lift these limits during a review window.",-1)),e.quota?(l(),Mt(s,{key:0,quota:e.quota,fmt:e.fmt},null,8,["quota","fmt"])):p("v-if",!0),e.quota?p("v-if",!0):(l(),d("div",GV,[(l(),d(X,null,re([1,2,3],(a,u)=>o("div",WV,[...t[5]||(t[5]=[o("div",{class:"quota-header"},[o("span",{class:"skeleton skeleton-line",style:{width:"40%"}}),o("span",{class:"skeleton skeleton-line",style:{width:"25%"}})],-1),o("div",{class:"quota-track"},null,-1)])])),64))]))]),N((l(),d("form",{class:"form needs-validation",name:"defaultsForm",novalidate:"",onSubmit:t[2]||(t[2]=me(a=>e.submitForm(a,()=>{e.saveDefault(a)}),["prevent"]))},[o("section",KV,[t[13]||(t[13]=o("div",{class:"paper-section-eyebrow"},"Terms to redact",-1)),t[14]||(t[14]=o("p",{class:"paper-section-copy"},"Pre-filled in every new anonymization. You can still edit the list per anonymization.",-1)),o("div",YV,[t[12]||(t[12]=o("label",{class:"paper-field-label",for:"terms"},"Default terms",-1)),N(o("textarea",JV,null,512),[[n,{state:e.viewState,set:a=>{e.terms=a},value:e.terms,form:"defaultsForm",options:{debounce:250}}]]),o("small",QV,[t[8]||(t[8]=b(" One term per line (regex allowed). Each match is replaced by ",-1)),o("code",null,c(e.site_options?.ANONYMIZATION_MASK||"XXX")+"-[N]",1),t[9]||(t[9]=b(", or use ",-1)),t[10]||(t[10]=o("code",null,"term=>replacement",-1)),t[11]||(t[11]=b(" to pick your own. ",-1))]),N(o("div",XV," Terms are in an invalid format ",512),[[Y,e.defaultsForm?.terms?.errors?.format]])])]),o("section",ZV,[t[23]||(t[23]=o("div",{class:"paper-section-eyebrow"},"Display",-1)),t[24]||(t[24]=o("p",{class:"paper-section-copy"},"What reviewers can see inside an anonymized repository.",-1)),o("div",xV,[o("div",e5,[N(o("input",t5,null,512),[[n,{state:e.viewState,set:a=>{e.options.link=a},value:e.options?.link,form:"defaultsForm",options:{}}]]),t[15]||(t[15]=o("label",{class:"form-check-label",for:"link"},"Keep links",-1)),t[16]||(t[16]=o("small",{class:"form-text text-muted"},"Unchecked, every link in text files is removed.",-1))]),o("div",o5,[N(o("input",s5,null,512),[[n,{state:e.viewState,set:a=>{e.options.image=a},value:e.op Line truncated
`);for(let k=0;k<v.length;k++){let m=v[k];if(m.startsWith("diff --git")){h(),f();continue}if(m.startsWith("--- ")){i&&i.lines.length&&h(),f().oldPath=m.replace(/^--- (a\/)?/,"").trim();continue}if(m.startsWith("+++ ")){f().newPath=m.replace(/^\+\+\+ (b\/)?/,"").trim();continue}if(!(m.startsWith("index ")||m.startsWith("similarity index")||m.startsWith("rename ")||m.startsWith("new file mode")||m.startsWith("deleted file mode")||m.startsWith("Binary files"))){if(m.startsWith("@@")){let y=m.match(/@@\s+-(\d+)(?:,\d+)?\s+\+(\d+)(?:,\d+)?\s+@@/);y&&(a=parseInt(y[1],10),u=parseInt(y[2],10)),f().lines.push({kind:"hunk",oldNo:"",newNo:"",text:m});continue}i&&(m.startsWith("+")?(i.lines.push({kind:"add",oldNo:"",newNo:u,text:m.slice(1)}),u++):m.startsWith("-")?(i.lines.push({kind:"remove",oldNo:a,newNo:"",text:m.slice(1)}),a++):(i.lines.push({kind:"ctx",oldNo:a,newNo:u,text:m.startsWith(" ")?m.slice(1):m}),a++,u++))}}return s(n,i),n.join("")}})({trustAsHtml:e=>e});function xq(e,t){return e==null?"":Number(e).toLocaleString(void 0,t==null?{}:{minimumFractionDigits:t,maximumFractionDigits:t})}function eM(e){return String(e??"").toUpperCase()}function tM(e,t,s=0){return(e||[]).slice(s,s+t)}function oM(e,t){return Array.isArray(e)?typeof t=="function"?e.filter(t):e:[]}function sM(e,t){if(!t)return e||[];let s=Array.isArray(t)?t:[t];return[...e||[]].sort((r,n)=>{for(let i of s){let a=i[0]==="-"?-1:1;i=i.replace(/^[+-]/,"");let u=i.split(".").reduce((h,v)=>h?.[v],r),f=i.split(".").reduce((h,v)=>h?.[v],n);if(typeof u=="string"&&(u=u.toLowerCase()),typeof f=="string"&&(f=f.toLowerCase()),u!==f)return(u==null?-1:f==null?1:u<f?-1:1)*a}return 0})}function nM(e,t){if(!e)return"";let s=new Date(e);if(isNaN(s))return String(e);if(!t||t==="mediumDate")return s.toLocaleDateString(void 0,{year:"numeric",month:"short",day:"numeric"});let r={yyyy:s.getFullYear(),MMM:s.toLocaleDateString(void 0,{month:"short"}),MM:String(s.getMonth()+1).padStart(2,"0"),dd:String(s.getDate()).padStart(2,"0"),d:s.getDate(),HH:String(s.getHours()).padStart(2,"0"),mm:String(s.getMinutes()).padStart(2,"0"),ss:String(s.getSeconds()).padStart(2,"0")};return t.replace(/yyyy|MMM|MM|dd|HH|mm|ss|d/g,n=>r[n])}function rM(e,t){return(t?.[e]??t?.[e===1?"one":"other"]??"").replace(/\{\}/g,e??0)}var Ul={ERRORS:{cache_busy:"Downloads are busy. Please try again shortly.",invalid_sort:"The selected sort order is invalid.",invalid_cursor:"This project page is no longer valid. Reload the list.",repository_changed:"The repository changed while processing. Please retry.",repository_job_cancelled:"The repository changed or was removed while processing.",storage_delete_failed:"Unable to remove cached files. Please try again later.",unknown_error:"Unknown error, contact the admin.",unreachable:"Anonymous GitHub is unreachable, contact the admin.",request_error:"Unable to download the file, check your connection or contact the admin.",not_found:"The requested resource could not be found.",not_connected:"You must be logged in to perform this action.",not_authorized:"You do not have permission to perform this action.",unable_to_connect_user:"Unable to connect your account. Please try again later.",user_not_found:"The requested user could not be found.",user_banned:"Your account has been banned. Contact the admin for more information.",repo_access_limited:"GitHub blocked access because the repository's organization restricts third-party OAuth apps. Ask an org owner to approve Anonymous GitHub under Settings \u2192 Third-party Access \u2192 OAuth app policy, or anonymize a personal fork instead.",repo_saml_enforcement:"The repository's organization enforces SAML single sign-on. Authorize your token for that organization (GitHub \u2192 Settings \u2192 Applications, or re-run the org's SSO sign-in), then retry. Alternatively, anonymize a personal fork.",repo_not_found:"The repository was not found on GitHub. Check the URL and spelling, make sure you are signed in to the account that can see it, and confirm the repo isn't hidden under an org that restricts third-party app access.",repo_empty:"The selected branch has no commits on GitHub. Push at least one commit, or pick a different branch, then retry.",repo_not_accessible:"Anonymous GitHub cannot access this repository. Verify the repository exists and that Anonymous GitHub has been authorized for the owning organization.",repository_archived:"This repository has been archived and its files are no longer available.",repository_expired:"The repository is expired.",invalid_status:"This action cannot be performed while the resource is in its current state.",repository_not_ready:"Anonymous GitHub is still processing the repository, it can take several minutes.",repository_not_accessible:"This repository is currently not accessible.",repo_is_updating:"Anonymous GitHub is still processing the repository, it can take several minutes.",invalid_repo:"The provided repository is not valid.",invalid_source_repository:"The repository source name Line truncated
/*! Bundled license information:
@vue/shared/dist/shared.esm-bundler.js:
+7
View File
@@ -22,6 +22,7 @@ import { IFile } from "./model/files/files.types";
import { FilterQuery } from "mongoose";
import { createLogger, serializeError } from "./logger";
import { githubTokenForStreamer } from "./github-token-context";
import { requestHeaders, startStage } from "./request-monitoring";
const logger = createLogger("anonymized-file");
@@ -379,6 +380,7 @@ export default class AnonymizedFile {
// use the streamer service
return got.stream(join(config.STREAMER_ENTRYPOINT, "api"), {
method: "POST",
headers: requestHeaders(),
json: {
token: await githubTokenForStreamer(await this.repository.getToken(), this.repository.model.source.repositoryName),
repoFullName: this.repository.model.source.repositoryName,
@@ -423,9 +425,11 @@ export default class AnonymizedFile {
this.size(),
this.repository.getToken(),
]);
const headersDone = startStage("streamer_headers");
const resStream = got
.stream(join(config.STREAMER_ENTRYPOINT, "api"), {
method: "POST",
headers: requestHeaders(),
json: {
sha,
size,
@@ -445,6 +449,7 @@ export default class AnonymizedFile {
// this, the parent response has no Content-Type and the browser
// guesses (text renders as download, images as octet-stream).
resStream.on("response", (upstream: { headers: Record<string, string | string[] | undefined> }) => {
headersDone();
if (res.headersSent) return;
const ct = upstream.headers["content-type"];
if (typeof ct === "string") {
@@ -455,6 +460,8 @@ export default class AnonymizedFile {
else if (isTextFile(this.anonymizedPath)) res.contentType("text/plain");
}
});
resStream.once("error", () => headersDone(true));
resStream.once("close", () => headersDone());
await streamResponse(resStream, res, (err: Error) => {
const { error } = streamerErrorToAnonymous(
err as Error & {
+6
View File
@@ -11,6 +11,7 @@ import Repository from "./Repository";
import { getCredential, replaceCredential, getCredentialToken } from "./credentials";
import config from "../config";
import { createLogger } from "./logger";
import { measureStage } from "./request-monitoring";
const logger = createLogger("github");
@@ -281,6 +282,7 @@ export function octokit(token: string) {
}
throw err;
});
oct.hook.wrap("request", (request, options) => measureStage("upstream", async () => request(options)));
return oct;
}
@@ -308,6 +310,10 @@ export async function checkToken(token: string) {
const checkedRepositoryTokens = new WeakMap<Repository, string>();
export async function getToken(repository: Repository) {
return measureStage("authorization", () => resolveRepositoryToken(repository));
}
async function resolveRepositoryToken(repository: Repository) {
repository.assertNotArchived();
logger.debug("getToken", { repoId: repository.repoId });
if (isConnected && !repository.model.isNew) {
+18 -3
View File
@@ -1,6 +1,8 @@
import { Worker } from "worker_threads";
import { existsSync } from "fs";
import { join } from "path";
import { startStage } from "./request-monitoring";
import { AsyncResource } from "async_hooks";
const MEMORY_BUDGET = Math.max(128, Number(process.env.ANONYMIZATION_MEMORY_MB) || 512) * 1024 * 1024;
const MAX_WORKERS = Math.min(4, Math.max(1, Number(process.env.ANONYMIZATION_WORKERS) || 1));
@@ -12,6 +14,8 @@ interface Job {
reject: (error: Error) => void;
timer?: NodeJS.Timeout;
abort?: () => void;
waitingDone: ReturnType<typeof startStage>;
transformDone: () => ReturnType<typeof startStage>;
}
const waiting: Job[] = [];
const idle: Worker[] = [];
@@ -23,16 +27,20 @@ export function anonymizeOnWorker(message: Job["message"], size: number, signal:
if (weight > MEMORY_BUDGET) return Promise.reject(new Error("anonymization_memory_budget_exceeded"));
if (waiting.length >= 16) return Promise.reject(new Error("anonymization_queue_full"));
return new Promise((resolve, reject) => {
const job: Job = { message, weight, signal, resolve, reject };
const waitingDone = startStage("worker_wait");
// Bind the stage factory to the submitting request, not the preceding job.
const transformDone = AsyncResource.bind(() => startStage("anonymize"));
const job: Job = { message, weight, signal, resolve, reject, waitingDone, transformDone };
job.abort = () => {
const index = waiting.indexOf(job);
if (index < 0) return;
waiting.splice(index, 1);
clearTimeout(job.timer);
signal.removeEventListener("abort", job.abort!);
waitingDone(true);
reject(new Error("anonymization_cancelled"));
};
if (signal.aborted) return reject(new Error("anonymization_cancelled"));
if (signal.aborted) { waitingDone(true); return reject(new Error("anonymization_cancelled")); }
signal.addEventListener("abort", job.abort, { once: true });
job.timer = setTimeout(job.abort, 30_000);
waiting.push(job);
@@ -42,6 +50,8 @@ export function anonymizeOnWorker(message: Job["message"], size: number, signal:
function pump() {
while (waiting.length && running < MAX_WORKERS && bytes + waiting[0].weight <= MEMORY_BUDGET) {
const job = waiting.shift()!;
job.waitingDone();
const transformDone = job.transformDone();
clearTimeout(job.timer);
job.signal.removeEventListener("abort", job.abort!);
running++;
@@ -51,12 +61,13 @@ function pump() {
try { worker = idle.pop() || new Worker(existsSync(compiled) ? compiled : join(__dirname, "anonymization.worker.ts"), {
execArgv: existsSync(compiled) ? [] : ["-r", require.resolve("ts-node/register/transpile-only")],
resourceLimits: { maxOldGenerationSizeMb: Math.ceil(MEMORY_BUDGET / 1024 / 1024) },
}); } catch (error) { running--; bytes -= job.weight; job.reject(error as Error); continue; }
}); } catch (error) { running--; bytes -= job.weight; transformDone(true); job.reject(error as Error); continue; }
worker.ref();
let finished = false;
const finish = (error?: Error, changed = false) => {
if (finished) return;
finished = true;
transformDone(!!error);
clearTimeout(deadline);
job.signal.removeEventListener("abort", cancel);
worker.removeListener("message", message);
@@ -89,3 +100,7 @@ export function reserveSpool(bytes: number) {
spoolBytes += bytes;
}
export function releaseSpool(bytes: number) { spoolBytes -= bytes; }
export function getAnonymizationPoolStats() {
return { running, waiting: waiting.length, idle: idle.length, reservedBytes: bytes, spoolBytes,
maxWorkers: MAX_WORKERS, maxWaiting: 16, memoryBudgetBytes: MEMORY_BUDGET };
}
+213
View File
@@ -0,0 +1,213 @@
import { createClient, RedisClientType } from "redis";
import { hostname } from "os";
import { readdir, readFile, readlink } from "fs/promises";
import { monitorEventLoopDelay, performance } from "perf_hooks";
import config from "../config";
import { AsyncCache } from "./async-cache";
import { activeRequestCount, LATENCY_BUCKETS, Metric, requestMetrics, Service } from "./request-monitoring";
const PREFIX = "performance:v1:";
const RETENTION = 2 * 3600;
let client: RedisClientType | undefined;
let stop: (() => Promise<void>) | undefined;
let flushNow: (() => Promise<void>) | undefined;
const reports = new AsyncCache<unknown>(10000, 2);
const emptyMetric = (metric: string, route: string, method: string): Metric => ({ metric, route, method,
count: 0, sumMs: 0, bytes: 0, aborted: 0, errors: 0, slow: 0, buckets: Array(LATENCY_BUCKETS.length + 1).fill(0) });
async function containerMemoryLimit() {
for (const path of ["/sys/fs/cgroup/memory.max", "/sys/fs/cgroup/memory/memory.limit_in_bytes"]) {
const raw = await readFile(path, "utf8").catch(() => "");
const value = Number(raw.trim());
if (value > 0 && value < Number.MAX_SAFE_INTEGER) return value;
}
return null;
}
async function redisDeadline<T>(work: Promise<T>, redis: RedisClientType): Promise<T> {
let timer: NodeJS.Timeout | undefined;
try {
return await Promise.race([work, new Promise<never>((_resolve, reject) => {
timer = setTimeout(() => {
reject(new Error("performance_monitoring_timeout"));
if (redis.isOpen) void redis.disconnect().then(() => client === redis ? redis.connect() : undefined).catch(() => {});
}, 2000); timer.unref();
})]);
} finally { clearTimeout(timer); }
}
export async function flushPerformanceMetrics() { await flushNow?.(); reports.clear(); }
export function percentileUpperBound(buckets: number[], percentile: number): number | null {
const total = buckets.reduce((sum, count) => sum + count, 0);
if (!total) return null;
let count = 0;
for (let i = 0; i < buckets.length; i++) {
count += buckets[i];
if (count >= Math.ceil(total * percentile)) return LATENCY_BUCKETS[i] ?? null;
}
return null;
}
/** PID-owned sockets only: /proc/net also includes healthcheck subprocesses. */
export async function socketSnapshot() {
try {
const descriptors = (await readdir("/proc/self/fd")).slice(0, 4096);
const sockets = new Set<string>();
for (let i = 0; i < descriptors.length; i += 32) {
const links = await Promise.all(descriptors.slice(i, i + 32).map(fd => readlink(`/proc/self/fd/${fd}`).catch(() => "")));
for (const link of links) { const match = /^socket:\[(\d+)\]$/.exec(link); if (match) sockets.add(match[1]); }
}
const states: Record<string, number> = {};
for (const file of ["/proc/self/net/tcp", "/proc/self/net/tcp6"]) {
const table = await readFile(file, "utf8");
for (const line of table.trim().split("\n").slice(1)) {
const columns = line.trim().split(/\s+/);
if (sockets.has(columns[9])) states[columns[3]] = (states[columns[3]] || 0) + 1;
}
}
return { descriptors: sockets.size, established: states["01"] || 0, closeWait: states["08"] || 0,
listening: states["0A"] || 0, truncated: descriptors.length === 4096 };
} catch { return null; }
}
/** One bounded batch every 15s. Redis outages never queue commands or hold HTTP requests. */
export function startPerformanceMonitoring(service: Service, workers: () => unknown = () => null) {
if (stop) return stop;
const redis = createClient({ disableOfflineQueue: true, socket: { host: config.REDIS_HOSTNAME,
port: config.REDIS_PORT, connectTimeout: 2000, reconnectStrategy: retries => Math.min(5000, 250 * (retries + 1)) } }) as RedisClientType;
client = redis;
redis.on("error", () => {});
void redis.connect().catch(() => {});
const loop = monitorEventLoopDelay({ resolution: 20 }); loop.enable();
let previousUtilization = performance.eventLoopUtilization();
let previousCpu = process.cpuUsage();
let previousCpuAt = performance.now();
let busy = false;
let droppedBatches = 0;
let lastFlushAt: number | null = null;
let stopped = false;
const instance = `${service}:${hostname()}`;
const flush = async () => {
if (busy || stopped) return;
busy = true;
const entries = requestMetrics.drain();
try {
if (!redis.isReady) { droppedBatches += entries.length ? 1 : 0; return; }
const utilization = performance.eventLoopUtilization(previousUtilization);
previousUtilization = performance.eventLoopUtilization();
const cpu = process.cpuUsage(previousCpu);
const cpuAt = performance.now();
const cpuPercent = (cpu.user + cpu.system) / ((cpuAt - previousCpuAt) * 1000) * 100;
previousCpu = process.cpuUsage(); previousCpuAt = cpuAt;
const runtime = { instance, service, sampledAt: Date.now(), uptime: process.uptime(),
cpuPercent, memoryLimitBytes: await containerMemoryLimit(),
memory: process.memoryUsage(), activeRequests: activeRequestCount(), sockets: await socketSnapshot(), workers: workers(),
eventLoop: { utilization: utilization.utilization, p95Ms: loop.percentile(95) / 1e6,
maxMs: Number.isFinite(loop.max) ? loop.max / 1e6 : 0 },
droppedMetrics: requestMetrics.dropped, droppedBatches, lastFlushAt };
loop.reset();
const tx = redis.multi();
const touched = new Set<string>();
for (const { minute, data } of entries) {
const key = `${PREFIX}minute:${service}:${minute}`;
touched.add(key);
const label = JSON.stringify([data.metric, data.route, data.method]);
const fields: Record<string, number> = { count: data.count, sumMs: data.sumMs, bytes: data.bytes,
aborted: data.aborted, errors: data.errors, slow: data.slow };
data.buckets.forEach((count, i) => { fields[`b${i}`] = count; });
for (const [field, value] of Object.entries(fields)) if (value) tx.hIncrBy(key, `${label}|${field}`, value);
}
for (const key of touched) tx.expire(key, RETENTION);
tx.set(`${PREFIX}instance:${instance}`, JSON.stringify(runtime), { EX: 90 });
tx.zAdd(`${PREFIX}instances`, { score: runtime.sampledAt, value: instance });
tx.zRemRangeByScore(`${PREFIX}instances`, 0, runtime.sampledAt - 90000);
tx.expire(`${PREFIX}instances`, 120);
tx.lPush(`${PREFIX}runtime:${service}`, JSON.stringify(runtime));
tx.lTrim(`${PREFIX}runtime:${service}`, 0, service === "api" ? 239 : 959);
tx.expire(`${PREFIX}runtime:${service}`, RETENTION);
await redisDeadline(tx.exec(), redis); lastFlushAt = Date.now();
} catch { droppedBatches++; }
finally { busy = false; }
};
const timer = setInterval(() => { void flush(); }, 15000); timer.unref();
flushNow = flush;
void flush();
stop = async () => {
stopped = true; clearInterval(timer); loop.disable();
if (redis.isOpen) await redis.disconnect();
client = undefined; stop = undefined; flushNow = undefined; reports.clear();
};
return stop;
}
export async function performanceReport(minutes: number) {
const windowMinutes = minutes === 60 ? 60 : 15;
if (!client?.isReady) return { available: false, windowMinutes, instances: [], routes: [], stages: [] };
const redis = client;
return reports.get(String(windowMinutes), async () => {
const minute = Math.floor(Date.now() / 60000) * 60000;
const groups = new Map<string, Metric & { service: Service }>();
const tx = redis.multi();
const requests: { service: Service; minute: number }[] = [];
for (const service of ["api", "streamer"] as const) {
for (let i = 0; i < windowMinutes; i++) {
requests.push({ service, minute: minute - i * 60000 });
tx.hGetAll(`${PREFIX}minute:${service}:${minute - i * 60000}`);
}
}
const values = await redisDeadline(tx.exec(), redis) as unknown as Record<string, string>[];
const series = new Map<number, { minute: number; requests: number; aborted: number; errors: number; sumMs: number }>();
values.forEach((hash, index) => {
for (const [key, raw] of Object.entries(hash || {})) {
const split = key.lastIndexOf("|");
const label = key.slice(0, split); const field = key.slice(split + 1);
const [metric, route, method] = JSON.parse(label) as string[];
const service = requests[index].service;
const id = `${service}:${label}`;
let row = groups.get(id);
if (!row) { row = { ...emptyMetric(metric, route, method), service }; groups.set(id, row); }
const value = Number(raw) || 0;
if (/^b\d+$/.test(field)) row.buckets[Number(field.slice(1))] += value;
else if (["count", "sumMs", "bytes", "aborted", "errors", "slow"].includes(field)) {
const numericField = field as "count" | "sumMs" | "bytes" | "aborted" | "errors" | "slow";
row[numericField] += value;
}
if (metric === "request" && service === "api") {
const timestamp = requests[index].minute;
const point = series.get(timestamp) || { minute: timestamp, requests: 0, aborted: 0, errors: 0, sumMs: 0 };
if (field === "count") point.requests += value;
if (field === "sumMs") point.sumMs += value;
if (field === "aborted") point.aborted += value;
if (field === "errors") point.errors += value;
series.set(timestamp, point);
}
}
});
const ids = await redisDeadline(redis.zRangeByScore(`${PREFIX}instances`, Date.now() - 90000, "+inf", { LIMIT: { offset: 0, count: 32 } }), redis);
const samples = ids.length ? await redisDeadline(redis.mGet(ids.map(id => `${PREFIX}instance:${id}`)), redis) : [];
const instances = samples.filter((sample): sample is string => !!sample).map(sample => JSON.parse(sample));
const runtimeSeries: { instance: string; service: string; sampledAt: number; rss: number; heapUsed: number; external: number;
cpuPercent: number; loopP95Ms: number; sockets: number | null; closeWait: number | null; waiting: number; reservedBytes: number }[] = [];
for (const service of ["api", "streamer"]) {
const samples = await redisDeadline(redis.lRange(`${PREFIX}runtime:${service}`, 0, service === "api" ? 239 : 959), redis);
for (const raw of samples) {
const sample = JSON.parse(raw);
if (sample.sampledAt >= Date.now() - windowMinutes * 60000) runtimeSeries.push({ service, instance: sample.instance,
sampledAt: sample.sampledAt, rss: sample.memory.rss, heapUsed: sample.memory.heapUsed, external: sample.memory.external,
cpuPercent: sample.cpuPercent, loopP95Ms: sample.eventLoop.p95Ms, sockets: sample.sockets?.descriptors ?? null,
closeWait: sample.sockets?.closeWait ?? null, waiting: sample.workers?.waiting || 0, reservedBytes: sample.workers?.reservedBytes || 0 });
}
}
const rows = [...groups.values()].map(row => ({ ...row, avgMs: row.count ? Math.round(row.sumMs / row.count) : 0,
p95UpperMs: percentileUpperBound(row.buckets, .95), p99UpperMs: percentileUpperBound(row.buckets, .99) }));
return { available: true, generatedAt: Date.now(), windowMinutes, instances,
routes: rows.filter(row => row.metric === "request").map(row => {
const firstByte = rows.find(sample => sample.metric === "first_byte" && sample.service === row.service && sample.route === row.route && sample.method === row.method);
return { ...row, firstByteCount: firstByte?.count || 0, firstByteP95UpperMs: firstByte?.p95UpperMs ?? null };
}).sort((a, b) => b.sumMs - a.sumMs),
stages: rows.filter(row => row.metric !== "request"), series: [...series.values()].sort((a, b) => a.minute - b.minute),
runtimeSeries, latencyBucketsMs: LATENCY_BUCKETS,
percentileNote: "Percentiles are histogram bucket upper bounds; null means no samples or above 60s." };
});
}
+159
View File
@@ -0,0 +1,159 @@
import { AsyncLocalStorage } from "async_hooks";
import { randomUUID } from "crypto";
import { performance } from "perf_hooks";
import { RequestHandler } from "express";
import { createLogger } from "./logger";
export const LATENCY_BUCKETS = [10, 50, 100, 250, 500, 1000, 2500, 5000, 10000, 30000, 60000];
export type Service = "api" | "streamer";
export type Stage = "repository" | "authorization" | "upstream" | "cache_lookup" | "cache_fill" | "worker_wait" | "anonymize" | "streamer_headers";
export interface Metric {
metric: string; route: string; method: string; count: number; sumMs: number;
bytes: number; aborted: number; errors: number; slow: number; buckets: number[];
}
const logger = createLogger("requests");
const context = new AsyncLocalStorage<{ id: string; service: Service; stages: Partial<Record<Stage, number>> }>();
const methods = new Set(["GET", "HEAD", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"]);
const staticRoutes = new Set([
"/", "/dashboard", "/anonymize", "/healthcheck", "/api/healthcheck", "/api/stat", "/api/stat/history",
"/api/options", "/api/message", "/api/user", "/api/user/quota", "/api/user/dashboard", "/api/repo",
"/api/anonymize-preview", "/api/admin/overview", "/api/admin/performance", "/api/admin/errors",
"/api/admin/errors/stats", "/api/admin/queues", "/api/admin/queues/metrics", "/github/app/webhook",
]);
/** Fixed labels: never use repository IDs, file names or query strings as metric keys. */
export function requestRoute(url: string, service: Service): string {
const path = url.split("?", 1)[0].replace(/\/+$/, "") || "/";
if (service === "streamer") return ["/api/download", "/api", "/healthcheck"].includes(path) ? path : "other";
if (staticRoutes.has(path)) return path;
if (/^\/api\/repo\//.test(path)) {
const match = path.match(/^\/api\/repo\/[^/]+\/(file|files|zip|options|refresh|coauthors|extend)(?:\/|$)/);
if (match) {
const subroute = match[1] === "files" && /\/(counts|search)$/.exec(path);
return `/api/repo/:repoId/${match[1]}${match[1] === "file" ? "/:path" : subroute ? `/${subroute[1]}` : ""}`;
}
const github = path.match(/^\/api\/repo\/[^/]+\/[^/]+\/(readme|branches)$/);
if (github) return `/api/repo/:owner/:repo/${github[1]}`;
return "/api/repo/:repoId";
}
if (path.startsWith("/w/")) return "/w/:repoId/:path";
if (/^\/(r|repository)\//.test(path)) return "/r/:repoId/:path";
if (/^\/api\/(pr|gist)\//.test(path)) return `/api/${path.split("/")[2]}/:id/:path`;
if (/^\/(script|css|favicon|fonts|partials)\//.test(path)) return "/assets/:path";
if (path.startsWith("/github/")) return "/github/:action";
if (path.startsWith("/api/admin/")) return "/api/admin/:action";
if (path.startsWith("/api/user/")) return "/api/user/:action";
return "other";
}
export class RequestMetrics {
private pending = new Map<string, { minute: number; data: Metric }>();
dropped = 0;
constructor(private capacity = 512, private now = () => Date.now()) {}
get size() { return this.pending.size; }
observe(metric: string, route: string, method: string, ms: number,
detail: { bytes?: number; aborted?: boolean; error?: boolean; slow?: boolean } = {}) {
const minute = Math.floor(this.now() / 60000) * 60000;
const key = JSON.stringify([minute, metric, route, method]);
let entry = this.pending.get(key);
if (!entry) {
if (this.pending.size >= this.capacity) { this.dropped++; return; }
entry = { minute, data: { metric, route, method, count: 0, sumMs: 0, bytes: 0,
aborted: 0, errors: 0, slow: 0, buckets: Array(LATENCY_BUCKETS.length + 1).fill(0) } };
this.pending.set(key, entry);
}
const duration = Math.max(0, Number.isFinite(ms) ? ms : 0);
const data = entry.data;
data.count++; data.sumMs += Math.round(duration); data.bytes += detail.bytes || 0;
data.aborted += Number(!!detail.aborted); data.errors += Number(!!detail.error); data.slow += Number(!!detail.slow);
let bucket = LATENCY_BUCKETS.findIndex(bound => duration <= bound);
if (bucket < 0) bucket = LATENCY_BUCKETS.length;
data.buckets[bucket]++;
}
drain() { const entries = [...this.pending.values()]; this.pending.clear(); return entries; }
}
export const requestMetrics = new RequestMetrics();
let activeRequests = 0;
export function activeRequestCount() { return activeRequests; }
export function requestHeaders(): Record<string, string> {
const id = context.getStore()?.id;
return id ? { "x-request-id": id } : {};
}
export function recordCacheHit(cache: "source" | "transformed", hit: boolean) {
if (context.getStore()) requestMetrics.observe(`${cache}_${hit ? "hit" : "miss"}`, "all", "all", 0);
}
/** Capture the context now: worker admission can resume from another request's callback. */
export function startStage(stage: Stage) {
const started = performance.now();
const trace = context.getStore();
let ended = false;
return (error = false) => {
if (ended) return; ended = true;
if (!trace) return;
const ms = performance.now() - started;
trace.stages[stage] = (trace.stages[stage] || 0) + ms;
requestMetrics.observe(stage, "all", "all", ms, { error });
};
}
export async function measureStage<T>(stage: Stage, work: () => Promise<T>): Promise<T> {
const done = startStage(stage);
try { const value = await work(); done(); return value; }
catch (error) { done(true); throw error; }
}
export function monitorRequests(service: Service, metrics = requestMetrics,
log: Pick<typeof logger, "info" | "warn"> = logger): RequestHandler {
return (req, res, next) => {
const forwarded = service === "streamer" ? req.get("x-request-id") : undefined;
const id = forwarded && /^[a-f0-9-]{36}$/.test(forwarded) ? forwarded : randomUUID();
const trace = { id, service, stages: {} };
const route = requestRoute(req.originalUrl, service);
const method = methods.has(req.method) ? req.method : "OTHER";
const started = performance.now();
let firstByteMs: number | undefined;
let bytes = 0;
let ended = false;
activeRequests++;
res.setHeader("X-Request-ID", id);
const writeHead = res.writeHead;
res.writeHead = function (this: typeof res, ...args: unknown[]): typeof res {
firstByteMs ??= performance.now() - started;
return Reflect.apply(writeHead, this, args);
};
const count = (chunk: unknown, encoding: unknown) => {
if (req.method === "HEAD") return;
if (typeof chunk === "string") bytes += Buffer.byteLength(chunk, typeof encoding === "string" ? encoding as BufferEncoding : undefined);
else if (chunk instanceof Uint8Array) bytes += chunk.byteLength;
};
// Preserve callbacks, return values and receiver. Installed before compression,
// these count encoded body bytes without changing stream backpressure.
const write = res.write;
res.write = function (this: typeof res, ...args: unknown[]): boolean {
count(args[0], args[1]);
return Reflect.apply(write, this, args);
};
const end = res.end;
res.end = function (this: typeof res, ...args: unknown[]): typeof res {
count(args[0], args[1]);
return Reflect.apply(end, this, args);
};
const done = () => {
if (ended) return; ended = true;
activeRequests--;
res.removeListener("finish", done); res.removeListener("close", done);
const ms = performance.now() - started;
const aborted = !res.writableFinished;
const transfer = /\/(file|zip)(?:\/|$)/.test(route) || route.startsWith("/w/") || service === "streamer";
const slow = ms > (transfer ? 10000 : 1000);
metrics.observe("request", route, method, ms, { bytes, aborted, error: res.statusCode >= 500, slow });
if (firstByteMs != null) metrics.observe("first_byte", route, method, firstByteMs);
const detail = { requestId: id, service, method, route, status: res.statusCode,
outcome: aborted ? "interrupted" : "completed", ms: Math.round(ms), firstByteMs: firstByteMs == null ? null : Math.round(firstByteMs), bytes, stages: trace.stages };
if (slow || aborted) log.warn(aborted ? "request interrupted" : "slow request", detail);
else if (!route.includes("healthcheck")) log.info("request", detail);
};
res.once("finish", done); res.once("close", done);
context.run(trace, next);
};
}
+33 -27
View File
@@ -18,6 +18,7 @@ import { octokit, waitForTokenGate } from "../GitHubUtils";
import FileModel from "../model/files/files.model";
import { IFile } from "../model/files/files.types";
import { createLogger, serializeError } from "../logger";
import { measureStage, recordCacheHit } from "../request-monitoring";
import config from "../../config";
import { isConnected } from "../../server/database";
import AnonymizedRepositoryModel from "../model/anonymizedRepositories/anonymizedRepositories.model";
@@ -286,7 +287,9 @@ export default class GitHubStream extends GitHubBase {
return undefined;
}
};
if (await cached()) return storage.read(repoId, cachePath);
const hit = await measureStage("cache_lookup", cached);
recordCacheHit("source", !!hit);
if (hit) return storage.read(repoId, cachePath);
if (expected.size != null && expected.size > config.MAX_FILE_SIZE) {
throw new AnonymousError("file_too_big", { httpStatus: 413, object: filePath });
}
@@ -295,37 +298,40 @@ export default class GitHubStream extends GitHubBase {
if (cacheFills.size >= 64) throw new AnonymousError("cache_busy", { httpStatus: 503 });
filling = coordinatedFill(key, cached, async () => {
await assertActive();
const content = await this.downloadWithFallback(await this.data.getToken(), expected.sha, filePath);
let count = 0;
const limited = new stream.Transform({ transform(chunk, encoding, callback) {
count += chunk.length;
callback(count > config.MAX_FILE_SIZE ? new AnonymousError("file_too_big", { httpStatus: 413 }) : null, chunk);
} });
content.on("error", error => {
if (error instanceof AnonymousError) { limited.destroy(error); return; }
const status = (error as { response?: { statusCode?: number }; status?: number; httpStatus?: number });
const httpStatus = status.response?.statusCode ?? status.status ?? status.httpStatus;
const code = httpStatus === 403 ? "file_not_accessible"
: httpStatus === 404 ? "file_not_found"
: httpStatus === 422 ? "file_too_big" : "upstream_error";
limited.destroy(new AnonymousError(code, { httpStatus: httpStatus || 502, cause: error, object: filePath }));
const token = await this.data.getToken();
return measureStage("upstream", async () => {
const content = await this.downloadWithFallback(token, expected.sha, filePath);
let count = 0;
const limited = new stream.Transform({ transform(chunk, encoding, callback) {
count += chunk.length;
callback(count > config.MAX_FILE_SIZE ? new AnonymousError("file_too_big", { httpStatus: 413 }) : null, chunk);
} });
content.on("error", error => {
if (error instanceof AnonymousError) { limited.destroy(error); return; }
const status = (error as { response?: { statusCode?: number }; status?: number; httpStatus?: number });
const httpStatus = status.response?.statusCode ?? status.status ?? status.httpStatus;
const code = httpStatus === 403 ? "file_not_accessible"
: httpStatus === 404 ? "file_not_found"
: httpStatus === 422 ? "file_too_big" : "upstream_error";
limited.destroy(new AnonymousError(code, { httpStatus: httpStatus || 502, cause: error, object: filePath }));
});
limited.once("close", () => content.destroy());
limited.on("error", () => {});
content.pipe(limited);
const deadline = setTimeout(() => limited.destroy(new AnonymousError("upstream_error", { httpStatus: 502 })), 120_000);
try {
await storage.write(repoId, cachePath, limited, this.type, expected.size);
try { await assertActive(); }
catch (error) { await storage.rm(repoId, cachePath); throw error; }
}
finally { clearTimeout(deadline); limited.destroy(); content.destroy(); }
return true;
});
limited.once("close", () => content.destroy());
limited.on("error", () => {});
content.pipe(limited);
const deadline = setTimeout(() => limited.destroy(new AnonymousError("upstream_error", { httpStatus: 502 })), 120_000);
try {
await storage.write(repoId, cachePath, limited, this.type, expected.size);
try { await assertActive(); }
catch (error) { await storage.rm(repoId, cachePath); throw error; }
}
finally { clearTimeout(deadline); limited.destroy(); content.destroy(); }
return true;
}).then(() => {});
cacheFills.set(key, filling);
void filling.finally(() => { if (cacheFills.get(key) === filling) cacheFills.delete(key); }).catch(() => {});
}
await filling;
await measureStage("cache_fill", () => filling!);
return storage.read(repoId, cachePath);
}
+3 -1
View File
@@ -8,6 +8,7 @@ import { isConnected } from "../server/database";
import RepositoryModel from "./model/anonymizedRepositories/anonymizedRepositories.model";
import { RepositoryStatus } from "./types";
import AnonymousError from "./AnonymousError";
import { recordCacheHit } from "./request-monitoring";
const root = join(tmpdir(), "anonymous-transformed-v1");
const TTL = 3600_000;
const DISK_BUDGET = 512 * 1024 * 1024;
@@ -62,8 +63,9 @@ export async function transformedFile(digest: string, options: unknown, output:
};
const hit = await read();
if (hit !== undefined) {
try { return await copyCached(hit); } catch { /* eviction: retry the transformation */ }
try { const copied = await copyCached(hit); recordCacheHit("transformed", true); return copied; } catch { /* eviction: retry the transformation */ }
}
recordCacheHit("transformed", false);
const pending = fills.get(key);
if (pending) {
await pending.catch(() => {});
+5
View File
@@ -12,6 +12,7 @@ import {
compileTerms,
} from "./anonymize-utils";
import { createLogger, serializeError } from "./logger";
import { startStage } from "./request-monitoring";
const logger = createLogger("zip-stream");
@@ -111,7 +112,11 @@ export async function streamAnonymizedZip(
},
});
}
const upstreamDone = startStage("upstream");
const downloadStream = got.stream(response.url);
downloadStream.once("end", () => upstreamDone());
downloadStream.once("error", () => upstreamDone(true));
downloadStream.once("close", () => upstreamDone(!downloadStream.readableEnded));
const archive = archiver("zip", {});
const parser = Parse() as Transform;
+5 -14
View File
@@ -36,6 +36,9 @@ import { getUser } from "./routes/route-utils";
import config from "../config";
import { resolveTrustProxy, isCloudflareIP } from "./trustProxy";
import { createLogger, serializeError } from "../core/logger";
import { monitorRequests } from "../core/request-monitoring";
import { startPerformanceMonitoring } from "../core/performance-monitoring";
import { getAnonymizationPoolStats } from "../core/anonymization-pool";
const logger = createLogger("server");
@@ -99,6 +102,8 @@ function indexResponse(req: express.Request, res: express.Response) {
export default async function start() {
const app = express();
app.use(monitorRequests("api"));
startPerformanceMonitoring("api", getAnonymizationPoolStats);
app.set("query parser", "extended");
app.use("/github/app/webhook", githubAppWebhook);
app.use(express.json());
@@ -228,20 +233,6 @@ export default async function start() {
keyGenerator,
});
app.use(function (req, res, next) {
const start = Date.now();
res.on("finish", function () {
const time = Date.now() - start;
logger.info("request", {
method: req.method,
status: res.statusCode,
url: join(req.baseUrl || "", req.url || ""),
ms: time,
});
});
next();
});
app.use("/github", rate, speedLimiter, githubAppRouter);
app.use("/github", rate, speedLimiter, connectionRouter);
+15 -2
View File
@@ -1,6 +1,7 @@
import { AsyncCache } from "../../core/async-cache";
import * as os from "os";
import { execSync } from "child_process";
import { execFile } from "child_process";
import { promisify } from "util";
import { Queue, JobType } from "bullmq";
import * as express from "express";
import AnonymousError from "../../core/AnonymousError";
@@ -30,8 +31,11 @@ import {
} from "../../core/logger";
import { createClient, RedisClientType } from "redis";
import config from "../../config";
import { performanceReport } from "../../core/performance-monitoring";
const logger = createLogger("admin");
const execFileAsync = promisify(execFile);
const diskUsageCache = new AsyncCache<string>(30000, 1);
let errorLogClient: RedisClientType | null = null;
@@ -78,6 +82,12 @@ router.use(
router.use("/tokens", adminTokensRouter);
router.get("/performance", async (req, res) => {
res.set("Cache-Control", "no-store");
try { res.json(await performanceReport(Number(req.query.minutes))); }
catch { res.json({ available: false, instances: [], routes: [], stages: [] }); }
});
function dashboardCache(
_req: express.Request,
res: express.Response,
@@ -627,7 +637,10 @@ router.get("/overview", async (req, res) => {
// Disk usage via df (root partition)
let diskTotal = 0, diskUsed = 0, diskFree = 0, diskPercent = 0, diskMount = "/";
try {
const dfOut = execSync("df -k / 2>/dev/null", { timeout: 3000 }).toString();
const dfOut = await diskUsageCache.get("root", async () => {
const result = await execFileAsync("df", ["-k", "/"], { timeout: 3000, maxBuffer: 16384 });
return result.stdout;
});
const lines = dfOut.trim().split("\n");
if (lines.length >= 2) {
const cols = lines[1].split(/\s+/);
+12 -1
View File
@@ -3,6 +3,9 @@ import AnonymizedFile from "../../core/AnonymizedFile";
import AnonymousError from "../../core/AnonymousError";
import { getRepo, handleError } from "./route-utils";
import { fileETag } from "./file-etag";
import config from "../../config";
import { lookup } from "mime-types";
import { isTextFile } from "../../core/anonymize-utils";
export const router = express.Router();
@@ -142,9 +145,17 @@ router.get(
// anonymization term list left old anonymizations cached under the
// same URL.
res.header("Cache-Control", "private, no-cache, must-revalidate");
if (req.headers["if-none-match"] === etag) {
if (req.fresh) {
return res.status(304).end();
}
if (req.method === "HEAD") {
const size = await f.size();
if (size != null && size > config.MAX_FILE_SIZE) throw new AnonymousError("file_too_big", { httpStatus: 413 });
const mime = lookup(originalPath);
if (mime && !originalPath.endsWith(".ts")) res.type(mime);
else if (isTextFile(originalPath)) res.type("text/plain");
res.end(); return;
}
await f.send(res);
await repo.countView();
} catch (error) {
+8
View File
@@ -14,6 +14,7 @@ import FileModel from "../../core/model/files/files.model";
import { createLogger, serializeError } from "../../core/logger";
import { githubTokenForStreamer } from "../../core/github-token-context";
import gh = require("parse-github-url");
import { requestHeaders, startStage } from "../../core/request-monitoring";
const logger = createLogger("repository-public");
@@ -52,16 +53,20 @@ router.get(
});
}
res.attachment(`${repo.repoId}.zip`);
if (req.method === "HEAD") { res.end(); return; }
await repo.countView();
if (config.STREAMER_ENTRYPOINT) {
// use the streamer service
const token = await repo.getToken();
const anonymizer = repo.generateAnonymizeTransformer("");
const headersDone = startStage("streamer_headers");
res.attachment(`${repo.repoId}.zip`);
const reqStream = got
.stream(join(config.STREAMER_ENTRYPOINT, "api/download"), {
method: "POST",
headers: requestHeaders(),
json: {
token: await githubTokenForStreamer(token, repo.model.source.repositoryName),
repoFullName: repo.model.source.repositoryName,
@@ -76,6 +81,7 @@ router.get(
},
})
.on("error", (err: Error & { response?: { statusCode?: number; body?: unknown } }) => {
headersDone(true);
const upstreamStatus = err?.response?.statusCode;
let upstreamBody: string | undefined;
let errCode = "zip_not_available";
@@ -130,6 +136,8 @@ router.get(
// instead of waiting forever for the ZIP's central directory.
if (streaming && !res.writableEnded) res.destroy();
});
reqStream.once("response", () => headersDone());
reqStream.once("close", () => headersDone());
reqStream.pipe(res);
res.on("close", () => {
reqStream.destroy();
+3 -1
View File
@@ -8,6 +8,7 @@ import { HTTPError } from "got";
import { RepositoryStatus } from "../../core/types";
import { createLogger, serializeError } from "../../core/logger";
import { isDisabledAccount } from "./auth-utils";
import { measureStage } from "../../core/request-monitoring";
const logger = createLogger("route");
@@ -81,7 +82,8 @@ export async function getRepo(
if (typeof req.params.repoId !== "string") {
throw new AnonymousError("invalid_path", { httpStatus: 400 });
}
const repo = await db.getRepository(req.params.repoId);
const repoId = req.params.repoId;
const repo = await measureStage("repository", () => db.getRepository(repoId));
if (opt.nocheck !== true) {
// redirect if the repository is expired
if (
+19 -1
View File
@@ -5,8 +5,11 @@ import AnonymizedFile from "../../core/AnonymizedFile";
import AnonymousError from "../../core/AnonymousError";
import * as marked from "marked";
import * as sanitizeHtml from "sanitize-html";
import { streamToString } from "../../core/anonymize-utils";
import { streamToString, isTextFile } from "../../core/anonymize-utils";
import { IFile } from "../../core/model/files/files.types";
import { fileETag } from "./file-etag";
import { lookup } from "mime-types";
import config from "../../config";
function escapeHtml(str: string): string {
return str
@@ -170,6 +173,19 @@ async function webView(
object: f,
});
}
res.set("Cache-Control", "private, no-cache, must-revalidate");
const pageETag = fileETag(JSON.stringify(["web-v1", repo.model.source.commit, repo.model.treeGeneration, repo.model.contentCacheRevision,
await f.sha(), repo.model.source.repositoryName, repo.model.source.branch, config.APP_HOSTNAME, config.ANONYMIZATION_MASK]),
requestPath, repo.options);
res.set("ETag", pageETag);
if (req.fresh) { res.status(304).end(); return; }
if (req.method === "HEAD") {
const size = await f.size();
if (size != null && size > config.MAX_FILE_SIZE) throw new AnonymousError("file_too_big", { httpStatus: 413 });
const mime = lookup(requestPath);
res.type(f.extension() === "md" ? "text/html" : mime && !requestPath.endsWith(".ts") ? mime : isTextFile(requestPath) ? "text/plain" : "application/octet-stream");
res.end(); return;
}
if (f.extension() == "md") {
try {
const content = await streamToString(await f.anonymizedContent());
@@ -177,6 +193,8 @@ async function webView(
const html = `<!DOCTYPE html><html><head><title>Content</title></head><link rel="stylesheet" href="/css/all.min.css" /><body><div class="container p-3 file-content markdown-body">${body}</div></body></html>`;
res.contentType("text/html").send(html);
} catch {
// Raw Markdown must re-enter rendering when a transient failure clears.
res.set("ETag", fileETag(`web-raw:${pageETag}`, requestPath, repo.options));
await f.send(res);
}
} else {
+5
View File
@@ -11,10 +11,14 @@ import AnonymousError from "../core/AnonymousError";
import { createLogger, serializeError } from "../core/logger";
import { connect } from "../server/database";
import { startTemporaryStorageMaintenance } from "../core/temporary-storage";
import { monitorRequests } from "../core/request-monitoring";
import { startPerformanceMonitoring } from "../core/performance-monitoring";
import { getAnonymizationPoolStats } from "../core/anonymization-pool";
const logger = createLogger("streamer");
const app = express();
app.use(monitorRequests("streamer"));
app.use(express.json());
app.use(
@@ -46,6 +50,7 @@ app.all("/{*path}", (req, res) => {
);
});
async function start() {
startPerformanceMonitoring("streamer", getAnonymizationPoolStats);
await connect({ appName: "Anonymous GitHub Streamer", maxPoolSize: 10, minPoolSize: 0 });
await startTemporaryStorageMaintenance();
app.listen(config.PORT, (error?: Error) => {
+107
View File
@@ -0,0 +1,107 @@
const { expect } = require("chai");
const express = require("express");
const http = require("node:http");
const { Readable } = require("node:stream");
require("ts-node/register/transpile-only");
const utils = require("../src/server/routes/route-utils");
const AnonymizedFile = require("../src/core/AnonymizedFile").default;
const fileRouter = require("../src/server/routes/file").default;
const webRouter = require("../src/server/routes/webview").default;
const config = require("../src/config").default;
describe("download headers and website revalidation", function () {
let server, restore, repo, transfers, blobSha;
beforeEach(async function () {
const originalGetRepo = utils.getRepo;
const originals = Object.fromEntries(["originalPath", "sha", "size", "send", "getFileInfo", "anonymizedContent"].map(key => [key, AnonymizedFile.prototype[key]]));
restore = () => { utils.getRepo = originalGetRepo; Object.assign(AnonymizedFile.prototype, originals); };
transfers = 0; blobSha = "first";
repo = { repoId: "fixture", options: { terms: ["author"], image: true, pdf: true, page: true, pageSource: { branch: "main", path: "/" } },
model: { source: { commit: "commit", branch: "main" }, treeGeneration: "first" }, isReady: async () => true, countView: async () => {} };
utils.getRepo = async () => repo;
AnonymizedFile.prototype.originalPath = async function () { return this.anonymizedPath; };
AnonymizedFile.prototype.sha = async () => blobSha;
AnonymizedFile.prototype.size = async () => 10;
AnonymizedFile.prototype.getFileInfo = async () => ({ name: "page.md", path: "", size: 10 });
AnonymizedFile.prototype.send = async function (res) { transfers++; res.send("fixture content"); };
AnonymizedFile.prototype.anonymizedContent = async () => { transfers++; return Readable.from(["# Anonymized page"]); };
const app = express(); app.use("/api/repo", fileRouter); app.use("/w", webRouter);
server = await new Promise(resolve => { const s = app.listen(0, "127.0.0.1", () => resolve(s)); });
});
afterEach(async function () { server.closeAllConnections(); await new Promise(resolve => server.close(resolve)); restore(); });
function fetch(path, method = "GET", headers = {}) {
return new Promise((resolve, reject) => {
http.request({ host: "127.0.0.1", port: server.address().port, path, method, headers }, res => {
let body = ""; res.on("data", chunk => { body += chunk; });
res.on("end", () => resolve({ status: res.statusCode, headers: res.headers, body }));
}).on("error", reject).end();
});
}
it("serves file HEAD metadata without reading content", async function () {
const response = await fetch("/api/repo/fixture/file/data.txt", "HEAD");
expect(response.status).to.equal(200); expect(response.body).to.equal("");
expect(response.headers["etag"]).to.match(/^"f-/); expect(response.headers["content-type"]).to.include("text/plain");
expect(transfers).to.equal(0);
});
it("preserves content gates for HEAD", async function () {
repo.options.pdf = false;
expect((await fetch("/api/repo/fixture/file/private.pdf", "HEAD")).status).to.equal(403);
expect(transfers).to.equal(0);
});
it("rejects oversized file HEAD without downloading", async function () {
AnonymizedFile.prototype.size = async () => config.MAX_FILE_SIZE + 1;
expect((await fetch("/api/repo/fixture/file/huge.txt", "HEAD")).status).to.equal(413); expect(transfers).to.equal(0);
});
it("accepts weak and multiple validators without transferring file content", async function () {
const first = await fetch("/api/repo/fixture/file/data.txt", "HEAD");
const response = await fetch("/api/repo/fixture/file/data.txt", "GET", { "If-None-Match": `"unrelated", W/${first.headers.etag}` });
expect(response.status).to.equal(304); expect(transfers).to.equal(0);
});
it("revalidates a Markdown website before fetching or rendering it again", async function () {
const first = await fetch("/w/fixture/page.md");
expect(first.status).to.equal(200); expect(first.body).to.include("Anonymized page"); expect(transfers).to.equal(1);
const second = await fetch("/w/fixture/page.md", "GET", { "If-None-Match": `W/${first.headers.etag}` });
expect(second.status).to.equal(304); expect(second.body).to.equal(""); expect(transfers).to.equal(1);
expect(second.headers["content-security-policy"]).to.include("sandbox");
});
it("renders Markdown again after a transient failure served the raw fallback", async function () {
const marked = require("marked"), render = marked.marked;
let first;
try {
marked.marked = () => { throw Error("transient rendering failure"); };
first = await fetch("/w/fixture/page.md");
expect(first.status).to.equal(200); expect(first.body).to.equal("fixture content");
} finally { marked.marked = render; }
const recovered = await fetch("/w/fixture/page.md", "GET", { "If-None-Match": first.headers.etag });
expect(recovered.status).to.equal(200); expect(recovered.body).to.include("Anonymized page");
expect(recovered.headers.etag).not.to.equal(first.headers.etag);
const revalidated = await fetch("/w/fixture/page.md", "GET", { "If-None-Match": recovered.headers.etag });
expect(revalidated.status).to.equal(304); expect(transfers).to.equal(3);
});
it("serves website HEAD without reading or rendering Markdown", async function () {
const response = await fetch("/w/fixture/page.md", "HEAD");
expect(response.status).to.equal(200); expect(response.headers["content-type"]).to.include("text/html"); expect(transfers).to.equal(0);
});
it("serves TypeScript website HEAD with the same text MIME policy as GET", async function () {
const response = await fetch("/w/fixture/code.ts", "HEAD");
expect(response.status).to.equal(200); expect(response.headers["content-type"]).to.include("text/plain"); expect(transfers).to.equal(0);
});
for (const change of ["terms", "commit", "tree", "cache", "blob", "source name"]) {
it(`invalidates the website validator when ${change} changes`, async function () {
const first = await fetch("/w/fixture/page.md");
if (change === "terms") repo.options.terms = ["new author"];
if (change === "commit") repo.model.source.commit = "new commit";
if (change === "tree") repo.model.treeGeneration = "new tree";
if (change === "cache") repo.model.contentCacheRevision = "new cache";
if (change === "blob") blobSha = "new blob";
if (change === "source name") repo.model.source.repositoryName = "new/source";
const second = await fetch("/w/fixture/page.md", "GET", { "If-None-Match": first.headers.etag });
expect(second.status).to.equal(200); expect(second.headers.etag).not.to.equal(first.headers.etag); expect(transfers).to.equal(2);
});
}
it("checks website permissions before honoring an old validator", async function () {
const first = await fetch("/w/fixture/page.md"); repo.options.page = false;
expect((await fetch("/w/fixture/page.md", "GET", { "If-None-Match": first.headers.etag })).status).to.equal(400);
expect(transfers).to.equal(1);
});
});
+32
View File
@@ -0,0 +1,32 @@
const { expect } = require("chai");
const express = require("express");
const http = require("node:http");
require("ts-node/register/transpile-only");
const utils = require("../src/server/routes/route-utils");
const admin = require("../src/server/routes/admin").default;
describe("performance report access", function () {
let server, original;
before(async function () {
original = utils.getUser;
utils.getUser = async req => ({ isAdmin: req.get("x-fixture-user") === "admin", model: { id: "fixture" } });
const app = express();
app.use((req, _res, next) => { req.isAuthenticated = () => !!req.get("x-fixture-user"); next(); });
app.use("/api/admin", admin);
server = await new Promise(resolve => { const s = app.listen(0, "127.0.0.1", () => resolve(s)); });
});
after(async function () { server.closeAllConnections(); await new Promise(resolve => server.close(resolve)); utils.getUser = original; });
async function request(user) {
return new Promise((resolve, reject) => {
http.get({ host: "127.0.0.1", port: server.address().port, path: "/api/admin/performance", headers: user ? { "x-fixture-user": user } : {} }, res => {
let body = ""; res.on("data", chunk => { body += chunk; }); res.on("end", () => resolve({ status: res.statusCode, body, headers: res.headers }));
}).on("error", reject);
});
}
it("rejects anonymous requests", async function () { expect((await request()).status).to.equal(401); });
it("rejects non-admin users", async function () { expect((await request("regular")).status).to.equal(401); });
it("allows admins and disables HTTP caching", async function () {
const result = await request("admin"); expect(result.status).to.equal(200);
expect(JSON.parse(result.body).available).to.equal(false); expect(result.headers["cache-control"]).to.equal("no-store");
});
});
@@ -0,0 +1,95 @@
const { expect } = require("chai");
const { createClient } = require("redis");
const net = require("node:net");
const { setTimeout } = require("node:timers");
require("ts-node/register/transpile-only");
const config = require("../src/config").default;
const { requestMetrics } = require("../src/core/request-monitoring");
const { startPerformanceMonitoring, flushPerformanceMetrics, performanceReport } = require("../src/core/performance-monitoring");
const port = Number(process.env.PERFORMANCE_REDIS_PORT);
(port ? describe : describe.skip)("request monitoring persistence", function () {
this.timeout(15000);
let stop, redis, previous;
before(async function () {
previous = { host: config.REDIS_HOSTNAME, port: config.REDIS_PORT };
config.REDIS_HOSTNAME = "127.0.0.1"; config.REDIS_PORT = port;
redis = createClient({ socket: { host: "127.0.0.1", port, reconnectStrategy: false } }); await redis.connect();
});
beforeEach(async function () {
const keys = [];
for await (const key of redis.scanIterator({ MATCH: "performance:v1:*" })) keys.push(key);
if (keys.length) await redis.del(keys);
requestMetrics.drain();
});
afterEach(async function () { if (stop) { await stop(); stop = null; } requestMetrics.drain(); });
after(async function () { await redis.disconnect(); config.REDIS_HOSTNAME = previous.host; config.REDIS_PORT = previous.port; });
async function start(service) {
stop = startPerformanceMonitoring(service, () => ({ running: 0, waiting: 0 }));
const deadline = Date.now() + 4000;
while (!(await performanceReport(15)).available) {
if (Date.now() > deadline) throw Error("Monitoring did not connect");
await new Promise(resolve => setTimeout(resolve, 20));
}
}
it("batches latency, failures, interruptions and resource history with bounded retention", async function () {
await start("api");
requestMetrics.observe("request", "/api/repo/:repoId/zip", "GET", 8000, { bytes: 12345 });
requestMetrics.observe("request", "/api/repo/:repoId/zip", "GET", 1500, { aborted: true, error: true });
requestMetrics.observe("upstream", "all", "all", 90);
await flushPerformanceMetrics();
const result = await performanceReport(15);
expect(result.available).to.equal(true); expect(result.routes).to.have.length(1);
expect(result.routes[0]).to.include({ count: 2, aborted: 1, errors: 1, bytes: 12345, avgMs: 4750, p95UpperMs: 10000 });
expect(result.stages[0]).to.include({ metric: "upstream", count: 1 });
expect(result.instances).to.have.length(1); expect(result.instances[0].memory.rss).to.be.greaterThan(0);
expect(result.runtimeSeries).to.have.length(1); expect(result.series[0].requests).to.equal(2);
const minuteKey = `performance:v1:minute:api:${Math.floor(Date.now() / 60000) * 60000}`;
expect(await redis.ttl(minuteKey)).to.be.within(7100, 7200);
expect(await redis.ttl(`performance:v1:instance:${result.instances[0].instance}`)).to.be.within(80, 90);
await flushPerformanceMetrics(); expect((await performanceReport(15)).routes[0].count).to.equal(2);
});
it("retains counters across process restarts and combines all streamers", async function () {
await start("api"); requestMetrics.observe("request", "/api/repo/:repoId/file/:path", "GET", 20);
await flushPerformanceMetrics(); await stop(); stop = null;
await start("streamer"); requestMetrics.observe("request", "/api", "POST", 10); await flushPerformanceMetrics();
const result = await performanceReport(60);
expect(result.routes.map(row => row.service).sort()).to.deep.equal(["api", "streamer"]);
expect(result.runtimeSeries.map(row => row.service).sort()).to.deep.equal(["api", "streamer"]);
expect(result.windowMinutes).to.equal(60);
});
it("bounds requested history and excludes expired process heartbeats", async function () {
await start("api"); await flushPerformanceMetrics();
await redis.zAdd("performance:v1:instances", { score: Date.now() - 100000, value: "retired" });
await redis.set("performance:v1:instance:retired", '{"instance":"retired"}');
const result = await performanceReport(1000000);
expect(result.windowMinutes).to.equal(15); expect(result.instances).to.have.length(1);
expect(result.instances[0].instance).not.to.equal("retired");
});
it("returns unavailable promptly and stays bounded when the Redis endpoint never replies", async function () {
const connections = [];
const blackhole = net.createServer(socket => { connections.push(socket); socket.on("error", () => {}); });
await new Promise(resolve => blackhole.listen(0, "127.0.0.1", resolve));
config.REDIS_PORT = blackhole.address().port;
try {
stop = startPerformanceMonitoring("api");
for (let i = 0; i < 10000; i++) requestMetrics.observe("request", "other", "GET", 10);
const started = Date.now(); await flushPerformanceMetrics();
expect((await performanceReport(60)).available).to.equal(false);
expect(Date.now() - started).to.be.lessThan(500); expect(requestMetrics.size).to.equal(0);
await stop(); stop = null;
} finally {
connections.forEach(socket => socket.destroy()); await new Promise(resolve => blackhole.close(resolve)); config.REDIS_PORT = port;
}
});
it("cuts off an in-flight batch when a connected Redis server stops responding", async function () {
await start("api");
requestMetrics.observe("request", "other", "GET", 15);
await redis.sendCommand(["CLIENT", "PAUSE", "2500", "ALL"]);
const started = Date.now();
await flushPerformanceMetrics();
expect(Date.now() - started).to.be.within(1800, 2800);
expect(requestMetrics.size).to.equal(0);
expect((await performanceReport(15)).available).to.equal(false);
});
});
+1
View File
@@ -217,6 +217,7 @@ describe("production regressions", function () {
function response() {
return { headers: {}, header(key, value) { this.headers[key] = value; return this; },
set(key, value) { return this.header(key, value); },
contentType() { return this; }, status(value) { this.statusCode = value; return this; }, end() {}, send() {} };
}
function fileRoute(originalName) {
+176
View File
@@ -0,0 +1,176 @@
const { expect } = require("chai");
const express = require("express");
const compression = require("compression");
const http = require("node:http");
const net = require("node:net");
const { setTimeout } = require("node:timers");
const { gunzipSync } = require("node:zlib");
const { Readable } = require("node:stream");
const { setTimeout: delay } = require("node:timers/promises");
require("ts-node/register/transpile-only");
const { RequestMetrics, monitorRequests, requestRoute, requestHeaders, startStage, requestMetrics, activeRequestCount } = require("../src/core/request-monitoring");
const { percentileUpperBound, socketSnapshot, performanceReport } = require("../src/core/performance-monitoring");
const GitHubStream = require("../src/core/source/GitHubStream").default;
const storage = require("../src/core/storage").default;
function fetch(server, path, options = {}) {
return new Promise((resolve, reject) => {
http.request({ host: "127.0.0.1", port: server.address().port, path, ...options }, res => {
const chunks = []; res.on("data", chunk => chunks.push(chunk)); res.on("error", reject);
res.on("end", () => resolve({ status: res.statusCode, headers: res.headers, body: Buffer.concat(chunks) }));
}).on("error", reject).end();
});
}
describe("bounded request monitoring", function () {
let server, metrics, logs, app;
beforeEach(function () {
metrics = new RequestMetrics(); logs = []; requestMetrics.drain();
app = express(); app.use(monitorRequests("api", metrics, { info: (...args) => logs.push(args), warn: (...args) => logs.push(args) }));
app.use(compression({ filter: () => true }));
});
afterEach(async function () {
if (server) { server.closeAllConnections(); await new Promise(resolve => server.close(resolve)); server = null; }
requestMetrics.drain();
});
async function listen() { server = await new Promise(resolve => { const s = app.listen(0, "127.0.0.1", () => resolve(s)); }); }
it("keeps labels bounded and removes source identities and query values", function () {
expect(requestRoute("/api/repo/private-name/file/secret.txt?token=abc", "api")).to.equal("/api/repo/:repoId/file/:path");
expect(requestRoute("/api/repo/secret-owner/secret-repo/readme?branch=secret", "api")).to.equal("/api/repo/:owner/:repo/readme");
expect(requestRoute("/api/repo/id/files/counts", "api")).to.equal("/api/repo/:repoId/files/counts");
const labels = new Set(Array.from({ length: 10000 }, (_, i) => requestRoute(`/unknown-${i}/private-${i}`, "api")));
expect([...labels]).to.deep.equal(["other"]);
expect(requestRoute("/healthcheck", "streamer")).to.equal("/healthcheck");
const bounded = new RequestMetrics(3);
for (let i = 0; i < 1000; i++) bounded.observe("request", String(i), "GET", 10);
expect(bounded.size).to.equal(3); expect(bounded.dropped).to.equal(997);
});
it("records histogram upper bounds and releases the batch after flushing", function () {
[1, 20, 51, 150, 501, 50000, 70000].forEach(ms => metrics.observe("request", "other", "GET", ms));
const row = metrics.drain()[0].data;
expect(row.count).to.equal(7); expect(row.sumMs).to.equal(120723);
expect(percentileUpperBound(row.buckets, .5)).to.equal(250);
expect(percentileUpperBound(row.buckets, .95)).to.equal(null);
expect(metrics.size).to.equal(0); expect(metrics.drain()).to.deep.equal([]);
});
for (const gzip of [false, true]) {
it(`preserves backpressure and ${gzip ? "gzip" : "plain"} bytes`, async function () {
this.timeout(10000);
const body = Buffer.alloc(2 * 1024 * 1024, "content-");
let drains = 0, callbacks = 0;
app.get("/api/repo/:repoId/file/*path", async (_req, res) => {
res.type("text/plain");
for (let i = 0; i < body.length; i += 16384) {
if (!res.write(body.subarray(i, i + 16384), () => callbacks++)) {
drains++;
await new Promise(resolve => {
const drained = () => { emitter.removeListener("drain", drained); resolve(); };
const emitter = res.on("drain", drained);
});
}
}
res.end();
});
await listen();
const result = await fetch(server, "/api/repo/private/file/private.txt?token=secret", { headers: { "Accept-Encoding": gzip ? "gzip" : "identity" } });
expect(gzip ? gunzipSync(result.body) : result.body).to.deep.equal(body);
expect(drains).to.be.greaterThan(0);
// Compression omits write callbacks; the plain writable must retain them.
expect(callbacks).to.equal(gzip ? 0 : body.length / 16384);
const row = metrics.drain().find(row => row.data.metric === "request").data;
expect(row.bytes).to.equal(result.body.length); expect(row.aborted).to.equal(0); expect(row.count).to.equal(1);
expect(JSON.stringify(logs)).not.to.include("private.txt"); expect(JSON.stringify(logs)).not.to.include("secret");
expect(logs[0][1].requestId).to.equal(result.headers["x-request-id"]);
expect(activeRequestCount()).to.equal(0);
});
}
it("counts HEAD as a zero-byte response even when a handler writes a body", async function () {
app.get("/", (_req, res) => res.send("body omitted by Node")); await listen();
const result = await fetch(server, "/", { method: "HEAD" });
expect(result.body.length).to.equal(0);
expect(metrics.drain().find(row => row.data.metric === "request").data.bytes).to.equal(0);
});
for (const headersSent of [false, true]) {
it(`records a disconnect ${headersSent ? "after" : "before"} headers exactly once`, async function () {
let arrived; const ready = new Promise(resolve => { arrived = resolve; });
let finished; const terminal = new Promise(resolve => { finished = resolve; });
app.get("/", (_req, res) => { if (headersSent) { res.type("text/plain"); res.write("partial"); } arrived(); res.on("close", finished); });
await listen();
const client = http.get({ host: "127.0.0.1", port: server.address().port, path: "/" }); client.on("error", () => {});
await ready; client.destroy(); await terminal;
expect(logs).to.have.length(1); expect(logs[0][0]).to.equal("request interrupted");
expect(logs[0][1].outcome).to.equal("interrupted");
const row = metrics.drain().find(row => row.data.metric === "request").data;
expect(row.count).to.equal(1); expect(row.aborted).to.equal(1); expect(activeRequestCount()).to.equal(0);
if (!headersSent) expect(logs[0][1].firstByteMs).to.equal(null);
});
}
it("keeps overlapping request IDs and asynchronous stages isolated", async function () {
let doneA, doneB;
app.get("/a", (_req, res) => { doneA = startStage("upstream"); setTimeout(() => { doneA(); doneA(); res.json(requestHeaders()); }, 30); });
app.get("/b", (_req, res) => { doneB = startStage("repository"); setTimeout(() => { doneB(); res.json(requestHeaders()); }, 10); });
await listen(); const results = await Promise.all([fetch(server, "/a"), fetch(server, "/b")]);
for (const result of results) expect(JSON.parse(result.body)["x-request-id"]).to.equal(result.headers["x-request-id"]);
expect(results[0].headers["x-request-id"]).not.to.equal(results[1].headers["x-request-id"]);
expect(logs.filter(row => row[1].stages.upstream)).to.have.length(1);
expect(logs.filter(row => row[1].stages.repository)).to.have.length(1);
expect(requestMetrics.drain().reduce((count, row) => count + row.data.count, 0)).to.equal(2);
expect(requestHeaders()).to.deep.equal({});
});
it("covers delays and errors before route handling", async function () {
app.use((_req, _res, next) => setTimeout(next, 35)); app.use(express.json());
app.get("/", (_req, res) => res.status(503).json({ error: "unavailable" })); await listen();
const response = await fetch(server, "/"); expect(response.status).to.equal(503);
const row = metrics.drain().find(row => row.data.metric === "request").data;
expect(row.errors).to.equal(1); expect(row.sumMs).to.be.at.least(30);
});
for (const failure of [false, true]) {
it(`includes GitHub response-header waits in upstream timing${failure ? " on failure" : ""}`, async function () {
const originals = { fileInfo: storage.fileInfo, write: storage.write, read: storage.read };
const source = new GitHubStream({ repoId: `headers-${failure}`, organization: "owner", repoName: "repo", commit: "abc", getToken: async () => "token" });
const upstreamError = Error("headers failed");
source.downloadWithFallback = async () => {
await delay(120);
if (failure) throw upstreamError;
return Readable.from([Buffer.from("content")]);
};
storage.fileInfo = async () => { throw Error("cache miss"); };
storage.write = async (_repo, _path, input) => { for await (const chunk of input) void chunk; };
storage.read = async () => Readable.from([Buffer.from("content")]);
app.get("/", async (_req, res) => {
try {
const input = await source.getFileContentCache("file.txt", source.data.repoId, () => ({ sha: "blob", size: 7 }));
input.destroy(); res.send("ok");
} catch (error) { res.status(502).send(error === upstreamError ? "original error" : "unexpected error"); }
});
try {
await listen(); const response = await fetch(server, "/");
expect(response.status).to.equal(failure ? 502 : 200);
if (failure) expect(response.body.toString()).to.equal("original error");
expect(logs[0][1].stages.upstream).to.be.at.least(100);
const row = requestMetrics.drain().find(entry => entry.data.metric === "upstream").data;
expect(row.count).to.equal(1); expect(row.errors).to.equal(Number(failure));
} finally { Object.assign(storage, originals); }
});
}
it("counts PID-owned network sockets without blocking the event loop", async function () {
const listener = net.createServer(socket => socket.end()); await new Promise(resolve => listener.listen(0, "127.0.0.1", resolve));
try {
const snapshot = await socketSnapshot();
if (process.platform === "linux") { expect(snapshot.listening).to.be.at.least(1); expect(snapshot.truncated).to.equal(false); }
} finally { await new Promise(resolve => listener.close(resolve)); }
});
it("reports missing monitoring as unavailable", async function () {
expect((await performanceReport(60000)).available).to.equal(false);
});
});
+53
View File
@@ -446,6 +446,59 @@ describe("Vue 3 UI", function () {
expect(ui.errors).to.deep.equal([]);
});
it("shows latency bounds, interruptions and resource samples on the admin overview", async function () {
const sample = { instance: "streamer:fixture", service: "streamer", sampledAt: Date.now(), cpuPercent: 2,
memory: { rss: 16 * 1024 * 1024 }, memoryLimitBytes: 128 * 1024 * 1024,
eventLoop: { p95Ms: 20 }, activeRequests: 2, sockets: { descriptors: 15, closeWait: 1 },
workers: { running: 1, waiting: 2 }, droppedBatches: 0, droppedMetrics: 0 };
const row = { service: "api", method: "GET", route: "/api/repo/:repoId/file/:path", count: 10,
avgMs: 120, p95UpperMs: 250, p99UpperMs: 500, aborted: 1, errors: 0, slow: 0, firstByteCount: 10, firstByteP95UpperMs: 50 };
ui = await browser("/", { "/api/user": { username: "admin", isAdmin: true },
"/api/admin/overview": { history: [] },
"/api/admin/performance": { available: true, instances: [sample], routes: [row], stages: [],
runtimeSeries: [{ instance: sample.instance, rss: 20 * 1024 * 1024 }] } });
await ui.go("/admin/");
const text = ui.window.document.querySelector(".app-view").textContent;
expect(text).to.include("Request performance"); expect(text).to.include("\u2264 250 ms");
expect(text).to.include("\u2264 50 ms"); expect(text).to.include("streamer:fixture"); expect(text).to.include("20.0 MB");
expect(ui.window.document.querySelector(".table-warning")).not.to.equal(null);
await ui.input('select[aria-label="Performance time range"]', "60", "change");
expect(ui.requests.filter(request => request.url.pathname === "/api/admin/performance").at(-1).url.searchParams.get("minutes")).to.equal("60");
expect(ui.errors).to.deep.equal([]);
});
it("shows missing monitoring as unavailable on the admin overview", async function () {
ui = await browser("/", { "/api/user": { username: "admin", isAdmin: true },
"/api/admin/overview": { history: [] }, "/api/admin/performance": { available: false } });
await ui.go("/admin/");
expect(ui.window.document.querySelector(".app-view").textContent).to.include("Performance monitoring is unavailable");
expect(ui.errors).to.deep.equal([]);
});
for (const failure of [false, true]) {
it(`refreshes the selected performance window after an old request ${failure ? "fails" : "completes"}`, async function () {
let finishOld, finishNew;
const report = minutes => ({ available: true, windowMinutes: minutes, instances: [], stages: [], runtimeSeries: [],
routes: [{ service: "api", method: "GET", route: `window-${minutes}`, count: 1, avgMs: 1, p95UpperMs: 10, p99UpperMs: 10 }] });
ui = await browser("/", { "/api/user": { username: "admin", isAdmin: true }, "/api/admin/overview": { history: [] },
"/api/admin/performance": request => new Promise(resolve => {
if (request.url.searchParams.get("minutes") === "15") finishOld = () => resolve(failure ? { __status: 503, body: {} } : report(15));
else finishNew = () => resolve(report(60));
}) });
await ui.go("/admin/");
await ui.input('select[aria-label="Performance time range"]', "60", "change");
expect(ui.requests.filter(request => request.url.pathname === "/api/admin/performance")).to.have.length(1);
finishOld(); await delay(20);
const calls = ui.requests.filter(request => request.url.pathname === "/api/admin/performance");
expect(calls.map(request => request.url.searchParams.get("minutes"))).to.deep.equal(["15", "60"]);
expect(ui.window.document.querySelector(".app-view").textContent).not.to.include("window-15");
expect(ui.window.document.querySelector(".app-view").textContent).not.to.include("Could not refresh performance monitoring");
finishNew(); await delay(20);
expect(ui.window.document.querySelector(".app-view").textContent).to.include("window-60");
expect(ui.errors).to.deep.equal([]);
});
}
it("offers one App sign-in for new and existing accounts", async function () {
ui = await browser("/signin", { "/api/options": { GITHUB_APP_ENABLED: true, GITHUB_OAUTH_ENABLED: true } });
expect(ui.window.document.querySelector('a[href="/github/app/login"]')).not.to.equal(null);
+23
View File
@@ -42,6 +42,29 @@ describe("ZIP proxy failures", function () {
function request(onResponse, headers = {}) {
return http.get(`http://127.0.0.1:${server.address().port}/test/zip`, { headers }, onResponse);
}
it("answers HEAD without counting a view, obtaining a token or starting a ZIP stream", async function () {
utils.getRepo = async () => ({ repoId: "test", countView: () => { throw Error("view counted"); },
getToken: () => { throw Error("token requested"); }, generateAnonymizeTransformer: () => { throw Error("transform requested"); } });
got.stream = () => { throw Error("archive requested"); };
const result = await new Promise((resolve, reject) => {
http.request(`http://127.0.0.1:${server.address().port}/test/zip`, { method: "HEAD" }, res => {
const chunks = []; res.on("data", chunk => chunks.push(chunk));
res.on("end", () => resolve({ status: res.statusCode, headers: res.headers, body: Buffer.concat(chunks) }));
}).on("error", reject).end();
});
expect(result.status).to.equal(200);
expect(result.headers["content-disposition"]).to.include("test.zip");
expect(result.body.length).to.equal(0);
});
it("still denies HEAD when downloads are disabled", async function () {
config.ENABLE_DOWNLOAD = false;
const status = await new Promise((resolve, reject) => {
http.request(`http://127.0.0.1:${server.address().port}/test/zip`, { method: "HEAD" }, res => {
res.resume(); res.on("end", () => resolve(res.statusCode));
}).on("error", reject).end();
});
expect(status).to.equal(403);
});
it("aborts a partial ZIP when the upstream fails after headers", async function () {
const result = new Promise((resolve, reject) => {
const client = request(response => {