@feng0531/dsh-vcs 0.3.65 → 0.3.68

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/svn-host.mjs CHANGED
@@ -1 +1 @@
1
- import{createHandler as xe}from"../index.mjs";import{XMLParser as ue,XMLValidator as me}from"fast-xml-parser";function k(o){if(typeof o!="string"||Buffer.byteLength(o)>2097152||/<!DOCTYPE|<!ENTITY/i.test(o)||me.validate(o)!==!0)throw Error("Invalid DAV XML");let a=new ue({preserveOrder:!0,ignoreAttributes:!1,parseTagValue:!1,parseAttributeValue:!1,trimValues:!1}).parse(o),n=0,r=(e,t,s=0)=>{if(s>32)throw Error("DAV XML too deep");let i=Object.create(null),p="";for(let c of e){if(++n>1e4)throw Error("DAV XML too many nodes");if(Object.hasOwn(c,"#text")){p+=c["#text"];continue}let f=Object.keys(c).filter(b=>b!==":@");if(f.length!==1)throw Error("Invalid DAV XML node");let l=f[0];if(l==="?xml"){if(s!==0)throw Error("Unexpected XML declaration");continue}let m={...t},w={};for(let[b,V]of Object.entries(c[":@"]||{}))b==="@_xmlns"?m[""]=V:b.startsWith("@_xmlns:")?m[b.slice(8)]=V:w[b]=V;let v=l.split(":");if(v.length>2)throw Error("Invalid DAV XML name");let h=v.length===2?v[0]:"",d=v.at(-1),u=m[h],S=u==="DAV:"?"D:"+d:u==="http://subversion.tigris.org/xmlns/dav/"?"S:"+d:u==="svn:"?"L:"+d:u==="http://subversion.tigris.org/xmlns/svn/"?"P:"+d:u==="http://subversion.tigris.org/xmlns/custom/"?"C:"+d:null;if(!S)throw Error("Unsupported DAV namespace");let D=["L:added-path","L:deleted-path","L:modified-path","L:replaced-path"].includes(S);if(Object.keys(w).length&&(!D||Object.keys(w).some(b=>!["@_node-kind","@_text-mods","@_prop-mods","@_copyfrom-path","@_copyfrom-rev"].includes(b))))throw Error("Unexpected DAV attribute");let g=r(c[l],m,s+1);if(D&&typeof g!="string")throw Error("Invalid DAV path text");let E=D?{...w,"#text":g}:g;if(["D:response","D:propstat","L:log-item","L:added-path","L:deleted-path","L:modified-path","L:replaced-path"].includes(S))(i[S]??=[]).push(E);else{if(Object.hasOwn(i,S))throw Error("Duplicate DAV expanded name");i[S]=E}}if(!Object.keys(i).length)return p;if(p.trim())throw Error("Mixed DAV XML content");return i};return r(a,{})}var ve=9223372036854775807n;function y(o){if(typeof o!="string"||o.length>19||!/^(0|[1-9][0-9]*)$/.test(o))throw new Error("Invalid SVN revision");let a=BigInt(o);if(a>ve)throw new Error("SVN revision exceeds supported range");return a}function H(o){let a=y(o);return a===0n?null:String(a-1n)}function j(o,{snapshot:a,cursor:n=a,limit:r=50}={}){let e=y(a),t=y(n);if(t>e||!Number.isInteger(r)||r<1||r>100||!Array.isArray(o)||o.length>r+1)throw new Error("Invalid SVN page bounds");let s=t+1n;for(let p of o){let c=y(p);if(c>t||c>=s)throw new Error("SVN revisions must descend within cursor");s=c}let i=o.slice(0,r);return{snapshot:a,revisions:i,nextRevision:o.length>r?H(i.at(-1)):null}}function J(o,a){let n=k(o);if(!n||typeof n!="object"||Object.keys(n).length!==1||!Object.hasOwn(n,"L:log-report"))throw Error("Invalid DAV log root");let r=n["L:log-report"],e;if(r==="")e=[];else{if(!r||typeof r!="object"||Object.keys(r).some(i=>i!=="L:log-item")||!Array.isArray(r["L:log-item"]))throw Error("Invalid DAV log structure");e=r["L:log-item"]}if(e.length>101)throw Error("DAV log entries exceed limit");let t=e.map(i=>{if(!i||typeof i!="object"||Array.isArray(i)||Object.keys(i).some(c=>!["D:version-name","D:comment","D:creator-displayname","L:date"].includes(c)))throw Error("Unsupported DAV log item");let p=c=>{let f=i[c]??"";if(typeof f!="string")throw Error("Invalid DAV log text");return f};return{revision:p("D:version-name"),message:p("D:comment"),author:p("D:creator-displayname"),date:p("L:date")}}),s=j(t.map(i=>i.revision),a);return{snapshot:s.snapshot,entries:t.slice(0,s.revisions.length),nextRevision:s.nextRevision}}function O(o){if(typeof o!="string"||o.length>32768||!o.startsWith("/")||/[\u0000-\u001f\u007f\\]/.test(o))throw new Error("Invalid SVN repository path");if(o!=="/"&&o.slice(1).split("/").some(a=>a===""||a==="."||a===".."))throw new Error("Noncanonical SVN repository path");return o}function R(o,a){return O(o),O(a),a==="/"||o===a||o.startsWith(a+"/")}function q(o,a){if(!Array.isArray(o)||o.length>1e4)throw new Error("Invalid SVN path nodes");let n=o.map(r=>{let e=["#text","@_action","@_kind","@_copyfrom-path","@_copyfrom-rev","@_text-mods","@_prop-mods"];if(!r||typeof r!="object"||Array.isArray(r)||Object.keys(r).some(t=>!e.includes(t)))throw new Error("Invalid SVN path node");for(let t of["@_text-mods","@_prop-mods"])if(Object.hasOwn(r,t)&&!["true","false","unknown"].includes(r[t]))throw new Error("Invalid SVN modification flag");return{...Object.hasOwn(r,"@_text-mods")?{textModified:r["@_text-mods"]}:{},...Object.hasOwn(r,"@_prop-mods")?{propertiesModified:r["@_prop-mods"]}:{},path:r["#text"],action:r["@_action"],kind:r["@_kind"],...Object.hasOwn(r,"@_copyfrom-path")?{copyFromPath:r["@_copyfrom-path"]}:{},...Object.hasOwn(r,"@_copyfrom-rev")?{copyFromRevision:r["@_copyfrom-rev"]}:{}}});return W(n,a)}function W(o,{scope:a,revision:n}={}){O(a);let r=y(n);if(!Array.isArray(o)||o.length>1e4||r===0n&&o.length!==0)throw new Error("Invalid SVN changed paths");let e=new Set;return o.map(s=>{if(!s||typeof s!="object"||Array.isArray(s)||Object.keys(s).some(h=>!["path","action","kind","copyFromPath","copyFromRevision","copySourceOutsideScope","textModified","propertiesModified"].includes(h)))throw new Error("Invalid SVN change structure");let{path:i,action:p,kind:c,copyFromPath:f,copyFromRevision:l}=s;if(O(i),e.has(i)||!["A","D","M","R"].includes(p)||!["file","dir","unknown"].includes(c))throw new Error("Invalid SVN change");e.add(i);let m=f!==void 0||l!==void 0;if(m&&(O(f),!["A","R"].includes(p)||y(l)>=r))throw new Error("Invalid SVN copy source");let w=Object.hasOwn(s,"copySourceOutsideScope");if(w&&(s.copySourceOutsideScope!==!0||m||!["A","R"].includes(p)))throw new Error("Invalid SVN redacted copy source");let v={path:i,action:p,kind:c,...w?{copySourceOutsideScope:!0}:{}};for(let h of["textModified","propertiesModified"])if(Object.hasOwn(s,h)){if(!["true","false","unknown"].includes(s[h]))throw new Error("Invalid SVN modification flag");v[h]=s[h]}return m&&(R(f,a)?(v.copyFromPath=f,v.copyFromRevision=l):v.copySourceOutsideScope=!0),v}).filter(s=>R(s.path,a))}function G(o,{scope:a,revision:n}={}){y(n);let r=k(o),e=(f,l)=>{if(!f||typeof f!="object"||Array.isArray(f)||Object.keys(f).some(m=>!l.includes(m)))throw Error("Invalid DAV detail structure")};e(r,["L:log-report"]);let t=r["L:log-report"];if(e(t,["L:log-item"]),t["L:log-item"]?.length!==1)throw Error("One DAV detail revision required");let s=t["L:log-item"][0],i={"L:added-path":"A","L:deleted-path":"D","L:modified-path":"M","L:replaced-path":"R"};if(e(s,["D:version-name","D:comment","D:creator-displayname","L:date",...Object.keys(i)]),s["D:version-name"]!==n)throw Error("DAV detail revision mismatch");let p=f=>{let l=s[f]??"";if(typeof l!="string")throw Error("Invalid DAV detail text");return l},c=[];for(let[f,l]of Object.entries(i))for(let m of s[f]??[]){let{"@_node-kind":w,...v}=m;c.push({...v,"@_kind":w??"unknown","@_action":l})}return{revision:n,author:p("D:creator-displayname"),date:p("L:date"),message:p("D:comment"),changes:q(c,{scope:a,revision:n})}}function I(o,a,n){if(O(a),y(n),typeof o!="string"||o.length>32768||/[\x00-\x20\x7f\\?#]/.test(o))throw new Error("Invalid SVN HTTPS root");let r=new URL(o);if(r.protocol!=="https:"||r.username||r.password||r.href!==o)throw new Error("Noncanonical SVN HTTPS root");let e=r.pathname.split("/").slice(1);e.at(-1)===""&&e.pop();for(let p of e){let c=decodeURIComponent(p);if(!p||c==="."||c===".."||/[\x00-\x1f\x7f/\\]/.test(c))throw new Error("Invalid SVN root path")}let t=o.endsWith("/")?o.slice(0,-1):o,s=a==="/"?"":a.split("/").map(p=>encodeURIComponent(p).replace(/[!'()*]/g,c=>"%"+c.charCodeAt(0).toString(16).toUpperCase())).join("/"),i=t+(s||(r.pathname==="/"?"/":""))+"@"+n;if(i.length>32768)throw new Error("SVN target too long");return i}function B({root:o,scope:a,path:n=a,snapshot:r,revision:e}={}){if(y(e)>y(r))throw new Error("SVN selected revision exceeds snapshot");if(!R(n,a))throw new Error("SVN detail path outside scope");let t=I(o,n,r);return{scope:a,revision:e,snapshot:r,args:["log","--xml","--verbose","--stop-on-copy","--non-interactive","--no-auth-cache","--limit","1","-r",e+":"+e,"--",t]}}function M({root:o,scope:a,path:n=a,snapshot:r,cursor:e=r,limit:t=50}={}){if(j([],{snapshot:r,cursor:e,limit:t}),!R(n,a))throw new Error("SVN history path outside scope");let s=I(o,n,r);return{snapshot:r,cursor:e,limit:t,args:["log","--xml","--stop-on-copy","--non-interactive","--no-auth-cache","--limit",String(t+1),"-r",e+":0","--",s]}}function C(o,a){let n=o?.args;if(!Array.isArray(n)||n.length>16||n.some(l=>typeof l!="string"))throw Error("Invalid SVN command");let r=n.at(-1),e=r.lastIndexOf("@"),t=r.slice(e+1);y(t);let s=a.root.endsWith("/")?a.root.slice(0,-1):a.root;I(a.root,"/",t);let i=r.slice(0,e);if(!(i===s||i.startsWith(s+"/")))throw Error("SVN command target outside root");let p=i.slice(s.length),c=p?"/"+p.slice(1).split("/").map(l=>decodeURIComponent(l)).join("/"):"/";if(!R(c,a.scope)||I(a.root,c,t)!==r)throw Error("SVN command target outside scope or noncanonical");let f;if(n[0]==="cat"||n[0]==="proplist"){y(n[n[0]==="cat"?4:5]);let l=n[n[0]==="cat"?4:5];if(l!==t)throw Error("SVN content operative and peg mismatch");f=n[0]==="cat"?["cat","--non-interactive","--no-auth-cache","-r",l,"--",r]:["proplist","--xml","--non-interactive","--no-auth-cache","-r",l,"--",r]}else if(n[0]==="log")if(n.includes("--verbose")){let l=n[9]?.split(":")[0];f=B({...a,path:c,snapshot:t,revision:l}).args}else{let l=n[8]?.split(":")[0],m=Number(n[6])-1;f=M({...a,path:c,snapshot:t,cursor:l,limit:m}).args}else throw Error("SVN command is not readonly history");if(JSON.stringify(n)!==JSON.stringify(f))throw Error("Unsupported SVN command shape");return{path:c,pegRevision:t,args:[...f]}}function K(o,{scope:a,revision:n,path:r}={}){if(y(n)===0n)throw new Error("SVN r0 has no change comparison");let e=W(o,{scope:a,revision:n}).find(i=>i.path===r);if(!e)throw new Error("SVN path is not an authorized changed member");if(e.kind!=="file")throw new Error("Only SVN file changes can be compared");let t=H(n),s=i=>i===null?{empty:!0}:{empty:!1,path:e.path,revision:i,pegRevision:i};return{path:e.path,action:e.action,left:s(e.action==="A"?null:t),right:s(e.action==="D"?null:n)}}import{XMLParser as ge,XMLValidator as we}from"fast-xml-parser";function Q(o,{target:a,allowEmpty:n=!1}={}){if(typeof a!="string"||!a||a.length>32768)throw new Error("Invalid SVN property target");if(typeof o!="string"||Buffer.byteLength(o,"utf8")>2*1024*1024||/<!DOCTYPE|<!ENTITY/i.test(o)||we.validate(o)!==!0)throw new Error("Invalid SVN property XML");let r=new ge({ignoreAttributes:!1,parseAttributeValue:!1,parseTagValue:!1,trimValues:!1,isArray:f=>f==="target"||f==="property"}).parse(o),e=(f,l)=>{if(!f||typeof f!="object"||Array.isArray(f)||Object.keys(f).some(m=>!l.includes(m)&&!(m==="#text"&&typeof f[m]=="string"&&/^[ \t\r\n]*$/.test(f[m]))))throw new Error("Invalid SVN property structure")};if(e(r,["?xml","properties"]),n===!0&&typeof r.properties=="string"&&/^[ \t\r\n]*$/.test(r.properties))return{target:a,names:[],special:!1};e(r.properties,["target"]);let t=r.properties.target;if(!Array.isArray(t)||t.length!==1)throw new Error("Expected one SVN property target");let s=t[0];if(e(s,["@_path","property"]),s["@_path"]!==a)throw new Error("SVN property target mismatch");let i=s.property??[];if(!Array.isArray(i)||i.length>1e4)throw new Error("Too many SVN properties");let p=[],c=new Set;for(let f of i){e(f,["@_name"]);let l=f["@_name"];if(typeof l!="string"||!l||l.length>1024||/[\x00-\x20\x7f]/.test(l)||c.has(l))throw new Error("Invalid SVN property name");c.add(l),p.push(l)}return{target:a,names:p,special:c.has("svn:special")}}import{createHash as Ve}from"node:crypto";import{XMLParser as ye,XMLValidator as Se}from"fast-xml-parser";function Z(o,{scope:a,revision:n}={}){if(y(n),typeof o!="string"||Buffer.byteLength(o,"utf8")>2*1024*1024||/<!DOCTYPE|<!ENTITY/i.test(o)||Se.validate(o)!==!0)throw new Error("Invalid SVN detail XML");let r=new ye({ignoreAttributes:!1,parseAttributeValue:!1,parseTagValue:!1,trimValues:!1,isArray:c=>c==="logentry"||c==="path"}).parse(o),e=(c,f)=>{if(!c||typeof c!="object"||Array.isArray(c)||Object.keys(c).some(l=>!f.includes(l)&&!(l==="#text"&&typeof c[l]=="string"&&/^[ \t\r\n]*$/.test(c[l]))))throw new Error("Invalid SVN detail structure")};if(e(r,["?xml","log"]),e(r.log,["logentry"]),!Array.isArray(r.log.logentry)||r.log.logentry.length!==1)throw new Error("Expected one SVN revision");let t=r.log.logentry[0];if(e(t,["@_revision","author","date","msg","paths"]),t["@_revision"]!==n)throw new Error("SVN detail revision mismatch");let s=c=>{let f=t[c]??"";if(typeof f!="string")throw new Error("Invalid SVN detail text");return f},i=[];t.paths!==void 0&&!(typeof t.paths=="string"&&/^[ \t\r\n]*$/.test(t.paths))&&(e(t.paths,["path"]),i=t.paths.path??[]);let p=q(i,{scope:a,revision:n});return{revision:n,author:s("author"),date:s("date"),message:s("msg"),pathsAvailable:Object.hasOwn(t,"paths"),changes:p}}import{randomBytes as be}from"node:crypto";function ee(o){if(!o||typeof o!="object")throw Error("Invalid SVN consent identity");let{sessionId:a,cwd:n,repositoryId:r,root:e,uuid:t,scope:s}=o;for(let i of[a,n,r])if(typeof i!="string"||!i||i.length>32768||i.includes("\0"))throw Error("Invalid SVN consent scope");if(I(e,"/","0"),O(s),typeof t!="string"||!/^[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}$/i.test(t))throw Error("Invalid SVN repository UUID");return JSON.stringify([a,n,r,e,t,s])}function F({now:o=Date.now,ttlMs:a=3e5,maxEntries:n=32,onRevoke:r=()=>{}}={}){if(!Number.isInteger(a)||a<1||a>3e5||!Number.isInteger(n)||n<1||n>32)throw Error("Invalid SVN consent bounds");if(typeof r!="function")throw Error("Invalid SVN revoke listener");let e=new Map,t=i=>{e.delete(i)&&r(i)},s=()=>{for(let[i,p]of e)p.expires<=o()&&t(i)};return{grant(i,{explicit:p=!1}={}){if(p!==!0)throw Error("Explicit SVN network consent required");let c=ee(i);s();for(let[l,m]of e)m.key===c&&t(l);for(;e.size>=n;)t(e.keys().next().value);let f=be(32).toString("hex");return e.set(f,{key:c,sessionId:i.sessionId,expires:o()+a}),f},assert(i,p){s();let c=ee(p),f=e.get(i);if(!f||f.key!==c)throw Object.assign(Error("SVN network consent missing, expired or identity changed"),{code:"vcs/svn-consent-required"})},revokeSession(i){for(let[p,c]of e)c.sessionId===i&&t(p)},revoke(i){t(i)},clear(){for(let i of[...e.keys()])t(i)}}}import{XMLParser as De,XMLValidator as Ee}from"fast-xml-parser";function re(o,a){let n=Ae(o),r=j(n.map(e=>e.revision),a);return{snapshot:r.snapshot,entries:n.slice(0,r.revisions.length),nextRevision:r.nextRevision}}function Ae(o){if(typeof o!="string"||Buffer.byteLength(o,"utf8")>2*1024*1024||/<!DOCTYPE|<!ENTITY/i.test(o)||Ee.validate(o)!==!0)throw new Error("Invalid SVN log XML");let n=new De({ignoreAttributes:!1,parseAttributeValue:!1,parseTagValue:!1,trimValues:!1,isArray:t=>t==="logentry"}).parse(o);if(!Object.hasOwn(n,"log")||Object.keys(n).some(t=>t!=="log"&&t!=="?xml"))throw new Error("Invalid SVN log root");let r=n.log;if(typeof r=="string"&&/^[ \t\r\n]*$/.test(r))return[];if(r&&typeof r=="object"&&typeof r["#text"]=="string"&&/^[ \t\r\n]*$/.test(r["#text"])&&delete r["#text"],!r||typeof r!="object"||Object.keys(r).some(t=>t!=="logentry")||!Array.isArray(r.logentry)||r.logentry.length>101)throw new Error("Invalid SVN log entries");let e=new Set;return r.logentry.map(t=>{if(!t||typeof t!="object"||Array.isArray(t)||Object.keys(t).some(p=>!["@_revision","author","date","msg","#text"].includes(p))||"#text"in t&&(typeof t["#text"]!="string"||!/^[ \t\r\n]*$/.test(t["#text"])))throw new Error("Unsupported SVN log entry structure");let s=t["@_revision"];if(y(s),e.has(s))throw new Error("Duplicate SVN revision");e.add(s);let i=p=>{let c=t[p]??"";if(typeof c!="string")throw new Error("Invalid SVN log text");return c};return{revision:s,author:i("author"),date:i("date"),message:i("msg")}})}function te({resolveIdentity:o,transport:a,now:n=Date.now,timeoutMs:r=15e3,transportFormat:e="cli"}={}){if(!["cli","dav"].includes(e))throw Error("Invalid SVN transport format");if(!Number.isInteger(r)||r<1||r>15e3)throw Error("Invalid SVN runtime deadline");if(typeof o!="function")throw Error("Local identity resolver required");let t=F({now:n}),s=F({now:n,onRevoke:h=>m(h)}),i=!1,p=()=>{if(i)throw new DOMException("SVN runtime disposed","AbortError")},c=async(h,d)=>{p(),d?.throwIfAborted();let u=await o(h,d);return p(),d?.throwIfAborted(),{...u,sessionId:h.sessionId,cwd:u.cwd,repositoryId:h.repositoryId}},f=h=>h.revision===void 0?{}:(y(h.revision),{snapshot:h.revision}),l=new Map,m=h=>{for(let d of l.get(h)||[])d.controller.abort(new DOMException("SVN consent revoked","AbortError"))},w=async(h,d)=>{p(),C(h,d.identity),d.signal?.throwIfAborted(),s.assert(d.token,d.identity);let u=new AbortController,S=()=>u.abort(d.signal.reason);d.signal?.addEventListener("abort",S,{once:!0});let D={controller:u,sessionId:d.identity.sessionId};l.has(d.token)||l.set(d.token,new Set),l.get(d.token).add(D);let g=setTimeout(()=>u.abort(new DOMException("SVN transport deadline exceeded","TimeoutError")),r);try{let E=async()=>{u.signal.throwIfAborted();let V=await c({sessionId:d.identity.sessionId,repositoryId:d.identity.repositoryId},u.signal);s.assert(d.token,V),u.signal.throwIfAborted()},b=await a(h,{...d,timeoutMs:r,signal:u.signal,assertAuthorized:E});if(u.signal.throwIfAborted(),!(typeof b=="string"||Buffer.isBuffer(b))||Buffer.byteLength(b)>d.maxBytes)throw Error("SVN transport output exceeds byte limit or has invalid type");return Buffer.isBuffer(b)&&h.args[0]!=="cat"?new TextDecoder("utf-8",{fatal:!0}).decode(b):b}catch(E){throw u.signal.aborted?u.signal.reason:E}finally{let E=l.get(d.token);E?.delete(D),E?.size||l.delete(d.token),clearTimeout(g),d.signal?.removeEventListener("abort",S)}},v={async describe(h,{signal:d}={}){let u=await c(h,d),S=f(u),D=t.grant(u,{explicit:!0});return{...S,offer:D,root:u.root,uuid:u.uuid,scope:u.scope,origin:new URL(u.root).origin,expiresAt:n()+3e5,remoteEnabled:typeof a=="function"}},async approve(h,{offer:d,explicit:u=!1,signal:S}={}){if(u!==!0)throw Error("Explicit SVN network consent required");let D=await c(h,S);if(t.assert(d,D),typeof a!="function")throw Error("SVN remote transport disabled");let g=f(D),E=s.grant(D,{explicit:!0});return t.revoke(d),{...g,token:E,expiresAt:n()+3e5}},async log(h,{token:d,snapshot:u,cursor:S,limit:D,path:g,signal:E}={}){let b=await c(h,E);if(s.assert(d,b),typeof a!="function")throw Error("SVN remote transport disabled");let V=M({...b,snapshot:u,cursor:S,limit:D,path:g});E?.throwIfAborted();let A=await w(V,{identity:b,token:d,signal:E,maxBytes:2097152,timeoutMs:15e3});E?.throwIfAborted();let x=await c(h,E);return s.assert(d,x),(e==="dav"?J:re)(A,V)},async detail(h,{token:d,snapshot:u,revision:S,signal:D}={}){let g=await c(h,D);if(s.assert(d,g),typeof a!="function")throw Error("SVN remote transport disabled");let E=B({...g,snapshot:u,revision:S}),b=await w(E,{identity:g,token:d,signal:D,maxBytes:2097152,timeoutMs:15e3});D?.throwIfAborted(),s.assert(d,await c(h,D));let V=e==="dav"?{...G(b,E),pathsAvailable:!0}:Z(b,E);return{...V,snapshot:u,changes:V.changes.map(A=>({...A,id:Ve("sha256").update(JSON.stringify([g.root,g.uuid,g.scope,u,S,A])).digest("hex")}))}},async compare(h,{token:d,snapshot:u,revision:S,id:D,signal:g}={}){if(typeof D!="string"||!/^[a-f0-9]{64}$/.test(D))throw Error("Invalid SVN change ID");let E=await v.detail(h,{token:d,snapshot:u,revision:S,signal:g}),b=E.changes.find(N=>N.id===D);if(!b)throw Error("SVN change is not selected revision member");let V=await c(h,g);s.assert(d,V);let A=K(E.changes.map(({id:N,..._})=>_),{scope:V.scope,revision:S,path:b.path}),x=async N=>{if(N.empty)return{label:"empty",text:"",absent:!0};let _=await c(h,g);s.assert(d,_);let U=I(_.root,N.path,N.pegRevision),Y={identity:_,token:d,signal:g,maxBytes:2097152,timeoutMs:15e3},he=await w({args:["proplist","--xml","--non-interactive","--no-auth-cache","-r",N.revision,"--",U]},Y);if(g?.throwIfAborted(),s.assert(d,await c(h,g)),Q(he,{target:U.slice(0,U.lastIndexOf("@")),allowEmpty:!0}).special)throw Error("SVN special files cannot be compared");let X=await w({args:["cat","--non-interactive","--no-auth-cache","-r",N.revision,"--",U]},Y);g?.throwIfAborted(),s.assert(d,await c(h,g));let $=Buffer.isBuffer(X)?X:Buffer.from(X);if($.length>2097152)throw Error("SVN text exceeds 2 MiB");if($.includes(0))return{label:"r"+N.revision,text:"",binary:!0};let z;try{z=new TextDecoder("utf-8",{fatal:!0}).decode($)}catch{return{label:"r"+N.revision,text:"",binary:!0}}return{label:"r"+N.revision,text:z}},T=await x(A.left),P=await x(A.right);return{path:b.path,revision:S,snapshot:u,left:T,right:P,binary:!!(T.binary||P.binary)}},async trace(h,{token:d,snapshot:u,revision:S,id:D,cursor:g,limit:E=50,signal:b}={}){if(typeof D!="string"||!/^[a-f0-9]{64}$/.test(D))throw Error("Invalid SVN change ID");let V=await v.detail(h,{token:d,snapshot:u,revision:S,signal:b}),A=V.changes.find(P=>P.id===D);if(!A||A.kind!=="file")throw Error("SVN tracing requires selected file member");let x=A.action==="D"?H(S):S;return{...await v.log(h,{token:d,snapshot:x,cursor:g??x,limit:E,path:A.path,signal:b}),selectionSnapshot:u,selectionRevision:S,path:A.path,pegRevision:x,stopOnCopy:!0,copySourceOutsideScope:!!A.copySourceOutsideScope}},async revoke(h,{token:d,signal:u}={}){let S=await c(h,u);return s.assert(d,S),s.revoke(d),m(d),{revoked:!0}},dispose(){i=!0,t.clear(),s.clear();for(let h of l.keys())m(h)},revokeSession(h){t.revokeSession(h),s.revokeSession(h);for(let[d,u]of l)[...u].some(S=>S.sessionId===h)&&m(d)}};return v}var oe={revoke:["token"],describe:[],approve:["offer","explicit"],log:["token","snapshot","cursor","limit"],detail:["token","snapshot","revision"],compare:["token","snapshot","revision","id"],trace:["token","snapshot","revision","id","cursor","limit"]};function ne(o,{timeoutMs:a=15e3}={}){if(!Number.isInteger(a)||a<1||a>15e3)throw Error("Invalid SVN RPC deadline");let n=0;return async(r,e,t)=>{let s=r.startsWith("vcs/svn-")?r.slice(8):"";if(!Object.hasOwn(oe,s))return{ok:!1,error:{code:"vcs/not-found",message:"Unknown SVN endpoint"}};if(s!=="revoke"&&n>=2)return{ok:!1,error:{code:"vcs/busy",message:"SVN history requests busy"}};s!=="revoke"&&n++;let i=new AbortController,p=()=>i.abort(t.reason);t?.addEventListener("abort",p,{once:!0}),t?.aborted&&p();let c=setTimeout(()=>i.abort(new DOMException("SVN operation deadline exceeded","TimeoutError")),a);try{if(t?.throwIfAborted(),!e||typeof e!="object"||Array.isArray(e))throw Error("Object payload required");if(Object.keys(e).some(v=>!["sessionId","repositoryId",...oe[s]].includes(v)))throw Error("Unknown SVN request field");for(let v of["sessionId","repositoryId"])if(typeof e[v]!="string"||!e[v]||e[v].length>1024||e[v].includes("\0"))throw Error("Invalid SVN Session address");if(s==="approve"&&(e.explicit!==!0||typeof e.offer!="string"||!/^[a-f0-9]{64}$/.test(e.offer)))throw Error("Explicit scoped offer required");if(!["describe","approve"].includes(s)){if(typeof e.token!="string"||!/^[a-f0-9]{64}$/.test(e.token))throw Error("SVN consent token required");if(s!=="revoke"&&y(e.snapshot),e.revision!==void 0&&y(e.revision),["detail","compare","trace"].includes(s)&&e.revision===void 0)throw Error("Selected SVN revision required");if(e.cursor!==void 0&&y(e.cursor),e.limit!==void 0&&(!Number.isInteger(e.limit)||e.limit<1||e.limit>100))throw Error("Invalid SVN page size");if(["compare","trace"].includes(s)&&(typeof e.id!="string"||!/^[a-f0-9]{64}$/.test(e.id)))throw Error("Selected SVN change ID required")}let{sessionId:f,repositoryId:l,...m}=e,w=await o[s]({sessionId:f,repositoryId:l},{...m,signal:i.signal});return i.signal.throwIfAborted(),{ok:!0,value:w}}catch(f){return{ok:!1,error:{code:t?.aborted||f?.name==="AbortError"?"vcs/cancelled":["vcs/rediscover-required","vcs/svn-consent-required"].includes(f?.code)?f.code:"vcs/operation-failed",message:i.signal.aborted?i.signal.reason instanceof Error?i.signal.reason.message:String(i.signal.reason):f instanceof Error?f.message:String(f)}}}finally{clearTimeout(c),t?.removeEventListener("abort",p),s!=="revoke"&&n--}}}function se(o,a,{transport:n,transportFormat:r="cli",now:e=Date.now,timeoutMs:t=15e3}={}){let s,i=xe(o,a,4,{now:e,bindSvnIdentityResolver:l=>{s=l}}),p=te({resolveIdentity:s,transport:n,transportFormat:r,now:e,timeoutMs:t}),c=ne(p,{timeoutMs:t}),f=new Set(["describe","approve","log","detail","compare","trace","revoke"]);return{handle:(l,m,w)=>f.has(l.startsWith("vcs/svn-")?l.slice(8):"")?c(l,m,w):i(l,m,w),revokeSession:l=>p.revokeSession(l),dispose:()=>p.dispose()}}import{request as Ie}from"node:https";function ie({request:o=Ie}={}){return async(a,{origin:n,method:r="GET",body:e="",depth:t,label:s,signal:i,maxBytes:p=2097152,timeoutMs:c=15e3}={})=>{let f=new URL(a);if(f.protocol!=="https:"||f.origin!==n||f.username||f.password||f.hash||f.href!==a||!["GET","PROPFIND","REPORT"].includes(r)||typeof e!="string"||Buffer.byteLength(e)>65536||!Number.isInteger(p)||p<1||p>2097152||!Number.isInteger(c)||c<1||c>15e3)throw Error("Invalid bounded HTTPS request");if(t!==void 0&&(r!=="PROPFIND"||t!==0))throw Error("Invalid DAV depth");if(s!==void 0){if(r!=="PROPFIND"||t!==0)throw Error("Invalid DAV label");y(s)}let l={...t===0?{Depth:"0"}:{},...s!==void 0?{Label:s}:{}};return i?.throwIfAborted(),new Promise((m,w)=>{let v,h,d=null,u=null,S=0,D=[],g=A=>{d??=A,h?.destroy(),v?.destroy()},E=()=>g(i.reason??new DOMException("HTTPS cancelled","AbortError")),b=setTimeout(()=>g(new DOMException("HTTPS deadline exceeded","TimeoutError")),c),V=()=>{clearTimeout(b),i?.removeEventListener("abort",E)};try{v=o(f,{method:r,agent:!1,rejectUnauthorized:!0,maxHeaderSize:16384,headers:{...l,"Accept-Encoding":"identity","Content-Type":"text/xml; charset=utf-8","Content-Length":String(Buffer.byteLength(e))}},A=>{h=A;let x=h.statusCode??0;if(x>=300&&x<400){g(Error("HTTPS redirects forbidden"));return}if(x<200||x>=300){g(Error("HTTPS status "+x));return}if(h.headers["content-encoding"]&&h.headers["content-encoding"]!=="identity"){g(Error("HTTPS compressed responses forbidden"));return}let T=h.headers["content-length"];if(T!==void 0&&(!/^[0-9]+$/.test(T)||Number(T)>p)){g(Error("HTTPS response exceeds byte limit"));return}h.on("data",P=>{S+=P.length,S>p?g(Error("HTTPS response exceeds byte limit")):d||D.push(P)}),h.on("error",g),h.on("end",()=>{if(!h.complete){g(Error("HTTPS response incomplete"));return}u=Buffer.concat(D)})}),v.on("error",A=>{d??=A}),v.once("close",()=>{V(),d?w(d):u!==null?m(u):w(Error("HTTPS response closed before completion"))}),i?.addEventListener("abort",E,{once:!0}),i?.aborted?E():v.end(e)}catch(A){V(),v?.destroy(),w(A)}})}}var Ne=o=>o.replaceAll("&","&amp;").replaceAll("<","&lt;").replaceAll(">","&gt;").replaceAll('"',"&quot;").replaceAll("'","&apos;");function ae(o,a){let n=C(o,a),r=n.args;if(r[0]!=="log")throw Error("DAV log report requires readonly log plan");let e=r.includes("--verbose"),t=r[e?9:8].split(":"),s=r[e?7:6],i=n.path==="/"?"":n.path.slice(1),p='<?xml version="1.0" encoding="utf-8"?><S:log-report xmlns:S="svn:" xmlns:D="DAV:"><S:start-revision>'+t[0]+"</S:start-revision><S:end-revision>"+t[1]+"</S:end-revision><S:limit>"+s+"</S:limit><S:strict-node-history/>"+(e?"<S:discover-changed-paths/>":"")+"<S:path>"+Ne(i)+"</S:path><S:revprop>svn:author</S:revprop><S:revprop>svn:date</S:revprop><S:revprop>svn:log</S:revprop></S:log-report>";if(Buffer.byteLength(p)>65536)throw Error("DAV request body exceeds limit");return{method:"REPORT",body:p,pegRevision:n.pegRevision,path:n.path,requiresVerifiedBaseline:!0}}function L(o,{root:a}={}){if(I(a,"/","0"),typeof o!="string"||!o||o.length>32768||/[\\?#]/.test(o)||[...o].some(t=>t.charCodeAt(0)<=32||t.charCodeAt(0)===127))throw Error("Invalid DAV href");if(!(o.startsWith("/")&&!o.startsWith("//"))&&!o.startsWith("https://"))throw Error("DAV href must be absolute path or HTTPS URL");let n=new URL(o,new URL(a).origin);if(n.protocol!=="https:"||n.origin!==new URL(a).origin||n.username||n.password||n.search||n.hash)throw Error("DAV href outside authorized origin");if(o.startsWith("https://")&&n.href!==o||o.startsWith("/")&&n.pathname!==o)throw Error("Noncanonical DAV href");let r=n.pathname.split("/").slice(1);r.at(-1)===""&&r.pop();for(let t of r){let s=decodeURIComponent(t);if(!t||s==="."||s===".."||[...s].some(i=>i.charCodeAt(0)<32||i.charCodeAt(0)===127||i==="/"||i.charCodeAt(0)===92))throw Error("Unsafe DAV href path")}let e=new URL(a).pathname.replace(/[/]$/,"");if(e&&n.pathname!==e&&!n.pathname.startsWith(e+"/"))throw Error("DAV href outside repository root");return n.href}function ce(o,{root:a,uuid:n,target:r}={}){let e=k(o),t=(l,m)=>{if(!l||typeof l!="object"||Array.isArray(l)||Object.keys(l).some(w=>!m.includes(w)&&!(w==="#text"&&typeof l[w]=="string"&&l[w].trim()==="")))throw Error("Unsupported DAV discovery structure")};t(e,["?xml","D:multistatus"]);let s=e["D:multistatus"];if(t(s,["D:response"]),!Array.isArray(s["D:response"])||s["D:response"].length!==1)throw Error("DAV discovery requires one response");let i=s["D:response"][0];if(t(i,["D:href","D:propstat"]),L(i["D:href"],{root:a})!==r)throw Error("DAV response target mismatch");if(!Array.isArray(i["D:propstat"])||i["D:propstat"].length!==1)throw Error("DAV discovery requires one property status");let p=i["D:propstat"][0];if(t(p,["D:prop","D:status"]),!/^HTTP[/]1[.][01] 200(?: OK)?$/.test(p["D:status"]))throw Error("DAV discovery properties unavailable");let c=p["D:prop"];if(t(c,["S:repository-uuid","D:version-controlled-configuration"]),c["S:repository-uuid"]!==n||typeof n!="string"||!/^[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}$/i.test(n))throw Error("DAV UUID does not match local identity");let f=c["D:version-controlled-configuration"];return t(f,["D:href"]),{uuid:n,vcc:L(f["D:href"],{root:a}),target:r}}function pe(o,{root:a,revision:n}={}){y(n);let r=k(o),e=(l,m)=>{if(!l||typeof l!="object"||Array.isArray(l)||Object.keys(l).some(w=>!m.includes(w)))throw Error("Invalid DAV baseline structure")};e(r,["D:multistatus"]);let t=r["D:multistatus"];if(e(t,["D:response"]),t["D:response"]?.length!==1)throw Error("One DAV baseline response required");let s=t["D:response"][0];e(s,["D:href","D:propstat"]);let i=L(s["D:href"],{root:a});if(s["D:propstat"]?.length!==1)throw Error("One DAV baseline status required");let p=s["D:propstat"][0];if(e(p,["D:status","D:prop"]),p["D:status"]!=="HTTP/1.1 200 OK")throw Error("DAV baseline status unavailable");let c=p["D:prop"];if(e(c,["D:version-name","D:baseline-collection"]),c["D:version-name"]!==n)throw Error("DAV baseline revision mismatch");e(c["D:baseline-collection"],["D:href"]);let f=L(c["D:baseline-collection"]["D:href"],{root:a});if(!f.endsWith("/"))throw Error("DAV baseline collection must end in slash");return{revision:n,baseline:i,collection:f}}function fe({request:o,assertAuthorized:a}={}){if(typeof o!="function"||typeof a!="function")throw Error("DAV chain requires transport and authorization");return async(n,{revision:r,signal:e}={})=>{y(r),I(n.root,n.scope,r);let t=n.root,s=new URL(t).origin,i=t.endsWith("/")?t:t+"/",p=async(m,w)=>{e?.throwIfAborted(),await a(n,{signal:e}),e?.throwIfAborted();let v=await o(m,{origin:s,signal:e,...w});if(e?.throwIfAborted(),await a(n,{signal:e}),e?.throwIfAborted(),!Buffer.isBuffer(v)||v.length>2097152)throw Error("Invalid DAV response bytes");return new TextDecoder("utf-8",{fatal:!0}).decode(v)},c=await p(i,{method:"PROPFIND",depth:0,body:'<D:propfind xmlns:D="DAV:" xmlns:S="http://subversion.tigris.org/xmlns/dav/"><D:prop><S:repository-uuid/><D:version-controlled-configuration/></D:prop></D:propfind>'}),{vcc:f}=ce(c,{root:t,target:i,uuid:n.uuid}),l=await p(f,{method:"PROPFIND",depth:0,label:r,body:'<D:propfind xmlns:D="DAV:"><D:prop><D:baseline-collection/><D:version-name/></D:prop></D:propfind>'});return{...pe(l,{root:t,revision:r}),uuid:n.uuid}}}function le(o,{root:a,target:n}={}){let r=k(o),e=(f,l)=>{if(!f||typeof f!="object"||Array.isArray(f)||Object.keys(f).some(m=>!l.includes(m)))throw Error("Invalid DAV property names structure")};e(r,["D:multistatus"]);let t=r["D:multistatus"];if(e(t,["D:response"]),t["D:response"]?.length!==1)throw Error("One DAV property response required");let s=t["D:response"][0];if(e(s,["D:href","D:propstat"]),L(s["D:href"],{root:a})!==n)throw Error("DAV property target mismatch");if(s["D:propstat"]?.length!==1)throw Error("One DAV property status required");let i=s["D:propstat"][0];if(e(i,["D:status","D:prop"]),i["D:status"]!=="HTTP/1.1 200 OK")throw Error("DAV property names unavailable");let p=i["D:prop"];if(p!==""&&(!p||typeof p!="object"||Array.isArray(p)||Object.keys(p).length>1e3||Object.entries(p).some(([f,l])=>!/^[DSPC]:[^:]+$/.test(f)||l!=="")))throw Error("DAV property names must be empty");let c=p===""?[]:Object.keys(p);return{names:c,special:c.includes("P:special")}}var ke=o=>o.replaceAll("&","&amp;").replaceAll("<","&lt;").replaceAll(">","&gt;").replaceAll('"',"&quot;");function de({request:o}={}){if(typeof o!="function")throw Error("Bounded DAV request required");return async(a,{identity:n,assertAuthorized:r,signal:e}={})=>{let t=C(a,n),s=t.args[0],i=fe({request:o,assertAuthorized:r}),p=await i(n,{revision:t.pegRevision,signal:e}),c=async(v,h)=>{e?.throwIfAborted(),await r(),e?.throwIfAborted();let d=await o(v,{origin:new URL(n.root).origin,...h,signal:e});if(e?.throwIfAborted(),await r(),e?.throwIfAborted(),!Buffer.isBuffer(d)||d.length>2097152)throw Error("Invalid DAV transport bytes");return d};if(s==="log"){let v=ae(a,n);return c(p.collection,{method:"REPORT",body:v.body})}let f=t.path.slice(1).split("/").map(v=>encodeURIComponent(v).replace(/[!'()*]/g,h=>"%"+h.charCodeAt(0).toString(16).toUpperCase())).join("/"),l=L(p.collection+f,{root:n.root}),m=await c(l,{method:"PROPFIND",depth:0,body:'<D:propfind xmlns:D="DAV:"><D:propname/></D:propfind>'}),w=le(new TextDecoder("utf-8",{fatal:!0}).decode(m),{root:n.root,target:l});if(s==="proplist")return'<properties><target path="'+ke(t.args.at(-1).slice(0,t.args.at(-1).lastIndexOf("@")))+'">'+(w.special?'<property name="svn:special"/>':"")+"</target></properties>";if(w.special)throw Error("SVN special content forbidden");return c(l,{method:"GET"})}}function mt(o,a){return se(o,a,{transportFormat:"dav",transport:de({request:ie()})})}export{mt as createSvnHttpsHost};
1
+ import{createHandler as Ie}from"../index.mjs";import{XMLParser as he,XMLValidator as me}from"fast-xml-parser";function O(r){if(typeof r!="string"||Buffer.byteLength(r)>2097152||/<!DOCTYPE|<!ENTITY/i.test(r)||me.validate(r)!==!0)throw Error("Invalid DAV XML");let t=new he({preserveOrder:!0,ignoreAttributes:!1,parseTagValue:!1,parseAttributeValue:!1,trimValues:!1}).parse(r),o=0,e=(s,n,i=0)=>{if(i>32)throw Error("DAV XML too deep");let a=Object.create(null),f="";for(let c of s){if(++o>1e4)throw Error("DAV XML too many nodes");if(Object.hasOwn(c,"#text")){f+=c["#text"];continue}let p=Object.keys(c).filter(D=>D!==":@");if(p.length!==1)throw Error("Invalid DAV XML node");let l=p[0];if(l==="?xml"){if(i!==0)throw Error("Unexpected XML declaration");continue}let m={...n},g={};for(let[D,V]of Object.entries(c[":@"]||{}))D==="@_xmlns"?m[""]=V:D.startsWith("@_xmlns:")?m[D.slice(8)]=V:g[D]=V;let y=l.split(":");if(y.length>2)throw Error("Invalid DAV XML name");let u=y.length===2?y[0]:"",d=y.at(-1),h=m[u],S=h==="DAV:"?"D:"+d:h==="http://subversion.tigris.org/xmlns/dav/"?"S:"+d:h==="svn:"?"L:"+d:h==="http://subversion.tigris.org/xmlns/svn/"?"P:"+d:h==="http://subversion.tigris.org/xmlns/custom/"?"C:"+d:null;if(!S)throw Error("Unsupported DAV namespace");let b=["L:added-path","L:deleted-path","L:modified-path","L:replaced-path"].includes(S);if(Object.keys(g).length&&(!b||Object.keys(g).some(D=>!["@_node-kind","@_text-mods","@_prop-mods","@_copyfrom-path","@_copyfrom-rev"].includes(D))))throw Error("Unexpected DAV attribute");let v=e(c[l],m,i+1);if(b&&typeof v!="string")throw Error("Invalid DAV path text");let E=b?{...g,"#text":v}:v;if(["D:response","D:propstat","L:log-item","L:added-path","L:deleted-path","L:modified-path","L:replaced-path"].includes(S))(a[S]??=[]).push(E);else{if(Object.hasOwn(a,S))throw Error("Duplicate DAV expanded name");a[S]=E}}if(!Object.keys(a).length)return f;if(f.trim())throw Error("Mixed DAV XML content");return a};return e(t,{})}var ve=9223372036854775807n;function w(r){if(typeof r!="string"||r.length>19||!/^(0|[1-9][0-9]*)$/.test(r))throw new Error("Invalid SVN revision");let t=BigInt(r);if(t>ve)throw new Error("SVN revision exceeds supported range");return t}function H(r){let t=w(r);return t===0n?null:String(t-1n)}function j(r,{snapshot:t,cursor:o=t,limit:e=50}={}){let s=w(t),n=w(o);if(n>s||!Number.isInteger(e)||e<1||e>100||!Array.isArray(r)||r.length>e+1)throw new Error("Invalid SVN page bounds");let i=n+1n;for(let f of r){let c=w(f);if(c>n||c>=i)throw new Error("SVN revisions must descend within cursor");i=c}let a=r.slice(0,e);return{snapshot:t,revisions:a,nextRevision:r.length>e?H(a.at(-1)):null}}function J(r,t){let o=O(r);if(!o||typeof o!="object"||Object.keys(o).length!==1||!Object.hasOwn(o,"L:log-report"))throw Error("Invalid DAV log root");let e=o["L:log-report"],s;if(e==="")s=[];else{if(!e||typeof e!="object"||Object.keys(e).some(a=>a!=="L:log-item")||!Array.isArray(e["L:log-item"]))throw Error("Invalid DAV log structure");s=e["L:log-item"]}if(s.length>101)throw Error("DAV log entries exceed limit");let n=s.map(a=>{if(!a||typeof a!="object"||Array.isArray(a)||Object.keys(a).some(c=>!["D:version-name","D:comment","D:creator-displayname","L:date"].includes(c)))throw Error("Unsupported DAV log item");let f=c=>{let p=a[c]??"";if(typeof p!="string")throw Error("Invalid DAV log text");return p};return{revision:f("D:version-name"),message:f("D:comment"),author:f("D:creator-displayname"),date:f("L:date")}}),i=j(n.map(a=>a.revision),t);return{snapshot:i.snapshot,entries:n.slice(0,i.revisions.length),nextRevision:i.nextRevision}}function k(r){if(typeof r!="string"||r.length>32768||!r.startsWith("/")||/[\u0000-\u001f\u007f\\]/.test(r))throw new Error("Invalid SVN repository path");if(r!=="/"&&r.slice(1).split("/").some(t=>t===""||t==="."||t===".."))throw new Error("Noncanonical SVN repository path");return r}function T(r,t){return k(r),k(t),t==="/"||r===t||r.startsWith(t+"/")}function q(r,t){if(!Array.isArray(r)||r.length>1e4)throw new Error("Invalid SVN path nodes");let o=r.map(e=>{let s=["#text","@_action","@_kind","@_copyfrom-path","@_copyfrom-rev","@_text-mods","@_prop-mods"];if(!e||typeof e!="object"||Array.isArray(e)||Object.keys(e).some(n=>!s.includes(n)))throw new Error("Invalid SVN path node");for(let n of["@_text-mods","@_prop-mods"])if(Object.hasOwn(e,n)&&!["true","false","unknown"].includes(e[n]))throw new Error("Invalid SVN modification flag");return{...Object.hasOwn(e,"@_text-mods")?{textModified:e["@_text-mods"]}:{},...Object.hasOwn(e,"@_prop-mods")?{propertiesModified:e["@_prop-mods"]}:{},path:e["#text"],action:e["@_action"],kind:e["@_kind"],...Object.hasOwn(e,"@_copyfrom-path")?{copyFromPath:e["@_copyfrom-path"]}:{},...Object.hasOwn(e,"@_copyfrom-rev")?{copyFromRevision:e["@_copyfrom-rev"]}:{}}});return W(o,t)}function W(r,{scope:t,revision:o}={}){k(t);let e=w(o);if(!Array.isArray(r)||r.length>1e4||e===0n&&r.length!==0)throw new Error("Invalid SVN changed paths");let s=new Set;return r.map(i=>{if(!i||typeof i!="object"||Array.isArray(i)||Object.keys(i).some(u=>!["path","action","kind","copyFromPath","copyFromRevision","copySourceOutsideScope","textModified","propertiesModified"].includes(u)))throw new Error("Invalid SVN change structure");let{path:a,action:f,kind:c,copyFromPath:p,copyFromRevision:l}=i;if(k(a),s.has(a)||!["A","D","M","R"].includes(f)||!["file","dir","unknown"].includes(c))throw new Error("Invalid SVN change");s.add(a);let m=p!==void 0||l!==void 0;if(m&&(k(p),!["A","R"].includes(f)||w(l)>=e))throw new Error("Invalid SVN copy source");let g=Object.hasOwn(i,"copySourceOutsideScope");if(g&&(i.copySourceOutsideScope!==!0||m||!["A","R"].includes(f)))throw new Error("Invalid SVN redacted copy source");let y={path:a,action:f,kind:c,...g?{copySourceOutsideScope:!0}:{}};for(let u of["textModified","propertiesModified"])if(Object.hasOwn(i,u)){if(!["true","false","unknown"].includes(i[u]))throw new Error("Invalid SVN modification flag");y[u]=i[u]}return m&&(T(p,t)?(y.copyFromPath=p,y.copyFromRevision=l):y.copySourceOutsideScope=!0),y}).filter(i=>T(i.path,t))}function G(r,{scope:t,revision:o}={}){w(o);let e=O(r),s=(p,l)=>{if(!p||typeof p!="object"||Array.isArray(p)||Object.keys(p).some(m=>!l.includes(m)))throw Error("Invalid DAV detail structure")};s(e,["L:log-report"]);let n=e["L:log-report"];if(s(n,["L:log-item"]),n["L:log-item"]?.length!==1)throw Error("One DAV detail revision required");let i=n["L:log-item"][0],a={"L:added-path":"A","L:deleted-path":"D","L:modified-path":"M","L:replaced-path":"R"};if(s(i,["D:version-name","D:comment","D:creator-displayname","L:date",...Object.keys(a)]),i["D:version-name"]!==o)throw Error("DAV detail revision mismatch");let f=p=>{let l=i[p]??"";if(typeof l!="string")throw Error("Invalid DAV detail text");return l},c=[];for(let[p,l]of Object.entries(a))for(let m of i[p]??[]){let{"@_node-kind":g,...y}=m;c.push({...y,"@_kind":g??"unknown","@_action":l})}return{revision:o,author:f("D:creator-displayname"),date:f("L:date"),message:f("D:comment"),changes:q(c,{scope:t,revision:o})}}function I(r,t,o){if(k(t),w(o),typeof r!="string"||r.length>32768||/[\x00-\x20\x7f\\?#]/.test(r))throw new Error("Invalid SVN HTTPS root");let e=new URL(r);if(e.protocol!=="https:"||e.username||e.password||e.href!==r)throw new Error("Noncanonical SVN HTTPS root");let s=e.pathname.split("/").slice(1);s.at(-1)===""&&s.pop();for(let f of s){let c=decodeURIComponent(f);if(!f||c==="."||c===".."||/[\x00-\x1f\x7f/\\]/.test(c))throw new Error("Invalid SVN root path")}let n=r.endsWith("/")?r.slice(0,-1):r,i=t==="/"?"":t.split("/").map(f=>encodeURIComponent(f).replace(/[!'()*]/g,c=>"%"+c.charCodeAt(0).toString(16).toUpperCase())).join("/"),a=n+(i||(e.pathname==="/"?"/":""))+"@"+o;if(a.length>32768)throw new Error("SVN target too long");return a}function B({root:r,scope:t,path:o=t,snapshot:e,revision:s}={}){if(w(s)>w(e))throw new Error("SVN selected revision exceeds snapshot");if(!T(o,t))throw new Error("SVN detail path outside scope");let n=I(r,o,e);return{scope:t,revision:s,snapshot:e,args:["log","--xml","--verbose","--stop-on-copy","--non-interactive","--no-auth-cache","--limit","1","-r",s+":"+s,"--",n]}}function M({root:r,scope:t,path:o=t,snapshot:e,cursor:s=e,limit:n=50}={}){if(j([],{snapshot:e,cursor:s,limit:n}),!T(o,t))throw new Error("SVN history path outside scope");let i=I(r,o,e);return{snapshot:e,cursor:s,limit:n,args:["log","--xml","--stop-on-copy","--non-interactive","--no-auth-cache","--limit",String(n+1),"-r",s+":0","--",i]}}function C(r,t){let o=r?.args;if(!Array.isArray(o)||o.length>16||o.some(l=>typeof l!="string"))throw Error("Invalid SVN command");let e=o.at(-1),s=e.lastIndexOf("@"),n=e.slice(s+1);w(n);let i=t.root.endsWith("/")?t.root.slice(0,-1):t.root;I(t.root,"/",n);let a=e.slice(0,s);if(!(a===i||a.startsWith(i+"/")))throw Error("SVN command target outside root");let f=a.slice(i.length),c=f?"/"+f.slice(1).split("/").map(l=>decodeURIComponent(l)).join("/"):"/";if(!T(c,t.scope)||I(t.root,c,n)!==e)throw Error("SVN command target outside scope or noncanonical");let p;if(o[0]==="cat"||o[0]==="proplist"){w(o[o[0]==="cat"?4:5]);let l=o[o[0]==="cat"?4:5];if(l!==n)throw Error("SVN content operative and peg mismatch");p=o[0]==="cat"?["cat","--non-interactive","--no-auth-cache","-r",l,"--",e]:["proplist","--xml","--non-interactive","--no-auth-cache","-r",l,"--",e]}else if(o[0]==="log")if(o.includes("--verbose")){let l=o[9]?.split(":")[0];p=B({...t,path:c,snapshot:n,revision:l}).args}else{let l=o[8]?.split(":")[0],m=Number(o[6])-1;p=M({...t,path:c,snapshot:n,cursor:l,limit:m}).args}else throw Error("SVN command is not readonly history");if(JSON.stringify(o)!==JSON.stringify(p))throw Error("Unsupported SVN command shape");return{path:c,pegRevision:n,args:[...p]}}function K(r,{scope:t,revision:o,path:e}={}){if(w(o)===0n)throw new Error("SVN r0 has no change comparison");let s=W(r,{scope:t,revision:o}).find(a=>a.path===e);if(!s)throw new Error("SVN path is not an authorized changed member");if(s.kind!=="file")throw new Error("Only SVN file changes can be compared");let n=H(o),i=a=>a===null?{empty:!0}:{empty:!1,path:s.path,revision:a,pegRevision:a};return{path:s.path,action:s.action,left:i(s.action==="A"?null:n),right:i(s.action==="D"?null:o)}}import{XMLParser as ge,XMLValidator as we}from"fast-xml-parser";function Q(r,{target:t,allowEmpty:o=!1}={}){if(typeof t!="string"||!t||t.length>32768)throw new Error("Invalid SVN property target");if(typeof r!="string"||Buffer.byteLength(r,"utf8")>2*1024*1024||/<!DOCTYPE|<!ENTITY/i.test(r)||we.validate(r)!==!0)throw new Error("Invalid SVN property XML");let e=new ge({ignoreAttributes:!1,parseAttributeValue:!1,parseTagValue:!1,trimValues:!1,isArray:p=>p==="target"||p==="property"}).parse(r),s=(p,l)=>{if(!p||typeof p!="object"||Array.isArray(p)||Object.keys(p).some(m=>!l.includes(m)&&!(m==="#text"&&typeof p[m]=="string"&&/^[ \t\r\n]*$/.test(p[m]))))throw new Error("Invalid SVN property structure")};if(s(e,["?xml","properties"]),o===!0&&typeof e.properties=="string"&&/^[ \t\r\n]*$/.test(e.properties))return{target:t,names:[],special:!1};s(e.properties,["target"]);let n=e.properties.target;if(!Array.isArray(n)||n.length!==1)throw new Error("Expected one SVN property target");let i=n[0];if(s(i,["@_path","property"]),i["@_path"]!==t)throw new Error("SVN property target mismatch");let a=i.property??[];if(!Array.isArray(a)||a.length>1e4)throw new Error("Too many SVN properties");let f=[],c=new Set;for(let p of a){s(p,["@_name"]);let l=p["@_name"];if(typeof l!="string"||!l||l.length>1024||/[\x00-\x20\x7f]/.test(l)||c.has(l))throw new Error("Invalid SVN property name");c.add(l),f.push(l)}return{target:t,names:f,special:c.has("svn:special")}}import{createHash as Ve}from"node:crypto";import{XMLParser as ye,XMLValidator as Se}from"fast-xml-parser";function Z(r,{scope:t,revision:o}={}){if(w(o),typeof r!="string"||Buffer.byteLength(r,"utf8")>2*1024*1024||/<!DOCTYPE|<!ENTITY/i.test(r)||Se.validate(r)!==!0)throw new Error("Invalid SVN detail XML");let e=new ye({ignoreAttributes:!1,parseAttributeValue:!1,parseTagValue:!1,trimValues:!1,isArray:c=>c==="logentry"||c==="path"}).parse(r),s=(c,p)=>{if(!c||typeof c!="object"||Array.isArray(c)||Object.keys(c).some(l=>!p.includes(l)&&!(l==="#text"&&typeof c[l]=="string"&&/^[ \t\r\n]*$/.test(c[l]))))throw new Error("Invalid SVN detail structure")};if(s(e,["?xml","log"]),s(e.log,["logentry"]),!Array.isArray(e.log.logentry)||e.log.logentry.length!==1)throw new Error("Expected one SVN revision");let n=e.log.logentry[0];if(s(n,["@_revision","author","date","msg","paths"]),n["@_revision"]!==o)throw new Error("SVN detail revision mismatch");let i=c=>{let p=n[c]??"";if(typeof p!="string")throw new Error("Invalid SVN detail text");return p},a=[];n.paths!==void 0&&!(typeof n.paths=="string"&&/^[ \t\r\n]*$/.test(n.paths))&&(s(n.paths,["path"]),a=n.paths.path??[]);let f=q(a,{scope:t,revision:o});return{revision:o,author:i("author"),date:i("date"),message:i("msg"),pathsAvailable:Object.hasOwn(n,"paths"),changes:f}}import{randomBytes as De}from"node:crypto";function ee(r){if(!r||typeof r!="object")throw Error("Invalid SVN consent identity");let{sessionId:t,cwd:o,repositoryId:e,root:s,uuid:n,scope:i}=r;for(let a of[t,o,e])if(typeof a!="string"||!a||a.length>32768||a.includes("\0"))throw Error("Invalid SVN consent scope");if(I(s,"/","0"),k(i),typeof n!="string"||!/^[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}$/i.test(n))throw Error("Invalid SVN repository UUID");return JSON.stringify([t,o,e,s,n,i])}function F({now:r=Date.now,ttlMs:t=3e5,maxEntries:o=32,onRevoke:e=()=>{}}={}){if(!Number.isInteger(t)||t<1||t>3e5||!Number.isInteger(o)||o<1||o>32)throw Error("Invalid SVN consent bounds");if(typeof e!="function")throw Error("Invalid SVN revoke listener");let s=new Map,n=a=>{s.delete(a)&&e(a)},i=()=>{for(let[a,f]of s)f.expires<=r()&&n(a)};return{grant(a,{explicit:f=!1}={}){if(f!==!0)throw Error("Explicit SVN network consent required");let c=ee(a);i();for(let[l,m]of s)m.key===c&&n(l);for(;s.size>=o;)n(s.keys().next().value);let p=De(32).toString("hex");return s.set(p,{key:c,sessionId:a.sessionId,expires:r()+t}),p},assert(a,f){i();let c=ee(f),p=s.get(a);if(!p||p.key!==c)throw Object.assign(Error("SVN network consent missing, expired or identity changed"),{code:"vcs/svn-consent-required"})},revokeSession(a){for(let[f,c]of s)c.sessionId===a&&n(f)},revoke(a){n(a)},clear(){for(let a of[...s.keys()])n(a)}}}import{XMLParser as be,XMLValidator as Ee}from"fast-xml-parser";function re(r,t){let o=Ae(r),e=j(o.map(s=>s.revision),t);return{snapshot:e.snapshot,entries:o.slice(0,e.revisions.length),nextRevision:e.nextRevision}}function Ae(r){if(typeof r!="string"||Buffer.byteLength(r,"utf8")>2*1024*1024||/<!DOCTYPE|<!ENTITY/i.test(r)||Ee.validate(r)!==!0)throw new Error("Invalid SVN log XML");let o=new be({ignoreAttributes:!1,parseAttributeValue:!1,parseTagValue:!1,trimValues:!1,isArray:n=>n==="logentry"}).parse(r);if(!Object.hasOwn(o,"log")||Object.keys(o).some(n=>n!=="log"&&n!=="?xml"))throw new Error("Invalid SVN log root");let e=o.log;if(typeof e=="string"&&/^[ \t\r\n]*$/.test(e))return[];if(e&&typeof e=="object"&&typeof e["#text"]=="string"&&/^[ \t\r\n]*$/.test(e["#text"])&&delete e["#text"],!e||typeof e!="object"||Object.keys(e).some(n=>n!=="logentry")||!Array.isArray(e.logentry)||e.logentry.length>101)throw new Error("Invalid SVN log entries");let s=new Set;return e.logentry.map(n=>{if(!n||typeof n!="object"||Array.isArray(n)||Object.keys(n).some(f=>!["@_revision","author","date","msg","#text"].includes(f))||"#text"in n&&(typeof n["#text"]!="string"||!/^[ \t\r\n]*$/.test(n["#text"])))throw new Error("Unsupported SVN log entry structure");let i=n["@_revision"];if(w(i),s.has(i))throw new Error("Duplicate SVN revision");s.add(i);let a=f=>{let c=n[f]??"";if(typeof c!="string")throw new Error("Invalid SVN log text");return c};return{revision:i,author:a("author"),date:a("date"),message:a("msg")}})}function te({resolveIdentity:r,transport:t,now:o=Date.now,timeoutMs:e=15e3,transportFormat:s="cli"}={}){if(!["cli","dav"].includes(s))throw Error("Invalid SVN transport format");if(!Number.isInteger(e)||e<1||e>15e3)throw Error("Invalid SVN runtime deadline");if(typeof r!="function")throw Error("Local identity resolver required");let n=F({now:o}),i=F({now:o,onRevoke:u=>m(u)}),a=!1,f=()=>{if(a)throw new DOMException("SVN runtime disposed","AbortError")},c=async(u,d)=>{f(),d?.throwIfAborted();let h=await r(u,d);return f(),d?.throwIfAborted(),{...h,sessionId:u.sessionId,cwd:h.cwd,repositoryId:u.repositoryId}},p=u=>u.revision===void 0?{}:(w(u.revision),{snapshot:u.revision}),l=new Map,m=u=>{for(let d of l.get(u)||[])d.controller.abort(new DOMException("SVN consent revoked","AbortError"))},g=async(u,d)=>{f(),C(u,d.identity),d.signal?.throwIfAborted(),i.assert(d.token,d.identity);let h=new AbortController,S=()=>h.abort(d.signal.reason);d.signal?.addEventListener("abort",S,{once:!0});let b={controller:h,sessionId:d.identity.sessionId};l.has(d.token)||l.set(d.token,new Set),l.get(d.token).add(b);let v=setTimeout(()=>h.abort(new DOMException("SVN transport deadline exceeded","TimeoutError")),e);try{let E=async()=>{h.signal.throwIfAborted();let V=await c({sessionId:d.identity.sessionId,repositoryId:d.identity.repositoryId},h.signal);i.assert(d.token,V),h.signal.throwIfAborted()},D=await t(u,{...d,timeoutMs:e,signal:h.signal,assertAuthorized:E});if(h.signal.throwIfAborted(),!(typeof D=="string"||Buffer.isBuffer(D))||Buffer.byteLength(D)>d.maxBytes)throw Error("SVN transport output exceeds byte limit or has invalid type");return Buffer.isBuffer(D)&&u.args[0]!=="cat"?new TextDecoder("utf-8",{fatal:!0}).decode(D):D}catch(E){throw h.signal.aborted?h.signal.reason:E}finally{let E=l.get(d.token);E?.delete(b),E?.size||l.delete(d.token),clearTimeout(v),d.signal?.removeEventListener("abort",S)}},y={async describe(u,{signal:d}={}){let h=await c(u,d),S=p(h),b=n.grant(h,{explicit:!0});return{...S,offer:b,root:h.root,uuid:h.uuid,scope:h.scope,origin:new URL(h.root).origin,expiresAt:o()+3e5,remoteEnabled:typeof t=="function"}},async approve(u,{offer:d,explicit:h=!1,signal:S}={}){if(h!==!0)throw Error("Explicit SVN network consent required");let b=await c(u,S);if(n.assert(d,b),typeof t!="function")throw Error("SVN remote transport disabled");let v=p(b),E=i.grant(b,{explicit:!0});return n.revoke(d),{...v,token:E,expiresAt:o()+3e5}},async log(u,{token:d,snapshot:h,cursor:S,limit:b,path:v,signal:E}={}){let D=await c(u,E);if(i.assert(d,D),typeof t!="function")throw Error("SVN remote transport disabled");let V=M({...D,snapshot:h,cursor:S,limit:b,path:v});E?.throwIfAborted();let A=await g(V,{identity:D,token:d,signal:E,maxBytes:2097152,timeoutMs:15e3});E?.throwIfAborted();let x=await c(u,E);return i.assert(d,x),(s==="dav"?J:re)(A,V)},async detail(u,{token:d,snapshot:h,revision:S,signal:b}={}){let v=await c(u,b);if(i.assert(d,v),typeof t!="function")throw Error("SVN remote transport disabled");let E=B({...v,snapshot:h,revision:S}),D=await g(E,{identity:v,token:d,signal:b,maxBytes:2097152,timeoutMs:15e3});b?.throwIfAborted(),i.assert(d,await c(u,b));let V=s==="dav"?{...G(D,E),pathsAvailable:!0}:Z(D,E);return{...V,snapshot:h,changes:V.changes.map(A=>({...A,id:Ve("sha256").update(JSON.stringify([v.root,v.uuid,v.scope,h,S,A])).digest("hex")}))}},async compare(u,{token:d,snapshot:h,revision:S,id:b,signal:v}={}){if(typeof b!="string"||!/^[a-f0-9]{64}$/.test(b))throw Error("Invalid SVN change ID");let E=await y.detail(u,{token:d,snapshot:h,revision:S,signal:v}),D=E.changes.find(N=>N.id===b);if(!D)throw Error("SVN change is not selected revision member");let V=await c(u,v);i.assert(d,V);let A=K(E.changes.map(({id:N,..._})=>_),{scope:V.scope,revision:S,path:D.path}),x=async N=>{if(N.empty)return{label:"empty",text:"",absent:!0};let _=await c(u,v);i.assert(d,_);let U=I(_.root,N.path,N.pegRevision),Y={identity:_,token:d,signal:v,maxBytes:2097152,timeoutMs:15e3},ue=await g({args:["proplist","--xml","--non-interactive","--no-auth-cache","-r",N.revision,"--",U]},Y);if(v?.throwIfAborted(),i.assert(d,await c(u,v)),Q(ue,{target:U.slice(0,U.lastIndexOf("@")),allowEmpty:!0}).special)throw Error("SVN special files cannot be compared");let X=await g({args:["cat","--non-interactive","--no-auth-cache","-r",N.revision,"--",U]},Y);v?.throwIfAborted(),i.assert(d,await c(u,v));let $=Buffer.isBuffer(X)?X:Buffer.from(X);if($.length>2097152)throw Error("SVN text exceeds 2 MiB");if($.includes(0))return{label:"r"+N.revision,text:"",binary:!0};let z;try{z=new TextDecoder("utf-8",{fatal:!0}).decode($)}catch{return{label:"r"+N.revision,text:"",binary:!0}}return{label:"r"+N.revision,text:z}},R=await x(A.left),P=await x(A.right);return{path:D.path,revision:S,snapshot:h,left:R,right:P,binary:!!(R.binary||P.binary)}},async trace(u,{token:d,snapshot:h,revision:S,id:b,cursor:v,limit:E=50,signal:D}={}){if(typeof b!="string"||!/^[a-f0-9]{64}$/.test(b))throw Error("Invalid SVN change ID");let V=await y.detail(u,{token:d,snapshot:h,revision:S,signal:D}),A=V.changes.find(P=>P.id===b);if(!A||A.kind!=="file")throw Error("SVN tracing requires selected file member");let x=A.action==="D"?H(S):S;return{...await y.log(u,{token:d,snapshot:x,cursor:v??x,limit:E,path:A.path,signal:D}),selectionSnapshot:h,selectionRevision:S,path:A.path,pegRevision:x,stopOnCopy:!0,copySourceOutsideScope:!!A.copySourceOutsideScope}},async revoke(u,{token:d,signal:h}={}){let S=await c(u,h);return i.assert(d,S),i.revoke(d),m(d),{revoked:!0}},dispose(){a=!0,n.clear(),i.clear();for(let u of l.keys())m(u)},revokeSession(u){n.revokeSession(u),i.revokeSession(u);for(let[d,h]of l)[...h].some(S=>S.sessionId===u)&&m(d)}};return y}var oe={revoke:["token"],describe:[],approve:["offer","explicit"],log:["token","snapshot","cursor","limit"],detail:["token","snapshot","revision"],compare:["token","snapshot","revision","id"],trace:["token","snapshot","revision","id","cursor","limit"]};function xe(r,t){if(!t||typeof t!="object"||Array.isArray(t))throw Error("Object payload required");if(Object.keys(t).some(o=>!["sessionId","repositoryId",...oe[r]].includes(o)))throw Error("Unknown SVN request field");for(let o of["sessionId","repositoryId"])if(typeof t[o]!="string"||!t[o]||t[o].length>1024||t[o].includes("\0"))throw Error("Invalid SVN Session address");if(r==="approve"&&(t.explicit!==!0||typeof t.offer!="string"||!/^[a-f0-9]{64}$/.test(t.offer)))throw Error("Explicit scoped offer required");if(!["describe","approve"].includes(r)){if(typeof t.token!="string"||!/^[a-f0-9]{64}$/.test(t.token))throw Error("SVN consent token required");if(r!=="revoke"&&w(t.snapshot),t.revision!==void 0&&w(t.revision),["detail","compare","trace"].includes(r)&&t.revision===void 0)throw Error("Selected SVN revision required");if(t.cursor!==void 0&&w(t.cursor),t.limit!==void 0&&(!Number.isInteger(t.limit)||t.limit<1||t.limit>100))throw Error("Invalid SVN page size");if(["compare","trace"].includes(r)&&(typeof t.id!="string"||!/^[a-f0-9]{64}$/.test(t.id)))throw Error("Selected SVN change ID required")}}function ne(r,{timeoutMs:t=15e3}={}){if(!Number.isInteger(t)||t<1||t>15e3)throw Error("Invalid SVN RPC deadline");let o=0;return async(e,s,n)=>{let i=e.startsWith("vcs/svn-")?e.slice(8):"";if(!Object.hasOwn(oe,i))return{ok:!1,error:{code:"vcs/not-found",message:"Unknown SVN endpoint"}};if(i!=="revoke"&&o>=2)return{ok:!1,error:{code:"vcs/busy",message:"SVN history requests busy"}};i!=="revoke"&&o++;let a=new AbortController,f=()=>a.abort(n.reason);n?.addEventListener("abort",f,{once:!0}),n?.aborted&&f();let c=setTimeout(()=>a.abort(new DOMException("SVN operation deadline exceeded","TimeoutError")),t);try{n?.throwIfAborted(),xe(i,s);let{sessionId:p,repositoryId:l,...m}=s,g=await r[i]({sessionId:p,repositoryId:l},{...m,signal:a.signal});return a.signal.throwIfAborted(),{ok:!0,value:g}}catch(p){let l=n?.aborted||p?.name==="AbortError"?"vcs/cancelled":["vcs/rediscover-required","vcs/svn-consent-required"].includes(p?.code)?p.code:"vcs/operation-failed",m=a.signal.aborted?a.signal.reason:p,g=m instanceof Error?m.message:String(m);return{ok:!1,error:{code:l,message:g}}}finally{clearTimeout(c),n?.removeEventListener("abort",f),i!=="revoke"&&o--}}}var Ne=new Set(["describe","approve","log","detail","compare","trace","revoke"]);function se(r,t,{transport:o,transportFormat:e="cli",now:s=Date.now,timeoutMs:n=15e3}={}){let i,a=Ie(r,t,4,{now:s,bindSvnIdentityResolver:p=>{i=p}}),f=te({resolveIdentity:i,transport:o,transportFormat:e,now:s,timeoutMs:n}),c=ne(f,{timeoutMs:n});return{handle(p,l,m){let g=p.startsWith("vcs/svn-")?p.slice(8):"";return Ne.has(g)?c(p,l,m):a(p,l,m)},revokeSession:p=>f.revokeSession(p),dispose:()=>f.dispose()}}import{request as Oe}from"node:https";function ie({request:r=Oe}={}){return async(t,{origin:o,method:e="GET",body:s="",depth:n,label:i,signal:a,maxBytes:f=2097152,timeoutMs:c=15e3}={})=>{let p=new URL(t);if(p.protocol!=="https:"||p.origin!==o||p.username||p.password||p.hash||p.href!==t||!["GET","PROPFIND","REPORT"].includes(e)||typeof s!="string"||Buffer.byteLength(s)>65536||!Number.isInteger(f)||f<1||f>2097152||!Number.isInteger(c)||c<1||c>15e3)throw Error("Invalid bounded HTTPS request");if(n!==void 0&&(e!=="PROPFIND"||n!==0))throw Error("Invalid DAV depth");if(i!==void 0){if(e!=="PROPFIND"||n!==0)throw Error("Invalid DAV label");w(i)}let l={...n===0?{Depth:"0"}:{},...i!==void 0?{Label:i}:{}};return a?.throwIfAborted(),new Promise((m,g)=>{let y,u,d=null,h=null,S=0,b=[],v=A=>{d??=A,u?.destroy(),y?.destroy()},E=()=>v(a.reason??new DOMException("HTTPS cancelled","AbortError")),D=setTimeout(()=>v(new DOMException("HTTPS deadline exceeded","TimeoutError")),c),V=()=>{clearTimeout(D),a?.removeEventListener("abort",E)};try{y=r(p,{method:e,agent:!1,rejectUnauthorized:!0,maxHeaderSize:16384,headers:{...l,"Accept-Encoding":"identity","Content-Type":"text/xml; charset=utf-8","Content-Length":String(Buffer.byteLength(s))}},A=>{u=A;let x=u.statusCode??0;if(x>=300&&x<400){v(Error("HTTPS redirects forbidden"));return}if(x<200||x>=300){v(Error("HTTPS status "+x));return}if(u.headers["content-encoding"]&&u.headers["content-encoding"]!=="identity"){v(Error("HTTPS compressed responses forbidden"));return}let R=u.headers["content-length"];if(R!==void 0&&(!/^[0-9]+$/.test(R)||Number(R)>f)){v(Error("HTTPS response exceeds byte limit"));return}u.on("data",P=>{S+=P.length,S>f?v(Error("HTTPS response exceeds byte limit")):d||b.push(P)}),u.on("error",v),u.on("end",()=>{if(!u.complete){v(Error("HTTPS response incomplete"));return}h=Buffer.concat(b)})}),y.on("error",A=>{d??=A}),y.once("close",()=>{V(),d?g(d):h!==null?m(h):g(Error("HTTPS response closed before completion"))}),a?.addEventListener("abort",E,{once:!0}),a?.aborted?E():y.end(s)}catch(A){V(),y?.destroy(),g(A)}})}}var ke=r=>r.replaceAll("&","&amp;").replaceAll("<","&lt;").replaceAll(">","&gt;").replaceAll('"',"&quot;").replaceAll("'","&apos;");function ae(r,t){let o=C(r,t),e=o.args;if(e[0]!=="log")throw Error("DAV log report requires readonly log plan");let s=e.includes("--verbose"),n=e[s?9:8].split(":"),i=e[s?7:6],a=o.path==="/"?"":o.path.slice(1),f='<?xml version="1.0" encoding="utf-8"?><S:log-report xmlns:S="svn:" xmlns:D="DAV:"><S:start-revision>'+n[0]+"</S:start-revision><S:end-revision>"+n[1]+"</S:end-revision><S:limit>"+i+"</S:limit><S:strict-node-history/>"+(s?"<S:discover-changed-paths/>":"")+"<S:path>"+ke(a)+"</S:path><S:revprop>svn:author</S:revprop><S:revprop>svn:date</S:revprop><S:revprop>svn:log</S:revprop></S:log-report>";if(Buffer.byteLength(f)>65536)throw Error("DAV request body exceeds limit");return{method:"REPORT",body:f,pegRevision:o.pegRevision,path:o.path,requiresVerifiedBaseline:!0}}function L(r,{root:t}={}){if(I(t,"/","0"),typeof r!="string"||!r||r.length>32768||/[\\?#]/.test(r)||[...r].some(n=>n.charCodeAt(0)<=32||n.charCodeAt(0)===127))throw Error("Invalid DAV href");if(!(r.startsWith("/")&&!r.startsWith("//"))&&!r.startsWith("https://"))throw Error("DAV href must be absolute path or HTTPS URL");let o=new URL(r,new URL(t).origin);if(o.protocol!=="https:"||o.origin!==new URL(t).origin||o.username||o.password||o.search||o.hash)throw Error("DAV href outside authorized origin");if(r.startsWith("https://")&&o.href!==r||r.startsWith("/")&&o.pathname!==r)throw Error("Noncanonical DAV href");let e=o.pathname.split("/").slice(1);e.at(-1)===""&&e.pop();for(let n of e){let i=decodeURIComponent(n);if(!n||i==="."||i===".."||[...i].some(a=>a.charCodeAt(0)<32||a.charCodeAt(0)===127||a==="/"||a.charCodeAt(0)===92))throw Error("Unsafe DAV href path")}let s=new URL(t).pathname.replace(/[/]$/,"");if(s&&o.pathname!==s&&!o.pathname.startsWith(s+"/"))throw Error("DAV href outside repository root");return o.href}function ce(r,{root:t,uuid:o,target:e}={}){let s=O(r),n=(l,m)=>{if(!l||typeof l!="object"||Array.isArray(l)||Object.keys(l).some(g=>!m.includes(g)&&!(g==="#text"&&typeof l[g]=="string"&&l[g].trim()==="")))throw Error("Unsupported DAV discovery structure")};n(s,["?xml","D:multistatus"]);let i=s["D:multistatus"];if(n(i,["D:response"]),!Array.isArray(i["D:response"])||i["D:response"].length!==1)throw Error("DAV discovery requires one response");let a=i["D:response"][0];if(n(a,["D:href","D:propstat"]),L(a["D:href"],{root:t})!==e)throw Error("DAV response target mismatch");if(!Array.isArray(a["D:propstat"])||a["D:propstat"].length!==1)throw Error("DAV discovery requires one property status");let f=a["D:propstat"][0];if(n(f,["D:prop","D:status"]),!/^HTTP[/]1[.][01] 200(?: OK)?$/.test(f["D:status"]))throw Error("DAV discovery properties unavailable");let c=f["D:prop"];if(n(c,["S:repository-uuid","D:version-controlled-configuration"]),c["S:repository-uuid"]!==o||typeof o!="string"||!/^[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}$/i.test(o))throw Error("DAV UUID does not match local identity");let p=c["D:version-controlled-configuration"];return n(p,["D:href"]),{uuid:o,vcc:L(p["D:href"],{root:t}),target:e}}function pe(r,{root:t,revision:o}={}){w(o);let e=O(r),s=(l,m)=>{if(!l||typeof l!="object"||Array.isArray(l)||Object.keys(l).some(g=>!m.includes(g)))throw Error("Invalid DAV baseline structure")};s(e,["D:multistatus"]);let n=e["D:multistatus"];if(s(n,["D:response"]),n["D:response"]?.length!==1)throw Error("One DAV baseline response required");let i=n["D:response"][0];s(i,["D:href","D:propstat"]);let a=L(i["D:href"],{root:t});if(i["D:propstat"]?.length!==1)throw Error("One DAV baseline status required");let f=i["D:propstat"][0];if(s(f,["D:status","D:prop"]),f["D:status"]!=="HTTP/1.1 200 OK")throw Error("DAV baseline status unavailable");let c=f["D:prop"];if(s(c,["D:version-name","D:baseline-collection"]),c["D:version-name"]!==o)throw Error("DAV baseline revision mismatch");s(c["D:baseline-collection"],["D:href"]);let p=L(c["D:baseline-collection"]["D:href"],{root:t});if(!p.endsWith("/"))throw Error("DAV baseline collection must end in slash");return{revision:o,baseline:a,collection:p}}function fe({request:r,assertAuthorized:t}={}){if(typeof r!="function"||typeof t!="function")throw Error("DAV chain requires transport and authorization");return async(o,{revision:e,signal:s}={})=>{w(e),I(o.root,o.scope,e);let n=o.root,i=new URL(n).origin,a=n.endsWith("/")?n:n+"/",f=async(m,g)=>{s?.throwIfAborted(),await t(o,{signal:s}),s?.throwIfAborted();let y=await r(m,{origin:i,signal:s,...g});if(s?.throwIfAborted(),await t(o,{signal:s}),s?.throwIfAborted(),!Buffer.isBuffer(y)||y.length>2097152)throw Error("Invalid DAV response bytes");return new TextDecoder("utf-8",{fatal:!0}).decode(y)},c=await f(a,{method:"PROPFIND",depth:0,body:'<D:propfind xmlns:D="DAV:" xmlns:S="http://subversion.tigris.org/xmlns/dav/"><D:prop><S:repository-uuid/><D:version-controlled-configuration/></D:prop></D:propfind>'}),{vcc:p}=ce(c,{root:n,target:a,uuid:o.uuid}),l=await f(p,{method:"PROPFIND",depth:0,label:e,body:'<D:propfind xmlns:D="DAV:"><D:prop><D:baseline-collection/><D:version-name/></D:prop></D:propfind>'});return{...pe(l,{root:n,revision:e}),uuid:o.uuid}}}function le(r,{root:t,target:o}={}){let e=O(r),s=(p,l)=>{if(!p||typeof p!="object"||Array.isArray(p)||Object.keys(p).some(m=>!l.includes(m)))throw Error("Invalid DAV property names structure")};s(e,["D:multistatus"]);let n=e["D:multistatus"];if(s(n,["D:response"]),n["D:response"]?.length!==1)throw Error("One DAV property response required");let i=n["D:response"][0];if(s(i,["D:href","D:propstat"]),L(i["D:href"],{root:t})!==o)throw Error("DAV property target mismatch");if(i["D:propstat"]?.length!==1)throw Error("One DAV property status required");let a=i["D:propstat"][0];if(s(a,["D:status","D:prop"]),a["D:status"]!=="HTTP/1.1 200 OK")throw Error("DAV property names unavailable");let f=a["D:prop"];if(f!==""&&(!f||typeof f!="object"||Array.isArray(f)||Object.keys(f).length>1e3||Object.entries(f).some(([p,l])=>!/^[DSPC]:[^:]+$/.test(p)||l!=="")))throw Error("DAV property names must be empty");let c=f===""?[]:Object.keys(f);return{names:c,special:c.includes("P:special")}}var Le=r=>r.replaceAll("&","&amp;").replaceAll("<","&lt;").replaceAll(">","&gt;").replaceAll('"',"&quot;");function de({request:r}={}){if(typeof r!="function")throw Error("Bounded DAV request required");return async(t,{identity:o,assertAuthorized:e,signal:s}={})=>{let n=C(t,o),i=n.args[0],a=fe({request:r,assertAuthorized:e}),f=await a(o,{revision:n.pegRevision,signal:s}),c=async(y,u)=>{s?.throwIfAborted(),await e(),s?.throwIfAborted();let d=await r(y,{origin:new URL(o.root).origin,...u,signal:s});if(s?.throwIfAborted(),await e(),s?.throwIfAborted(),!Buffer.isBuffer(d)||d.length>2097152)throw Error("Invalid DAV transport bytes");return d};if(i==="log"){let y=ae(t,o);return c(f.collection,{method:"REPORT",body:y.body})}let p=n.path.slice(1).split("/").map(y=>encodeURIComponent(y).replace(/[!'()*]/g,u=>"%"+u.charCodeAt(0).toString(16).toUpperCase())).join("/"),l=L(f.collection+p,{root:o.root}),m=await c(l,{method:"PROPFIND",depth:0,body:'<D:propfind xmlns:D="DAV:"><D:propname/></D:propfind>'}),g=le(new TextDecoder("utf-8",{fatal:!0}).decode(m),{root:o.root,target:l});if(i==="proplist")return'<properties><target path="'+Le(n.args.at(-1).slice(0,n.args.at(-1).lastIndexOf("@")))+'">'+(g.special?'<property name="svn:special"/>':"")+"</target></properties>";if(g.special)throw Error("SVN special content forbidden");return c(l,{method:"GET"})}}function gt(r,t){return se(r,t,{transportFormat:"dav",transport:de({request:ie()})})}export{gt as createSvnHttpsHost};
package/docs/RELEASING.md CHANGED
@@ -1,65 +1,52 @@
1
- # 发布清单
1
+ # 维护者发布流程
2
2
 
3
- 当前包名 @feng0531/dsh-vcs;正式发布统一走 GitHub Actions,不在本地 npm publish。公开下载不改变非商业许可证。
3
+ 本文用于维护项目。使用者请阅读[README](<../README.md>)和[安装指南](https://github.com/f-e-n-g-0531/dsh-vcs/blob/main/docs/INSTALL.md)。
4
4
 
5
- ## GitHub + npm 联合发布
5
+ ## 发布内容
6
6
 
7
- 在 GitHub 仓库 Settings → Secrets and variables → Actions 添加 NPM_AUTOMATION_TOKEN。该令牌必须有 @feng0531/dsh-vcs 发布权限,并满足 npm 的 2FA 策略(需要时启用 bypass 2FA);名字叫 AUTOMATION_TOKEN 不会自动赋予权限。不要在聊天、源码或日志中公开值。
7
+ 每个新版本必须有独立的中文发布说明,放在 `docs/releases/版本号.md`。按“本版变化、安装、限制、验证”组织,不把自动提交列表、开发轮次或内部恢复记录当作用户说明。没有新增功能时明确写出,不夸大改动。
8
8
 
9
- 对应标签 CI 全绿及人工验收后,从 main 手动运行 Publish GitHub and npm 工作流,填写已有版本标签。构建、验证和打包步骤不注入 npm 凭据;仅 npm 发布步骤使用 NODE_AUTH_TOKEN: ${{ secrets.NPM_AUTOMATION_TOKEN }},且禁用生命周期脚本。同一 tgz 先发布 npm,再作为 GitHub 正式 Release 附件上传并附 SHA-256。
9
+ 更新包版本和锁文件,在变更记录中填写同一版本。核查许可证、第三方声明及公开文件,排除凭据和私人数据。
10
10
 
11
- 两平台不支持原子事务:npm 失败则不发布 GitHub;npm 成功而 GitHub 失败时,重跑仅在已发布 npm integrity 与当前包完全一致时继续,否则拒绝覆盖。GitHub 已存在 Release 时也拒绝覆盖,需先检查现场。新版本不移动旧标签。
11
+ ## 1. 本地检查
12
12
 
13
- ## 1. 公开发布前的决定
13
+ 需要 Node.js 22+、Git、SVN、svnadmin。在干净检出中执行:
14
14
 
15
- - [x] 仓库地址已确认:https://github.com/f-e-n-g-0531/dsh-vcs 。
16
- - [x] 正式采用 [非商业源码公开许可证 1.0](<../LICENSE>),禁止商用,允许非商业修改和分发。
17
- - [ ] 每次发布检查源码权属及第三方声明;商业另行授权须覆盖全部有关权利人。
18
- - [ ] 确认安全报告渠道、维护责任与支持范围。
19
- - [ ] 审查待公开文件和 Git 历史,排除个人路径、令牌、用户数据、私人记录和敏感图片。
15
+ npm ci --include=dev
16
+ npm run check
17
+ npm run verify:package
18
+ npm run test:install
20
19
 
21
- ## 2. 干净环境验证
20
+ 测试失败或跳过须调查;隔离安装只检查包和宿主注册,不是用户实际 DSH 验收。
22
21
 
23
- 使用 Node.js 22 或更新版本,在干净检出目录中执行:
22
+ ## 2. 固定版本并验证
24
23
 
25
- npm ci && npm run check
26
- npm run verify:package
27
- npm run test:install
28
- npm pack --dry-run
29
- npm pack
24
+ 1. 提交并推送版本修改,创建新标签 `vX.Y.Z`;不移动已有标签。
25
+ 2. 等待标签 CI 的 Windows/Ubuntu × Node 22/24 全部成功。
26
+ 3. 手动在同一标签运行 **Browser layout checks**,等待成功。
27
+ 4. 下载四个 CI 候选 tgz,确认字节大小和 SHA-256 全部一致。
30
28
 
31
- 确认锁文件安装、构建和测试成功,记录失败与跳过项。完整 Git / SVN 测试需要 git、svn、svnadmin。npm pack 的 prepack 会执行 check;审查包内容是否包含客户端、编辑器、CSS、worker、宿主入口、适配器、语言文件与插件 patch。
29
+ 分支检查不能代替同标签检查。失败后先定位;需要修改源码或测试时使用新的版本和标签。
32
30
 
33
- 构建会复制 Monaco 的许可证与第三方声明进入分发资源;仍应核查最终包中的声明及其他依赖义务。排除测试数据、依赖目录与私人材料。
31
+ ## 3. 联合发布
34
32
 
35
- Git 来源安装依赖 prepare 构建,需要另行验证生命周期和构建依赖。pnpm 可能需要 allowBuilds 审批;拒绝脚本的环境应优先使用已构建 tgz,不应直接关闭安全策略。
33
+ 在 Actions 从 main 运行 **Publish GitHub and npm**,输入已验证的标签。
36
34
 
37
- ## 3. 安装与真实页面验收
35
+ 工作流检出该标签,重新检查并打包。它先上传 npm,再验证公开 npm 元数据与实际下载包,最后创建 GitHub 正式 Release,上传同一个 tgz 和 SHA-256 校验文件。
38
36
 
39
- dsh plugin --profile web add <absolute-tgz>
37
+ 仓库需配置 `NPM_AUTOMATION_TOKEN`,具有该包发布权限并符合 npm 2FA 策略。凭据只用于上传步骤;不要写进源码、发布说明或聊天。
40
38
 
41
- 替换为包的绝对路径,必要时加引号。先确认目标 profile 和宿主兼容性;安装改变运行配置,不是无副作用检查。
39
+ ## 4. 发布后核验
42
40
 
43
- **宿主重启只能由用户执行。** 用户重启后刷新现有 DSH Web 地址,在真实 Session 中验证:
41
+ - npm 版本与 latest 正确,实际 tarball 可下载。
42
+ - GitHub Release 非草稿、非预发布,附件可下载。
43
+ - 两端实际包与四候选的字节大小和 SHA-256 一致。
44
+ - 补记最终结论,但不修改已发布标签或替换工件。
44
45
 
45
- - [ ] 插件入口可见;顶部单仓库下拉,无额外仓库左栏。
46
- - [ ] 当前 / 上级仓库、一层子目录发现,以及明确相对子目录的定向发现。
47
- - [ ] Git 三模式、SVN BASE 与属性;新增、删除、重命名、未跟踪和冲突提示。
48
- - [ ] UTF-8、GBK、带 BOM 的 UTF-16;二进制与超大文件提示。
49
- - [ ] 并排 / 行内、空白选项、换行、导航、主题和编辑器加载失败降级。
50
- - [ ] Session / 仓库切换与重新发现不展示过期内容;预览不修改工作区或暂存区。
51
- - [ ] 发现授权过期后的手动重新发现,以及取消请求的实际反馈。
46
+ npm 与 GitHub 不是原子发布。若 npm 已接受上传但公开下载暂时 404,**不要再次上传**;先只读确认公开包。确认相同后恢复原失败任务,工作流会核对已有 npm integrity 并跳过上传。不匹配则停止;已有 GitHub Release 不覆盖。
52
47
 
53
- 记录宿主和浏览器版本、步骤及已知限制。自动化测试通过不等于这些项目已完成;未执行时明确写“未验收”,不补写成功结论。
48
+ ## 实机验证与安装边界
54
49
 
55
- ## 4. 标签 CI 与发布工作流
50
+ 自动浏览器测试不等于当前 DSH 实机通过。发布说明须如实写出未确认项;用户明确授权在此边界下发布时,不伪造实机结论。
56
51
 
57
- 普通分支 push、PR 和日常脚本修改不触发 CI;只有维护者主动推送版本标签(`vX.Y.Z`)才触发 Windows/Linux 测试矩阵。完成本地检查后,确认包版本并执行 `git tag vX.Y.Z`、`git push origin vX.Y.Z`(替换成真实版本,标签须指向包含新 CI 规则的提交)。不要移动已经发布的标签。等待该标签的 CI 全部通过并完成真实页面验收后再进行发布。在 Actions 手动运行 Release workflow 并填写已有 tag:工作流验证版本、重新构建与测试、检查包并生成 SHA256SUMS.txt,随后先发布 npm 再公开 GitHub Release。手动触发即授权两平台正式发布。未通过的真实页面验收不得勾选完成。
58
-
59
- CI 会严格校验标签为 `vX.Y.Z` 且与包版本相同。联合发布会检查同一标签、同一提交的最新 CI 必须成功;未运行、失败或尚在运行时拒绝创建草稿。这不替代真实 DSH 页面人工验收。
60
-
61
- ## 5. 分发
62
-
63
- 确认完整附带许可证及第三方声明,不把本项目标记为标准开源软件。在已确认的仓库中按实际验证结果编写发布说明,附审核过的 tgz 和校验值,注明兼容环境与未验证事项。是否打 tag / 创建 GitHub Release 由维护者决定;不为本次整理虚构日期、历史版本或功能变更。
64
-
65
- 相关约定见 [README](<../README.md>)、[贡献指南](<../CONTRIBUTING.md>)、[变更记录](<../CHANGELOG.md>) 和 [安全说明](<../SECURITY.md>)。
52
+ 发布不安装、不升级、不重启运行中的 DSH。用户选择安装后,在原 Web 页面检查入口、仓库发现、差异、历史以及 Session 切换;由用户自行重启。维护者不能启动替代服务器冒充当前页面。
package/git-history.mjs CHANGED
@@ -1,18 +1,31 @@
1
1
  // Search values are literal, bounded and never revision/options or pathspec expressions.
2
- export function historySearchArgs(search){
3
- if(search===undefined)return {args:[],paths:[]};
4
- if(!search||typeof search!=='object'||Array.isArray(search)||Object.keys(search).some(k=>!['message','author','path'].includes(k)))throw new Error('Invalid history search');
5
- const args=['--fixed-strings','--regexp-ignore-case'],paths=[];
6
- for(const key of ['message','author','path']){
7
- const value=search[key];if(value===undefined)continue;
8
- if(typeof value!=='string'||value.length>1024||/[\0\r\n]/.test(value))throw new Error('Invalid history search value');
9
- if(!value)continue;
10
- if(key==='path'){
11
- if(value.startsWith('/')||value.includes('\\')||value.split('/').some(p=>!p||p==='.'||p==='..'))throw new Error('Invalid history search path');
12
- paths.push(value);
13
- }else args.push((key==='message'?'--grep=':'--author=')+value);
14
- }
15
- return {args,paths};
2
+ export function historySearchArgs(search) {
3
+ if (search === undefined) return {args: [], paths: []};
4
+ const keys = ['message', 'author', 'path'];
5
+ if (!search || typeof search !== 'object' || Array.isArray(search) ||
6
+ Object.keys(search).some(key => !keys.includes(key))) {
7
+ throw new Error('Invalid history search');
8
+ }
9
+ const args = ['--fixed-strings', '--regexp-ignore-case'];
10
+ const paths = [];
11
+ for (const key of keys) {
12
+ const value = search[key];
13
+ if (value === undefined) continue;
14
+ if (typeof value !== 'string' || value.length > 1024 || /[\0\r\n]/.test(value)) {
15
+ throw new Error('Invalid history search value');
16
+ }
17
+ if (!value) continue;
18
+ if (key === 'path') {
19
+ if (value.startsWith('/') || value.includes('\\') ||
20
+ value.split('/').some(part => !part || part === '.' || part === '..')) {
21
+ throw new Error('Invalid history search path');
22
+ }
23
+ paths.push(value);
24
+ } else {
25
+ args.push((key === 'message' ? '--grep=' : '--author=') + value);
26
+ }
27
+ }
28
+ return {args, paths};
16
29
  }
17
30
  // Each line-porcelain record carries its own metadata; never interpret filename as an access path.
18
31
  export function decodeBlamePath(value){
package/image-preview.mjs CHANGED
@@ -1,22 +1,53 @@
1
1
  // JPEG gets a bounded structural gate; display additionally requires browser decode.
2
- export async function prepareRaster(buffer,{signal}={}){
3
- signal?.throwIfAborted();
4
- if(Buffer.isBuffer(buffer)&&buffer[0]===255&&buffer[1]===216){const result=prepareBaselineJpeg(buffer);signal?.throwIfAborted();return result;}
5
- if(Buffer.isBuffer(buffer)&&buffer.toString('ascii',0,4)==='RIFF'){const result=prepareSimpleWebp(buffer);signal?.throwIfAborted();return result;}
6
- return preparePng(buffer,{signal});
2
+ export async function prepareRaster(buffer, {signal} = {}) {
3
+ signal?.throwIfAborted();
4
+ if (Buffer.isBuffer(buffer) && buffer[0] === 255 && buffer[1] === 216) {
5
+ const result = prepareBaselineJpeg(buffer);
6
+ signal?.throwIfAborted();
7
+ return result;
8
+ }
9
+ if (Buffer.isBuffer(buffer) && buffer.toString('ascii', 0, 4) === 'RIFF') {
10
+ const result = prepareSimpleWebp(buffer);
11
+ signal?.throwIfAborted();
12
+ return result;
13
+ }
14
+ return preparePng(buffer, {signal});
7
15
  }
16
+
8
17
  // Simple static WebP only; extended/animated containers remain rejected.
9
- export function prepareSimpleWebp(buffer){
10
- if(!Buffer.isBuffer(buffer)||buffer.length<26||buffer.length>2097152||buffer.toString('ascii',0,4)!=='RIFF'||buffer.toString('ascii',8,12)!=='WEBP'||buffer.readUInt32LE(4)!==buffer.length-8)throw Error('Invalid WebP RIFF or byte limit');
11
- const kind=buffer.toString('ascii',12,16),size=buffer.readUInt32LE(16),end=20+size;if(end+(size&1)!==buffer.length||(size&1&&buffer[end]!==0))throw Error('Invalid WebP single chunk length or padding');
12
- let width,height;
13
- if(kind==='VP8 '){
14
- if(size<10)throw Error('Truncated WebP VP8 header');const tag=buffer.readUIntLE(20,3),partition=tag>>>5;
15
- if((tag&1)||((tag>>>1)&7)>3||!(tag&16)||!partition||partition>size-3||!buffer.subarray(23,26).equals(Buffer.from([157,1,42])))throw Error('Unsupported WebP VP8 frame');
16
- const w=buffer.readUInt16LE(26),h=buffer.readUInt16LE(28);if(w>>>14||h>>>14)throw Error('WebP scaled frame unsupported');width=w&16383;height=h&16383;
17
- }else throw Error('Only simple static WebP is supported');
18
- if(!width||!height||width>8192||height>8192||width*height>16000000)throw Error('WebP dimensions exceed limit');
19
- return {mime:'image/webp',width,height,bytes:buffer.length,originalBytes:buffer.length,data:Buffer.from(buffer),metadataStripped:false};
18
+ export function prepareSimpleWebp(buffer) {
19
+ if (!Buffer.isBuffer(buffer) || buffer.length < 26 || buffer.length > 2097152 ||
20
+ buffer.toString('ascii', 0, 4) !== 'RIFF' || buffer.toString('ascii', 8, 12) !== 'WEBP' ||
21
+ buffer.readUInt32LE(4) !== buffer.length - 8) {
22
+ throw Error('Invalid WebP RIFF or byte limit');
23
+ }
24
+ const kind = buffer.toString('ascii', 12, 16);
25
+ const size = buffer.readUInt32LE(16);
26
+ const end = 20 + size;
27
+ if (end + (size & 1) !== buffer.length || (size & 1 && buffer[end] !== 0)) {
28
+ throw Error('Invalid WebP single chunk length or padding');
29
+ }
30
+ let width, height;
31
+ if (kind === 'VP8 ') {
32
+ if (size < 10) throw Error('Truncated WebP VP8 header');
33
+ const tag = buffer.readUIntLE(20, 3);
34
+ const partition = tag >>> 5;
35
+ if ((tag & 1) || ((tag >>> 1) & 7) > 3 || !(tag & 16) || !partition ||
36
+ partition > size - 3 || !buffer.subarray(23, 26).equals(Buffer.from([157, 1, 42]))) {
37
+ throw Error('Unsupported WebP VP8 frame');
38
+ }
39
+ const w = buffer.readUInt16LE(26), h = buffer.readUInt16LE(28);
40
+ if (w >>> 14 || h >>> 14) throw Error('WebP scaled frame unsupported');
41
+ width = w & 16383;
42
+ height = h & 16383;
43
+ } else {
44
+ throw Error('Only simple static WebP is supported');
45
+ }
46
+ if (!width || !height || width > 8192 || height > 8192 || width * height > 16000000) {
47
+ throw Error('WebP dimensions exceed limit');
48
+ }
49
+ return {mime: 'image/webp', width, height, bytes: buffer.length, originalBytes: buffer.length,
50
+ data: Buffer.from(buffer), metadataStripped: false};
20
51
  }
21
52
  export function prepareBaselineJpeg(buffer){
22
53
  if(!Buffer.isBuffer(buffer)||buffer.length<4||buffer.length>2097152||buffer[0]!==255||buffer[1]!==216)throw Error('Invalid JPEG input or byte limit');
package/index.mjs CHANGED
@@ -24,9 +24,31 @@ export async function resolveSessionCwd(ctx, sessionId, signal) {
24
24
  const rediscover = () => Object.assign(new Error('Repository authorization is missing, expired, or changed. Rediscover repositories for this Session and retry.'), { code: 'vcs/rediscover-required' });
25
25
 
26
26
  import {historySearchArgs} from './git-history.mjs';
27
+
28
+ // Exact request fields are a protocol contract, independent of dispatch.
29
+ const REQUEST_FIELDS = {
30
+ 'vcs/repositories': ['sessionId', 'subdirectory'],
31
+ 'vcs/svn-identity': ['sessionId', 'repositoryId'],
32
+ 'vcs/references': ['sessionId', 'repositoryId'],
33
+ 'vcs/status': ['sessionId', 'repositoryId', 'mode'],
34
+ 'vcs/compare': ['sessionId', 'repositoryId', 'mode', 'id'],
35
+ 'vcs/history': ['sessionId', 'repositoryId', 'snapshot', 'offset', 'limit', 'search'],
36
+ 'vcs/commit': ['sessionId', 'repositoryId', 'commit', 'parentIndex'],
37
+ 'vcs/commit-compare': ['sessionId', 'repositoryId', 'commit', 'parentIndex', 'id'],
38
+ 'vcs/file-history': ['sessionId', 'repositoryId', 'commit', 'parentIndex', 'id', 'offset', 'limit', 'follow'],
39
+ 'vcs/blame': ['sessionId', 'repositoryId', 'commit', 'parentIndex', 'id', 'startLine', 'lineLimit'],
40
+ 'vcs/revision-changes': ['sessionId', 'repositoryId', 'base', 'target'],
41
+ 'vcs/revision-compare': ['sessionId', 'repositoryId', 'base', 'target', 'id'],
42
+ 'vcs/revision-image': ['sessionId', 'repositoryId', 'base', 'target', 'id', 'side'],
43
+ 'vcs/tree': ['sessionId', 'repositoryId', 'commit'],
44
+ 'vcs/tree-file': ['sessionId', 'repositoryId', 'commit', 'path'],
45
+ 'vcs/tree-segment': ['sessionId', 'repositoryId', 'commit', 'path', 'offset'],
46
+ 'vcs/commit-image': ['sessionId', 'repositoryId', 'commit', 'parentIndex', 'id', 'side'],
47
+ 'vcs/workspace-image': ['sessionId', 'repositoryId', 'mode', 'id', 'side'],
48
+ };
27
49
  function validatePayload(endpoint, payload) {
28
50
  if (!payload || typeof payload !== 'object' || Array.isArray(payload)) throw invalid('An object payload is required.');
29
- const fields = endpoint==='vcs/svn-identity'?['sessionId','repositoryId']:endpoint==='vcs/tree-segment'?['sessionId','repositoryId','commit','path','offset']:endpoint==='vcs/workspace-image'?['sessionId','repositoryId','mode','id','side']:endpoint==='vcs/revision-image'?['sessionId','repositoryId','base','target','id','side'] : endpoint==='vcs/tree-file'?['sessionId','repositoryId','commit','path'] : endpoint==='vcs/references'?['sessionId','repositoryId'] : endpoint==='vcs/commit-image'?['sessionId','repositoryId','commit','parentIndex','id','side'] : endpoint==='vcs/tree'?['sessionId','repositoryId','commit'] : endpoint==='vcs/file-history'?['sessionId','repositoryId','commit','parentIndex','id','offset','limit','follow'] : ['vcs/revision-changes','vcs/revision-compare','vcs/revision-image'].includes(endpoint) ? ['sessionId','repositoryId','base','target',...(endpoint==='vcs/revision-compare'?['id']:[])] : ['vcs/commit-compare','vcs/blame'].includes(endpoint) ? ['sessionId','repositoryId','commit','parentIndex','id',...(endpoint==='vcs/blame'?['startLine','lineLimit']:[])] : endpoint === 'vcs/commit' ? ['sessionId','repositoryId','commit','parentIndex'] : endpoint === 'vcs/history' ? ['sessionId','repositoryId','snapshot','offset','limit','search'] : endpoint === 'vcs/repositories' ? ['sessionId', 'subdirectory'] : ['sessionId', 'repositoryId', 'mode', ...(endpoint === 'vcs/compare' ? ['id'] : [])];
51
+ const fields = REQUEST_FIELDS[endpoint];
30
52
  if (Object.keys(payload).some(key => !fields.includes(key))) throw invalid('Only Session-addressed repository requests are supported; unknown payload field.');
31
53
  if (endpoint === 'vcs/repositories') {
32
54
  if (payload.subdirectory !== undefined) {
@@ -61,6 +83,48 @@ function validatePayload(endpoint, payload) {
61
83
  }
62
84
  }
63
85
 
86
+ /** Dispatch only after grants and capacity have been checked by createHandler. */
87
+ async function readRepository(api, endpoint, repository, payload, {cwd, mode, signal}) {
88
+ switch (endpoint) {
89
+ case 'vcs/tree-segment':
90
+ return api.getHistoricalSegment({...repository}, {commit: payload.commit, path: payload.path, offset: payload.offset ?? 0, signal});
91
+ case 'vcs/workspace-image':
92
+ return api.getWorkspaceImage({...repository}, {mode, id: payload.id, side: payload.side ?? 'right', signal});
93
+ case 'vcs/revision-image':
94
+ return api.getRevisionImage({...repository}, {base: payload.base, target: payload.target, id: payload.id, side: payload.side ?? 'right', signal});
95
+ case 'vcs/references':
96
+ return api.listReferences({...repository}, {signal});
97
+ case 'vcs/commit-image':
98
+ return api.getCommitImage({...repository}, {commit: payload.commit, parentIndex: payload.parentIndex ?? 0, id: payload.id, side: payload.side ?? 'right', signal});
99
+ case 'vcs/tree-file':
100
+ return api.getHistoricalFile({...repository}, {commit: payload.commit, path: payload.path, signal});
101
+ case 'vcs/tree':
102
+ return api.getHistoricalTree({...repository}, {commit: payload.commit, signal});
103
+ case 'vcs/blame':
104
+ return api.getFileBlame({...repository}, {commit: payload.commit, parentIndex: payload.parentIndex ?? 0, id: payload.id,
105
+ ...(payload.startLine !== undefined ? {startLine: payload.startLine} : {}),
106
+ ...(payload.lineLimit !== undefined ? {lineLimit: payload.lineLimit} : {}), signal});
107
+ case 'vcs/file-history':
108
+ return api.listFileHistory({...repository}, {commit: payload.commit, parentIndex: payload.parentIndex ?? 0, id: payload.id,
109
+ offset: payload.offset ?? 0, limit: payload.limit ?? 50,
110
+ ...(payload.follow !== undefined ? {follow: payload.follow} : {}), signal});
111
+ case 'vcs/revision-changes':
112
+ return api.getRevisionChanges({...repository}, {base: payload.base, target: payload.target, signal});
113
+ case 'vcs/revision-compare':
114
+ return api.getRevisionComparison({...repository}, {base: payload.base, target: payload.target, id: payload.id, signal});
115
+ case 'vcs/commit-compare':
116
+ return api.getCommitComparison({...repository}, {commit: payload.commit, parentIndex: payload.parentIndex ?? 0, id: payload.id, signal});
117
+ case 'vcs/commit':
118
+ return api.getCommitDetails({...repository}, {commit: payload.commit, parentIndex: payload.parentIndex ?? 0, signal});
119
+ case 'vcs/history':
120
+ return api.listHistory({...repository}, {snapshot: payload.snapshot, search: payload.search, offset: payload.offset ?? 0, limit: payload.limit ?? 50, signal});
121
+ case 'vcs/status':
122
+ return {cwd, repository: {...repository}, changes: await api.listChanges({...repository}, mode), mode};
123
+ case 'vcs/compare':
124
+ return api.getComparison({...repository}, {mode, id: payload.id});
125
+ }
126
+ }
127
+
64
128
  /** Limit active adapter operations and retain only bounded, short-lived discovery grants. */
65
129
  export function createHandler(ctx, api = adapter, maxActive = 4, { now = Date.now, ttlMs = 5 * 60_000, maxSessions = 32, maxRepositories = 512, bindSvnIdentityResolver } = {}) {
66
130
  let active = 0, activeImages = 0, activeSegments = 0;
@@ -135,37 +199,7 @@ export function createHandler(ctx, api = adapter, maxActive = 4, { now = Date.no
135
199
  if(activeImages>=2)return failure('vcs/busy','Too many image requests. Please retry.');
136
200
  activeImages++;imageSlot=true;
137
201
  }
138
- const value = endpoint === 'vcs/tree-segment'
139
- ? await api.getHistoricalSegment({...repository},{commit:payload.commit,path:payload.path,offset:payload.offset??0,signal})
140
- : endpoint === 'vcs/workspace-image'
141
- ? await api.getWorkspaceImage({...repository},{mode,id:payload.id,side:payload.side??'right',signal})
142
- : endpoint === 'vcs/revision-image'
143
- ? await api.getRevisionImage({...repository},{base:payload.base,target:payload.target,id:payload.id,side:payload.side??'right',signal})
144
- : endpoint === 'vcs/references'
145
- ? await api.listReferences({...repository},{signal})
146
- : endpoint === 'vcs/commit-image'
147
- ? await api.getCommitImage({...repository},{commit:payload.commit,parentIndex:payload.parentIndex??0,id:payload.id,side:payload.side??'right',signal})
148
- : endpoint === 'vcs/tree-file'
149
- ? await api.getHistoricalFile({...repository},{commit:payload.commit,path:payload.path,signal})
150
- : endpoint === 'vcs/tree'
151
- ? await api.getHistoricalTree({...repository},{commit:payload.commit,signal})
152
- : endpoint === 'vcs/blame'
153
- ? await api.getFileBlame({...repository},{commit:payload.commit,parentIndex:payload.parentIndex??0,id:payload.id,...(payload.startLine!==undefined?{startLine:payload.startLine}:{}),...(payload.lineLimit!==undefined?{lineLimit:payload.lineLimit}:{}),signal})
154
- : endpoint === 'vcs/file-history'
155
- ? await api.listFileHistory({...repository},{commit:payload.commit,parentIndex:payload.parentIndex??0,id:payload.id,offset:payload.offset??0,limit:payload.limit??50,...(payload.follow!==undefined?{follow:payload.follow}:{}),signal})
156
- : endpoint === 'vcs/revision-changes'
157
- ? await api.getRevisionChanges({...repository},{base:payload.base,target:payload.target,signal})
158
- : endpoint === 'vcs/revision-compare'
159
- ? await api.getRevisionComparison({...repository},{base:payload.base,target:payload.target,id:payload.id,signal})
160
- : endpoint === 'vcs/commit-compare'
161
- ? await api.getCommitComparison({...repository},{commit:payload.commit,parentIndex:payload.parentIndex??0,id:payload.id,signal})
162
- : endpoint === 'vcs/commit'
163
- ? await api.getCommitDetails({...repository},{commit:payload.commit,parentIndex:payload.parentIndex??0,signal})
164
- : endpoint === 'vcs/history'
165
- ? await api.listHistory({...repository},{snapshot:payload.snapshot,search:payload.search,offset:payload.offset??0,limit:payload.limit??50,signal})
166
- : endpoint === 'vcs/status'
167
- ? { cwd, repository: { ...repository }, changes: await api.listChanges({ ...repository }, mode), mode }
168
- : await api.getComparison({ ...repository }, { mode, id: payload.id });
202
+ const value = await readRepository(api, endpoint, repository, payload, {cwd, mode, signal});
169
203
  await assertCurrent(payload.sessionId, cwd, entry, signal);
170
204
  if (entry.repositories.get(payload.repositoryId) !== repository) throw rediscover();
171
205
  return { ok: true, value };
@@ -0,0 +1,47 @@
1
+ import {spawn} from 'node:child_process';
2
+
3
+ const MAX_OUTPUT = 16 * 1024 * 1024;
4
+ export const TIMEOUT = 15000;
5
+
6
+ // Local adapter primitive, not the remote SVN transport. Preserve its error contract.
7
+ export function run(command, args, cwd, max = MAX_OUTPUT, { signal, timeoutMs = TIMEOUT } = {}) {
8
+ return new Promise((resolve, reject) => {
9
+ if (signal?.aborted) { reject(Object.assign(new Error('Discovery cancelled'), { code: 'ABORT_ERR' })); return; }
10
+ // Inherited Git overrides must not redirect reads to a different repository.
11
+ const env = Object.fromEntries(Object.entries(process.env).filter(([key]) => !key.toUpperCase().startsWith('GIT_')));
12
+ Object.assign(env, { GIT_OPTIONAL_LOCKS: '0', GIT_TERMINAL_PROMPT: '0', GIT_NO_REPLACE_OBJECTS: '1', GIT_NO_LAZY_FETCH: '1', GIT_ALLOW_PROTOCOL: '', LC_ALL: process.platform === 'linux' ? 'C.UTF-8' : 'en_US.UTF-8' });
13
+ const child = spawn(command, args, { cwd, shell: false, windowsHide: true, stdio: ['ignore', 'pipe', 'pipe'], env });
14
+ const out = [], err = [];
15
+ let size = 0, error, done = false;
16
+ const stop = failure => {
17
+ error ??= failure;
18
+ child.kill();
19
+ };
20
+ const abort = () => stop(Object.assign(new Error('Discovery cancelled'), { code: 'ABORT_ERR' }));
21
+ signal?.addEventListener('abort', abort, { once: true });
22
+ const timer = setTimeout(() => stop(Object.assign(new Error(command + ' timed out'), { code: 'TIMEOUT' })), timeoutMs);
23
+ // Both streams consume the same budget; never expose partial success.
24
+ const collect = chunks => bytes => {
25
+ size += bytes.length;
26
+ if (size > max) {
27
+ stop(Object.assign(new Error('Output exceeds size limit'), {code: 'TOO_LARGE'}));
28
+ } else {
29
+ chunks.push(bytes);
30
+ }
31
+ };
32
+ child.stdout.on('data', collect(out));
33
+ child.stderr.on('data', collect(err));
34
+ const finish = failure => {
35
+ if (done) return;
36
+ done = true;
37
+ clearTimeout(timer);
38
+ signal?.removeEventListener('abort', abort);
39
+ if (failure) reject(failure);
40
+ else resolve(Buffer.concat(out));
41
+ };
42
+ child.on('error', finish);
43
+ child.on('close', code => finish(error || (code !== 0 ? Object.assign(new Error(command + ': ' + Buffer.concat(err).toString('utf8').trim()), { code: 'VCS_COMMAND', exitCode: code }) : null)));
44
+ });
45
+ }
46
+ export const git = (root, args, max, options) => run('git', ['--no-pager', '--literal-pathspecs', '-c', 'core.fsmonitor=false', ...args], root, max, options);
47
+ export const svn = (root, args, max, options) => run('svn', ['--non-interactive', ...args], root, max, options);