fix: verify reconnects through supported GitHub routes and protect active jobs

This commit is contained in:
Thomas Durieux committed 2026-10-07 11:24:59 +00:00
commit e6073941b9
18 files changed
+533 -67

No files matched your search

+8 -1
View File
@@ -8,6 +8,7 @@ const crypto = require("crypto");
const fs = require("fs");
const esbuild = require("esbuild");
const { compileTemplate } = require("vue/compiler-sfc");
const { Script } = require("node:vm");
const { promisify } = require("node:util");
const coreJsFiles = [
@@ -67,7 +68,13 @@ function buildCoreJs(cb) {
async function buildVendorJs() {
const lazyAssets = {};
await Promise.all(Object.entries(lazyGroups).map(async ([name, files]) => {
await promisify(pipeline)(orderedSrc(files), concat(`${name}.min.js`), uglify(), dest("public/script"));
// PDF.js is already minified upstream. Re-minifying it breaks rendering
// for documents such as #857, even though page.render() resolves.
if (name === "pdf") {
for (const file of files) new Script(fs.readFileSync(file, "utf8"), { filename: file });
}
const transforms = name === "pdf" ? [] : [uglify()];
await promisify(pipeline)(orderedSrc(files), concat(`${name}.min.js`), ...transforms, dest("public/script"));
lazyAssets[name] = `/script/${name}.${hashFile(`public/script/${name}.min.js`)}.min.js`;
}));
const app = await esbuild.build({
+2 -2
View File
@@ -1,10 +1,10 @@
{
"core.min.js": "core.3b07189932.min.js",
"vendor.min.js": "vendor.f9925bc8f9.min.js",
"vendor.min.js": "vendor.7107f60347.min.js",
"mermaid.min.js": "mermaid.f848a72d16.min.js",
"all.min.css": "all.5fbafcda4e.min.css",
"markdown.min.js": "markdown.44f2e93820.min.js",
"pdf.min.js": "pdf.0175d53d2f.min.js",
"pdf.min.js": "pdf.4d9cc6ff49.min.js",
"notebook.min.js": "notebook.c3144c18ae.min.js",
"org.min.js": "org.f61f262915.min.js",
"editor.min.js": "editor.4f142d3adc.min.js"
+2 -2
View File
@@ -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="resource.status !== 'archived' &amp;&amp; connections.appEnabled &amp;&amp; connections.appConnected">
<div class="connection-resource-actions" v-if="!['archived', 'removed', 'preparing', 'queue', 'download', 'removing', 'expiring'].includes(resource.status) &amp;&amp; connections.appEnabled &amp;&amp; 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="resource.status !== 'archived' &amp;&amp; resource.connection === 'github-app' &amp;&amp; connections.oauthConnected" class="connection-link" :disabled="busy" @click="changeConnection(resource, 'oauth', false)">Use OAuth</button>
<button v-if="!['archived', 'removed', 'preparing', 'queue', 'download', 'removing', 'expiring'].includes(resource.status) &amp;&amp; resource.connection === 'github-app' &amp;&amp; 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>
+22 -2
View File
@@ -1,2 +1,22 @@
((t,e)=>{"object"==typeof exports&&"object"==typeof module?module.exports=t.pdfjsLib=e():"function"==typeof define&&define.amd?define("pdfjs-dist/build/pdf",[],()=>t.pdfjsLib=e()):"object"==typeof exports?exports["pdfjs-dist/build/pdf"]=t.pdfjsLib=e():t["pdfjs-dist/build/pdf"]=t.pdfjsLib=e()})(globalThis,()=>(()=>{var __webpack_modules__=[,(t,e,r)=>{Object.defineProperty(e,"__esModule",{value:!0}),e.VerbosityLevel=e.Util=e.UnknownErrorException=e.UnexpectedResponseException=e.TextRenderingMode=e.RenderingIntentFlag=e.PromiseCapability=e.PermissionFlag=e.PasswordResponses=e.PasswordException=e.PageActionEventType=e.OPS=e.MissingPDFException=e.MAX_IMAGE_SIZE_TO_CACHE=e.LINE_FACTOR=e.LINE_DESCENT_FACTOR=e.InvalidPDFException=e.ImageKind=e.IDENTITY_MATRIX=e.FormatError=e.FeatureTest=e.FONT_IDENTITY_MATRIX=e.DocumentActionEventType=e.CMapCompressionType=e.BaseException=e.BASELINE_FACTOR=e.AnnotationType=e.AnnotationReplyType=e.AnnotationPrefix=e.AnnotationMode=e.AnnotationFlag=e.AnnotationFieldFlag=e.AnnotationEditorType=e.AnnotationEditorPrefix=e.AnnotationEditorParamsType=e.AnnotationBorderStyleType=e.AnnotationActionEventType=e.AbortException=void 0,e.assert=function(t,e){t||a(e)},e.bytesToString=h,e.createValidAbsoluteUrl=function(e){let t=1<arguments.length&&void 0!==arguments[1]?arguments[1]:null,r=2<arguments.length&&void 0!==arguments[2]?arguments[2]:null;if(e)try{if(r&&"string"==typeof e){if(r.addDefaultProtocol&&e.startsWith("www.")){let t=e.match(/\./g);2<=t?.length&&(e="http://"+e)}if(r.tryConvertEncoding)try{e=p(e)}catch{}}var i=t?new URL(e,t):new URL(e);if((t=>{switch(t?.protocol){case"http:":case"https:":case"ftp:":case"mailto:":case"tel:":return 1;default:return}})(i))return i}catch{}return null},e.getModificationDate=function(){var t=0<arguments.length&&void 0!==arguments[0]?arguments[0]:new Date;return[t.getUTCFullYear().toString(),(t.getUTCMonth()+1).toString().padStart(2,"0"),t.getUTCDate().toString().padStart(2,"0"),t.getUTCHours().toString().padStart(2,"0"),t.getUTCMinutes().toString().padStart(2,"0"),t.getUTCSeconds().toString().padStart(2,"0")].join("")},e.getUuid=function(){if("undefined"!=typeof crypto&&"function"==typeof crypto?.randomUUID)return crypto.randomUUID();var e=new Uint8Array(32);if("undefined"!=typeof crypto&&"function"==typeof crypto?.getRandomValues)crypto.getRandomValues(e);else for(let t=0;t<32;t++)e[t]=Math.floor(255*Math.random());return h(e)},e.getVerbosityLevel=function(){return s},e.info=function(t){s>=i.INFOS&&console.log("Info: "+t)},e.isArrayBuffer=function(t){return"object"==typeof t&&void 0!==t?.byteLength},e.isArrayEqual=function(r,i){if(r.length!==i.length)return!1;for(let t=0,e=r.length;t<e;t++)if(r[t]!==i[t])return!1;return!0},e.isNodeJS=void 0,e.normalizeUnicode=function(t){return f||(f=/([\u00a0\u00b5\u037e\u0eb3\u2000-\u200a\u202f\u2126\ufb00-\ufb04\ufb06\ufb20-\ufb36\ufb38-\ufb3c\ufb3e\ufb40-\ufb41\ufb43-\ufb44\ufb46-\ufba1\ufba4-\ufba9\ufbae-\ufbb1\ufbd3-\ufbdc\ufbde-\ufbe7\ufbea-\ufbf8\ufbfc-\ufbfd\ufc00-\ufc5d\ufc64-\ufcf1\ufcf5-\ufd3d\ufd88\ufdf4\ufdfa-\ufdfb\ufe71\ufe77\ufe79\ufe7b\ufe7d]+)|(\ufb05+)/gu,g=new Map([["ſt","ſt"]])),t.replaceAll(f,(t,e,r)=>e?e.normalize("NFKC"):g.get(r))},e.objectFromMap=function(t){var e,r,i=Object.create(null);for([e,r]of t)i[e]=r;return i},e.objectSize=function(t){return Object.keys(t).length},e.setVerbosityLevel=function(t){Number.isInteger(t)&&(s=t)},e.shadow=o,e.string32=function(t){return String.fromCharCode(t>>24&255,t>>16&255,t>>8&255,255&t)},e.stringToBytes=d,e.stringToPDFString=function(r){if("ï"<=r[0]){let t;if("þ"===r[0]&&"ÿ"===r[1]?t="utf-16be":"ÿ"===r[0]&&"þ"===r[1]?t="utf-16le":"ï"===r[0]&&"»"===r[1]&&"¿"===r[2]&&(t="utf-8"),t)try{var e=new TextDecoder(t,{fatal:!0}),i=d(r);return e.decode(i)}catch(r){n(`stringToPDFString: "${r}".`)}}var s=[];for(let t=0,e=r.length;t<e;t++){var a=u[r.charCodeAt(t)];s.push(a?String.fromCharCode(a):r.charAt(t))}return s.join("")},e.stringToUTF8String=p,e.unreachable=a,e.utf8StringToString=function(t){return unescape(encodeURIComponent(t))},e.warn=n,r(2),r(84),r(86),r(87),r(89),r(93),r(101),r(102),r(105),r(107),r(109),r(113),r(116),r(123);r=!("object"!=typeof process||process+""!="[object process]"||process.versions.nw||process.versions.electron&&process.type&&"browser"!==process.type);e.isNodeJS=r,e.IDENTITY_MATRIX=[1,0,0,1,0,0],e.FONT_IDENTITY_MATRIX=[.001,0,0,.001,0,0],e.MAX_IMAGE_SIZE_TO_CACHE=1e7,e.LINE_FACTOR=1.35,e.LINE_DESCENT_FACTOR=.35,e.BASELINE_FACTOR=.25925925925925924,e.RenderingIntentFlag={ANY:1,DISPLAY:2,PRINT:4,SAVE:8,ANNOTATIONS_FORMS:16,ANNOTATIONS_STORAGE:32,ANNOTATIONS_DISABLE:64,OPLIST:256},e.AnnotationMode={DISABLE:0,ENABLE:1,ENABLE_FORMS:2,ENABLE_STORAGE:3},e.AnnotationEditorPrefix="pdfjs_internal_editor_",e.AnnotationEditorType={DISABLE:-1,NONE:0,FREETEXT:3,STAMP:13,INK:15},e.AnnotationEditorParamsType={RESIZE:1,CREATE:2,FREETEXT_SIZE:11,FREETEXT_COLOR:12,FREETEXT_OPACITY:13,INK_COLOR:21,INK_THICKNESS:22,INK_OPACITY:23},e.PermissionFlag=Line truncated
`}setFont(t){var e=this.current,r=this.commonObjs.get(t[0]);let i=t[1],s=(e.font=r,!this.embedFonts||r.missingFile||this.embeddedFonts[r.loadedName]||(this.addFontStyle(r),this.embeddedFonts[r.loadedName]=r),e.fontMatrix=r.fontMatrix||v.FONT_IDENTITY_MATRIX,"normal");r.black?s="900":r.bold&&(s="bold");t=r.italic?"italic":"normal";i<0?(i=-i,e.fontDirection=-1):e.fontDirection=1,e.fontSize=i,e.fontFamily=r.loadedName,e.fontWeight=s,e.fontStyle=t,e.tspan=this.svgFactory.createElement("svg:tspan"),e.tspan.setAttributeNS(null,"y",b(-e.y)),e.xcoords=[],e.ycoords=[]}endText(){var t=this.current;t.textRenderingMode&v.TextRenderingMode.ADD_TO_PATH_FLAG&&t.txtElement?.hasChildNodes()&&(t.element=t.txtElement,this.clip("nonzero"),this.endPath())}setLineWidth(t){0<t&&(this.current.lineWidth=t)}setLineCap(t){this.current.lineCap=s[t]}setLineJoin(t){this.current.lineJoin=a[t]}setMiterLimit(t){this.current.miterLimit=t}setStrokeAlpha(t){this.current.strokeAlpha=t}setStrokeRGBColor(t,e,r){this.current.strokeColor=v.Util.makeHexColor(t,e,r)}setFillAlpha(t){this.current.fillAlpha=t}setFillRGBColor(t,e,r){this.current.fillColor=v.Util.makeHexColor(t,e,r),this.current.tspan=this.svgFactory.createElement("svg:tspan"),this.current.xcoords=[],this.current.ycoords=[]}setStrokeColorN(t){this.current.strokeColor=this._makeColorN_Pattern(t)}setFillColorN(t){this.current.fillColor=this._makeColorN_Pattern(t)}shadingFill(t){var{width:e,height:r}=this.viewport,i=v.Util.inverseTransform(this.transformMatrix),[e,r,i,s]=v.Util.getAxialAlignedBoundingBox([0,0,e,r],i),a=this.svgFactory.createElement("svg:rect");a.setAttributeNS(null,"x",e),a.setAttributeNS(null,"y",r),a.setAttributeNS(null,"width",i-e),a.setAttributeNS(null,"height",s-r),a.setAttributeNS(null,"fill",this._makeShadingPattern(t)),this.current.fillAlpha<1&&a.setAttributeNS(null,"fill-opacity",this.current.fillAlpha),this._ensureTransformGroup().append(a)}_makeColorN_Pattern(t){return"TilingPattern"===t[0]?this._makeTilingPattern(t):this._makeShadingPattern(t)}_makeTilingPattern(t){var e=t[1],r=t[2],i=t[3]||v.IDENTITY_MATRIX,[s,a,n,o]=t[4],l=t[5],h=t[6],t=t[7],d="shading"+f++,[s,a,n,o]=v.Util.normalizeRect([...v.Util.applyTransform([s,a],i),...v.Util.applyTransform([n,o],i)]),[c,u]=v.Util.singularValueDecompose2dScale(i),l=l*c,c=h*u,h=this.svgFactory.createElement("svg:pattern"),u=(h.setAttributeNS(null,"id",d),h.setAttributeNS(null,"patternUnits","userSpaceOnUse"),h.setAttributeNS(null,"width",l),h.setAttributeNS(null,"height",c),h.setAttributeNS(null,"x",""+s),h.setAttributeNS(null,"y",""+a),this.svg),l=this.transformMatrix,c=this.current.fillColor,p=this.current.strokeColor,n=this.svgFactory.create(n-s,o-a);if(this.svg=n,this.transformMatrix=i,2===t){let t=v.Util.makeHexColor(...e);this.current.fillColor=t,this.current.strokeColor=t}return this.executeOpTree(this.convertOpList(r)),this.svg=u,this.transformMatrix=l,this.current.fillColor=c,this.current.strokeColor=p,h.append(n.childNodes[0]),this.defs.append(h),`url(#${d})`}_makeShadingPattern(i){switch((i="string"==typeof i?this.objs.get(i):i)[0]){case"RadialAxial":var s="shading"+f++,e=i[3];let r;switch(i[1]){case"axial":let t=i[4],e=i[5];(r=this.svgFactory.createElement("svg:linearGradient")).setAttributeNS(null,"id",s),r.setAttributeNS(null,"gradientUnits","userSpaceOnUse"),r.setAttributeNS(null,"x1",t[0]),r.setAttributeNS(null,"y1",t[1]),r.setAttributeNS(null,"x2",e[0]),r.setAttributeNS(null,"y2",e[1]);break;case"radial":var a=i[4],n=i[5],o=i[6],l=i[7];(r=this.svgFactory.createElement("svg:radialGradient")).setAttributeNS(null,"id",s),r.setAttributeNS(null,"gradientUnits","userSpaceOnUse"),r.setAttributeNS(null,"cx",n[0]),r.setAttributeNS(null,"cy",n[1]),r.setAttributeNS(null,"r",l),r.setAttributeNS(null,"fx",a[0]),r.setAttributeNS(null,"fy",a[1]),r.setAttributeNS(null,"fr",o);break;default:throw new Error("Unknown RadialAxial type: "+i[1])}for(let t of e){var h=this.svgFactory.createElement("svg:stop");h.setAttributeNS(null,"offset",t[0]),h.setAttributeNS(null,"stop-color",t[1]),r.append(h)}return this.defs.append(r),`url(#${s})`;case"Mesh":return(0,v.warn)("Unimplemented pattern Mesh"),null;case"Dummy":return"hotpink";default:throw new Error("Unknown IR type: "+i[0])}}setDash(t,e){this.current.dashArray=t,this.current.dashPhase=e}constructPath(e,r){let t=this.current,i=t.x,s=t.y,a=[],n=0;for(let t of e)switch(0|t){case v.OPS.rectangle:i=r[n++],s=r[n++];let t=i+r[n++],e=s+r[n++];a.push("M",b(i),b(s),"L",b(t),b(s),"L",b(t),b(e),"L",b(i),b(e),"Z");break;case v.OPS.moveTo:i=r[n++],s=r[n++],a.push("M",b(i),b(s));break;case v.OPS.lineTo:i=r[n++],s=r[n++],a.push("L",b(i),b(s));break;case v.OPS.curveTo:i=r[n+4],s=r[n+5],a.push("C",b(r[n]),b(r[n+1]),b(r[n+2]),b(r[n+3]),b(i),b(s)),n+=6;break;case v.OPS.curveTo2:a.push("C",b(i),b(s),b(r[n]),b(r[n+1]),b(r[n+2]),b(r[n+3])),i=r[n+2],s=r[n+3],n+=4;break;case v.OPS.curveTo3:i=r[n+2],s=r[n+3],a.push("C",b(r[n]),b(r[n+1]),b(i),b(s),b(i),b(s)),n+=4;break;case v.OPS.closePath:a.push("Z")} Line truncated
/**
* @licstart The following is the entire license notice for the
* JavaScript code in this page
*
* Copyright 2023 Mozilla Foundation
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
* @licend The above is the entire license notice for the
* JavaScript code in this page
*/
!function webpackUniversalModuleDefinition(t,e){"object"==typeof exports&&"object"==typeof module?module.exports=t.pdfjsLib=e():"function"==typeof define&&define.amd?define("pdfjs-dist/build/pdf",[],(()=>t.pdfjsLib=e())):"object"==typeof exports?exports["pdfjs-dist/build/pdf"]=t.pdfjsLib=e():t["pdfjs-dist/build/pdf"]=t.pdfjsLib=e()}(globalThis,(()=>(()=>{"use strict";var __webpack_modules__=[,(t,e,n)=>{Object.defineProperty(e,"__esModule",{value:!0});e.VerbosityLevel=e.Util=e.UnknownErrorException=e.UnexpectedResponseException=e.TextRenderingMode=e.RenderingIntentFlag=e.PromiseCapability=e.PermissionFlag=e.PasswordResponses=e.PasswordException=e.PageActionEventType=e.OPS=e.MissingPDFException=e.MAX_IMAGE_SIZE_TO_CACHE=e.LINE_FACTOR=e.LINE_DESCENT_FACTOR=e.InvalidPDFException=e.ImageKind=e.IDENTITY_MATRIX=e.FormatError=e.FeatureTest=e.FONT_IDENTITY_MATRIX=e.DocumentActionEventType=e.CMapCompressionType=e.BaseException=e.BASELINE_FACTOR=e.AnnotationType=e.AnnotationReplyType=e.AnnotationPrefix=e.AnnotationMode=e.AnnotationFlag=e.AnnotationFieldFlag=e.AnnotationEditorType=e.AnnotationEditorPrefix=e.AnnotationEditorParamsType=e.AnnotationBorderStyleType=e.AnnotationActionEventType=e.AbortException=void 0;e.assert=function assert(t,e){t||unreachable(e)};e.bytesToString=bytesToString;e.createValidAbsoluteUrl=function createValidAbsoluteUrl(t){let e=arguments.length>1&&void 0!==arguments[1]?arguments[1]:null,n=arguments.length>2&&void 0!==arguments[2]?arguments[2]:null;if(!t)return null;try{if(n&&"string"==typeof t){if(n.addDefaultProtocol&&t.startsWith("www.")){const e=t.match(/\./g);e?.length>=2&&(t=`http://${t}`)}if(n.tryConvertEncoding)try{t=stringToUTF8String(t)}catch{}}const i=e?new URL(t,e):new URL(t);if(function _isValidProtocol(t){switch(t?.protocol){case"http:":case"https:":case"ftp:":case"mailto:":case"tel:":return!0;default:return!1}}(i))return i}catch{}return null};e.getModificationDate=function getModificationDate(){let t=arguments.length>0&&void 0!==arguments[0]?arguments[0]:new Date;return[t.getUTCFullYear().toString(),(t.getUTCMonth()+1).toString().padStart(2,"0"),t.getUTCDate().toString().padStart(2,"0"),t.getUTCHours().toString().padStart(2,"0"),t.getUTCMinutes().toString().padStart(2,"0"),t.getUTCSeconds().toString().padStart(2,"0")].join("")};e.getUuid=function getUuid(){if("undefined"!=typeof crypto&&"function"==typeof crypto?.randomUUID)return crypto.randomUUID();const t=new Uint8Array(32);if("undefined"!=typeof crypto&&"function"==typeof crypto?.getRandomValues)crypto.getRandomValues(t);else for(let e=0;e<32;e++)t[e]=Math.floor(255*Math.random());return bytesToString(t)};e.getVerbosityLevel=function getVerbosityLevel(){return s};e.info=function info(t){s>=r.INFOS&&console.log(`Info: ${t}`)};e.isArrayBuffer=function isArrayBuffer(t){return"object"==typeof t&&void 0!==t?.byteLength};e.isArrayEqual=function isArrayEqual(t,e){if(t.length!==e.length)return!1;for(let n=0,i=t.length;n<i;n++)if(t[n]!==e[n])return!1;return!0};e.isNodeJS=void 0;e.normalizeUnicode=function normalizeUnicode(t){if(!c){c=/([\u00a0\u00b5\u037e\u0eb3\u2000-\u200a\u202f\u2126\ufb00-\ufb04\ufb06\ufb20-\ufb36\ufb38-\ufb3c\ufb3e\ufb40-\ufb41\ufb43-\ufb44\ufb46-\ufba1\ufba4-\ufba9\ufbae-\ufbb1\ufbd3-\ufbdc\ufbde-\ufbe7\ufbea-\ufbf8\ufbfc-\ufbfd\ufc00-\ufc5d\ufc64-\ufcf1\ufcf5-\ufd3d\ufd88\ufdf4\ufdfa-\ufdfb\ufe71\ufe77\ufe79\ufe7b\ufe7d]+)|(\ufb05+)/gu;h=new Map([["ſt","ſt"]])}return t.replaceAll(c,((t,e,n)=>e?e.normalize("NFKC"):h.get(n)))};e.objectFromMap=function objectFromMap(t){const e=Object.create(null);for(const[n,i]of t)e[n]=i;return e};e.objectSize=function objectSize(t){return Object.keys(t).length};e.setVerbosityLevel=function setVerbosityLevel(t){Number.isInteger(t)&&(s=t)};e.shadow=shadow;e.string32=function string32(t){return String.fromCharCode(t>>24&255,t>>16&255,t>>8&255,255&t)};e.stringToBytes=stringToBytes;e.stringToPDFString=function stringToPDFString(t){if(t[0]>="ï"){let e;"þ"===t[0]&&"ÿ"===t[1]?e="utf-16be":"ÿ"===t[0]&&"þ"===t[1]?e="utf-16le":"ï"===t[0]&&"»"===t[1]&&"¿"===t[2]&&(e="utf-8");if(e)try{const n=new TextDecoder(e,{fatal:!0}),i=stringToBytes(t);return n.decode(i)}catch(t){warn(`stringToPDFString: "${t}".`)}}const e=[];for(let n=0,i=t.length;n<i;n++){const i=l[t.charCodeAt(n)];e.push(i?String.fromCharCode(i):t.charAt(n))}return e.join("")};e.stringToUTF8String=stringToUTF8String;e.unreachable=unreachable;e.utf8StringToString=function utf8StringToString(t){return unescape(encodeURIComponent(t))};e.warn=warn;n(2);n(84);n(86);n(87);n(89);n(93);n(101);n(102);n(105);n(107);n(109);n(113);n(116);n(123);const i=!("object"!=typeof process||process+""!="[object process]"||process.versions.nw||process.versions.electron&&process.type&&"browser"!==process.type);e.isNodeJS=i;e.IDENTITY_MATRIX=[1,0,0,1,0,0];e.FONT_IDENTITY_MATRIX=[.001,0,0,.001,0,0];e.MAX_IMAGE_SIZE_TO_CACHE=1e7;e.LINE_FACTOR=1.35;e.LINE_DESCENT_FACTOR=.35;e.BASELINE_FACTOR=.25925925925925924;e.RenderingIntentFlag={ANY:1,DISPLAY:2,PRLine truncated
+2 -2
View File
@@ -1,4 +1,4 @@
"use strict";(()=>{var zl=Object.defineProperty;var Hl=(e,t)=>{for(var o in t)zl(e,o,{get:t[o],enumerable:!0})};var ye={notebook:"/script/notebook.c3144c18ae.min.js",org:"/script/org.f61f262915.min.js",markdown:"/script/markdown.44f2e93820.min.js",pdf:"/script/pdf.0175d53d2f.min.js",editor:"/script/editor.4f142d3adc.min.js"};var rn=new Map,Gl=ye;function Wt(e){if(!rn.has(e)){let t=document.createElement("script");t.src=Gl[e];let o=new Promise((r,n)=>{t.onload=r,t.onerror=()=>{rn.delete(e),t.remove(),n(new Error(`Unable to load ${e}. Please retry.`))}});rn.set(e,o),document.head.appendChild(t)}return rn.get(e)}async function tr(){await Wt("editor"),ace.config.set("basePath","/script/external/ace/")}function ds(e){let t=Object.create(null);for(let o of e.split(","))t[o]=1;return o=>o in t}var Me={},Kt=[],rt=()=>{},Xn=()=>!1,us=e=>e.charCodeAt(0)===111&&e.charCodeAt(1)===110&&(e.charCodeAt(2)>122||e.charCodeAt(2)<97),cs=e=>e.startsWith("onUpdate:"),Ge=Object.assign,Ss=(e,t)=>{let o=e.indexOf(t);o>-1&&e.splice(o,1)},jl=Object.prototype.hasOwnProperty,Ae=(e,t)=>jl.call(e,t),Ee=Array.isArray,Pt=e=>Ds(e)==="[object Map]",ls=e=>Ds(e)==="[object Set]",sr=e=>Ds(e)==="[object Date]";var _e=e=>typeof e=="function",Fe=e=>typeof e=="string",ct=e=>typeof e=="symbol",Ve=e=>e!==null&&typeof e=="object",Zn=e=>(Ve(e)||_e(e))&&_e(e.then)&&_e(e.catch),rr=Object.prototype.toString,Ds=e=>rr.call(e),xn=e=>Ds(e).slice(8,-1),an=e=>Ds(e)==="[object Object]",ln=e=>Fe(e)&&e!=="NaN"&&e[0]!=="-"&&""+parseInt(e,10)===e,ps=ds(",key,ref,ref_for,ref_key,onVnodeBeforeMount,onVnodeMounted,onVnodeBeforeUpdate,onVnodeUpdated,onVnodeBeforeUnmount,onVnodeUnmounted");var dn=e=>{let t=Object.create(null);return(o=>t[o]||(t[o]=e(o)))},Wl=/-\w/g,Ke=dn(e=>e.replace(Wl,t=>t.slice(1).toUpperCase())),Kl=/\B([A-Z])/g,qt=dn(e=>e.replace(Kl,"-$1").toLowerCase()),Yt=dn(e=>e.charAt(0).toUpperCase()+e.slice(1)),Rs=dn(e=>e?`on${Yt(e)}`:""),ht=(e,t)=>!Object.is(e,t),Ts=(e,...t)=>{for(let o=0;o<e.length;o++)e[o](...t)},un=(e,t,o,r=!1)=>{Object.defineProperty(e,t,{configurable:!0,enumerable:!1,writable:r,value:o})},eo=e=>{let t=parseFloat(e);return isNaN(t)?e:t};var nr,Os=()=>nr||(nr=typeof globalThis<"u"?globalThis:typeof self<"u"?self:typeof window<"u"?window:typeof global<"u"?global:{});function Pe(e){if(Ee(e)){let t={};for(let o=0;o<e.length;o++){let r=e[o],n=Fe(r)?Xl(r):Pe(r);if(n)for(let i in n)t[i]=n[i]}return t}else if(Fe(e)||Ve(e))return e}var Yl=/;(?![^(]*\))/g,Jl=/:([^]+)/,Ql=/\/\*[^]*?\*\//g;function Xl(e){let t={};return e.replace(Ql,"").split(Yl).forEach(o=>{if(o){let r=o.split(Jl);r.length>1&&(t[r[0].trim()]=r[1].trim())}}),t}function O(e){let t="";if(Fe(e))t=e;else if(Ee(e))for(let o=0;o<e.length;o++){let r=O(e[o]);r&&(t+=r+" ")}else if(Ve(e))for(let o in e)e[o]&&(t+=o+" ");return t.trim()}var ir="itemscope,allowfullscreen,formnovalidate,ismap,nomodule,novalidate,readonly",ar=ds(ir),Zl=ds(ir+",async,autofocus,autoplay,controls,default,defer,disabled,inert,loop,open,required,reversed,scoped,seamless,checked,muted,multiple,selected");function cn(e){return!!e||e===""}function xl(e,t){if(e.length!==t.length)return!1;let o=!0;for(let r=0;o&&r<e.length;r++)o=fs(e[r],t[r]);return o}function or(e,t){if(e.size!==t.size)return!1;let o=Array.from(t),r=new Uint8Array(o.length);for(let n of e){let i=-1;for(let a=0;a<o.length;a++)if(!r[a]&&fs(n,o[a])){i=a;break}if(i<0)return!1;r[i]=1}return!0}function fs(e,t){if(e===t)return!0;let o=sr(e),r=sr(t);if(o||r)return o&&r?e.getTime()===t.getTime():!1;if(o=ct(e),r=ct(t),o||r)return e===t;if(o=Ee(e),r=Ee(t),o||r)return o&&r?xl(e,t):!1;if(o=Ve(e),r=Ve(t),o||r){if(!o||!r)return!1;if(o=Pt(e),r=Pt(t),o||r||(o=ls(e),r=ls(t),o||r))return o&&r?or(e,t):!1;let n=Object.keys(e).length,i=Object.keys(t).length;if(n!==i)return!1;for(let a in e){let u=e.hasOwnProperty(a),m=t.hasOwnProperty(a);if(u&&!m||!u&&m||!fs(e[a],t[a]))return!1}}return String(e)===String(t)}var lr=e=>!!(e&&e.__v_isRef===!0),c=e=>Fe(e)?e:e==null?"":Ee(e)||Ve(e)&&(e.toString===rr||!_e(e.toString))?lr(e)?c(e.value):JSON.stringify(e,dr,2):String(e),dr=(e,t)=>lr(t)?dr(e,t.value):Pt(t)?{[`Map(${t.size})`]:[...t.entries()].reduce((o,[r,n],i)=>(o[Qn(r,i)+" =>"]=n,o),{})}:ls(t)?{[`Set(${t.size})`]:[...t.values()].map(o=>Qn(o))}:ct(t)?Qn(t):Ve(t)&&!Ee(t)&&!an(t)?String(t):t,Qn=(e,t="")=>{var o;return ct(e)?`Symbol(${(o=e.description)!=null?o:t})`:e};function ed(e,...t){console.warn(`[Vue warn] ${e}`,...t)}var Ye,Ps=class{constructor(t=!1){this.detached=t,this._active=!0,this._on=0,this.effects=[],this.cleanups=[],this._isPaused=!1,this._warnOnRun=!0,this.__v_skip=!0,!t&&Ye&&(Ye.active?(this.parent=Ye,this.index=(Ye.scopes||(Ye.scopes=[])).push(this)-1):(this._active=!1,this._warnOnRun=!1))}get active(){return this._active}pause(){if(this._active){this._isPaused=!0;let t,o;if(this.scopes){let r=this.scopes.slice();for(t=0,o=r.length;t<o;t++)r[t].pause()}for(t=0,o=this.effects.length;t<o;t++)this.effects[t].pause()}}resume(){if(this._active&&this._isPaused){this._isPaused=!Line truncated
"use strict";(()=>{var zl=Object.defineProperty;var Hl=(e,t)=>{for(var o in t)zl(e,o,{get:t[o],enumerable:!0})};var ye={notebook:"/script/notebook.c3144c18ae.min.js",org:"/script/org.f61f262915.min.js",markdown:"/script/markdown.44f2e93820.min.js",pdf:"/script/pdf.4d9cc6ff49.min.js",editor:"/script/editor.4f142d3adc.min.js"};var rn=new Map,Gl=ye;function Wt(e){if(!rn.has(e)){let t=document.createElement("script");t.src=Gl[e];let o=new Promise((r,n)=>{t.onload=r,t.onerror=()=>{rn.delete(e),t.remove(),n(new Error(`Unable to load ${e}. Please retry.`))}});rn.set(e,o),document.head.appendChild(t)}return rn.get(e)}async function tr(){await Wt("editor"),ace.config.set("basePath","/script/external/ace/")}function ds(e){let t=Object.create(null);for(let o of e.split(","))t[o]=1;return o=>o in t}var Me={},Kt=[],rt=()=>{},Xn=()=>!1,us=e=>e.charCodeAt(0)===111&&e.charCodeAt(1)===110&&(e.charCodeAt(2)>122||e.charCodeAt(2)<97),cs=e=>e.startsWith("onUpdate:"),Ge=Object.assign,Ss=(e,t)=>{let o=e.indexOf(t);o>-1&&e.splice(o,1)},jl=Object.prototype.hasOwnProperty,Ae=(e,t)=>jl.call(e,t),Ee=Array.isArray,Pt=e=>Ds(e)==="[object Map]",ls=e=>Ds(e)==="[object Set]",sr=e=>Ds(e)==="[object Date]";var _e=e=>typeof e=="function",Fe=e=>typeof e=="string",ct=e=>typeof e=="symbol",Ve=e=>e!==null&&typeof e=="object",Zn=e=>(Ve(e)||_e(e))&&_e(e.then)&&_e(e.catch),rr=Object.prototype.toString,Ds=e=>rr.call(e),xn=e=>Ds(e).slice(8,-1),an=e=>Ds(e)==="[object Object]",ln=e=>Fe(e)&&e!=="NaN"&&e[0]!=="-"&&""+parseInt(e,10)===e,ps=ds(",key,ref,ref_for,ref_key,onVnodeBeforeMount,onVnodeMounted,onVnodeBeforeUpdate,onVnodeUpdated,onVnodeBeforeUnmount,onVnodeUnmounted");var dn=e=>{let t=Object.create(null);return(o=>t[o]||(t[o]=e(o)))},Wl=/-\w/g,Ke=dn(e=>e.replace(Wl,t=>t.slice(1).toUpperCase())),Kl=/\B([A-Z])/g,qt=dn(e=>e.replace(Kl,"-$1").toLowerCase()),Yt=dn(e=>e.charAt(0).toUpperCase()+e.slice(1)),Rs=dn(e=>e?`on${Yt(e)}`:""),ht=(e,t)=>!Object.is(e,t),Ts=(e,...t)=>{for(let o=0;o<e.length;o++)e[o](...t)},un=(e,t,o,r=!1)=>{Object.defineProperty(e,t,{configurable:!0,enumerable:!1,writable:r,value:o})},eo=e=>{let t=parseFloat(e);return isNaN(t)?e:t};var nr,Os=()=>nr||(nr=typeof globalThis<"u"?globalThis:typeof self<"u"?self:typeof window<"u"?window:typeof global<"u"?global:{});function Pe(e){if(Ee(e)){let t={};for(let o=0;o<e.length;o++){let r=e[o],n=Fe(r)?Xl(r):Pe(r);if(n)for(let i in n)t[i]=n[i]}return t}else if(Fe(e)||Ve(e))return e}var Yl=/;(?![^(]*\))/g,Jl=/:([^]+)/,Ql=/\/\*[^]*?\*\//g;function Xl(e){let t={};return e.replace(Ql,"").split(Yl).forEach(o=>{if(o){let r=o.split(Jl);r.length>1&&(t[r[0].trim()]=r[1].trim())}}),t}function O(e){let t="";if(Fe(e))t=e;else if(Ee(e))for(let o=0;o<e.length;o++){let r=O(e[o]);r&&(t+=r+" ")}else if(Ve(e))for(let o in e)e[o]&&(t+=o+" ");return t.trim()}var ir="itemscope,allowfullscreen,formnovalidate,ismap,nomodule,novalidate,readonly",ar=ds(ir),Zl=ds(ir+",async,autofocus,autoplay,controls,default,defer,disabled,inert,loop,open,required,reversed,scoped,seamless,checked,muted,multiple,selected");function cn(e){return!!e||e===""}function xl(e,t){if(e.length!==t.length)return!1;let o=!0;for(let r=0;o&&r<e.length;r++)o=fs(e[r],t[r]);return o}function or(e,t){if(e.size!==t.size)return!1;let o=Array.from(t),r=new Uint8Array(o.length);for(let n of e){let i=-1;for(let a=0;a<o.length;a++)if(!r[a]&&fs(n,o[a])){i=a;break}if(i<0)return!1;r[i]=1}return!0}function fs(e,t){if(e===t)return!0;let o=sr(e),r=sr(t);if(o||r)return o&&r?e.getTime()===t.getTime():!1;if(o=ct(e),r=ct(t),o||r)return e===t;if(o=Ee(e),r=Ee(t),o||r)return o&&r?xl(e,t):!1;if(o=Ve(e),r=Ve(t),o||r){if(!o||!r)return!1;if(o=Pt(e),r=Pt(t),o||r||(o=ls(e),r=ls(t),o||r))return o&&r?or(e,t):!1;let n=Object.keys(e).length,i=Object.keys(t).length;if(n!==i)return!1;for(let a in e){let u=e.hasOwnProperty(a),m=t.hasOwnProperty(a);if(u&&!m||!u&&m||!fs(e[a],t[a]))return!1}}return String(e)===String(t)}var lr=e=>!!(e&&e.__v_isRef===!0),c=e=>Fe(e)?e:e==null?"":Ee(e)||Ve(e)&&(e.toString===rr||!_e(e.toString))?lr(e)?c(e.value):JSON.stringify(e,dr,2):String(e),dr=(e,t)=>lr(t)?dr(e,t.value):Pt(t)?{[`Map(${t.size})`]:[...t.entries()].reduce((o,[r,n],i)=>(o[Qn(r,i)+" =>"]=n,o),{})}:ls(t)?{[`Set(${t.size})`]:[...t.values()].map(o=>Qn(o))}:ct(t)?Qn(t):Ve(t)&&!Ee(t)&&!an(t)?String(t):t,Qn=(e,t="")=>{var o;return ct(e)?`Symbol(${(o=e.description)!=null?o:t})`:e};function ed(e,...t){console.warn(`[Vue warn] ${e}`,...t)}var Ye,Ps=class{constructor(t=!1){this.detached=t,this._active=!0,this._on=0,this.effects=[],this.cleanups=[],this._isPaused=!1,this._warnOnRun=!0,this.__v_skip=!0,!t&&Ye&&(Ye.active?(this.parent=Ye,this.index=(Ye.scopes||(Ye.scopes=[])).push(this)-1):(this._active=!1,this._warnOnRun=!1))}get active(){return this._active}pause(){if(this._active){this._isPaused=!0;let t,o;if(this.scopes){let r=this.scopes.slice();for(t=0,o=r.length;t<o;t++)r[t].pause()}for(t=0,o=this.effects.length;t<o;t++)this.effects[t].pause()}}resume(){if(this._active&&this._isPaused){this._isPaused=!Line 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 "${vc(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 os(e,t){return qe(new Error,{type:e,[na]:!0},t)}function Vt(e,t){return e instanceof Error&&na in e&&(t==null||!!(e.type&t))}var hc=["params","query","hash"];function vc(e){if(typeof e=="string")return e;if(e.path!=null)return e.path;let t={};for(let o of hc)o in e&&(t[o]=e[o]);return JSON.stringify(t,null,2)}function oa(e){let t={};if(e===""||e==="?")return t;let o=(e[0]==="?"?e.slice(1):e).split("&");for(let r=0;r<o.length;++r){let n=o[r].replace(Gi," "),i=n.indexOf("="),a=_s(i<0?n:n.slice(0,i)),u=i<0?null:_s(n.slice(i+1));if(a in t){let m=t[a];gt(m)||(m=t[a]=[m]),m.push(u)}else t[a]=u}return t}function Go(e){let t="";for(let o in e){let r=e[o];if(o=ic(o),r==null){r!==void 0&&(t+=(t.length?"&":"")+o);continue}(gt(r)?r.map(n=>n&&Mo(n)):[r&&Mo(r)]).forEach(n=>{n!==void 0&&(t+=(t.length?"&":"")+o,n!=null&&(t+="="+n))})}return t}function ra(e){let t={};for(let o in e){let r=e[o];r!==void 0&&(t[o]=gt(r)?r.map(n=>n==null?null:""+n):r==null?r:""+r)}return t}var ia=Symbol(""),jo=Symbol(""),Gn=Symbol(""),jn=Symbol(""),Wn=Symbol("");function Cs(){let e=[];function t(r){return e.push(r),()=>{let n=e.indexOf(r);n>-1&&e.splice(n,1)}}function o(){e=[]}return{add:t,list:()=>e.slice(),reset:o}}function Bt(e,t,o,r,n,i=a=>a()){let a=r&&(r.enterCallbacks[n]=r.enterCallbacks[n]||[]);return()=>new Promise((u,m)=>{let h=p=>{p===!1?m(os(Be.NAVIGATION_ABORTED,{from:o,to:t})):p instanceof Error?m(p):mc(p)?m(os(Be.NAVIGATION_GUARD_REDIRECT,{from:t,to:p})):(a&&r.enterCallbacks[n]===a&&typeof p=="function"&&a.push(p),u())},v=i(()=>e.call(r&&r.instances[n],t,o,h)),k=Promise.resolve(v);e.length<3&&(k=k.then(h)),k.catch(p=>m(p))})}function Kn(e,t,o,r,n=i=>i()){let i=[];for(let a of e)for(let u in a.components){let m=a.components[u];if(!(t!=="beforeRouteEnter"&&!a.instances[u]))if(Hi(m)){let h=(m.__vccOpts||m)[t];h&&i.push(Bt(h,o,r,a,u,n))}else{let h=m();i.push(()=>h.then(v=>{if(!v)throw new Error(`Couldn't resolve component "${u}" at "${a.path}"`);let k=Qu(v)?v.default:v;a.mods[u]=v,a.components[u]=k;let p=(k.__vccOpts||k)[t];return p&&Bt(p,o,r,a,u,n)()}))}}return i}function aa(e,t){let o=[],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=>ns(h,u))?r.push(u):o.push(u));let m=e.matched[a];m&&(t.matched.find(h=>ns(h,m))||n.push(m))}return[o,r,n]}var yc=()=>location.protocol+"//"+location.host;function ga(e,t){let{pathname:o,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),Fo(u,"")}return Fo(o,e)+r+n}function gc(e,t,o,r){let n=[],i=[],a=null,u=({state:p})=>{let g=ga(e,location),w=o.value,b=t.value,T=0;if(p){if(o.value=g,t.value=p,a&&a===w){a=null;return}T=b?p.position-b.position:0}else r(g);n.forEach(N=>{N(o.value,w,{delta:T,type:Hn.pop,direction:T?T>0?Bn.forward:Bn.back:Bn.unknown})})};function m(){a=o.value}function h(p){n.push(p);let g=()=>{let w=n.indexOf(p);w>-1&&n.splice(w,1)};return i.push(g),g}function v(){if(document.visibilityState==="hidden"){let{history:p}=window;if(!p.state)return;p.replaceState(qe({},p.state,{scroll:on()}),"")}}function k(){for(let p of i)p();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:m,listen:h,destroy:k}}function la(e,t,o,r=!1,n=!1){return{back:e,current:t,forward:o,replaced:r,position:window.history.length,scroll:n?on():null}}function bc(e){let{history:t,location:o}=window,r={value:ga(e,o)},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(m,h,v){let k=e.indexOf("#"),p=k>-1?(o.host&&document.querySelector("base")?e:e.slice(k))+m:yc()+e+m;try{t[v?"replaceState":"pushState"](h,"",p),n.value=h}catch(g){console.error(g),o[v?"replace":"assign"](p)}}function a(m,h){i(m,qe({},t.state,la(n.value.back,m,n.value.forward,!0),h,{position:n.value.position}),!0),r.value=m}function u(m,h){let v=qe({},n.value,t.state,{forward:m,scroll:on()});i(v.current,v,!0),i(m,qe({},la(r.value,m,null),{position:v.position+1},h),!1),r.value=m}return{location:r,state:n,push:u,replace:a}}function ba(e){e=Zi(e);let t=bc(e),o=gc(e,t.state,t.location,t.replace);function r(i,a=!0){a||o.pauseListeners(),history.go(i)}let n=qe({locatLine truncated
@@ -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
+31
View File
@@ -0,0 +1,31 @@
// Run with node scripts/benchmark-anonymizer.js [path-to-anonymize-utils.ts].
// Fixtures are synthetic; reported timings exclude fixture creation and I/O.
const path = require("node:path");
const { performance } = require("node:perf_hooks");
require("ts-node/register/transpile-only");
const { ContentAnonimizer } = require(process.argv[2]
? path.resolve(process.argv[2]) : "../src/core/anonymize-utils");
const terms = ["first.last", "user@example.org", "dept.example.edu", "lab.io", "project.org", "Author X. Name",
...Array.from({ length: 41 }, (_, i) => `Researcher${i}`)];
const row = "123,456,78.91234,-45.123456\n";
const cases = [
["2 MB CSV, 47 absent terms", () => row.repeat(70000), terms],
["100 MB CSV, 47 absent terms", () => row.repeat(3600000), terms],
["50 MB CSV, sparse matches", () => `first.last\n${row.repeat(1800000)}\nhttps://host/user@example.org`, terms],
["22 MB text, 1.2 million replacements", () => "Alice 123 first.last,45.678,-23.456\n".repeat(600000), ["Alice", ...terms]],
];
for (const [name, fixture, rules] of cases) {
const input = fixture();
const startCompile = performance.now();
const anonymizer = new ContentAnonimizer({ terms: rules });
const compileMs = performance.now() - startCompile;
const started = performance.now();
try {
const output = anonymizer.anonymize(input);
console.log(JSON.stringify({ name, inputBytes: Buffer.byteLength(input), outputBytes: Buffer.byteLength(output),
compileMs: +compileMs.toFixed(1), matchMs: +(performance.now() - started).toFixed(1) }));
} catch (error) {
console.log(JSON.stringify({ name, inputBytes: Buffer.byteLength(input),
compileMs: +compileMs.toFixed(1), matchMs: +(performance.now() - started).toFixed(1), error: error.code || error.message }));
}
}
+131 -43
View File
@@ -6,6 +6,7 @@ import { isBinaryFileSync } from "isbinaryfile";
import { lookup as lookupMime } from "mime-types";
import config from "../config";
import { TermPresenceIndex } from "./term-presence";
import {
parseTermSpec,
termVariants,
@@ -211,9 +212,13 @@ const markdownImageRegex =
interface CompiledTermVariant {
// RE2 for regular patterns; time-limited native fallback for JS extensions.
pattern: RE2JS | RegExp;
// A boundary-free, fixed-width native search can cheaply rule out literal
// terms before RE2 scans a large file. A hit still uses the usual matcher.
// A boundary-free native search for fixed-width patterns can rule out
// absent terms. Candidate replacement retains RE2's boundary semantics.
literalPrefilter?: RegExp;
asciiPattern?: RegExp;
asciiReplacement?: string;
asciiBefore?: boolean;
asciiAfter?: boolean;
before: boolean;
after: boolean;
mask: string;
@@ -269,11 +274,14 @@ function compileTerms(terms: string[] | undefined): CompiledTermVariant[] {
if (!useAsRegex || hasCatastrophicBacktracking(term)) {
term = term.replace(/[-[\]{}()*+?.,\\^$|#]/g, "\\$&");
}
// Only plain literals qualify: expanding their letters produces fixed
// character classes, never user-controlled backtracking. Do not apply
// this shortcut to regex syntax, including escaped literals.
const isLiteral = !/[\\^$.*+?()[\]{}|]/.test(term);
// Plain text and unquantified dots have fixed width in code points.
// Exclude all other regex syntax so the native search cannot backtrack
// exponentially. This includes dotted names, emails and domains.
const isFixedWidth = !/[\\^$*+?()[\]{}|]/.test(term);
for (const variant of termVariants(term)) {
// Stripping a combining-mark-only term can produce an empty variant.
// Keep zero-width matches on RE2's progress-aware iterator.
const fixedWidth = isFixedWidth && variant.pattern.length > 0;
const bounded = withWordBoundaries(variant.pattern, {
sniffSource: variant.sniff,
unicode: variant.unicode,
@@ -286,9 +294,15 @@ function compileTerms(terms: string[] | undefined): CompiledTermVariant[] {
RE2JS.CASE_INSENSITIVE
);
compiled.push({ pattern, before, after, mask,
asciiPattern: fixedWidth ? new RegExp(bounded.replace(/\./g, "[^\\n]"), "giu") : undefined,
asciiReplacement: fixedWidth ? mask.replace(/\$/g, () => "$$") : undefined,
asciiBefore: !variant.unicode && bounded.startsWith("\\b"),
asciiAfter: !variant.unicode && bounded.endsWith("\\b"),
// RE2 uses Unicode case folding even for the non-diacritic pass.
// Omit boundaries so this search can only rule out absent terms.
literalPrefilter: isLiteral ? new RegExp(variant.pattern, "iu") : undefined });
literalPrefilter: fixedWidth
// RE2's dot excludes only LF; JS dot also excludes CR and U+2028/9.
? new RegExp(variant.pattern.replace(/\./g, "[^\\n]"), "iu") : undefined });
} catch {
// Retain JavaScript-only syntax and large repetition counts under
// the execution deadline; RE2 handles the common case without backtracking.
@@ -303,13 +317,37 @@ function compileTerms(terms: string[] | undefined): CompiledTermVariant[] {
return compiled;
}
interface CompiledTermPlan {
terms: CompiledTermVariant[];
presence: TermPresenceIndex | null;
}
// Every file in a ZIP uses the same rules. Reuse immutable compiled patterns
// across transformers, with bounded retention for unrelated repositories.
const termPlans = new Map<string, CompiledTermPlan>();
function compiledTermPlan(terms: string[] | undefined): CompiledTermPlan {
const key = JSON.stringify([config.ANONYMIZATION_MASK, terms || []]);
const cached = termPlans.get(key);
if (cached) return cached;
const compiled = compileTerms(terms);
const plan = {
terms: compiled,
presence: compiled.length && compiled.length <= 256 && compiled.every(term => term.literalPrefilter)
? new TermPresenceIndex(compiled.map(term => term.literalPrefilter!)) : null,
};
if (key.length <= 8192 && compiled.length <= 256) {
if (termPlans.size >= 64) termPlans.delete(termPlans.keys().next().value!);
termPlans.set(key, plan);
}
return plan;
}
export class ContentAnonimizer {
public wasAnonymized = false;
// Compiled once per instance and reused for every anonymize() call.
// Streamed files invoke anonymize() many times per file (one per chunk),
// so caching here avoids rebuilding regexes on every chunk.
// Per-instance metadata references the bounded cache of compiled rules.
private compiledTerms: CompiledTermVariant[];
private selfLinkRegexes: RegExp[] | null = null;
private termPresence: TermPresenceIndex | null = null;
constructor(
readonly opt: {
@@ -321,7 +359,11 @@ export class ContentAnonimizer {
repoId?: string;
}
) {
this.compiledTerms = compileTerms(opt.terms);
const plan = compiledTermPlan(opt.terms);
// Keep per-instance metadata independent. Regex matching is synchronous;
// native replacement resets lastIndex and RE2 creates a fresh matcher.
this.compiledTerms = plan.terms.map(term => ({ ...term }));
this.termPresence = plan.presence;
if (opt.repoName && opt.branchName) {
const r = escapeRegex(opt.repoName);
const b = escapeRegex(opt.branchName);
@@ -370,19 +412,36 @@ export class ContentAnonimizer {
}
private replaceTerms(content: string): string {
for (const c of this.compiledTerms) {
// remove whole url if it contains the term
if (!this.compiledTerms.length) return content;
let candidates = this.termPresence?.candidates(content);
// On ASCII input both engines have identical fixed-width matching and
// word boundaries. Native replacement avoids allocating one JS match
// object per CSV value. Non-ASCII input keeps RE2 validation below.
let asciiContent = (!candidates || candidates.size > 0) && /^\p{ASCII}*$/u.test(content);
for (let index = 0; index < this.compiledTerms.length; index++) {
if (candidates && !candidates.has(index)) continue;
const c = this.compiledTerms[index];
// An absent term cannot occur inside a URL either.
if (c.literalPrefilter && !c.literalPrefilter.test(content)) continue;
const previous = content;
content = content.replace(urlRegex, (match) => {
if (replaceTerm(match, c) !== match) {
if (replaceTerm(match, c, true) !== match) {
this.wasAnonymized = true;
return c.mask;
}
return match;
});
// remove the term in the text
const replaced = replaceTerm(content, c);
// URL replacement may already have inserted a non-ASCII custom mask.
if (content !== previous) asciiContent = asciiContent && /^\p{ASCII}*$/u.test(c.mask);
const replaced = replaceTerm(content, c, asciiContent);
if (replaced !== content) this.wasAnonymized = true;
content = replaced;
// Masks and changed boundaries can introduce later matches. Re-index
// only after a real change, never once per absent term.
if (content !== previous) {
asciiContent = asciiContent && /^\p{ASCII}*$/u.test(c.mask);
candidates = this.termPresence?.candidates(content, index);
}
}
return content;
}
@@ -437,38 +496,62 @@ function runWithAnonymizationDeadline(run: () => string): string {
return anonymizationScript.runInNewContext({ run }, { timeout: 1000 });
}
function replaceTerm(content: string, term: CompiledTermVariant): string {
if (term.literalPrefilter && !term.literalPrefilter.test(content)) return content;
function replaceTerm(content: string, term: CompiledTermVariant, asciiContent = false): string {
if (term.pattern instanceof RegExp) {
return content.replace(term.pattern, () => term.mask);
}
// Fixed-width literal searches run natively. Validate each candidate with
// RE2 on a small window to preserve its case folding and boundary semantics
// without scanning megabytes of notebook outputs in the JS regex engine.
const candidates = term.literalPrefilter
? content.matchAll(new RegExp(term.literalPrefilter.source, "giu"))
: null;
const matcher = candidates ? null : term.pattern.matcher(content);
if (term.literalPrefilter && asciiContent && term.asciiPattern) {
return content.replace(term.asciiPattern, term.asciiReplacement!);
}
if (term.literalPrefilter) {
const validated = new Map<string, boolean>();
const candidates = new RegExp(term.literalPrefilter.source, "giu");
const pieces: string[] = [];
let cursor = 0;
let match: RegExpExecArray | null;
while ((match = candidates.exec(content))) {
const candidate = match[0], start = match.index, end = start + candidate.length;
let valid = true;
if (term.before && /[\p{L}\p{N}_]$/u.test(content.slice(Math.max(0, start - 2), start))) valid = false;
if (term.after && /^[\p{L}\p{N}_]/u.test(content.slice(end, end + 2))) valid = false;
// On ASCII candidates the fixed-width native and RE2 searches agree.
// Check RE2's ASCII word boundaries explicitly: JS /iu\b/ also treats
// Kelvin sign and long s as word characters, while RE2 does not.
if (valid && /^\p{ASCII}*$/u.test(candidate)) {
if (term.asciiBefore && asciiWordAt(content, start - 1) === asciiWordAt(content, start)) valid = false;
if (term.asciiAfter && asciiWordAt(content, end - 1) === asciiWordAt(content, end)) valid = false;
} else if (valid) {
// Keep RE2 authoritative for Unicode case folding. Repeated values in
// datasets share small contexts; cap this cache independently of size.
const offset = Math.max(0, start - 2);
const window = content.slice(offset, end + 2);
const key = `${start - offset}:${window}`;
const cached = validated.get(key);
if (cached === undefined) {
const check = (term.pattern as RE2JS).matcher(window);
valid = check.find(start - offset) && check.start() === start - offset && check.end() === end - offset;
if (validated.size < 256) validated.set(key, valid);
} else valid = cached;
}
if (!valid) {
// A boundary-rejected candidate must not hide an overlapping match,
// e.g. the second a-a in "xa-a-a". Advance by one Unicode code point.
candidates.lastIndex = start + (content.codePointAt(start)! > 0xffff ? 2 : 1);
continue;
}
pieces.push(content.slice(cursor, start), term.mask);
cursor = end;
}
if (!pieces.length) return content;
pieces.push(content.slice(cursor));
return pieces.join("");
}
const matcher = term.pattern.matcher(content);
const pieces: string[] = [];
let cursor = 0;
const matches = function* () {
if (candidates) {
for (const candidate of candidates) {
const start = candidate.index!;
const end = start + candidate[0].length;
const offset = Math.max(0, start - 2);
const check = (term.pattern as RE2JS).matcher(content.slice(offset, end + 2));
if (check.find(start - offset) && check.start() === start - offset && check.end() === end - offset) {
yield { start, end };
}
}
} else {
while (matcher!.find()) yield { start: matcher!.start(), end: matcher!.end() };
}
};
for (const { start, end } of matches()) {
// RE2 has no lookahead. Check the generated Unicode word boundaries
// outside the engine, without executing any user-supplied native regex.
while (matcher.find()) {
const start = matcher.start(), end = matcher.end();
if (term.before && /[\p{L}\p{N}_]$/u.test(content.slice(Math.max(0, start - 2), start))) continue;
if (term.after && /^[\p{L}\p{N}_]/u.test(content.slice(end, end + 2))) continue;
pieces.push(content.slice(cursor, start), term.mask);
@@ -478,3 +561,8 @@ function replaceTerm(content: string, term: CompiledTermVariant): string {
pieces.push(content.slice(cursor));
return pieces.join("");
}
function asciiWordAt(content: string, index: number): boolean {
const code = content.charCodeAt(index);
return code >= 48 && code <= 57 || code >= 65 && code <= 90 || code >= 97 && code <= 122 || code === 95;
}
+4 -4
View File
@@ -253,7 +253,7 @@ export async function boundAppToken(ownerId: string, binding: RepositoryAccess,
return token;
}
export async function selectRepositoryAccess(ownerId: string, fullName: string, choice?: unknown, repositoryId?: number): Promise<{ token: string; binding: RepositoryAccess }> {
export async function selectRepositoryAccess(ownerId: string, fullName: string, choice?: unknown, repositoryId?: number): Promise<{ token: string; binding: RepositoryAccess; fullName?: string }> {
if (!/^[^/\s]+\/[^/\s]+$/.test(fullName)) throw appError("repo_not_found", 400);
if (choice !== undefined && choice !== "oauth" && choice !== "github-app") throw appError("invalid_connection", 400);
const hasApp = config.GITHUB_APP_ENABLED && await CredentialModel.exists({ ownerId, provider: APP_PROVIDER });
@@ -262,14 +262,14 @@ export async function selectRepositoryAccess(ownerId: string, fullName: string,
if (!repo) {
const userToken = await appUserToken(ownerId);
const publicRepo = await githubRequest<GitHubRepositoryInfo>(
`/repos/${fullName.split("/").map(encodeURIComponent).join("/")}`, userToken);
repositoryId === undefined ? `/repos/${fullName.split("/").map(encodeURIComponent).join("/")}` : `/repositories/${repositoryId}`, userToken);
if (publicRepo.private !== false || publicRepo.visibility !== "public" || !Number.isSafeInteger(publicRepo.id)) throw appError("github_app_access_required");
const binding: RepositoryAccess = { kind: "github-app", publicRead: true,
repositoryId: publicRepo.id, revision: randomUUID() };
return { binding, token: await boundAppToken(ownerId, binding, fullName) };
return { binding, fullName: publicRepo.full_name, token: await boundAppToken(ownerId, binding, publicRepo.full_name) };
}
const binding: RepositoryAccess = { kind: "github-app", repositoryId: repo.id, installationId: repo.installationId, revision: randomUUID() };
return { binding, token: await boundAppToken(ownerId, binding, fullName) };
return { binding, fullName: repo.full_name, token: await boundAppToken(ownerId, binding, repo.full_name) };
}
const token = await getCredentialToken(ownerId);
if (!token) throw appError("github_oauth_required");
+67
View File
@@ -0,0 +1,67 @@
/**
* Candidate index for nonempty, capture-free, fixed-width compiler patterns.
* False positives are safe; false negatives are not.
*/
export class TermPresenceIndex {
private groups: { indices: number[]; characters: string[] | null }[] = [];
constructor(private readonly patterns: RegExp[]) {
for (let index = 0; index < patterns.length; index++) {
const characters = firstCharacters(patterns[index].source);
const group = { indices: [index], characters };
// Merge groups whose first characters can overlap under Unicode folding.
// Then a combined search can identify one group without hiding another
// alternative at the same position (Alice versus AliceSmith, for example).
for (let i = this.groups.length - 1; i >= 0; i--) {
const other = this.groups[i];
if (overlap(group.characters, other.characters)) {
group.indices.push(...other.indices);
group.characters = group.characters && other.characters
? [...new Set([...group.characters, ...other.characters])] : null;
this.groups.splice(i, 1);
}
}
this.groups.push(group);
}
}
candidates(content: string, after = -1): Set<number> {
const found = new Set<number>();
const remaining = this.groups.map(group => group.indices.filter(index => index > after))
.filter(indices => indices.length);
let offset = 0;
while (remaining.length) {
// Fixed-width inputs contain no captures or quantifiers. Removing a
// discovered group avoids revisiting millions of identical CSV values.
const search = new RegExp(remaining.map(indices =>
`(${indices.map(index => this.patterns[index].source).join("|")})`).join("|"), "giu");
search.lastIndex = offset;
const match = search.exec(content);
if (!match) break;
const selected = match.slice(1).findIndex(value => value !== undefined);
for (const index of remaining[selected]) found.add(index);
remaining.splice(selected, 1);
// Another group's match may start inside this one. Search from the next
// code point, not from the end of the match, to retain nested candidates.
offset = match.index + (content.codePointAt(match.index)! > 0xffff ? 2 : 1);
}
return found;
}
}
function firstCharacters(source: string): string[] | null {
if (source.startsWith("[")) {
const characters = source.slice(1, source.indexOf("]"));
// Only the compiler's expanded letter classes have an enumerable prefix.
// Dot's [^\n], escapes and any future syntax conservatively join all groups.
return /^[\p{L}\p{N}]+$/u.test(characters) ? Array.from(characters) : null;
}
if (source.startsWith("\\")) return null;
return [Array.from(source)[0]];
}
function overlap(left: string[] | null, right: string[] | null): boolean {
if (!left || !right) return true;
const pattern = new RegExp(`^(?:${left.map(char => char.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")).join("|")})$`, "iu");
return right.some(char => pattern.test(char));
}
+5 -4
View File
@@ -191,7 +191,7 @@ 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", "removing", "expiring"].includes(model.status || "")) throw appError("repository_busy", 409);
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 name = source.repositoryName || source.repositoryFullName || "";
const repositoryId = model.githubAccess?.kind === "github-app" && connection === "github-app"
@@ -201,9 +201,10 @@ router.post("/connections/migrate", async (req, res) => {
// recreated at the same name. Replacing a source requires its own preview.
if (model.githubAccess?.kind === "github-app" && selected.binding.kind === "github-app" &&
model.githubAccess.repositoryId !== selected.binding.repositoryId) throw appError("connection_changed", 409);
const parts = name.split("/").map(encodeURIComponent).join("/");
const repositoryPath = repositoryId === undefined ? `/repos/${parts}` : `/repositories/${repositoryId}`;
await githubRequest(`${repositoryPath}/${isRepo ? `commits/${encodeURIComponent(source.commit || "")}` : `pulls/${source.pullRequestId}`}`, selected.token);
const parts = (selected.fullName || name).split("/").map(encodeURIComponent).join("/");
const repositoryPath = `/repos/${parts}`;
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,
githubAccess: model.githubAccess ? model.githubAccess : { $exists: false } };
+6 -1
View File
@@ -112,10 +112,11 @@ router.get(
upstreamStatus,
upstreamBody: upstreamBody?.slice(0, 500),
url: config.STREAMER_ENTRYPOINT
? join(config.STREAMER_ENTRYPOINT, "api/zip")
? join(config.STREAMER_ENTRYPOINT, "api/download")
: undefined,
err: serializeError(err),
});
const streaming = res.headersSent;
handleError(
new AnonymousError(errCode, {
url: req.originalUrl,
@@ -124,6 +125,10 @@ router.get(
}),
res
);
// Once ZIP bytes have been sent, handleError cannot send JSON.
// Terminate the truncated response so the client reports failure
// instead of waiting forever for the ZIP's central directory.
if (streaming && !res.writableEnded) res.destroy();
});
reqStream.pipe(res);
res.on("close", () => {
+28
View File
@@ -0,0 +1,28 @@
const { expect } = require("chai");
const { Readable } = require("node:stream");
require("ts-node/register/transpile-only");
const { AnonymizeTransformer, ContentAnonimizer } = require("../src/core/anonymize-utils");
const terms = ["first.last", "user@example.org", "dept.example.edu", "lab.io", "project.org", "Author X. Name",
...Array.from({ length: 41 }, (_, i) => `Researcher${i}`)];
describe("large files with many anonymization terms", function () {
this.timeout(10000);
it("handles a 50 MB numeric dataset without exhausting the one-second deadline", function () {
const input = "123,456,78.91234,-45.123456\n".repeat(1800000);
expect(new ContentAnonimizer({ terms }).anonymize(input)).to.equal(input);
});
it("redacts dotted terms and their URLs around a large payload", async function () {
const payload = "123,456,78.91234,-45.123456\n".repeat(200000);
const input = Buffer.from(`first.last\n${payload}\nhttps://host/user@example.org/path\nfirstXlast`);
const stream = new AnonymizeTransformer({ filePath: "coords.csv", terms });
Readable.from([input.subarray(0, 6), input.subarray(6)]).pipe(stream);
const chunks = [];
for await (const chunk of stream) chunks.push(chunk);
expect(Buffer.concat(chunks).toString()).to.equal(`XXXX-1\n${payload}\nXXXX-2\nXXXX-1`);
});
it("handles many repeated matches while preserving the surrounding values", function () {
const input = "Alice,123,45.678,-23.456\n".repeat(100000);
expect(new ContentAnonimizer({ terms: ["Alice", ...terms] }).anonymize(input))
.to.equal("XXXX-1,123,45.678,-23.456\n".repeat(100000));
});
});
+8 -1
View File
@@ -55,11 +55,18 @@ for (const type of ["repository", "pull-request"]) {
expect(writes[0].update).to.deep.equal({ $set: { githubAccess: selected.binding } });
expect(writes[0].filter.githubAccess).to.deep.equal(model.githubAccess);
expect(writes[0].filter.source).to.deep.equal(model.source);
expect(checks).to.deep.equal([{ url: type === "repository" ? "/repositories/42/commits/saved-sha" : "/repositories/42/pulls/7", token: "fresh-token" }]);
expect(checks).to.deep.equal([{ url: type === "repository" ? "/repos/owner/private/commits/saved-sha" : "/repos/owner/private/pulls/7", token: "fresh-token" }]);
});
it("rejects a replacement repository at the same name", async function () {
selected.binding.repositoryId = 99; await fails("connection_changed"); expect(checks).to.have.length(0);
});
for (const status of ["preparing", "queue", "download", "removing", "expiring"]) {
it("rejects reconnect during " + status, async function () {
model.status = status;
await fails("repository_busy");
expect(checks).to.have.length(0);
});
}
it("requires access to the existing snapshot before saving", async function () {
stub(app, "githubRequest", async () => { throw new Error("github_app_access_required"); });
await fails("github_app_access_required");
+18 -1
View File
@@ -66,6 +66,23 @@ describe("asset build", function () {
}
});
it("ships the upstream PDF.js bytes without a second minification pass", function () {
const upstream = fs.readFileSync(path.join(path.dirname(gulpfile), groups.pdfFiles[0]));
fs.writeFileSync(path.join(directory, groups.pdfFiles[0]), upstream);
const result = build();
expect(result.status, result.stderr).to.equal(0);
const bundled = fs.readFileSync(path.join(directory, "public/script/pdf.min.js"));
expect(bundled.equals(upstream)).to.equal(true);
});
it("rejects invalid PDF.js without publishing a manifest", function () {
fs.writeFileSync(path.join(directory, groups.pdfFiles[0]), "function {");
const result = build();
expect(result.status).not.to.equal(0);
expect(result.stderr).to.include("SyntaxError");
expect(fs.existsSync(path.join(directory, "public/asset-manifest.json"))).to.equal(false);
});
it("fails on missing input without publishing a manifest", function () {
fs.unlinkSync(path.join(directory, groups.coreJsFiles[0]));
const result = build();
@@ -75,7 +92,7 @@ describe("asset build", function () {
});
it("fails on invalid JavaScript without publishing a manifest", function () {
fs.writeFileSync(path.join(directory, groups.pdfFiles[0]), "function {");
fs.writeFileSync(path.join(directory, groups.markdownFiles[0]), "function {");
const result = build();
expect(result.status).not.to.equal(0);
expect(result.stderr).to.include("uglify");
+25 -2
View File
@@ -335,7 +335,7 @@ describeMongo("GitHub App credential and repository integration", function () {
it("does not require scoped-token minting for an uninstalled public repository", async () => {
await app.saveAppGrant(owner.id, data());
mock(url => url.includes("/user/installations") ? { installations: [] } :
{ id: 7, private: false, visibility: "public" }, () => ({ status: 403 }));
{ id: 7, full_name: "other/public", private: false, visibility: "public" }, () => ({ status: 403 }));
const selected = await app.selectRepositoryAccess(owner.id, "other/public", "github-app");
expect(selected.token).to.match(/^public-read:/);
expect(calls.some(call => call.url.endsWith("/token/scoped"))).to.equal(false);
@@ -496,6 +496,29 @@ describeMongo("GitHub App credential and repository integration", function () {
expect(await Credentials.countDocuments()).to.equal(0);
expect(await Users.countDocuments()).to.equal(1);
});
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 resource = await Repos.create({ repoId: "public-reconnect", owner: owner.id, status: "ready",
source: { type: "GitHubStream", repositoryName: "other/public", commit: "abc", branch: "main" },
githubAccess: { kind: "github-app", repositoryId: 42, publicRead: true, revision: "old" } });
mock((url, options) => {
if (url.includes("/user/installations?")) return { installations: [] };
if (url.endsWith("/repositories/42") || url.endsWith("/repos/other/public")) return {
id: 42, full_name: "other/public", private: false, visibility: "public",
};
if (url.endsWith("/repos/other/public/commits/abc")) {
expect(new globalThis.Headers(options.headers).get("authorization")).to.equal("Bearer ghu_access1");
return { sha: "abc" };
}
throw new Error("Unexpected request " + url);
});
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 request("/github/connections/migrate", body)).status).to.equal(200);
expect((await Repos.findById(resource.id)).githubAccess.publicRead).to.equal(true);
});
it("recovers a renamed private pull request through its stable repository ID", async () => {
const PRs = require("../src/core/model/anonymizedPullRequests/anonymizedPullRequests.model").default;
await app.saveAppGrant(owner.id, data());
@@ -510,7 +533,7 @@ describeMongo("GitHub App credential and repository integration", function () {
if (url.endsWith("/repositories/42")) return { id: 42 };
if (url.endsWith("/app/installations/4")) return { app_id: 123, permissions: { contents: "read", metadata: "read" } };
if (url.endsWith("/access_tokens")) return { token: "ghs_reinstalled", expires_at: new Date(Date.now() + 3600000).toISOString() };
if (url.endsWith("/repositories/42/pulls/7")) return { number: 7 };
if (url.endsWith("/repos/owner/new-name/pulls/7")) return { number: 7 };
throw new Error("Unexpected request " + url);
});
const body = { type: "pull-request", id: resource.pullRequestId, connection: "github-app" };
+75
View File
@@ -0,0 +1,75 @@
const { expect } = require("chai");
require("ts-node/register/transpile-only");
const { TermPresenceIndex } = require("../src/core/term-presence");
const { compileTerms, ContentAnonimizer } = require("../src/core/anonymize-utils");
function reference(terms, text) {
const anonymizer = new ContentAnonimizer({ terms });
anonymizer.termPresence = null;
for (const term of anonymizer.compiledTerms) delete term.literalPrefilter;
return anonymizer.anonymize(text);
}
describe("term candidate index", function () {
it("retains shared prefixes, nested matches and Unicode-folded alternatives", function () {
const patterns = [/Alice/iu, /AliceSmith/iu, /Smith/iu, /[sśŝşšș]/iu, /ſ/iu, /K/iu, /k/iu, /研究/iu];
const index = new TermPresenceIndex(patterns);
for (const text of ["AliceSmith", "ALICESMITH K ſ 研究", "nothing", "ś", "k", "aAliceSmith"]) {
for (let after = -1; after < patterns.length; after++) {
const candidates = index.candidates(text, after);
patterns.forEach((pattern, i) => {
if (i > after && pattern.test(text)) expect(candidates.has(i), `${text}: ${i}`).to.equal(true);
if (i <= after) expect(candidates.has(i)).to.equal(false);
});
}
}
});
it("keeps dot's CR and Unicode line-separator matches", function () {
for (const text of ["first\rlast", "first\u2028last", "first\u2029last", "first\nlast", "first😀last"]) {
expect(new ContentAnonimizer({ terms: ["first.last"] }).anonymize(text)).to.equal(reference(["first.last"], text));
}
});
it("retains ordered cascades, URL removal, and matches created across replacement boundaries", function () {
const cases = [
[["Alice=>Bob", "Bob=>Hidden"], "Alice Bob https://host/Alice"],
[["Alice=>", "@Bob=>Hidden"], "@AliceBob"],
[["a-a", "Alice"], "xa-a-a Alice https://host/Alice/file"],
[["first.last", "firstXlast=>other"], "firstXlast first.last"],
[["Alice=>Bob", "Bob"], "https://host/Alice"],
[["k", "s"], "K ſ ék k_ xK Kx"],
[["Alice=>Bob", "B.b=>Done"], "Alice"],
[["k=>Kk"], "https://host/k"],
[["Alice=>$&", "Bob=>$$"], "Alice Bob"],
[["\u0301"], "abc"],
[["a.*b", "Alice"], "axxb Alice"],
];
for (const [terms, text] of cases) {
expect(new ContentAnonimizer({ terms }).anonymize(text), JSON.stringify([terms, text])).to.equal(reference(terms, text));
}
});
it("matches the RE2 path for fixed-width terms and surrounding characters", function () {
const terms = ["a-a", "a.a", "k", "s", "k.s", "Alice", "@Alice", "Σ", "研究", "😀", "..", "a.", "a..b"];
const edges = ["", "x", "_", "é", "K", "ſ", "😀", "𐐀", "\r", "\n", "\u2028"];
for (const term of terms) {
const input = edges.flatMap(left => edges.map(right => `${left}${term}${right} ${left}${term.replaceAll(".", "\r")}${right}`)).join(" ");
expect(new ContentAnonimizer({ terms: [term] }).anonymize(input), term).to.equal(reference([term], input));
}
});
it("does not reuse stale rules after the caller edits its term list", function () {
const terms = ["Alice=>first"];
expect(new ContentAnonimizer({ terms }).anonymize("Alice")).to.equal("first");
terms[0] = "Alice=>second";
expect(new ContentAnonimizer({ terms }).anonymize("Alice")).to.equal("second");
expect(new ContentAnonimizer({ terms: ["Alice=>first"] }).anonymize("Alice")).to.equal("first");
});
it("does not index quantified, capturing, or escaped user patterns", function () {
for (const term of ["a.*b", "a+b", "(Alice)", "a\\.b", "a(?=b)"]) {
expect(compileTerms([term]).every(c => !c.literalPrefilter), term).to.equal(true);
}
});
});
+5 -2
View File
@@ -155,10 +155,11 @@ describe("Vue 3 UI", function () {
expect(ui.errors).to.deep.equal([]);
});
it("hides connection changes on archived resources", async function () {
for (const status of ["archived", "preparing", "queue", "download", "removing", "expiring"]) {
it(`hides connection changes on ${status} resources`, async function () {
ui = await browser("/connections", {
"/github/connections": { appEnabled: true, appConnected: true, oauthConnected: true,
resources: [{ type: "repository", id: "archived", connection: "github-app", status: "archived", eligible: true }] },
resources: [{ type: "repository", id: "archived", connection: "github-app", status, eligible: true }] },
});
const resource = ui.window.document.querySelector(".connection-resource");
expect(resource).not.to.equal(null);
@@ -166,6 +167,8 @@ describe("Vue 3 UI", function () {
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";
+94
View File
@@ -0,0 +1,94 @@
const { expect } = require("chai");
const express = require("express");
const compression = require("compression");
const { gunzipSync } = require("node:zlib");
const http = require("node:http");
const { PassThrough } = require("node:stream");
const got = require("got");
require("ts-node/register/transpile-only");
const config = require("../src/config").default;
const utils = require("../src/server/routes/route-utils");
const router = require("../src/server/routes/repository-public").default;
describe("ZIP proxy failures", function () {
let restore, upstream, server;
beforeEach(async function () {
const previous = { download: config.ENABLE_DOWNLOAD, endpoint: config.STREAMER_ENTRYPOINT,
getRepo: utils.getRepo, getUser: utils.getUser, stream: got.stream };
restore = () => {
config.ENABLE_DOWNLOAD = previous.download;
config.STREAMER_ENTRYPOINT = previous.endpoint;
utils.getRepo = previous.getRepo; utils.getUser = previous.getUser; got.stream = previous.stream;
};
config.ENABLE_DOWNLOAD = true;
config.STREAMER_ENTRYPOINT = "http://streamer.test/";
utils.getUser = async () => { throw new Error("anonymous"); };
utils.getRepo = async () => ({ repoId: "test", options: {},
model: { source: { repositoryName: "owner/repo", commit: "abc" } },
countView: async () => {}, getToken: async () => "",
generateAnonymizeTransformer: () => ({ opt: {} }),
});
upstream = new PassThrough();
got.stream = () => upstream;
const app = express(); app.use(compression({ filter: () => true })); app.use(router);
server = await new Promise(resolve => {
const listening = app.listen(0, "127.0.0.1", () => resolve(listening));
});
});
afterEach(async function () {
upstream.destroy(); server.closeAllConnections();
await new Promise(resolve => server.close(resolve)); restore();
});
function request(onResponse, headers = {}) {
return http.get(`http://127.0.0.1:${server.address().port}/test/zip`, { headers }, onResponse);
}
it("aborts a partial ZIP when the upstream fails after headers", async function () {
const result = new Promise((resolve, reject) => {
const client = request(response => {
expect(response.statusCode).to.equal(200);
response.once("data", () => upstream.destroy(new Error("upstream disconnected")));
response.on("aborted", resolve);
response.on("end", () => reject(new Error("truncated ZIP reported as complete")));
response.on("error", () => {});
});
client.on("error", reject);
});
upstream.write(Buffer.from("PK partial archive"));
await result;
});
it("finishes compressed JSON errors before any ZIP bytes are sent", async function () {
const error = "unavailable".repeat(300);
const result = new Promise((resolve, reject) => {
request(response => {
const chunks = [];
response.on("data", chunk => chunks.push(chunk));
response.on("error", reject);
response.on("end", () => resolve({
status: response.statusCode, encoding: response.headers["content-encoding"],
body: JSON.parse(gunzipSync(Buffer.concat(chunks)).toString()),
}));
}, { "Accept-Encoding": "gzip" }).on("error", reject);
});
upstream.once("newListener", event => {
if (event === "error") globalThis.queueMicrotask(() => upstream.destroy(Object.assign(new Error("unavailable"), {
response: { statusCode: 503, body: JSON.stringify({ error }) },
})));
});
expect(await result).to.deep.equal({ status: 502, encoding: "gzip", body: { error } });
});
it("keeps JSON errors before any ZIP bytes are sent", async function () {
const result = new Promise((resolve, reject) => {
request(response => {
let body = ""; response.on("data", chunk => { body += chunk; });
response.on("end", () => resolve({ status: response.statusCode, body: JSON.parse(body) }));
}).on("error", reject);
});
// Fail only after the route has attached its upstream listener.
upstream.once("newListener", event => {
if (event === "error") globalThis.queueMicrotask(() => upstream.destroy(Object.assign(new Error("unavailable"), {
response: { statusCode: 503, body: '{"error":"zip_not_available"}' },
})));
});
expect(await result).to.deep.equal({ status: 502, body: { error: "zip_not_available" } });
});
});