mirror of
https://github.com/tdurieux/anonymous_github.git
synced 2026-10-20 22:51:18 +02:00
fix: recover stale pull requests and refresh renamed repository caches
This commit is contained in:
1 parent
ccccf07491
commit
e268bce1c7
7 files changed
+47
-13
No files matched your search
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"core.min.js": "core.3b07189932.min.js",
|
||||
"vendor.min.js": "vendor.7107f60347.min.js",
|
||||
"vendor.min.js": "vendor.0b87208272.min.js",
|
||||
"mermaid.min.js": "mermaid.f848a72d16.min.js",
|
||||
"all.min.css": "all.5fbafcda4e.min.css",
|
||||
"markdown.min.js": "markdown.44f2e93820.min.js",
|
||||
|
||||
@@ -44,12 +44,12 @@
|
||||
<div class="connection-resource" v-for="resource in connections.resources" :key="resource.type + resource.id">
|
||||
<div class="connection-resource-name"><strong>{{ resource.name || resource.id }}</strong><small>{{ resource.type }} · {{ resource.id }}</small></div>
|
||||
<span class="connection-badge" :class="{'connection-badge-muted': resource.connection === 'oauth'}">{{ resource.connection === 'github-app' ? 'Read-only App' : 'Legacy OAuth' }}</span>
|
||||
<div class="connection-resource-actions" v-if="!['archived', 'removed', 'preparing', 'queue', 'download', 'removing', 'expiring'].includes(resource.status) && connections.appEnabled && connections.appConnected">
|
||||
<div class="connection-resource-actions" v-if="!['archived', 'removed', 'preparing', 'queue', 'removing', 'expiring'].includes(resource.status) && (resource.status !== 'download' || (resource.type === 'pull-request' && (!resource.statusDate || new Date(resource.statusDate).getTime() <= Date.now() - 300000))) && connections.appEnabled && connections.appConnected">
|
||||
<button class="btn btn-outline-ink" :disabled="busy" @click="changeConnection(resource, 'github-app', true)">Check read-only access</button>
|
||||
<button v-if="resource.eligible" class="btn btn-ink" :disabled="busy" @click="changeConnection(resource, 'github-app', false)">{{ resource.connection === 'github-app' ? 'Reconnect read-only access' : 'Switch to read-only access' }}</button>
|
||||
<span class="connection-help" role="status" v-if="resource.eligible === false">Access unavailable. Allow this repository on GitHub, then check again.</span>
|
||||
</div>
|
||||
<button v-if="!['archived', 'removed', 'preparing', 'queue', 'download', 'removing', 'expiring'].includes(resource.status) && resource.connection === 'github-app' && connections.oauthConnected" class="connection-link" :disabled="busy" @click="changeConnection(resource, 'oauth', false)">Use OAuth</button>
|
||||
<button v-if="!['archived', 'removed', 'preparing', 'queue', 'removing', 'expiring'].includes(resource.status) && (resource.status !== 'download' || (resource.type === 'pull-request' && (!resource.statusDate || new Date(resource.statusDate).getTime() <= Date.now() - 300000))) && resource.connection === 'github-app' && connections.oauthConnected" class="connection-link" :disabled="busy" @click="changeConnection(resource, 'oauth', false)">Use OAuth</button>
|
||||
</div>
|
||||
<p class="connection-footnote"><i class="fas fa-info-circle" aria-hidden="true"></i> Removing access stops new fetches from GitHub. Published content keeps its current expiration and removal settings.</p>
|
||||
</section>
|
||||
|
||||
Vendored
+1
-1
@@ -17,7 +17,7 @@ while being at
|
||||
`).map((he,me)=>me===0?he:ke+he).join(`
|
||||
`)}else typeof j=="number"||typeof j=="boolean"?ue=String(j):ue=JSON.stringify(j);Z.push(`${se}${ue}${re}`)}),Z.push("}"),Z.join(`
|
||||
`)}function h(N){let R=[],I="",C=/(\w+):(>=|<=|!=|>|<|=)?([^\s]+)/g,A=0,P;for(;P=C.exec(N);)I+=N.slice(A,P.index),A=C.lastIndex,R.push({key:P[1],op:P[2]||"=",val:P[3]});return I+=N.slice(A),{filters:R,free:I.trim().toLowerCase()}}function v(N,R){for(let I of R.filters){let C=(P,Z,Y)=>{let j=parseFloat(P),z=parseFloat(Z);return Y==="="?String(P)===String(Z):Y==="!="?String(P)!==String(Z):Y===">="?j>=z:Y==="<="?j<=z:Y===">"?j>z:Y==="<"?j<z:!0},A;if(I.key==="code")A=N.displayMessage;else if(I.key==="module")A=N.module;else if(I.key==="status")A=N._status;else if(I.key==="url")A=N._url;else if(I.key==="repo")A=N._repoId;else if(I.key==="level")A=N.level;else continue;if(A==null||!C(A,I.val,I.op))return!1}return!(R.free&&((N.displayMessage||"")+" "+(N.module||"")+" "+(N._url||"")+" "+JSON.stringify(N.raw||[])).toLowerCase().indexOf(R.free)===-1)}function k(){let N=h(e.query.search||"");e.parsedFilterCount=N.filters.length;let R=e.query.bucket,I=e.entries.filter(A=>R&&A._bucket!==R?!1:v(A,N)),C=e.query.group;if(C){let A=Y=>C==="module"?Y.module:Y.displayMessage||Y.message||"_",P=new Map;for(let Y of I){let j=A(Y);if(P.has(j)){let z=P.get(j);z.count++,z._related.push(Y),new Date(Y.ts)>new Date(z.ts)&&(z.ts=Y.ts,z._url=Y._url,z._status=Y._status),new Date(Y.ts)<new Date(z._firstSeen)&&(z._firstSeen=Y.ts)}else{let z=Object.assign({},Y);z._key=`${C}:${j}`,z._related=[Y],z._firstSeen=Y.ts,z._lastHourCount=0,z.count=1,P.set(j,z)}}let Z=Date.now()-3600*1e3;for(let Y of P.values())Y._lastHourCount=Y._related.filter(j=>new Date(j.ts).getTime()>=Z).length;I=Array.from(P.values())}else I=I.map((A,P)=>(A._key="row:"+P+":"+A.ts,A._related=[A],A._firstSeen=A.ts,A._lastHourCount=0,A.count=1,A));e.query.sort==="count"?I.sort((A,P)=>P.count-A.count||new Date(P.ts)-new Date(A.ts)):I.sort((A,P)=>new Date(P.ts)-new Date(A.ts)),e.visible=I}function p(N){let R=N?e.entries.length:0,I=N?e.pageSize:Math.max(e.pageSize,e.entries.length||e.pageSize);t.get("/api/admin/errors",{params:{offset:R,limit:I}}).then(C=>{let A=(C.data.entries||[]).map(u);e.entries=N?e.entries.concat(A):A,e.available=!!C.data.available,e.cap=C.data.max||e.cap,e.total=C.data.total||e.entries.length,k()},C=>console.error(C))}e.loadMore=()=>p(!0),e.canLoadMore=()=>e.entries.length<e.total;function g(){t.get("/api/admin/errors/stats").then(N=>{let R=N.data||{},I=R.prev24h?Math.round((R.last24h-R.prev24h)/R.prev24h*100):0;e.stats={last24h:R.last24h||0,prev24h:R.prev24h||0,delta:I,severity:R.severity||{error:0,warn:0,info:0},unique:R.unique||{error:0,warn:0,info:0},buckets:R.buckets||[],dropped:R.dropped||0}},N=>console.error(N))}function w(){p(),g()}e.barPx=(N,R)=>{let I=e.stats.buckets||[],C=0;for(let Y of I)C=Math.max(C,(Y.error||0)+(Y.warn||0)+(Y.info||0));if(!C)return 0;let A=(N.error||0)+(N.warn||0)+(N.info||0);if(!A)return 0;let P=Math.round(A/C*60),Z=N[R]||0;return Math.round(Z/A*P)},e.bucketTitle=N=>`${new Date(N.hour).toLocaleTimeString([],{hour:"2-digit",minute:"2-digit"})} \xB7 ${N.error||0} err \xB7 ${N.warn||0} warn \xB7 ${N.info||0} info`,e.toggle=N=>{e.expanded[N._key]=!e.expanded[N._key]},e.setBucket=N=>{e.query.bucket=N},e.refreshNow=w,e.clearAll=()=>{confirm("Clear all captured errors?")&&t.delete("/api/admin/errors").then(w,N=>console.error(N))},e.exportCsv=()=>{let N=["ts","level","module","displayMessage","_status","_url","_repoId"],R=[N.join(",")];for(let P of e.visible)R.push(N.map(Z=>{let Y=P[Z]==null?"":String(P[Z]);return/[",\n]/.test(Y)?`"${Y.replace(/"/g,'""')}"`:Y}).join(","));let I=new Blob([R.join(`
|
||||
`)],{type:"text/csv;charset=utf-8"}),C=URL.createObjectURL(I),A=document.createElement("a");A.href=C,A.download=`errors-${new Date().toISOString().slice(0,19)}.csv`,document.body.appendChild(A),A.click(),document.body.removeChild(A),URL.revokeObjectURL(C)};function b(N){e.copyHint=`${N} copied`,n.timeout(()=>{e.copyHint=""},1500)}e.copyJson=N=>{navigator.clipboard.writeText(N._detailJson).then(()=>b("JSON"))},e.copyCurl=N=>{if(!N._url)return;let I=`curl -X ${N._method||"GET"} '${window.location.origin}${N._url}'`;navigator.clipboard.writeText(I).then(()=>b("curl"))},w();let T=r(()=>{e.query.autoRefresh&&w()},15e3);e.on("dispose",()=>r.cancel(T)),e.watch("query.search",k),e.watch("query.bucket",k),e.watch("query.sort",k),e.watch("query.group",k)},Ga=function(e,t,o,r){if(e.Math=Math,e.watch("user.status",()=>{e.user==null&&o.url("/")}),e.user==null){o.url("/");return}e.data=null,e.loading=!0,e.error=null;function n(p){if(p==null)return"\u2014";for(var g=["B","KB","MB","GB","TB"],w=0,b=p;b>=1024&&w<g.length-1;)b/=1024,w++;return b.toFixed(w>0?1:0)+" "+g[w]}e.humanBytes=n;function i(p){if(!p)return"\u2014";var g=Math.floor(p/86400),w=Math.floor(p%86400/3600),b=Math.floor(p%3600/60);return g>0?g+"d "+(w<10?"0":"")+w+"h":w>0?w+"h "+(b<10?"0":"")+b+"m":b+"m"}e.humanDuration=i;function a(p){return p==null?"\u2014":p>=1e6?(p/1e6).toFixed(1)+"M":p>=1e3?(p/1e3).toFixed(1)+"K":String(p)}e.humanNum=a,e.queueTotal=function(p){return p?(p.waiting||0)+(p.active||0)+(p.delayed||0)+(p.failed||0):0},e.statusCount=function(p){if(!e.data||!e.data.repos)return 0;for(var g=e.data.repos.statusBreakdown||[],w=0;w<g.length;w++)if(g[w]._id===p)return g[w].count;return 0},e.barPct=function(p){var g=e.data&&e.data.repos?e.data.repos.total:0;if(!g)return 0;var w=[p];p==="expired"&&w.push("expiring"),p==="removed"&&w.push("removing"),p==="preparing"&&w.push("download");var b=0;return w.forEach(function(T){b+=e.statusCount(T)}),Math.max(.4,b/g*100)},e.errPct=function(p){if(!e.data||!e.data.errors)return 0;var g=Math.max(e.data.errors.severity.error,e.data.errors.severity.warn,e.data.errors.severity.info,1);return e.data.errors.severity[p]/g*100};function u(p){var g=p||[];return g.map(function(w,b){var T=g[b-1]||{},N=Object.assign({},w);return N.dailyRepositories=b?Math.max(0,(w.nbRepositories||0)-(T.nbRepositories||0)):0,N.dailyUsers=b?Math.max(0,(w.nbUsers||0)-(T.nbUsers||0)):0,N.dailyPageViews=b?Math.max(0,(w.nbPageViews||0)-(T.nbPageViews||0)):0,N})}function m(p){var g=p&&p.length?p[p.length-1]:{};return{repositories:g.dailyRepositories||0,users:g.dailyUsers||0,pageViews:g.dailyPageViews||0}}var h={};e.historyBarH=function(p,g){return!p||!h[g]?0:Math.max(1,Math.round(p[g]/h[g]*140))},e.historyLabel=function(p){if(!p||!p.date)return"";var g=new Date(p.date);return g.getUTCMonth()+1+"/"+g.getUTCDate()};function v(){t.get("/api/admin/overview").then(function(p){p.data.history=u(p.data.history),p.data.daily={today:m(p.data.history)},e.data=p.data,e.loading=!1,e.error=null,h={},(p.data.history||[]).forEach(function(g){["dailyPageViews","dailyRepositories","dailyUsers","nbUsers"].forEach(function(w){(!h[w]||g[w]>h[w])&&(h[w]=g[w])})})},function(p){e.loading=!1,e.error=p.data&&p.data.error||"Failed to load overview"})}v();var k=r(v,3e4);e.on("dispose",function(){r.cancel(k)})};var ja=[{path:"/connections",template:"partials/connections.htm",title:"GitHub connections \u2013 Anonymous GitHub",preserveExplorer:!1,setup:(e,t)=>Ma(e,t.http)},{path:"/signin",template:"partials/signin.htm",title:"Sign in \u2013 Anonymous GitHub",preserveExplorer:!1,setup:(e,t)=>$a(e,t.http)},{path:"/",template:"partials/home.htm",title:"Anonymous GitHub \u2013 Share the code, not the author",preserveExplorer:!1,setup:(e,t)=>Ta(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)=>Oa(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)=>Yn(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)=>Yn(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)=>Yn(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)=>Aa(e,t.http,t.params)},{path:"/conferences",template:"partials/conferences.htm",title:"Your conferences \u2013 Anonymous GitHub",preserveExplorer:!1,setup:(e,t)=>Pa(e,t.http,t.location)
Line truncated
|
||||
`)],{type:"text/csv;charset=utf-8"}),C=URL.createObjectURL(I),A=document.createElement("a");A.href=C,A.download=`errors-${new Date().toISOString().slice(0,19)}.csv`,document.body.appendChild(A),A.click(),document.body.removeChild(A),URL.revokeObjectURL(C)};function b(N){e.copyHint=`${N} copied`,n.timeout(()=>{e.copyHint=""},1500)}e.copyJson=N=>{navigator.clipboard.writeText(N._detailJson).then(()=>b("JSON"))},e.copyCurl=N=>{if(!N._url)return;let I=`curl -X ${N._method||"GET"} '${window.location.origin}${N._url}'`;navigator.clipboard.writeText(I).then(()=>b("curl"))},w();let T=r(()=>{e.query.autoRefresh&&w()},15e3);e.on("dispose",()=>r.cancel(T)),e.watch("query.search",k),e.watch("query.bucket",k),e.watch("query.sort",k),e.watch("query.group",k)},Ga=function(e,t,o,r){if(e.Math=Math,e.watch("user.status",()=>{e.user==null&&o.url("/")}),e.user==null){o.url("/");return}e.data=null,e.loading=!0,e.error=null;function n(p){if(p==null)return"\u2014";for(var g=["B","KB","MB","GB","TB"],w=0,b=p;b>=1024&&w<g.length-1;)b/=1024,w++;return b.toFixed(w>0?1:0)+" "+g[w]}e.humanBytes=n;function i(p){if(!p)return"\u2014";var g=Math.floor(p/86400),w=Math.floor(p%86400/3600),b=Math.floor(p%3600/60);return g>0?g+"d "+(w<10?"0":"")+w+"h":w>0?w+"h "+(b<10?"0":"")+b+"m":b+"m"}e.humanDuration=i;function a(p){return p==null?"\u2014":p>=1e6?(p/1e6).toFixed(1)+"M":p>=1e3?(p/1e3).toFixed(1)+"K":String(p)}e.humanNum=a,e.queueTotal=function(p){return p?(p.waiting||0)+(p.active||0)+(p.delayed||0)+(p.failed||0):0},e.statusCount=function(p){if(!e.data||!e.data.repos)return 0;for(var g=e.data.repos.statusBreakdown||[],w=0;w<g.length;w++)if(g[w]._id===p)return g[w].count;return 0},e.barPct=function(p){var g=e.data&&e.data.repos?e.data.repos.total:0;if(!g)return 0;var w=[p];p==="expired"&&w.push("expiring"),p==="removed"&&w.push("removing"),p==="preparing"&&w.push("download");var b=0;return w.forEach(function(T){b+=e.statusCount(T)}),Math.max(.4,b/g*100)},e.errPct=function(p){if(!e.data||!e.data.errors)return 0;var g=Math.max(e.data.errors.severity.error,e.data.errors.severity.warn,e.data.errors.severity.info,1);return e.data.errors.severity[p]/g*100};function u(p){var g=p||[];return g.map(function(w,b){var T=g[b-1]||{},N=Object.assign({},w);return N.dailyRepositories=b?Math.max(0,(w.nbRepositories||0)-(T.nbRepositories||0)):0,N.dailyUsers=b?Math.max(0,(w.nbUsers||0)-(T.nbUsers||0)):0,N.dailyPageViews=b?Math.max(0,(w.nbPageViews||0)-(T.nbPageViews||0)):0,N})}function m(p){var g=p&&p.length?p[p.length-1]:{};return{repositories:g.dailyRepositories||0,users:g.dailyUsers||0,pageViews:g.dailyPageViews||0}}var h={};e.historyBarH=function(p,g){return!p||!h[g]?0:Math.max(1,Math.round(p[g]/h[g]*140))},e.historyLabel=function(p){if(!p||!p.date)return"";var g=new Date(p.date);return g.getUTCMonth()+1+"/"+g.getUTCDate()};function v(){t.get("/api/admin/overview").then(function(p){p.data.history=u(p.data.history),p.data.daily={today:m(p.data.history)},e.data=p.data,e.loading=!1,e.error=null,h={},(p.data.history||[]).forEach(function(g){["dailyPageViews","dailyRepositories","dailyUsers","nbUsers"].forEach(function(w){(!h[w]||g[w]>h[w])&&(h[w]=g[w])})})},function(p){e.loading=!1,e.error=p.data&&p.data.error||"Failed to load overview"})}v();var k=r(v,3e4);e.on("dispose",function(){r.cancel(k)})};var ja=[{path:"/connections",template:"partials/connections.htm",title:"GitHub connections \u2013 Anonymous GitHub",preserveExplorer:!1,setup:(e,t)=>Ma(e,t.http)},{path:"/signin",template:"partials/signin.htm",title:"Sign in \u2013 Anonymous GitHub",preserveExplorer:!1,setup:(e,t)=>$a(e,t.http)},{path:"/",template:"partials/home.htm",title:"Anonymous GitHub \u2013 Share the code, not the author",preserveExplorer:!1,setup:(e,t)=>Ta(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)=>Oa(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)=>Yn(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)=>Yn(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)=>Yn(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)=>Aa(e,t.http,t.params)},{path:"/conferences",template:"partials/conferences.htm",title:"Your conferences \u2013 Anonymous GitHub",preserveExplorer:!1,setup:(e,t)=>Pa(e,t.http,t.location)
Line truncated
|
||||
anonymous_github`)])])])])]),f(" Limitations "),s("h4",{class:"faq-cat"},[s("i",{class:"fas fa-exclamation-triangle mr-2"}),y("Limitations ")]),s("div",{class:"panel-group",id:"faq-limitations",role:"tablist","aria-multiselectable":"true"},[s("div",{class:"panel panel-default mb-3"},[s("div",{class:"panel-heading p-3",role:"tab",id:"headingLimitation"},[s("h3",{class:"panel-title"},[s("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? ")])]),s("div",{id:"limitation",class:"panel-collapse collapse",role:"tabpanel","aria-labelledby":"headingLimitation"},[s("div",{class:"panel-body p-3"},[s("ul",null,[s("li",null," Anonymous GitHub only anonymizes text-based files. Binary files (compiled executables, archives, etc.) are served as-is without anonymization. "),s("li",null," Files larger than 8 MB are not supported. "),s("li",null," Static site generators (such as Jekyll) used with GitHub Pages are not fully supported, although Markdown files are converted to HTML. "),s("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). ")])])])])]),f(" Privacy & Security "),s("h4",{class:"faq-cat"},[s("i",{class:"fas fa-shield-alt mr-2"}),y("Privacy & Security ")]),s("div",{class:"panel-group",id:"faq-privacy",role:"tablist","aria-multiselectable":"true"},[s("div",{class:"panel panel-default mb-3"},[s("div",{class:"panel-heading p-3",role:"tab",id:"headingData"},[s("h3",{class:"panel-title"},[s("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? ")])]),s("div",{id:"data",class:"panel-collapse collapse",role:"tabpanel","aria-labelledby":"headingData"},[s("div",{class:"panel-body p-3"},[s("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. ")])])]),s("div",{class:"panel panel-default mb-3"},[s("div",{class:"panel-heading p-3",role:"tab",id:"headingPermissions"},[s("h3",{class:"panel-title"},[s("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? ")])]),s("div",{id:"permissions",class:"panel-collapse collapse",role:"tabpanel","aria-labelledby":"headingPermissions"},[s("div",{class:"panel-body p-3"},[s("p",null," Anonymous GitHub only reads your repositories \u2014 it never pushes commits, opens issues, modifies settings, or deletes anything. From your perspective as a user, the service is read-only. "),s("p",null,[y(" However, GitHub's OAuth scopes do not offer a read-only option for private repositories: the only scope that grants access to private repos is "),s("code",null,"repo"),y(", which is documented as full read/write access. To support users who want to anonymize a private repository, Anonymous GitHub must request that scope, and GitHub then displays the broader permission text at sign-in. The application itself only ever performs read operations against the GitHub API. ")]),s("p",null,[y(" If you only anonymize public repositories, the source code is open and can be audited on the "),s("a",{href:"https://github.com/tdurieux/anonymous_github/",target:"_blank"},"GitHub repository"),y(", or you can self-host your own instance. ")])])])]),s("div",{class:"panel panel-default mb-3"},[s("div",{class:"panel-heading p-3",role:"tab",id:"headingViewer"},[s("h3",{class:"panel-title"},[s("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? ")])]),s("div",{id:"viewer",class:"panel-collapse collapse",role:"tabpanel","aria-labelledby":"headingViewer"},[s("div",{class:"panel-body p-3"},[s("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. ")])])])]),f(" Self-Hosting "),s("h4",{class:"faq-cat"},[s("i",{class:"fas fa-server mr-2"}),y("Self-Hosting ")]),s("div",{class:"panel-group",id:"faq-hosting",role:"tablist","aria-multiselectable":"true"},[s("div",{class:"panel panel-default mb-3"},[s("div",{class:"panel-heading p-3",role:"tab",id:"headingDeploy"},[s("h3",{class:"panel-title"},[s("a",{class:"collapsed",role:"button","data-toggle":"collapse","data-parent":"#faq-ho
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]=s("li",{class:"nav-item d-none d-lg-block"},[s("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"},[s("i",{class:"fas fa-bug d-lg-none mr-1","aria-hidden":"true"}),y("Report a bug ")])],-1)),e.isDarkMode?f("v-if",!0):(l(),d("li",xO,[s("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]=we(o=>{e.darkMode(!0)},["prevent"]))},[...t[7]||(t[7]=[s("i",{class:"fas fa-moon","aria-hidden":"true"},null,-1)])])])),e.isDarkMode?(l(),d("li",eA,[s("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]=we(o=>{e.darkMode(!1)},["prevent"]))},[...t[8]||(t[8]=[s("i",{class:"fas fa-sun","aria-hidden":"true"},null,-1)])])])):f("v-if",!0),e.user?f("v-if",!0):(l(),d("li",tA,[...t[9]||(t[9]=[s("a",{class:"nav-link btn-signin",target:"_self",href:"/signin","data-offset":"30"},[s("i",{class:"fab fa-github mr-1"}),y(" Sign in ")],-1)])])),e.user?(l(),d("li",sA,[s("a",nA,[e.user?.photo?(l(),d("img",{key:0,width:"22",height:"22",class:"rounded-circle",src:e.safeUrl(e.user?.photo)},null,8,oA)):f("v-if",!0),s("span",rA,c(e.user?.username),1)]),s("div",iA,[s("h6",aA,"Signed in as @"+c(e.user?.username),1),t[10]||(t[10]=ge('<a class="dropdown-item" href="/profile"><i class="fas fa-sliders-h" aria-hidden="true"></i><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"><i class="fas fa-link" aria-hidden="true"></i><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"><i class="fas fa-bug" aria-hidden="true"></i><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><div class="dropdown-divider"></div><a class="dropdown-item" href="/api/user/logout" target="_self"><i class="fas fa-sign-out-alt" aria-hidden="true"></i><span>Sign out</span></a>',8))])])):f("v-if",!0)])])],2),e.generalMessage?(l(),d("div",{key:0,class:"navbar shadow generalMessage",innerHTML:e.sanitize(e.generalMessage)},null,8,lA)):f("v-if",!0)],64)}var dA={class:"paper-landing"},uA={class:"paper-hero"},cA={class:"paper-hero-copy"},pA={class:"paper-cta-row"},fA={key:0,href:"/signin",target:"_self",class:"btn-hero"},mA={key:1,href:"/anonymize",class:"btn-hero"},hA={class:"paper-stats paper-wrap",id:"metrics","aria-label":"Usage over the last 60 days"},vA={class:"paper-stats-grid"},yA={class:"paper-stat-value"},gA={class:"paper-stat-label"},bA=["viewBox"],wA=["x","y","width","height"],kA={key:1,class:"paper-stat-delta"},_A={class:"paper-features paper-wrap","aria-label":"Product features"},EA={class:"paper-features-list",role:"tablist","aria-label":"Features","aria-orientation":"vertical"},CA=["onClick","onKeydown","id","aria-controls","aria-selected","tabindex"],NA={class:"paper-eyebrow"},SA={class:"paper-feature-title"},DA={class:"paper-feature-more"},RA={class:"paper-feature-title paper-feature-title-m","aria-hidden":"true"},TA=["href","target"],OA={class:"paper-features-stage"},AA=["id","aria-labelledby"],IA={class:"paper-frame"},VA={class:"paper-frame-bar","aria-hidden":"true"},PA={class:"url"},qA=["src","alt"],MA={class:"paper-footer"},$A={class:"paper-footer-inner"},UA={class:"paper-footer-col"},LA={key:0,href:"/dashboard"},FA={key:1,href:"/conferences"},zA={class:"paper-footer-foot"};function pl(e,t){return l(),d("div",dA,[f(" Hero: copy on the left, the product on the right "),s("section",uA,[s("div",cA,[t[2]||(t[2]=s("div",{class:"paper-eyebrow"},"For double-blind peer review",-1)),t[3]||(t[3]=s("h1",{class:"hero-title"},[y(" Share the code,"),s("br"),s("span",{class:"accent"},"not the author.")],-1)),t[4]||(t[4]=s("p",{class:"hero-subtitle"}," Anonymous GitHub makes a read-only mirror of your repository, pull request, or gist with every trace of identity removed, so you can attach a link to your submission without breaking anonymity. ",-1)),s("div",pA,[e.user?f("v-if",!0):(l(),d("a",fA,[...t[0]||(t[0]=[s("i",{class:"fab fa-github mr-2","aria-hidden":"true"},null,-1),y("Sign in with GitHub ",-1)])])),e.user?(l(),d("a",mA," Anonymize a repository \
Line truncated
|
||||
|
||||
@@ -5,6 +5,7 @@ import UserModel from "../../core/model/users/users.model";
|
||||
import CredentialModel from "../../core/model/credentials/credentials.model";
|
||||
import InstallationModel from "../../core/model/github-installation";
|
||||
import RepositoryModel from "../../core/model/anonymizedRepositories/anonymizedRepositories.model";
|
||||
import CachedRepositoryModel from "../../core/model/repositories/repositories.model";
|
||||
import PullRequestModel from "../../core/model/anonymizedPullRequests/anonymizedPullRequests.model";
|
||||
import GistModel from "../../core/model/anonymizedGists/anonymizedGists.model";
|
||||
import { getCredentialToken } from "../../core/credentials";
|
||||
@@ -163,14 +164,14 @@ router.get("/connections", async (req, res) => {
|
||||
}
|
||||
catch (error) { appErrorCode = error instanceof Error ? error.message : "github_app_reconnect_required"; }
|
||||
}
|
||||
const repos = await RepositoryModel.find({ owner: user.id, status: { $ne: "removed" } }).select("repoId source.repositoryName githubAccess status").lean();
|
||||
const prs = await PullRequestModel.find({ owner: user.id, status: { $ne: "removed" } }).select("pullRequestId source.repositoryFullName githubAccess status").lean();
|
||||
const repos = await RepositoryModel.find({ owner: user.id, status: { $ne: "removed" } }).select("repoId source.repositoryName githubAccess status statusDate").lean();
|
||||
const prs = await PullRequestModel.find({ owner: user.id, status: { $ne: "removed" } }).select("pullRequestId source.repositoryFullName githubAccess status statusDate").lean();
|
||||
const gistCount = await GistModel.countDocuments({ owner: user.id, status: { $ne: "removed" } });
|
||||
res.json({ csrf: req.session.githubConnectionCSRF, appEnabled: config.GITHUB_APP_ENABLED && config.GITHUB_APP_NEW_CONNECTIONS,
|
||||
oauthEnabled: config.GITHUB_OAUTH_ENABLED, oauthConnected: !!(await getCredentialToken(user.id)), appConnected, appError: appErrorCode,
|
||||
installations, gistCount, resources: [
|
||||
...repos.map(r => ({ type: "repository", id: r.repoId, name: r.source.repositoryName, connection: r.githubAccess?.kind || "oauth", status: r.status })),
|
||||
...prs.map(r => ({ type: "pull-request", id: r.pullRequestId, name: r.source.repositoryFullName, connection: r.githubAccess?.kind || "oauth", status: r.status })),
|
||||
...repos.map(r => ({ type: "repository", id: r.repoId, name: r.source.repositoryName, connection: r.githubAccess?.kind || "oauth", status: r.status, statusDate: r.statusDate })),
|
||||
...prs.map(r => ({ type: "pull-request", id: r.pullRequestId, name: r.source.repositoryFullName, connection: r.githubAccess?.kind || "oauth", status: r.status, statusDate: r.statusDate })),
|
||||
] });
|
||||
} catch (error) { handleError(error, res, req); }
|
||||
});
|
||||
@@ -191,8 +192,10 @@ router.post("/connections/migrate", async (req, res) => {
|
||||
const isRepo = type === "repository";
|
||||
const model = isRepo ? await RepositoryModel.findOne({ repoId: id, owner: user.id }) : await PullRequestModel.findOne({ pullRequestId: id, owner: user.id });
|
||||
if (!model || ["removed", "archived"].includes(model.status || "")) throw appError("repo_not_found", 404);
|
||||
if (["preparing", "queue", "download", "removing", "expiring"].includes(model.status || "")) throw appError("repository_busy", 409);
|
||||
const source = model.source as { repositoryName?: string; repositoryFullName?: string; commit?: string; pullRequestId?: number };
|
||||
const stalePullRequest = !isRepo && model.status === "download" &&
|
||||
(!model.statusDate || model.statusDate.getTime() <= Date.now() - 5 * 60 * 1000);
|
||||
if (!stalePullRequest && ["preparing", "queue", "download", "removing", "expiring"].includes(model.status || "")) throw appError("repository_busy", 409);
|
||||
const source = model.source as { repositoryName?: string; repositoryFullName?: string; repositoryId?: string; commit?: string; pullRequestId?: number };
|
||||
const name = source.repositoryName || source.repositoryFullName || "";
|
||||
const repositoryId = model.githubAccess?.kind === "github-app" && connection === "github-app"
|
||||
? model.githubAccess.repositoryId : undefined;
|
||||
@@ -206,7 +209,14 @@ router.post("/connections/migrate", async (req, res) => {
|
||||
const token = selected.binding.publicRead ? await appUserToken(user.id) : selected.token;
|
||||
await githubRequest(`${repositoryPath}/${isRepo ? `commits/${encodeURIComponent(source.commit || "")}` : `pulls/${source.pullRequestId}`}`, token);
|
||||
if (preview === true) return res.json({ eligible: true, connection });
|
||||
const filter = { _id: model._id, owner: user.id, status: model.status, source: model.source,
|
||||
// This shared cache stores GitHub identity metadata, independently of the
|
||||
// resource binding. Match the numeric ID so a replacement is never renamed.
|
||||
if (isRepo && source.repositoryId && selected.fullName && selected.binding.kind === "github-app") {
|
||||
await CachedRepositoryModel.updateOne({ _id: source.repositoryId, externalId: `gh_${selected.binding.repositoryId}` },
|
||||
{ $set: { name: selected.fullName, url: `https://github.com/${selected.fullName}` } });
|
||||
}
|
||||
const filter = { _id: model._id, owner: user.id, status: model.status,
|
||||
...(stalePullRequest ? { statusDate: model.statusDate ?? null } : {}), source: model.source,
|
||||
githubAccess: model.githubAccess ? model.githubAccess : { $exists: false } };
|
||||
const change = { $set: { githubAccess: selected.binding,
|
||||
...(selected.fullName && selected.fullName !== name
|
||||
|
||||
@@ -16,7 +16,7 @@ for (const type of ["repository", "pull-request"]) {
|
||||
}
|
||||
beforeEach(function () {
|
||||
restores = []; writes = []; checks = []; owner = "owner";
|
||||
model = { _id: "saved-resource", owner: "owner", status: "ready",
|
||||
model = { _id: "saved-resource", owner: "owner", status: "ready", statusDate: new Date(),
|
||||
repoId: "published-url", pullRequestId: "published-url",
|
||||
source: { repositoryName: "owner/private", repositoryFullName: "owner/private", commit: "saved-sha", pullRequestId: 7 },
|
||||
githubAccess: { kind: "github-app", repositoryId: 42, installationId: 100, revision: "old" },
|
||||
@@ -85,6 +85,14 @@ for (const type of ["repository", "pull-request"]) {
|
||||
stub(app, "githubRequest", async () => { throw new Error("github_app_access_required"); });
|
||||
await fails("github_app_access_required");
|
||||
});
|
||||
if (type === "pull-request") {
|
||||
it("allows stale downloads but guards their timestamp against a new refresh", async function () {
|
||||
model.status = "download";
|
||||
model.statusDate = new Date(Date.now() - 6 * 60 * 1000);
|
||||
await run();
|
||||
expect(writes[0].filter.statusDate).to.equal(model.statusDate);
|
||||
});
|
||||
}
|
||||
it("does not let another user repair the binding", async function () {
|
||||
owner = "other"; await fails("repo_not_found");
|
||||
});
|
||||
|
||||
@@ -499,8 +499,10 @@ describeMongo("GitHub App credential and repository integration", function () {
|
||||
it("reconnects an uninstalled public repository with a real user token", async () => {
|
||||
const Repos = require("../src/core/model/anonymizedRepositories/anonymizedRepositories.model").default;
|
||||
await app.saveAppGrant(owner.id, data());
|
||||
const CachedRepos = require("../src/core/model/repositories/repositories.model").default;
|
||||
const cached = await CachedRepos.create({ externalId: "gh_42", name: "other/old-public", url: "https://github.com/other/old-public" });
|
||||
const resource = await Repos.create({ repoId: "public-reconnect", owner: owner.id, status: "ready",
|
||||
source: { type: "GitHubStream", repositoryName: "other/public", commit: "abc", branch: "main" },
|
||||
source: { type: "GitHubStream", repositoryName: "other/old-public", repositoryId: cached.id, commit: "abc", branch: "main" },
|
||||
githubAccess: { kind: "github-app", repositoryId: 42, publicRead: true, revision: "old" } });
|
||||
mock((url, options) => {
|
||||
if (url.includes("/user/installations?")) return { installations: [] };
|
||||
@@ -515,8 +517,11 @@ describeMongo("GitHub App credential and repository integration", function () {
|
||||
});
|
||||
const body = { type: "repository", id: resource.repoId, connection: "github-app" };
|
||||
expect((await request("/github/connections/migrate", { ...body, preview: true })).status).to.equal(200);
|
||||
expect((await CachedRepos.findById(cached.id)).name).to.equal("other/old-public");
|
||||
expect((await request("/github/connections/migrate", body)).status).to.equal(200);
|
||||
expect((await Repos.findById(resource.id)).githubAccess.publicRead).to.equal(true);
|
||||
expect((await Repos.findById(resource.id)).source.repositoryName).to.equal("other/public");
|
||||
expect((await CachedRepos.findById(cached.id)).name).to.equal("other/public");
|
||||
});
|
||||
|
||||
it("recovers a renamed private pull request through its stable repository ID", async () => {
|
||||
|
||||
@@ -169,6 +169,17 @@ describe("Vue 3 UI", function () {
|
||||
|
||||
}
|
||||
|
||||
for (const age of [0, 6 * 60 * 1000]) {
|
||||
it("allows reconnect only for stale pull-request downloads: " + age, async function () {
|
||||
ui = await browser("/connections", {
|
||||
"/github/connections": { appEnabled: true, appConnected: true, resources: [
|
||||
{ type: "pull-request", id: "stale", connection: "github-app", status: "download", statusDate: new Date(Date.now() - age).toISOString() },
|
||||
] },
|
||||
});
|
||||
expect(ui.window.document.querySelector(".connection-resource-actions") !== null).to.equal(age > 0);
|
||||
expect(ui.errors).to.deep.equal([]);
|
||||
});
|
||||
}
|
||||
for (const connection of ["oauth", "github-app"]) {
|
||||
it(`previews and saves ${connection} access with CSRF protection`, async function () {
|
||||
const action = connection === "github-app" ? "Reconnect read-only access" : "Switch to read-only access";
|
||||
|
||||
Reference in new issue
Block a user