@feng0531/dsh-vcs 0.4.10 → 0.5.1
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/README.md +8 -5
- package/dist/client.js +3 -3
- package/dist/editor.js +59 -59
- package/dist/frontend.json +1 -1
- package/dist/interface.js +3 -3
- package/dist/svn-host.mjs +1 -1
- package/docs/RELEASING.md +41 -9
- package/index.mjs +33 -9
- package/local-command.mjs +58 -25
- package/package.json +11 -3
- package/src/commit-signature.mjs +26 -13
- package/src/historical-access.mjs +139 -0
- package/src/operation-budget.mjs +44 -0
- package/src/preview-capabilities.mjs +19 -0
- package/vcs.mjs +140 -91
- package/workspace-large-text.mjs +4 -3
package/dist/svn-host.mjs
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import{createHandler as Ge}from"../index.mjs";import{createHash as $e}from"node:crypto";var Le=9223372036854775807n;function w(e){if(typeof e!="string"||e.length>19||!/^(0|[1-9][0-9]*)$/.test(e))throw new Error("Invalid SVN revision");let o=BigInt(e);if(o>Le)throw new Error("SVN revision exceeds supported range");return o}function q(e){let o=w(e);return o===0n?null:String(o-1n)}function H(e,{snapshot:o,cursor:r=o,limit:t=50}={}){let s=w(o),n=w(r);if(n>s||!Number.isInteger(t)||t<1||t>100||!Array.isArray(e)||e.length>t+1)throw new Error("Invalid SVN page bounds");let a=n+1n;for(let p of e){let f=w(p);if(f>n||f>=a)throw new Error("SVN revisions must descend within cursor");a=f}let i=e.slice(0,t);return{snapshot:o,revisions:i,nextRevision:e.length>t?q(i.at(-1)):null}}function j(e){if(typeof e!="string"||e.length>32768||!e.startsWith("/")||/[\u0000-\u001f\u007f\\]/.test(e))throw new Error("Invalid SVN repository path");if(e!=="/"&&e.slice(1).split("/").some(o=>o===""||o==="."||o===".."))throw new Error("Noncanonical SVN repository path");return e}function C(e,o){return j(e),j(o),o==="/"||e===o||e.startsWith(o+"/")}function O(e,o,r){if(j(o),w(r),typeof e!="string"||e.length>32768||/[\x00-\x20\x7f\\?#]/.test(e))throw new Error("Invalid SVN HTTPS root");let t=new URL(e);if(t.protocol!=="https:"||t.username||t.password||t.href!==e)throw new Error("Noncanonical SVN HTTPS root");let s=t.pathname.split("/").slice(1);s.at(-1)===""&&s.pop();for(let p of s){let f=decodeURIComponent(p);if(!p||f==="."||f===".."||/[\x00-\x1f\x7f/\\]/.test(f))throw new Error("Invalid SVN root path")}let n=e.endsWith("/")?e.slice(0,-1):e,a=o==="/"?"":o.split("/").map(p=>encodeURIComponent(p).replace(/[!'()*]/g,f=>"%"+f.charCodeAt(0).toString(16).toUpperCase())).join("/"),i=n+(a||(t.pathname==="/"?"/":""))+"@"+r;if(i.length>32768)throw new Error("SVN target too long");return i}function F({root:e,scope:o,path:r=o,snapshot:t,revision:s}={}){if(w(s)>w(t))throw new Error("SVN selected revision exceeds snapshot");if(!C(r,o))throw new Error("SVN detail path outside scope");let n=O(e,r,t);return{scope:o,revision:s,snapshot:t,args:["log","--xml","--verbose","--stop-on-copy","--non-interactive","--no-auth-cache","--limit","1","-r",s+":"+s,"--",n]}}function $({root:e,scope:o,path:r=o,snapshot:t,cursor:s=t,limit:n=50}={}){if(H([],{snapshot:t,cursor:s,limit:n}),!C(r,o))throw new Error("SVN history path outside scope");let a=O(e,r,t);return{snapshot:t,cursor:s,limit:n,args:["log","--xml","--stop-on-copy","--non-interactive","--no-auth-cache","--limit",String(n+1),"-r",s+":0","--",a]}}function M(e,o){let r=e?.args;if(!Array.isArray(r)||r.length>16||r.some(l=>typeof l!="string"))throw Error("Invalid SVN command");let t=r.at(-1),s=t.lastIndexOf("@"),n=t.slice(s+1);w(n);let a=o.root.endsWith("/")?o.root.slice(0,-1):o.root;O(o.root,"/",n);let i=t.slice(0,s);if(!(i===a||i.startsWith(a+"/")))throw Error("SVN command target outside root");let p=i.slice(a.length),f=p?"/"+p.slice(1).split("/").map(l=>decodeURIComponent(l)).join("/"):"/";if(!C(f,o.scope)||O(o.root,f,n)!==t)throw Error("SVN command target outside scope or noncanonical");let c;if(r[0]==="cat"||r[0]==="proplist"||r[0]==="info"){w(r[r[0]==="cat"?4:5]);let l=r[r[0]==="cat"?4:5];if(l!==n)throw Error("SVN content operative and peg mismatch");c=r[0]==="cat"?["cat","--non-interactive","--no-auth-cache","-r",l,"--",t]:[r[0],"--xml","--non-interactive","--no-auth-cache","-r",l,"--",t]}else if(r[0]==="log")if(r.includes("--verbose")){let l=r[9]?.split(":")[0];c=F({...o,path:f,snapshot:n,revision:l}).args}else{let l=r[8]?.split(":")[0],m=Number(r[6])-1;c=$({...o,path:f,snapshot:n,cursor:l,limit:m}).args}else throw Error("SVN command is not readonly history");if(JSON.stringify(r)!==JSON.stringify(c))throw Error("Unsupported SVN command shape");return{path:f,pegRevision:n,args:[...c]}}function W(e,o){if(!Array.isArray(e)||e.length>1e4)throw new Error("Invalid SVN path nodes");let r=e.map(t=>{let s=["#text","@_action","@_kind","@_copyfrom-path","@_copyfrom-rev","@_text-mods","@_prop-mods"];if(!t||typeof t!="object"||Array.isArray(t)||Object.keys(t).some(n=>!s.includes(n)))throw new Error("Invalid SVN path node");for(let n of["@_text-mods","@_prop-mods"])if(Object.hasOwn(t,n)&&!["true","false","unknown"].includes(t[n]))throw new Error("Invalid SVN modification flag");return{...Object.hasOwn(t,"@_text-mods")?{textModified:t["@_text-mods"]}:{},...Object.hasOwn(t,"@_prop-mods")?{propertiesModified:t["@_prop-mods"]}:{},path:t["#text"],action:t["@_action"],kind:t["@_kind"],...Object.hasOwn(t,"@_copyfrom-path")?{copyFromPath:t["@_copyfrom-path"]}:{},...Object.hasOwn(t,"@_copyfrom-rev")?{copyFromRevision:t["@_copyfrom-rev"]}:{}}});return Z(r,o)}function Z(e,{scope:o,revision:r}={}){j(o);let t=w(r);if(!Array.isArray(e)||e.length>1e4||t===0n&&e.length!==0)throw new Error("Invalid SVN changed paths");let s=new Set;return e.map(a=>{if(!a||typeof a!="object"||Array.isArray(a)||Object.keys(a).some(S=>!["path","action","kind","copyFromPath","copyFromRevision","copySourceOutsideScope","textModified","propertiesModified"].includes(S)))throw new Error("Invalid SVN change structure");let{path:i,action:p,kind:f,copyFromPath:c,copyFromRevision:l}=a;if(j(i),s.has(i)||!["A","D","M","R"].includes(p)||!["file","dir","unknown"].includes(f))throw new Error("Invalid SVN change");s.add(i);let m=c!==void 0||l!==void 0;if(m&&(j(c),!["A","R"].includes(p)||w(l)>=t))throw new Error("Invalid SVN copy source");let g=Object.hasOwn(a,"copySourceOutsideScope");if(g&&(a.copySourceOutsideScope!==!0||m||!["A","R"].includes(p)))throw new Error("Invalid SVN redacted copy source");let y={path:i,action:p,kind:f,...g?{copySourceOutsideScope:!0}:{}};for(let S of["textModified","propertiesModified"])if(Object.hasOwn(a,S)){if(!["true","false","unknown"].includes(a[S]))throw new Error("Invalid SVN modification flag");y[S]=a[S]}return m&&(C(c,o)?(y.copyFromPath=c,y.copyFromRevision=l):y.copySourceOutsideScope=!0),y}).filter(a=>C(a.path,o))}function ie(e,{scope:o,revision:r,path:t}={}){if(w(r)===0n)throw new Error("SVN r0 has no change comparison");let s=Z(e,{scope:o,revision:r}).find(i=>i.path===t);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=q(r),a=i=>i===null?{empty:!0}:{empty:!1,path:s.path,revision:i,pegRevision:i};return{path:s.path,action:s.action,left:a(s.action==="A"?null:n),right:a(s.action==="D"?null:r)}}import{randomBytes as Re}from"node:crypto";function ae(e){if(!e||typeof e!="object")throw Error("Invalid SVN consent identity");let{sessionId:o,cwd:r,repositoryId:t,root:s,uuid:n,scope:a}=e;for(let i of[o,r,t])if(typeof i!="string"||!i||i.length>32768||i.includes("\0"))throw Error("Invalid SVN consent scope");if(O(s,"/","0"),j(a),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([o,r,t,s,n,a])}function Q({now:e=Date.now,ttlMs:o=3e5,maxEntries:r=32,onRevoke:t=()=>{}}={}){if(!Number.isInteger(o)||o<1||o>3e5||!Number.isInteger(r)||r<1||r>32)throw Error("Invalid SVN consent bounds");if(typeof t!="function")throw Error("Invalid SVN revoke listener");let s=new Map,n=i=>{s.delete(i)&&t(i)},a=()=>{for(let[i,p]of s)p.expires<=e()&&n(i)};return{grant(i,{explicit:p=!1}={}){if(p!==!0)throw Error("Explicit SVN network consent required");let f=ae(i);a();for(let[l,m]of s)m.key===f&&n(l);for(;s.size>=r;)n(s.keys().next().value);let c=Re(32).toString("hex");return s.set(c,{key:f,sessionId:i.sessionId,expires:e()+o}),c},assert(i,p){a();let f=ae(p),c=s.get(i);if(!c||c.key!==f)throw Object.assign(Error("SVN network consent missing, expired or identity changed"),{code:"vcs/svn-consent-required"})},revokeSession(i){for(let[p,f]of s)f.sessionId===i&&n(p)},revoke(i){n(i)},clear(){for(let i of[...s.keys()])n(i)}}}import{XMLParser as Pe,XMLValidator as je}from"fast-xml-parser";function L(e){if(typeof e!="string"||Buffer.byteLength(e)>2097152||/<!DOCTYPE|<!ENTITY/i.test(e)||je.validate(e)!==!0)throw Error("Invalid DAV XML");let o=new Pe({preserveOrder:!0,ignoreAttributes:!1,parseTagValue:!1,parseAttributeValue:!1,trimValues:!1}).parse(e),r=0,t=(s,n,a=0)=>{if(a>32)throw Error("DAV XML too deep");let i=Object.create(null),p="";for(let f of s){if(++r>1e4)throw Error("DAV XML too many nodes");if(Object.hasOwn(f,"#text")){p+=f["#text"];continue}let c=Object.keys(f).filter(v=>v!==":@");if(c.length!==1)throw Error("Invalid DAV XML node");let l=c[0];if(l==="?xml"){if(a!==0)throw Error("Unexpected XML declaration");continue}let m={...n},g={};for(let[v,b]of Object.entries(f[":@"]||{}))v==="@_xmlns"?m[""]=b:v.startsWith("@_xmlns:")?m[v.slice(8)]=b:g[v]=b;let y=l.split(":");if(y.length>2)throw Error("Invalid DAV XML name");let S=y.length===2?y[0]:"",A=y.at(-1),k=m[S],I=k==="DAV:"?"D:"+A:k==="http://subversion.tigris.org/xmlns/dav/"?"S:"+A:k==="svn:"?"L:"+A:k==="http://subversion.tigris.org/xmlns/svn/"?"P:"+A:k==="http://subversion.tigris.org/xmlns/custom/"?"C:"+A:null;if(!I)throw Error("Unsupported DAV namespace");let u=["L:added-path","L:deleted-path","L:modified-path","L:replaced-path"].includes(I);if(Object.keys(g).length&&(!u||Object.keys(g).some(v=>!["@_node-kind","@_text-mods","@_prop-mods","@_copyfrom-path","@_copyfrom-rev"].includes(v))))throw Error("Unexpected DAV attribute");let d=t(f[l],m,a+1);if(u&&typeof d!="string")throw Error("Invalid DAV path text");let h=u?{...g,"#text":d}:d;if(["D:response","D:propstat","L:log-item","L:added-path","L:deleted-path","L:modified-path","L:replaced-path"].includes(I))(i[I]??=[]).push(h);else{if(Object.hasOwn(i,I))throw Error("Duplicate DAV expanded name");i[I]=h}}if(!Object.keys(i).length)return p;if(p.trim())throw Error("Mixed DAV XML content");return i};return t(o,{})}function ce(e,{scope:o,revision:r}={}){w(r);let t=L(e),s=(c,l)=>{if(!c||typeof c!="object"||Array.isArray(c)||Object.keys(c).some(m=>!l.includes(m)))throw Error("Invalid DAV detail structure")};s(t,["L:log-report"]);let n=t["L:log-report"];if(s(n,["L:log-item"]),n["L:log-item"]?.length!==1)throw Error("One DAV detail revision required");let a=n["L:log-item"][0],i={"L:added-path":"A","L:deleted-path":"D","L:modified-path":"M","L:replaced-path":"R"};if(s(a,["D:version-name","D:comment","D:creator-displayname","L:date",...Object.keys(i)]),a["D:version-name"]!==r)throw Error("DAV detail revision mismatch");let p=c=>{let l=a[c]??"";if(typeof l!="string")throw Error("Invalid DAV detail text");return l},f=[];for(let[c,l]of Object.entries(i))for(let m of a[c]??[]){let{"@_node-kind":g,...y}=m;f.push({...y,"@_kind":g??"unknown","@_action":l})}return{revision:r,author:p("D:creator-displayname"),date:p("L:date"),message:p("D:comment"),changes:W(f,{scope:o,revision:r})}}function pe(e,o){let r=L(e);if(!r||typeof r!="object"||Object.keys(r).length!==1||!Object.hasOwn(r,"L:log-report"))throw Error("Invalid DAV log root");let t=r["L:log-report"],s;if(t==="")s=[];else{if(!t||typeof t!="object"||Object.keys(t).some(i=>i!=="L:log-item")||!Array.isArray(t["L:log-item"]))throw Error("Invalid DAV log structure");s=t["L:log-item"]}if(s.length>101)throw Error("DAV log entries exceed limit");let n=s.map(i=>{if(!i||typeof i!="object"||Array.isArray(i)||Object.keys(i).some(f=>!["D:version-name","D:comment","D:creator-displayname","L:date"].includes(f)))throw Error("Unsupported DAV log item");let p=f=>{let c=i[f]??"";if(typeof c!="string")throw Error("Invalid DAV log text");return c};return{revision:p("D:version-name"),message:p("D:comment"),author:p("D:creator-displayname"),date:p("L:date")}}),a=H(n.map(i=>i.revision),o);return{snapshot:a.snapshot,entries:n.slice(0,a.revisions.length),nextRevision:a.nextRevision}}import{XMLParser as _e,XMLValidator as Ce}from"fast-xml-parser";function fe(e,{scope:o,revision:r}={}){if(w(r),typeof e!="string"||Buffer.byteLength(e,"utf8")>2*1024*1024||/<!DOCTYPE|<!ENTITY/i.test(e)||Ce.validate(e)!==!0)throw new Error("Invalid SVN detail XML");let t=new _e({ignoreAttributes:!1,parseAttributeValue:!1,parseTagValue:!1,trimValues:!1,isArray:f=>f==="logentry"||f==="path"}).parse(e),s=(f,c)=>{if(!f||typeof f!="object"||Array.isArray(f)||Object.keys(f).some(l=>!c.includes(l)&&!(l==="#text"&&typeof f[l]=="string"&&/^[ \t\r\n]*$/.test(f[l]))))throw new Error("Invalid SVN detail structure")};if(s(t,["?xml","log"]),s(t.log,["logentry"]),!Array.isArray(t.log.logentry)||t.log.logentry.length!==1)throw new Error("Expected one SVN revision");let n=t.log.logentry[0];if(s(n,["@_revision","author","date","msg","paths"]),n["@_revision"]!==r)throw new Error("SVN detail revision mismatch");let a=f=>{let c=n[f]??"";if(typeof c!="string")throw new Error("Invalid SVN detail text");return c},i=[];n.paths!==void 0&&!(typeof n.paths=="string"&&/^[ \t\r\n]*$/.test(n.paths))&&(s(n.paths,["path"]),i=n.paths.path??[]);let p=W(i,{scope:o,revision:r});return{revision:r,author:a("author"),date:a("date"),message:a("msg"),pathsAvailable:Object.hasOwn(n,"paths"),changes:p}}import{XMLParser as Ue,XMLValidator as He}from"fast-xml-parser";function le(e,o){let r=Me(e),t=H(r.map(s=>s.revision),o);return{snapshot:t.snapshot,entries:r.slice(0,t.revisions.length),nextRevision:t.nextRevision}}function Me(e){if(typeof e!="string"||Buffer.byteLength(e,"utf8")>2*1024*1024||/<!DOCTYPE|<!ENTITY/i.test(e)||He.validate(e)!==!0)throw new Error("Invalid SVN log XML");let r=new Ue({ignoreAttributes:!1,parseAttributeValue:!1,parseTagValue:!1,trimValues:!1,isArray:n=>n==="logentry"}).parse(e);if(!Object.hasOwn(r,"log")||Object.keys(r).some(n=>n!=="log"&&n!=="?xml"))throw new Error("Invalid SVN log root");let t=r.log;if(typeof t=="string"&&/^[ \t\r\n]*$/.test(t))return[];if(t&&typeof t=="object"&&typeof t["#text"]=="string"&&/^[ \t\r\n]*$/.test(t["#text"])&&delete t["#text"],!t||typeof t!="object"||Object.keys(t).some(n=>n!=="logentry")||!Array.isArray(t.logentry)||t.logentry.length>101)throw new Error("Invalid SVN log entries");let s=new Set;return t.logentry.map(n=>{if(!n||typeof n!="object"||Array.isArray(n)||Object.keys(n).some(p=>!["@_revision","author","date","msg","#text"].includes(p))||"#text"in n&&(typeof n["#text"]!="string"||!/^[ \t\r\n]*$/.test(n["#text"])))throw new Error("Unsupported SVN log entry structure");let a=n["@_revision"];if(w(a),s.has(a))throw new Error("Duplicate SVN revision");s.add(a);let i=p=>{let f=n[p]??"";if(typeof f!="string")throw new Error("Invalid SVN log text");return f};return{revision:a,author:i("author"),date:i("date"),message:i("msg")}})}import{XMLParser as qe,XMLValidator as Be}from"fast-xml-parser";function R(e,{root:o}={}){if(O(o,"/","0"),typeof e!="string"||!e||e.length>32768||/[\\?#]/.test(e)||[...e].some(n=>n.charCodeAt(0)<=32||n.charCodeAt(0)===127))throw Error("Invalid DAV href");if(!(e.startsWith("/")&&!e.startsWith("//"))&&!e.startsWith("https://"))throw Error("DAV href must be absolute path or HTTPS URL");let r=new URL(e,new URL(o).origin);if(r.protocol!=="https:"||r.origin!==new URL(o).origin||r.username||r.password||r.search||r.hash)throw Error("DAV href outside authorized origin");if(e.startsWith("https://")&&r.href!==e||e.startsWith("/")&&r.pathname!==e)throw Error("Noncanonical DAV href");let t=r.pathname.split("/").slice(1);t.at(-1)===""&&t.pop();for(let n of t){let a=decodeURIComponent(n);if(!n||a==="."||a===".."||[...a].some(i=>i.charCodeAt(0)<32||i.charCodeAt(0)===127||i==="/"||i.charCodeAt(0)===92))throw Error("Unsafe DAV href path")}let s=new URL(o).pathname.replace(/[/]$/,"");if(s&&r.pathname!==s&&!r.pathname.startsWith(s+"/"))throw Error("DAV href outside repository root");return r.href}function de(e,{target:o,revision:r,root:t,uuid:s}={}){if(typeof e!="string"||Buffer.byteLength(e)>2097152||/<!DOCTYPE|<!ENTITY/i.test(e)||Be.validate(e)!==!0)throw Error("Invalid SVN node kind XML");let a=new qe({ignoreAttributes:!1,parseAttributeValue:!1,parseTagValue:!1,trimValues:!1,isArray:p=>p==="entry"}).parse(e)?.info?.entry;if(!Array.isArray(a)||a.length!==1)throw Error("Expected one SVN node kind entry");let i=a[0];if(i.url!==o||i["@_revision"]!==r||i.repository?.root!==t||i.repository?.uuid!==s)throw Error("SVN node kind identity mismatch");if(!["file","dir"].includes(i["@_kind"]))throw Error("Unknown SVN node kind");return i["@_kind"]}function ue(e,{root:o,target:r}={}){let t=L(e),s=(c,l)=>{if(!c||typeof c!="object"||Array.isArray(c)||Object.keys(c).some(m=>!l.includes(m)))throw Error("Invalid DAV node kind structure")};s(t,["D:multistatus"]),s(t["D:multistatus"],["D:response"]);let n=t["D:multistatus"]["D:response"];if(n?.length!==1)throw Error("Expected one DAV node kind response");let a=n[0];s(a,["D:href","D:propstat"]);let i=R(a["D:href"],{root:o});if(a["D:propstat"]?.length!==1)throw Error("Expected one DAV node kind status");let p=a["D:propstat"][0];if(s(p,["D:status","D:prop"]),!/^HTTP[/]1[.][01] 200(?: OK)?$/.test(p["D:status"]))throw Error("DAV node kind unavailable");s(p["D:prop"],["D:resourcetype"]);let f=p["D:prop"]["D:resourcetype"];if(f===""){if(i!==r)throw Error("DAV node kind target mismatch");return"file"}if(s(f,["D:collection"]),f["D:collection"]!=="")throw Error("Unknown DAV node kind");if(i!==r&&i!==r+"/")throw Error("DAV node kind target mismatch");return"dir"}import{XMLParser as Xe,XMLValidator as Fe}from"fast-xml-parser";function he(e,{target:o,allowEmpty:r=!1}={}){if(typeof o!="string"||!o||o.length>32768)throw new Error("Invalid SVN property target");if(typeof e!="string"||Buffer.byteLength(e,"utf8")>2*1024*1024||/<!DOCTYPE|<!ENTITY/i.test(e)||Fe.validate(e)!==!0)throw new Error("Invalid SVN property XML");let t=new Xe({ignoreAttributes:!1,parseAttributeValue:!1,parseTagValue:!1,trimValues:!1,isArray:c=>c==="target"||c==="property"}).parse(e),s=(c,l)=>{if(!c||typeof c!="object"||Array.isArray(c)||Object.keys(c).some(m=>!l.includes(m)&&!(m==="#text"&&typeof c[m]=="string"&&/^[ \t\r\n]*$/.test(c[m]))))throw new Error("Invalid SVN property structure")};if(s(t,["?xml","properties"]),r===!0&&typeof t.properties=="string"&&/^[ \t\r\n]*$/.test(t.properties))return{target:o,names:[],special:!1};s(t.properties,["target"]);let n=t.properties.target;if(!Array.isArray(n)||n.length!==1)throw new Error("Expected one SVN property target");let a=n[0];if(s(a,["@_path","property"]),a["@_path"]!==o)throw new Error("SVN property target mismatch");let i=a.property??[];if(!Array.isArray(i)||i.length>1e4)throw new Error("Too many SVN properties");let p=[],f=new Set;for(let c of i){s(c,["@_name"]);let l=c["@_name"];if(typeof l!="string"||!l||l.length>1024||/[\x00-\x20\x7f]/.test(l)||f.has(l))throw new Error("Invalid SVN property name");f.add(l),p.push(l)}return{target:o,names:p,special:f.has("svn:special")}}var ye=2*1024*1024,me=15e3,ve=3e5,ge=50,We=/^[a-f0-9]{64}$/,ee=e=>{if(typeof e!="string"||!We.test(e))throw Error("Invalid SVN change ID")},re=(e,o,r)=>{let t=e.changes.find(s=>s.id===o);return r===void 0||t?.kind===r?t:void 0},we=e=>e.slice(0,e.lastIndexOf("@")),Ye=(e,o,r,t)=>$e("sha256").update(JSON.stringify([e.root,e.uuid,e.scope,o,r,t])).digest("hex"),ze=(e,o,r)=>{if(!(typeof e=="string"||Buffer.isBuffer(e))||Buffer.byteLength(e)>r)throw Error("SVN transport output exceeds byte limit or has invalid type");return Buffer.isBuffer(e)&&o.args[0]!=="cat"?new TextDecoder("utf-8",{fatal:!0}).decode(e):e},Y=(e,o,r)=>({identity:e,token:o,signal:r,maxBytes:ye});function Se({resolveIdentity:e,transport:o,now:r=Date.now,timeoutMs:t=me,transportFormat:s="cli"}={}){if(!["cli","dav"].includes(s))throw Error("Invalid SVN transport format");if(!Number.isInteger(t)||t<1||t>me)throw Error("Invalid SVN runtime deadline");if(typeof e!="function")throw Error("Local identity resolver required");let n=!1,a=()=>{if(n)throw new DOMException("SVN runtime disposed","AbortError")},i=new Map,p=u=>{for(let d of i.get(u)||[])d.controller.abort(new DOMException("SVN consent revoked","AbortError"))},f=Q({now:r}),c=Q({now:r,onRevoke:u=>p(u)}),l=typeof o=="function",m=()=>{if(!l)throw Error("SVN remote transport disabled")},g=async(u,d)=>{a(),d?.throwIfAborted();let h=await e(u,d);return a(),d?.throwIfAborted(),{...h,sessionId:u.sessionId,cwd:h.cwd,repositoryId:u.repositoryId}},y=u=>u.revision===void 0?{}:(w(u.revision),{snapshot:u.revision}),S=async(u,d,h)=>{let v=await g(u,h);return c.assert(d,v),v},A=async(u,d,h)=>{h?.throwIfAborted(),c.assert(d,await g(u,h))},k=async(u,d)=>{a(),M(u,d.identity),d.signal?.throwIfAborted(),c.assert(d.token,d.identity);let h=new AbortController,v=()=>h.abort(d.signal.reason);d.signal?.addEventListener("abort",v,{once:!0});let b={controller:h,sessionId:d.identity.sessionId};i.has(d.token)||i.set(d.token,new Set),i.get(d.token).add(b);let D=setTimeout(()=>h.abort(new DOMException("SVN transport deadline exceeded","TimeoutError")),t);try{let E=async()=>{h.signal.throwIfAborted();let N=await g({sessionId:d.identity.sessionId,repositoryId:d.identity.repositoryId},h.signal);c.assert(d.token,N),h.signal.throwIfAborted()},V=await o(u,{...d,timeoutMs:t,signal:h.signal,assertAuthorized:E});return h.signal.throwIfAborted(),ze(V,u,d.maxBytes)}catch(E){throw h.signal.aborted?h.signal.reason:E}finally{let E=i.get(d.token);E?.delete(b),E?.size||i.delete(d.token),clearTimeout(D),d.signal?.removeEventListener("abort",v)}},I={async describe(u,{signal:d}={}){let h=await g(u,d),v=y(h),b=f.grant(h,{explicit:!0});return{...v,offer:b,root:h.root,uuid:h.uuid,scope:h.scope,origin:new URL(h.root).origin,expiresAt:r()+ve,remoteEnabled:l}},async approve(u,{offer:d,explicit:h=!1,signal:v}={}){if(h!==!0)throw Error("Explicit SVN network consent required");let b=await g(u,v);f.assert(d,b),m();let D=y(b),E=c.grant(b,{explicit:!0});return f.revoke(d),{...D,token:E,expiresAt:r()+ve}},async log(u,{token:d,snapshot:h,cursor:v,limit:b,path:D,signal:E}={}){let V=await S(u,d,E);m();let N=$({...V,snapshot:h,cursor:v,limit:b,path:D});E?.throwIfAborted();let x=await k(N,Y(V,d,E));return await A(u,d,E),(s==="dav"?pe:le)(x,N)},async detail(u,{token:d,snapshot:h,revision:v,signal:b}={}){let D=await S(u,d,b);m();let E=F({...D,snapshot:h,revision:v}),V=await k(E,Y(D,d,b));await A(u,d,b);let N=s==="dav"?{...ce(V,E),pathsAvailable:!0}:fe(V,E);return{...N,snapshot:h,changes:N.changes.map(x=>({...x,id:Ye(D,h,v,x)}))}},async compare(u,{token:d,snapshot:h,revision:v,id:b,signal:D}={}){ee(b);let E=await I.detail(u,{token:d,snapshot:h,revision:v,signal:D}),V=re(E,b);if(!V)throw Error("SVN change is not selected revision member");let N=await S(u,d,D),x=ie(E.changes.map(({id:T,..._})=>_),{scope:N.scope,revision:v,path:V.path}),P=async T=>{if(T.empty)return{label:"empty",text:"",absent:!0};let _=await S(u,d,D),U=O(_.root,T.path,T.pegRevision),X=Y(_,d,D),Te=await k({args:["proplist","--xml","--non-interactive","--no-auth-cache","-r",T.revision,"--",U]},X);if(await A(u,d,D),he(Te,{target:we(U),allowEmpty:!0}).special)throw Error("SVN special files cannot be compared");let G=await k({args:["cat","--non-interactive","--no-auth-cache","-r",T.revision,"--",U]},X);await A(u,d,D);let J=Buffer.isBuffer(G)?G:Buffer.from(G);if(J.length>ye)throw Error("SVN text exceeds 2 MiB");if(J.includes(0))return{label:"r"+T.revision,text:"",binary:!0};let se;try{se=new TextDecoder("utf-8",{fatal:!0}).decode(J)}catch{return{label:"r"+T.revision,text:"",binary:!0}}return{label:"r"+T.revision,text:se}},K=async T=>{let _=await S(u,d,D),U=O(_.root,T.path,T.pegRevision),X=await k({args:["info","--xml","--non-interactive","--no-auth-cache","-r",T.revision,"--",U]},Y(_,d,D));return await A(u,d,D),de(X,{target:we(U),revision:T.revision,root:_.root,uuid:_.uuid})},B=x.left;V.action==="R"&&!B.empty&&await K(B)==="dir"&&(B={empty:!0});let oe=await P(B),ne=await P(x.right);return{path:V.path,revision:v,snapshot:h,left:oe,right:ne,binary:!!(oe.binary||ne.binary)}},async trace(u,{token:d,snapshot:h,revision:v,id:b,cursor:D,limit:E=ge,signal:V}={}){ee(b);let N=await I.detail(u,{token:d,snapshot:h,revision:v,signal:V}),x=re(N,b,"file");if(!x)throw Error("SVN tracing requires selected file member");let P=x.action==="D"?q(v):v;return{...await I.log(u,{token:d,snapshot:P,cursor:D??P,limit:E,path:x.path,signal:V}),selectionSnapshot:h,selectionRevision:v,path:x.path,pegRevision:P,stopOnCopy:!0,copySourceOutsideScope:!!x.copySourceOutsideScope}},async copytrace(u,{token:d,snapshot:h,revision:v,id:b,cursor:D,limit:E=ge,signal:V}={}){ee(b);let N=await I.detail(u,{token:d,snapshot:h,revision:v,signal:V}),x=re(N,b,"file");if(!x||x.copySourceOutsideScope||x.copyFromPath===void 0||x.copyFromRevision===void 0)throw Error("Selected file has no authorized copy source");let P=x.copyFromRevision;return{...await I.log(u,{token:d,snapshot:P,cursor:D??P,limit:E,path:x.copyFromPath,signal:V}),selectionSnapshot:h,selectionRevision:v,path:x.copyFromPath,pegRevision:P,sourceForPath:x.path,stopOnCopy:!0}},async revoke(u,{token:d,signal:h}={}){return await S(u,d,h),c.revoke(d),p(d),{revoked:!0}},dispose(){n=!0,f.clear(),c.clear();for(let u of i.keys())p(u)},revokeSession(u){f.revokeSession(u),c.revokeSession(u);for(let[d,h]of i)[...h].some(v=>v.sessionId===u)&&p(d)}};return I}var te=(e,o)=>({ok:!1,error:{code:e,message:o,details:{}}}),De={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"],copytrace:["token","snapshot","revision","id","cursor","limit"]};function Ke(e,o){if(!o||typeof o!="object"||Array.isArray(o))throw Error("Object payload required");if(Object.keys(o).some(r=>!["sessionId","repositoryId",...De[e]].includes(r)))throw Error("Unknown SVN request field");for(let r of["sessionId","repositoryId"])if(typeof o[r]!="string"||!o[r]||o[r].length>1024||o[r].includes("\0"))throw Error("Invalid SVN Session address");if(e==="approve"&&(o.explicit!==!0||typeof o.offer!="string"||!/^[a-f0-9]{64}$/.test(o.offer)))throw Error("Explicit scoped offer required");if(!["describe","approve"].includes(e)){if(typeof o.token!="string"||!/^[a-f0-9]{64}$/.test(o.token))throw Error("SVN consent token required");if(e!=="revoke"&&w(o.snapshot),o.revision!==void 0&&w(o.revision),["detail","compare","trace","copytrace"].includes(e)&&o.revision===void 0)throw Error("Selected SVN revision required");if(o.cursor!==void 0&&w(o.cursor),o.limit!==void 0&&(!Number.isInteger(o.limit)||o.limit<1||o.limit>100))throw Error("Invalid SVN page size");if(["compare","trace","copytrace"].includes(e)&&(typeof o.id!="string"||!/^[a-f0-9]{64}$/.test(o.id)))throw Error("Selected SVN change ID required")}}function be(e,{timeoutMs:o=15e3}={}){if(!Number.isInteger(o)||o<1||o>15e3)throw Error("Invalid SVN RPC deadline");let r=0;return async(t,s,n)=>{let a=t.startsWith("vcs/svn-")?t.slice(8):"";if(!Object.hasOwn(De,a))return te("vcs/not-found","Unknown SVN endpoint");if(a!=="revoke"&&r>=2)return te("vcs/busy","SVN history requests busy");a!=="revoke"&&r++;let i=new AbortController,p=()=>i.abort(n.reason);n?.addEventListener("abort",p,{once:!0}),n?.aborted&&p();let f=setTimeout(()=>i.abort(new DOMException("SVN operation deadline exceeded","TimeoutError")),o);try{n?.throwIfAborted(),Ke(a,s);let{sessionId:c,repositoryId:l,...m}=s,g=await e[a]({sessionId:c,repositoryId:l},{...m,signal:i.signal});return i.signal.throwIfAborted(),{ok:!0,value:g}}catch(c){let l=n?.aborted||c?.name==="AbortError"?"vcs/cancelled":["vcs/rediscover-required","vcs/svn-consent-required"].includes(c?.code)?c.code:"vcs/operation-failed",m=i.signal.aborted?i.signal.reason:c,g=m instanceof Error?m.message:String(m);return te(l,g)}finally{clearTimeout(f),n?.removeEventListener("abort",p),a!=="revoke"&&r--}}}var Je=new Set(["describe","approve","log","detail","compare","trace","copytrace","revoke"]);function Ee(e,o,{transport:r,transportFormat:t="cli",now:s=Date.now,timeoutMs:n=15e3}={}){let a,i=Ge(e,o,4,{now:s,bindSvnIdentityResolver:c=>{a=c}}),p=Se({resolveIdentity:a,transport:r,transportFormat:t,now:s,timeoutMs:n}),f=be(p,{timeoutMs:n});return{handle(c,l,m){let g=c.startsWith("vcs/svn-")?c.slice(8):"";return Je.has(g)?f(c,l,m):i(c,l,m)},revokeSession:c=>p.revokeSession(c),dispose:()=>p.dispose()}}import{request as Ze}from"node:https";function Ae({request:e=Ze}={}){return async(o,{origin:r,method:t="GET",body:s="",depth:n,label:a,signal:i,maxBytes:p=2097152,timeoutMs:f=15e3}={})=>{let c=new URL(o);if(c.protocol!=="https:"||c.origin!==r||c.username||c.password||c.hash||c.href!==o||!["GET","PROPFIND","REPORT"].includes(t)||typeof s!="string"||Buffer.byteLength(s)>65536||!Number.isInteger(p)||p<1||p>2097152||!Number.isInteger(f)||f<1||f>15e3)throw Error("Invalid bounded HTTPS request");if(n!==void 0&&(t!=="PROPFIND"||n!==0))throw Error("Invalid DAV depth");if(a!==void 0){if(t!=="PROPFIND"||n!==0)throw Error("Invalid DAV label");w(a)}let l={...n===0?{Depth:"0"}:{},...a!==void 0?{Label:a}:{}};return i?.throwIfAborted(),new Promise((m,g)=>{let y,S,A=null,k=null,I=0,u=[],d=D=>{A??=D,S?.destroy(),y?.destroy()},h=()=>d(i.reason??new DOMException("HTTPS cancelled","AbortError")),v=setTimeout(()=>d(new DOMException("HTTPS deadline exceeded","TimeoutError")),f),b=()=>{clearTimeout(v),i?.removeEventListener("abort",h)};try{y=e(c,{method:t,agent:!1,rejectUnauthorized:!0,maxHeaderSize:16384,headers:{...l,"Accept-Encoding":"identity","Content-Type":"text/xml; charset=utf-8","Content-Length":String(Buffer.byteLength(s))}},D=>{S=D;let E=S.statusCode??0;if(E>=300&&E<400){d(Error("HTTPS redirects forbidden"));return}if(E<200||E>=300){d(Error("HTTPS status "+E));return}if(S.headers["content-encoding"]&&S.headers["content-encoding"]!=="identity"){d(Error("HTTPS compressed responses forbidden"));return}let V=S.headers["content-length"];if(V!==void 0&&(!/^[0-9]+$/.test(V)||Number(V)>p)){d(Error("HTTPS response exceeds byte limit"));return}S.on("data",N=>{I+=N.length,I>p?d(Error("HTTPS response exceeds byte limit")):A||u.push(N)}),S.on("error",d),S.on("end",()=>{if(!S.complete){d(Error("HTTPS response incomplete"));return}k=Buffer.concat(u)})}),y.on("error",D=>{A??=D}),y.once("close",()=>{b(),A?g(A):k!==null?m(k):g(Error("HTTPS response closed before completion"))}),i?.addEventListener("abort",h,{once:!0}),i?.aborted?h():y.end(s)}catch(D){b(),y?.destroy(),g(D)}})}}var Qe=e=>e.replaceAll("&","&").replaceAll("<","<").replaceAll(">",">").replaceAll('"',""").replaceAll("'","'");function Ve(e,o){let r=M(e,o),t=r.args;if(t[0]!=="log")throw Error("DAV log report requires readonly log plan");let s=t.includes("--verbose"),n=t[s?9:8].split(":"),a=t[s?7:6],i=r.path==="/"?"":r.path.slice(1),p='<?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>"+a+"</S:limit><S:strict-node-history/>"+(s?"<S:discover-changed-paths/>":"")+"<S:path>"+Qe(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:r.pegRevision,path:r.path,requiresVerifiedBaseline:!0}}function xe(e,{root:o,uuid:r,target:t}={}){let s=L(e),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 a=s["D:multistatus"];if(n(a,["D:response"]),!Array.isArray(a["D:response"])||a["D:response"].length!==1)throw Error("DAV discovery requires one response");let i=a["D:response"][0];if(n(i,["D:href","D:propstat"]),R(i["D:href"],{root:o})!==t)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(n(p,["D:prop","D:status"]),!/^HTTP[/]1[.][01] 200(?: OK)?$/.test(p["D:status"]))throw Error("DAV discovery properties unavailable");let f=p["D:prop"];if(n(f,["S:repository-uuid","D:version-controlled-configuration"]),f["S:repository-uuid"]!==r||typeof r!="string"||!/^[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}$/i.test(r))throw Error("DAV UUID does not match local identity");let c=f["D:version-controlled-configuration"];return n(c,["D:href"]),{uuid:r,vcc:R(c["D:href"],{root:o}),target:t}}function Ie(e,{root:o,revision:r}={}){w(r);let t=L(e),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(t,["D:multistatus"]);let n=t["D:multistatus"];if(s(n,["D:response"]),n["D:response"]?.length!==1)throw Error("One DAV baseline response required");let a=n["D:response"][0];s(a,["D:href","D:propstat"]);let i=R(a["D:href"],{root:o});if(a["D:propstat"]?.length!==1)throw Error("One DAV baseline status required");let p=a["D:propstat"][0];if(s(p,["D:status","D:prop"]),p["D:status"]!=="HTTP/1.1 200 OK")throw Error("DAV baseline status unavailable");let f=p["D:prop"];if(s(f,["D:version-name","D:baseline-collection"]),f["D:version-name"]!==r)throw Error("DAV baseline revision mismatch");s(f["D:baseline-collection"],["D:href"]);let c=R(f["D:baseline-collection"]["D:href"],{root:o});if(!c.endsWith("/"))throw Error("DAV baseline collection must end in slash");return{revision:r,baseline:i,collection:c}}function Ne({request:e,assertAuthorized:o}={}){if(typeof e!="function"||typeof o!="function")throw Error("DAV chain requires transport and authorization");return async(r,{revision:t,signal:s}={})=>{w(t),O(r.root,r.scope,t);let n=r.root,a=new URL(n).origin,i=n.endsWith("/")?n:n+"/",p=async(m,g)=>{s?.throwIfAborted(),await o(r,{signal:s}),s?.throwIfAborted();let y=await e(m,{origin:a,signal:s,...g});if(s?.throwIfAborted(),await o(r,{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)},f=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:c}=xe(f,{root:n,target:i,uuid:r.uuid}),l=await p(c,{method:"PROPFIND",depth:0,label:t,body:'<D:propfind xmlns:D="DAV:"><D:prop><D:baseline-collection/><D:version-name/></D:prop></D:propfind>'});return{...Ie(l,{root:n,revision:t}),uuid:r.uuid}}}function ke(e,{root:o,target:r}={}){let t=L(e),s=(c,l)=>{if(!c||typeof c!="object"||Array.isArray(c)||Object.keys(c).some(m=>!l.includes(m)))throw Error("Invalid DAV property names structure")};s(t,["D:multistatus"]);let n=t["D:multistatus"];if(s(n,["D:response"]),n["D:response"]?.length!==1)throw Error("One DAV property response required");let a=n["D:response"][0];if(s(a,["D:href","D:propstat"]),R(a["D:href"],{root:o})!==r)throw Error("DAV property target mismatch");if(a["D:propstat"]?.length!==1)throw Error("One DAV property status required");let i=a["D:propstat"][0];if(s(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(([c,l])=>!/^[DSPC]:[^:]+$/.test(c)||l!=="")))throw Error("DAV property names must be empty");let f=p===""?[]:Object.keys(p);return{names:f,special:f.includes("P:special")}}var z=e=>e.replaceAll("&","&").replaceAll("<","<").replaceAll(">",">").replaceAll('"',""");function Oe({request:e}={}){if(typeof e!="function")throw Error("Bounded DAV request required");return async(o,{identity:r,assertAuthorized:t,signal:s}={})=>{let n=M(o,r),a=n.args[0],i=Ne({request:e,assertAuthorized:t}),p=await i(r,{revision:n.pegRevision,signal:s}),f=async(y,S)=>{s?.throwIfAborted(),await t(),s?.throwIfAborted();let A=await e(y,{origin:new URL(r.root).origin,...S,signal:s});if(s?.throwIfAborted(),await t(),s?.throwIfAborted(),!Buffer.isBuffer(A)||A.length>2097152)throw Error("Invalid DAV transport bytes");return A};if(a==="log"){let y=Ve(o,r);return f(p.collection,{method:"REPORT",body:y.body})}let c=n.path.slice(1).split("/").map(y=>encodeURIComponent(y).replace(/[!'()*]/g,S=>"%"+S.charCodeAt(0).toString(16).toUpperCase())).join("/"),l=R(p.collection+c,{root:r.root});if(a==="info"){let y=await f(l,{method:"PROPFIND",depth:0,body:'<D:propfind xmlns:D="DAV:"><D:prop><D:resourcetype/></D:prop></D:propfind>'}),S=ue(new TextDecoder("utf-8",{fatal:!0}).decode(y),{root:r.root,target:l}),A=n.args.at(-1).slice(0,n.args.at(-1).lastIndexOf("@"));return'<info><entry kind="'+S+'" revision="'+n.pegRevision+'"><url>'+z(A)+"</url><repository><root>"+z(r.root)+"</root><uuid>"+z(r.uuid)+"</uuid></repository></entry></info>"}let m=await f(l,{method:"PROPFIND",depth:0,body:'<D:propfind xmlns:D="DAV:"><D:propname/></D:propfind>'}),g=ke(new TextDecoder("utf-8",{fatal:!0}).decode(m),{root:r.root,target:l});if(a==="proplist")return'<properties><target path="'+z(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 f(l,{method:"GET"})}}function Ft(e,o){return Ee(e,o,{transportFormat:"dav",transport:Oe({request:Ae()})})}export{Ft as createSvnHttpsHost};
|
|
1
|
+
import{createHandler as Qe}from"../index.mjs";import{createHash as Ke}from"node:crypto";function ie({left:e={},right:o={},large:r=!1,allowLarge:t=!1,conflicted:i=!1}={}){let n=[e,o],a=n.some(s=>s.binary)?"binary":n.some(s=>s.previewReason==="size-limit")?"size-limit":n.find(s=>s.previewReason)?.previewReason??null;return{reason:a,canLoadLarge:!r&&t&&!i&&a==="size-limit"&&n.every(s=>s.largeEligible!==!1),complete:a===null,limits:{previewBytes:2097152,largeBytes:8388608}}}var _e=9223372036854775807n;function w(e){if(typeof e!="string"||e.length>19||!/^(0|[1-9][0-9]*)$/.test(e))throw new Error("Invalid SVN revision");let o=BigInt(e);if(o>_e)throw new Error("SVN revision exceeds supported range");return o}function X(e){let o=w(e);return o===0n?null:String(o-1n)}function C(e,{snapshot:o,cursor:r=o,limit:t=50}={}){let i=w(o),n=w(r);if(n>i||!Number.isInteger(t)||t<1||t>100||!Array.isArray(e)||e.length>t+1)throw new Error("Invalid SVN page bounds");let a=n+1n;for(let p of e){let l=w(p);if(l>n||l>=a)throw new Error("SVN revisions must descend within cursor");a=l}let s=e.slice(0,t);return{snapshot:o,revisions:s,nextRevision:e.length>t?X(s.at(-1)):null}}function _(e){if(typeof e!="string"||e.length>32768||!e.startsWith("/")||/[\u0000-\u001f\u007f\\]/.test(e))throw new Error("Invalid SVN repository path");if(e!=="/"&&e.slice(1).split("/").some(o=>o===""||o==="."||o===".."))throw new Error("Noncanonical SVN repository path");return e}function U(e,o){return _(e),_(o),o==="/"||e===o||e.startsWith(o+"/")}function L(e,o,r){if(_(o),w(r),typeof e!="string"||e.length>32768||/[\x00-\x20\x7f\\?#]/.test(e))throw new Error("Invalid SVN HTTPS root");let t=new URL(e);if(t.protocol!=="https:"||t.username||t.password||t.href!==e)throw new Error("Noncanonical SVN HTTPS root");let i=t.pathname.split("/").slice(1);i.at(-1)===""&&i.pop();for(let p of i){let l=decodeURIComponent(p);if(!p||l==="."||l===".."||/[\x00-\x1f\x7f/\\]/.test(l))throw new Error("Invalid SVN root path")}let n=e.endsWith("/")?e.slice(0,-1):e,a=o==="/"?"":o.split("/").map(p=>encodeURIComponent(p).replace(/[!'()*]/g,l=>"%"+l.charCodeAt(0).toString(16).toUpperCase())).join("/"),s=n+(a||(t.pathname==="/"?"/":""))+"@"+r;if(s.length>32768)throw new Error("SVN target too long");return s}function H({root:e,scope:o,path:r=o,snapshot:t,revision:i}={}){if(w(i)>w(t))throw new Error("SVN selected revision exceeds snapshot");if(!U(r,o))throw new Error("SVN detail path outside scope");let n=L(e,r,t);return{scope:o,revision:i,snapshot:t,args:["log","--xml","--verbose","--stop-on-copy","--non-interactive","--no-auth-cache","--limit","1","-r",i+":"+i,"--",n]}}function $({root:e,scope:o,path:r=o,snapshot:t,cursor:i=t,limit:n=50}={}){if(C([],{snapshot:t,cursor:i,limit:n}),!U(r,o))throw new Error("SVN history path outside scope");let a=L(e,r,t);return{snapshot:t,cursor:i,limit:n,args:["log","--xml","--stop-on-copy","--non-interactive","--no-auth-cache","--limit",String(n+1),"-r",i+":0","--",a]}}function M(e,o){let r=e?.args;if(!Array.isArray(r)||r.length>16||r.some(f=>typeof f!="string"))throw Error("Invalid SVN command");let t=r.at(-1),i=t.lastIndexOf("@"),n=t.slice(i+1);w(n);let a=o.root.endsWith("/")?o.root.slice(0,-1):o.root;L(o.root,"/",n);let s=t.slice(0,i);if(!(s===a||s.startsWith(a+"/")))throw Error("SVN command target outside root");let p=s.slice(a.length),l=p?"/"+p.slice(1).split("/").map(f=>decodeURIComponent(f)).join("/"):"/";if(!U(l,o.scope)||L(o.root,l,n)!==t)throw Error("SVN command target outside scope or noncanonical");let c;if(r[0]==="cat"||r[0]==="proplist"||r[0]==="info"){w(r[r[0]==="cat"?4:5]);let f=r[r[0]==="cat"?4:5];if(f!==n)throw Error("SVN content operative and peg mismatch");c=r[0]==="cat"?["cat","--non-interactive","--no-auth-cache","-r",f,"--",t]:[r[0],"--xml","--non-interactive","--no-auth-cache","-r",f,"--",t]}else if(r[0]==="log")if(r.includes("--verbose")){let f=r[9]?.split(":")[0];c=H({...o,path:l,snapshot:n,revision:f}).args}else{let f=r[8]?.split(":")[0],m=Number(r[6])-1;c=$({...o,path:l,snapshot:n,cursor:f,limit:m}).args}else throw Error("SVN command is not readonly history");if(JSON.stringify(r)!==JSON.stringify(c))throw Error("Unsupported SVN command shape");return{path:l,pegRevision:n,args:[...c]}}function W(e,o){if(!Array.isArray(e)||e.length>1e4)throw new Error("Invalid SVN path nodes");let r=e.map(t=>{let i=["#text","@_action","@_kind","@_copyfrom-path","@_copyfrom-rev","@_text-mods","@_prop-mods"];if(!t||typeof t!="object"||Array.isArray(t)||Object.keys(t).some(n=>!i.includes(n)))throw new Error("Invalid SVN path node");for(let n of["@_text-mods","@_prop-mods"])if(Object.hasOwn(t,n)&&!["true","false","unknown"].includes(t[n]))throw new Error("Invalid SVN modification flag");return{...Object.hasOwn(t,"@_text-mods")?{textModified:t["@_text-mods"]}:{},...Object.hasOwn(t,"@_prop-mods")?{propertiesModified:t["@_prop-mods"]}:{},path:t["#text"],action:t["@_action"],kind:t["@_kind"],...Object.hasOwn(t,"@_copyfrom-path")?{copyFromPath:t["@_copyfrom-path"]}:{},...Object.hasOwn(t,"@_copyfrom-rev")?{copyFromRevision:t["@_copyfrom-rev"]}:{}}});return ee(r,o)}function ee(e,{scope:o,revision:r}={}){_(o);let t=w(r);if(!Array.isArray(e)||e.length>1e4||t===0n&&e.length!==0)throw new Error("Invalid SVN changed paths");let i=new Set;return e.map(a=>{if(!a||typeof a!="object"||Array.isArray(a)||Object.keys(a).some(S=>!["path","action","kind","copyFromPath","copyFromRevision","copySourceOutsideScope","textModified","propertiesModified"].includes(S)))throw new Error("Invalid SVN change structure");let{path:s,action:p,kind:l,copyFromPath:c,copyFromRevision:f}=a;if(_(s),i.has(s)||!["A","D","M","R"].includes(p)||!["file","dir","unknown"].includes(l))throw new Error("Invalid SVN change");i.add(s);let m=c!==void 0||f!==void 0;if(m&&(_(c),!["A","R"].includes(p)||w(f)>=t))throw new Error("Invalid SVN copy source");let g=Object.hasOwn(a,"copySourceOutsideScope");if(g&&(a.copySourceOutsideScope!==!0||m||!["A","R"].includes(p)))throw new Error("Invalid SVN redacted copy source");let y={path:s,action:p,kind:l,...g?{copySourceOutsideScope:!0}:{}};for(let S of["textModified","propertiesModified"])if(Object.hasOwn(a,S)){if(!["true","false","unknown"].includes(a[S]))throw new Error("Invalid SVN modification flag");y[S]=a[S]}return m&&(U(c,o)?(y.copyFromPath=c,y.copyFromRevision=f):y.copySourceOutsideScope=!0),y}).filter(a=>U(a.path,o))}function ae(e,{scope:o,revision:r,path:t}={}){if(w(r)===0n)throw new Error("SVN r0 has no change comparison");let i=ee(e,{scope:o,revision:r}).find(s=>s.path===t);if(!i)throw new Error("SVN path is not an authorized changed member");if(i.kind!=="file")throw new Error("Only SVN file changes can be compared");let n=X(r),a=s=>s===null?{empty:!0}:{empty:!1,path:i.path,revision:s,pegRevision:s};return{path:i.path,action:i.action,left:a(i.action==="A"?null:n),right:a(i.action==="D"?null:r)}}import{randomBytes as je}from"node:crypto";function ce(e){if(!e||typeof e!="object")throw Error("Invalid SVN consent identity");let{sessionId:o,cwd:r,repositoryId:t,root:i,uuid:n,scope:a}=e;for(let s of[o,r,t])if(typeof s!="string"||!s||s.length>32768||s.includes("\0"))throw Error("Invalid SVN consent scope");if(L(i,"/","0"),_(a),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([o,r,t,i,n,a])}function re({now:e=Date.now,ttlMs:o=3e5,maxEntries:r=32,onRevoke:t=()=>{}}={}){if(!Number.isInteger(o)||o<1||o>3e5||!Number.isInteger(r)||r<1||r>32)throw Error("Invalid SVN consent bounds");if(typeof t!="function")throw Error("Invalid SVN revoke listener");let i=new Map,n=s=>{i.delete(s)&&t(s)},a=()=>{for(let[s,p]of i)p.expires<=e()&&n(s)};return{grant(s,{explicit:p=!1}={}){if(p!==!0)throw Error("Explicit SVN network consent required");let l=ce(s);a();for(let[f,m]of i)m.key===l&&n(f);for(;i.size>=r;)n(i.keys().next().value);let c=je(32).toString("hex");return i.set(c,{key:l,sessionId:s.sessionId,expires:e()+o}),c},assert(s,p){a();let l=ce(p),c=i.get(s);if(!c||c.key!==l)throw Object.assign(Error("SVN network consent missing, expired or identity changed"),{code:"vcs/svn-consent-required"})},revokeSession(s){for(let[p,l]of i)l.sessionId===s&&n(p)},revoke(s){n(s)},clear(){for(let s of[...i.keys()])n(s)}}}import{XMLParser as Ue,XMLValidator as Be}from"fast-xml-parser";function O(e){if(typeof e!="string"||Buffer.byteLength(e)>2097152||/<!DOCTYPE|<!ENTITY/i.test(e)||Be.validate(e)!==!0)throw Error("Invalid DAV XML");let o=new Ue({preserveOrder:!0,ignoreAttributes:!1,parseTagValue:!1,parseAttributeValue:!1,trimValues:!1}).parse(e),r=0,t=(i,n,a=0)=>{if(a>32)throw Error("DAV XML too deep");let s=Object.create(null),p="";for(let l of i){if(++r>1e4)throw Error("DAV XML too many nodes");if(Object.hasOwn(l,"#text")){p+=l["#text"];continue}let c=Object.keys(l).filter(v=>v!==":@");if(c.length!==1)throw Error("Invalid DAV XML node");let f=c[0];if(f==="?xml"){if(a!==0)throw Error("Unexpected XML declaration");continue}let m={...n},g={};for(let[v,E]of Object.entries(l[":@"]||{}))v==="@_xmlns"?m[""]=E:v.startsWith("@_xmlns:")?m[v.slice(8)]=E:g[v]=E;let y=f.split(":");if(y.length>2)throw Error("Invalid DAV XML name");let S=y.length===2?y[0]:"",x=y.at(-1),N=m[S],I=N==="DAV:"?"D:"+x:N==="http://subversion.tigris.org/xmlns/dav/"?"S:"+x:N==="svn:"?"L:"+x:N==="http://subversion.tigris.org/xmlns/svn/"?"P:"+x:N==="http://subversion.tigris.org/xmlns/custom/"?"C:"+x:null;if(!I)throw Error("Unsupported DAV namespace");let u=["L:added-path","L:deleted-path","L:modified-path","L:replaced-path"].includes(I);if(Object.keys(g).length&&(!u||Object.keys(g).some(v=>!["@_node-kind","@_text-mods","@_prop-mods","@_copyfrom-path","@_copyfrom-rev"].includes(v))))throw Error("Unexpected DAV attribute");let d=t(l[f],m,a+1);if(u&&typeof d!="string")throw Error("Invalid DAV path text");let h=u?{...g,"#text":d}:d;if(["D:response","D:propstat","L:log-item","L:added-path","L:deleted-path","L:modified-path","L:replaced-path"].includes(I))(s[I]??=[]).push(h);else{if(Object.hasOwn(s,I))throw Error("Duplicate DAV expanded name");s[I]=h}}if(!Object.keys(s).length)return p;if(p.trim())throw Error("Mixed DAV XML content");return s};return t(o,{})}function pe(e,{scope:o,revision:r}={}){w(r);let t=O(e),i=(c,f)=>{if(!c||typeof c!="object"||Array.isArray(c)||Object.keys(c).some(m=>!f.includes(m)))throw Error("Invalid DAV detail structure")};i(t,["L:log-report"]);let n=t["L:log-report"];if(i(n,["L:log-item"]),n["L:log-item"]?.length!==1)throw Error("One DAV detail revision required");let a=n["L:log-item"][0],s={"L:added-path":"A","L:deleted-path":"D","L:modified-path":"M","L:replaced-path":"R"};if(i(a,["D:version-name","D:comment","D:creator-displayname","L:date",...Object.keys(s)]),a["D:version-name"]!==r)throw Error("DAV detail revision mismatch");let p=c=>{let f=a[c]??"";if(typeof f!="string")throw Error("Invalid DAV detail text");return f},l=[];for(let[c,f]of Object.entries(s))for(let m of a[c]??[]){let{"@_node-kind":g,...y}=m;l.push({...y,"@_kind":g??"unknown","@_action":f})}return{revision:r,author:p("D:creator-displayname"),date:p("L:date"),message:p("D:comment"),changes:W(l,{scope:o,revision:r})}}function le(e,o){let r=O(e);if(!r||typeof r!="object"||Object.keys(r).length!==1||!Object.hasOwn(r,"L:log-report"))throw Error("Invalid DAV log root");let t=r["L:log-report"],i;if(t==="")i=[];else{if(!t||typeof t!="object"||Object.keys(t).some(s=>s!=="L:log-item")||!Array.isArray(t["L:log-item"]))throw Error("Invalid DAV log structure");i=t["L:log-item"]}if(i.length>101)throw Error("DAV log entries exceed limit");let n=i.map(s=>{if(!s||typeof s!="object"||Array.isArray(s)||Object.keys(s).some(l=>!["D:version-name","D:comment","D:creator-displayname","L:date"].includes(l)))throw Error("Unsupported DAV log item");let p=l=>{let c=s[l]??"";if(typeof c!="string")throw Error("Invalid DAV log text");return c};return{revision:p("D:version-name"),message:p("D:comment"),author:p("D:creator-displayname"),date:p("L:date")}}),a=C(n.map(s=>s.revision),o);return{snapshot:a.snapshot,entries:n.slice(0,a.revisions.length),nextRevision:a.nextRevision}}import{XMLParser as Ce,XMLValidator as Me}from"fast-xml-parser";function fe(e,{scope:o,revision:r}={}){if(w(r),typeof e!="string"||Buffer.byteLength(e,"utf8")>2*1024*1024||/<!DOCTYPE|<!ENTITY/i.test(e)||Me.validate(e)!==!0)throw new Error("Invalid SVN detail XML");let t=new Ce({ignoreAttributes:!1,parseAttributeValue:!1,parseTagValue:!1,trimValues:!1,isArray:l=>l==="logentry"||l==="path"}).parse(e),i=(l,c)=>{if(!l||typeof l!="object"||Array.isArray(l)||Object.keys(l).some(f=>!c.includes(f)&&!(f==="#text"&&typeof l[f]=="string"&&/^[ \t\r\n]*$/.test(l[f]))))throw new Error("Invalid SVN detail structure")};if(i(t,["?xml","log"]),i(t.log,["logentry"]),!Array.isArray(t.log.logentry)||t.log.logentry.length!==1)throw new Error("Expected one SVN revision");let n=t.log.logentry[0];if(i(n,["@_revision","author","date","msg","paths"]),n["@_revision"]!==r)throw new Error("SVN detail revision mismatch");let a=l=>{let c=n[l]??"";if(typeof c!="string")throw new Error("Invalid SVN detail text");return c},s=[];n.paths!==void 0&&!(typeof n.paths=="string"&&/^[ \t\r\n]*$/.test(n.paths))&&(i(n.paths,["path"]),s=n.paths.path??[]);let p=W(s,{scope:o,revision:r});return{revision:r,author:a("author"),date:a("date"),message:a("msg"),pathsAvailable:Object.hasOwn(n,"paths"),changes:p}}import{XMLParser as Xe,XMLValidator as Fe}from"fast-xml-parser";function de(e,o){let r=qe(e),t=C(r.map(i=>i.revision),o);return{snapshot:t.snapshot,entries:r.slice(0,t.revisions.length),nextRevision:t.nextRevision}}function qe(e){if(typeof e!="string"||Buffer.byteLength(e,"utf8")>2*1024*1024||/<!DOCTYPE|<!ENTITY/i.test(e)||Fe.validate(e)!==!0)throw new Error("Invalid SVN log XML");let r=new Xe({ignoreAttributes:!1,parseAttributeValue:!1,parseTagValue:!1,trimValues:!1,isArray:n=>n==="logentry"}).parse(e);if(!Object.hasOwn(r,"log")||Object.keys(r).some(n=>n!=="log"&&n!=="?xml"))throw new Error("Invalid SVN log root");let t=r.log;if(typeof t=="string"&&/^[ \t\r\n]*$/.test(t))return[];if(t&&typeof t=="object"&&typeof t["#text"]=="string"&&/^[ \t\r\n]*$/.test(t["#text"])&&delete t["#text"],!t||typeof t!="object"||Object.keys(t).some(n=>n!=="logentry")||!Array.isArray(t.logentry)||t.logentry.length>101)throw new Error("Invalid SVN log entries");let i=new Set;return t.logentry.map(n=>{if(!n||typeof n!="object"||Array.isArray(n)||Object.keys(n).some(p=>!["@_revision","author","date","msg","#text"].includes(p))||"#text"in n&&(typeof n["#text"]!="string"||!/^[ \t\r\n]*$/.test(n["#text"])))throw new Error("Unsupported SVN log entry structure");let a=n["@_revision"];if(w(a),i.has(a))throw new Error("Duplicate SVN revision");i.add(a);let s=p=>{let l=n[p]??"";if(typeof l!="string")throw new Error("Invalid SVN log text");return l};return{revision:a,author:s("author"),date:s("date"),message:s("msg")}})}import{XMLParser as He,XMLValidator as $e}from"fast-xml-parser";function R(e,{root:o}={}){if(L(o,"/","0"),typeof e!="string"||!e||e.length>32768||/[\\?#]/.test(e)||[...e].some(n=>n.charCodeAt(0)<=32||n.charCodeAt(0)===127))throw Error("Invalid DAV href");if(!(e.startsWith("/")&&!e.startsWith("//"))&&!e.startsWith("https://"))throw Error("DAV href must be absolute path or HTTPS URL");let r=new URL(e,new URL(o).origin);if(r.protocol!=="https:"||r.origin!==new URL(o).origin||r.username||r.password||r.search||r.hash)throw Error("DAV href outside authorized origin");if(e.startsWith("https://")&&r.href!==e||e.startsWith("/")&&r.pathname!==e)throw Error("Noncanonical DAV href");let t=r.pathname.split("/").slice(1);t.at(-1)===""&&t.pop();for(let n of t){let a=decodeURIComponent(n);if(!n||a==="."||a===".."||[...a].some(s=>s.charCodeAt(0)<32||s.charCodeAt(0)===127||s==="/"||s.charCodeAt(0)===92))throw Error("Unsafe DAV href path")}let i=new URL(o).pathname.replace(/[/]$/,"");if(i&&r.pathname!==i&&!r.pathname.startsWith(i+"/"))throw Error("DAV href outside repository root");return r.href}function ue(e,{target:o,revision:r,root:t,uuid:i}={}){if(typeof e!="string"||Buffer.byteLength(e)>2097152||/<!DOCTYPE|<!ENTITY/i.test(e)||$e.validate(e)!==!0)throw Error("Invalid SVN node kind XML");let a=new He({ignoreAttributes:!1,parseAttributeValue:!1,parseTagValue:!1,trimValues:!1,isArray:p=>p==="entry"}).parse(e)?.info?.entry;if(!Array.isArray(a)||a.length!==1)throw Error("Expected one SVN node kind entry");let s=a[0];if(s.url!==o||s["@_revision"]!==r||s.repository?.root!==t||s.repository?.uuid!==i)throw Error("SVN node kind identity mismatch");if(!["file","dir"].includes(s["@_kind"]))throw Error("Unknown SVN node kind");return s["@_kind"]}function he(e,{root:o,target:r}={}){let t=O(e),i=(c,f)=>{if(!c||typeof c!="object"||Array.isArray(c)||Object.keys(c).some(m=>!f.includes(m)))throw Error("Invalid DAV node kind structure")};i(t,["D:multistatus"]),i(t["D:multistatus"],["D:response"]);let n=t["D:multistatus"]["D:response"];if(n?.length!==1)throw Error("Expected one DAV node kind response");let a=n[0];i(a,["D:href","D:propstat"]);let s=R(a["D:href"],{root:o});if(a["D:propstat"]?.length!==1)throw Error("Expected one DAV node kind status");let p=a["D:propstat"][0];if(i(p,["D:status","D:prop"]),!/^HTTP[/]1[.][01] 200(?: OK)?$/.test(p["D:status"]))throw Error("DAV node kind unavailable");i(p["D:prop"],["D:resourcetype"]);let l=p["D:prop"]["D:resourcetype"];if(l===""){if(s!==r)throw Error("DAV node kind target mismatch");return"file"}if(i(l,["D:collection"]),l["D:collection"]!=="")throw Error("Unknown DAV node kind");if(s!==r&&s!==r+"/")throw Error("DAV node kind target mismatch");return"dir"}import{XMLParser as We,XMLValidator as ze}from"fast-xml-parser";function me(e,{target:o,allowEmpty:r=!1}={}){if(typeof o!="string"||!o||o.length>32768)throw new Error("Invalid SVN property target");if(typeof e!="string"||Buffer.byteLength(e,"utf8")>2*1024*1024||/<!DOCTYPE|<!ENTITY/i.test(e)||ze.validate(e)!==!0)throw new Error("Invalid SVN property XML");let t=new We({ignoreAttributes:!1,parseAttributeValue:!1,parseTagValue:!1,trimValues:!1,isArray:c=>c==="target"||c==="property"}).parse(e),i=(c,f)=>{if(!c||typeof c!="object"||Array.isArray(c)||Object.keys(c).some(m=>!f.includes(m)&&!(m==="#text"&&typeof c[m]=="string"&&/^[ \t\r\n]*$/.test(c[m]))))throw new Error("Invalid SVN property structure")};if(i(t,["?xml","properties"]),r===!0&&typeof t.properties=="string"&&/^[ \t\r\n]*$/.test(t.properties))return{target:o,names:[],special:!1};i(t.properties,["target"]);let n=t.properties.target;if(!Array.isArray(n)||n.length!==1)throw new Error("Expected one SVN property target");let a=n[0];if(i(a,["@_path","property"]),a["@_path"]!==o)throw new Error("SVN property target mismatch");let s=a.property??[];if(!Array.isArray(s)||s.length>1e4)throw new Error("Too many SVN properties");let p=[],l=new Set;for(let c of s){i(c,["@_name"]);let f=c["@_name"];if(typeof f!="string"||!f||f.length>1024||/[\x00-\x20\x7f]/.test(f)||l.has(f))throw new Error("Invalid SVN property name");l.add(f),p.push(f)}return{target:o,names:p,special:l.has("svn:special")}}var Se=2*1024*1024,ve=15e3,ge=3e5,we=50,Ge=/^[a-f0-9]{64}$/,te=e=>{if(typeof e!="string"||!Ge.test(e))throw Error("Invalid SVN change ID")},oe=(e,o,r)=>{let t=e.changes.find(i=>i.id===o);return r===void 0||t?.kind===r?t:void 0},ye=e=>e.slice(0,e.lastIndexOf("@")),Ye=(e,o,r,t)=>Ke("sha256").update(JSON.stringify([e.root,e.uuid,e.scope,o,r,t])).digest("hex"),Je=(e,o,r)=>{if(!(typeof e=="string"||Buffer.isBuffer(e))||Buffer.byteLength(e)>r)throw Error("SVN transport output exceeds byte limit or has invalid type");return Buffer.isBuffer(e)&&o.args[0]!=="cat"?new TextDecoder("utf-8",{fatal:!0}).decode(e):e},z=(e,o,r)=>({identity:e,token:o,signal:r,maxBytes:Se});function De({resolveIdentity:e,transport:o,now:r=Date.now,timeoutMs:t=ve,transportFormat:i="cli"}={}){if(!["cli","dav"].includes(i))throw Error("Invalid SVN transport format");if(!Number.isInteger(t)||t<1||t>ve)throw Error("Invalid SVN runtime deadline");if(typeof e!="function")throw Error("Local identity resolver required");let n=!1,a=()=>{if(n)throw new DOMException("SVN runtime disposed","AbortError")},s=new Map,p=u=>{for(let d of s.get(u)||[])d.controller.abort(new DOMException("SVN consent revoked","AbortError"))},l=re({now:r}),c=re({now:r,onRevoke:u=>p(u)}),f=typeof o=="function",m=()=>{if(!f)throw Error("SVN remote transport disabled")},g=async(u,d)=>{a(),d?.throwIfAborted();let h=await e(u,d);return a(),d?.throwIfAborted(),{...h,sessionId:u.sessionId,cwd:h.cwd,repositoryId:u.repositoryId}},y=u=>u.revision===void 0?{}:(w(u.revision),{snapshot:u.revision}),S=async(u,d,h)=>{let v=await g(u,h);return c.assert(d,v),v},x=async(u,d,h)=>{h?.throwIfAborted(),c.assert(d,await g(u,h))},N=async(u,d)=>{a(),M(u,d.identity),d.signal?.throwIfAborted(),c.assert(d.token,d.identity);let h=new AbortController,v=()=>h.abort(d.signal.reason);d.signal?.addEventListener("abort",v,{once:!0});let E={controller:h,sessionId:d.identity.sessionId};s.has(d.token)||s.set(d.token,new Set),s.get(d.token).add(E);let D=setTimeout(()=>h.abort(new DOMException("SVN transport deadline exceeded","TimeoutError")),t);try{let b=async()=>{h.signal.throwIfAborted();let T=await g({sessionId:d.identity.sessionId,repositoryId:d.identity.repositoryId},h.signal);c.assert(d.token,T),h.signal.throwIfAborted()},A=await o(u,{...d,timeoutMs:t,signal:h.signal,assertAuthorized:b});return h.signal.throwIfAborted(),Je(A,u,d.maxBytes)}catch(b){throw h.signal.aborted?h.signal.reason:b}finally{let b=s.get(d.token);b?.delete(E),b?.size||s.delete(d.token),clearTimeout(D),d.signal?.removeEventListener("abort",v)}},I={async describe(u,{signal:d}={}){let h=await g(u,d),v=y(h),E=l.grant(h,{explicit:!0});return{...v,offer:E,root:h.root,uuid:h.uuid,scope:h.scope,origin:new URL(h.root).origin,expiresAt:r()+ge,remoteEnabled:f}},async approve(u,{offer:d,explicit:h=!1,signal:v}={}){if(h!==!0)throw Error("Explicit SVN network consent required");let E=await g(u,v);l.assert(d,E),m();let D=y(E),b=c.grant(E,{explicit:!0});return l.revoke(d),{...D,token:b,expiresAt:r()+ge}},async log(u,{token:d,snapshot:h,cursor:v,limit:E,path:D,signal:b}={}){let A=await S(u,d,b);m();let T=$({...A,snapshot:h,cursor:v,limit:E,path:D});b?.throwIfAborted();let V=await N(T,z(A,d,b));return await x(u,d,b),(i==="dav"?le:de)(V,T)},async detail(u,{token:d,snapshot:h,revision:v,signal:E}={}){let D=await S(u,d,E);m();let b=H({...D,snapshot:h,revision:v}),A=await N(b,z(D,d,E));await x(u,d,E);let T=i==="dav"?{...pe(A,b),pathsAvailable:!0}:fe(A,b);return{...T,snapshot:h,changes:T.changes.map(V=>({...V,id:Ye(D,h,v,V)}))}},async compare(u,{token:d,snapshot:h,revision:v,id:E,signal:D}={}){te(E);let b=await I.detail(u,{token:d,snapshot:h,revision:v,signal:D}),A=oe(b,E);if(!A)throw Error("SVN change is not selected revision member");let T=await S(u,d,D),V=ae(b.changes.map(({id:k,...j})=>j),{scope:T.scope,revision:v,path:A.path}),P=async k=>{if(k.empty)return{label:"empty",text:"",absent:!0};let j=await S(u,d,D),B=L(j.root,k.path,k.pegRevision),q=z(j,d,D),Oe=await N({args:["proplist","--xml","--non-interactive","--no-auth-cache","-r",k.revision,"--",B]},q);if(await x(u,d,D),me(Oe,{target:ye(B),allowEmpty:!0}).special)throw Error("SVN special files cannot be compared");let Z=await N({args:["cat","--non-interactive","--no-auth-cache","-r",k.revision,"--",B]},q);await x(u,d,D);let Q=Buffer.isBuffer(Z)?Z:Buffer.from(Z);if(Q.length>Se)throw Error("SVN text exceeds 2 MiB");if(Q.includes(0))return{label:"r"+k.revision,text:"",binary:!0};let se;try{se=new TextDecoder("utf-8",{fatal:!0}).decode(Q)}catch{return{label:"r"+k.revision,text:"",binary:!0}}return{label:"r"+k.revision,text:se}},G=async k=>{let j=await S(u,d,D),B=L(j.root,k.path,k.pegRevision),q=await N({args:["info","--xml","--non-interactive","--no-auth-cache","-r",k.revision,"--",B]},z(j,d,D));return await x(u,d,D),ue(q,{target:ye(B),revision:k.revision,root:j.root,uuid:j.uuid})},F=V.left;A.action==="R"&&!F.empty&&await G(F)==="dir"&&(F={empty:!0});let Y=await P(F),J=await P(V.right);return{path:A.path,revision:v,snapshot:h,left:Y,right:J,binary:!!(Y.binary||J.binary),preview:ie({left:Y,right:J,allowLarge:!1})}},async trace(u,{token:d,snapshot:h,revision:v,id:E,cursor:D,limit:b=we,signal:A}={}){te(E);let T=await I.detail(u,{token:d,snapshot:h,revision:v,signal:A}),V=oe(T,E,"file");if(!V)throw Error("SVN tracing requires selected file member");let P=V.action==="D"?X(v):v;return{...await I.log(u,{token:d,snapshot:P,cursor:D??P,limit:b,path:V.path,signal:A}),selectionSnapshot:h,selectionRevision:v,path:V.path,pegRevision:P,stopOnCopy:!0,copySourceOutsideScope:!!V.copySourceOutsideScope}},async copytrace(u,{token:d,snapshot:h,revision:v,id:E,cursor:D,limit:b=we,signal:A}={}){te(E);let T=await I.detail(u,{token:d,snapshot:h,revision:v,signal:A}),V=oe(T,E,"file");if(!V||V.copySourceOutsideScope||V.copyFromPath===void 0||V.copyFromRevision===void 0)throw Error("Selected file has no authorized copy source");let P=V.copyFromRevision;return{...await I.log(u,{token:d,snapshot:P,cursor:D??P,limit:b,path:V.copyFromPath,signal:A}),selectionSnapshot:h,selectionRevision:v,path:V.copyFromPath,pegRevision:P,sourceForPath:V.path,stopOnCopy:!0}},async revoke(u,{token:d,signal:h}={}){return await S(u,d,h),c.revoke(d),p(d),{revoked:!0}},dispose(){n=!0,l.clear(),c.clear();for(let u of s.keys())p(u)},revokeSession(u){l.revokeSession(u),c.revokeSession(u);for(let[d,h]of s)[...h].some(v=>v.sessionId===u)&&p(d)}};return I}var ne=(e,o)=>({ok:!1,error:{code:e,message:o,details:{}}}),Ee={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"],copytrace:["token","snapshot","revision","id","cursor","limit"]};function Ze(e,o){if(!o||typeof o!="object"||Array.isArray(o))throw Error("Object payload required");if(Object.keys(o).some(r=>!["sessionId","repositoryId",...Ee[e]].includes(r)))throw Error("Unknown SVN request field");for(let r of["sessionId","repositoryId"])if(typeof o[r]!="string"||!o[r]||o[r].length>1024||o[r].includes("\0"))throw Error("Invalid SVN Session address");if(e==="approve"&&(o.explicit!==!0||typeof o.offer!="string"||!/^[a-f0-9]{64}$/.test(o.offer)))throw Error("Explicit scoped offer required");if(!["describe","approve"].includes(e)){if(typeof o.token!="string"||!/^[a-f0-9]{64}$/.test(o.token))throw Error("SVN consent token required");if(e!=="revoke"&&w(o.snapshot),o.revision!==void 0&&w(o.revision),["detail","compare","trace","copytrace"].includes(e)&&o.revision===void 0)throw Error("Selected SVN revision required");if(o.cursor!==void 0&&w(o.cursor),o.limit!==void 0&&(!Number.isInteger(o.limit)||o.limit<1||o.limit>100))throw Error("Invalid SVN page size");if(["compare","trace","copytrace"].includes(e)&&(typeof o.id!="string"||!/^[a-f0-9]{64}$/.test(o.id)))throw Error("Selected SVN change ID required")}}function be(e,{timeoutMs:o=15e3}={}){if(!Number.isInteger(o)||o<1||o>15e3)throw Error("Invalid SVN RPC deadline");let r=0;return async(t,i,n)=>{let a=t.startsWith("vcs/svn-")?t.slice(8):"";if(!Object.hasOwn(Ee,a))return ne("vcs/not-found","Unknown SVN endpoint");if(a!=="revoke"&&r>=2)return ne("vcs/busy","SVN history requests busy");a!=="revoke"&&r++;let s=new AbortController,p=()=>s.abort(n.reason);n?.addEventListener("abort",p,{once:!0}),n?.aborted&&p();let l=setTimeout(()=>s.abort(new DOMException("SVN operation deadline exceeded","TimeoutError")),o);try{n?.throwIfAborted(),Ze(a,i);let{sessionId:c,repositoryId:f,...m}=i,g=await e[a]({sessionId:c,repositoryId:f},{...m,signal:s.signal});return s.signal.throwIfAborted(),{ok:!0,value:g}}catch(c){let f=n?.aborted||c?.name==="AbortError"?"vcs/cancelled":["vcs/rediscover-required","vcs/svn-consent-required"].includes(c?.code)?c.code:"vcs/operation-failed",m=s.signal.aborted?s.signal.reason:c,g=m instanceof Error?m.message:String(m);return ne(f,g)}finally{clearTimeout(l),n?.removeEventListener("abort",p),a!=="revoke"&&r--}}}var er=new Set(["describe","approve","log","detail","compare","trace","copytrace","revoke"]);function xe(e,o,{transport:r,transportFormat:t="cli",now:i=Date.now,timeoutMs:n=15e3}={}){let a,s=Qe(e,o,4,{now:i,bindSvnIdentityResolver:c=>{a=c}}),p=De({resolveIdentity:a,transport:r,transportFormat:t,now:i,timeoutMs:n}),l=be(p,{timeoutMs:n});return{handle(c,f,m){let g=c.startsWith("vcs/svn-")?c.slice(8):"";return er.has(g)?l(c,f,m):s(c,f,m)},revokeSession:c=>p.revokeSession(c),dispose:()=>p.dispose()}}import{request as rr}from"node:https";function Ae({request:e=rr}={}){return async(o,{origin:r,method:t="GET",body:i="",depth:n,label:a,signal:s,maxBytes:p=2097152,timeoutMs:l=15e3}={})=>{let c=new URL(o);if(c.protocol!=="https:"||c.origin!==r||c.username||c.password||c.hash||c.href!==o||!["GET","PROPFIND","REPORT"].includes(t)||typeof i!="string"||Buffer.byteLength(i)>65536||!Number.isInteger(p)||p<1||p>2097152||!Number.isInteger(l)||l<1||l>15e3)throw Error("Invalid bounded HTTPS request");if(n!==void 0&&(t!=="PROPFIND"||n!==0))throw Error("Invalid DAV depth");if(a!==void 0){if(t!=="PROPFIND"||n!==0)throw Error("Invalid DAV label");w(a)}let f={...n===0?{Depth:"0"}:{},...a!==void 0?{Label:a}:{}};return s?.throwIfAborted(),new Promise((m,g)=>{let y,S,x=null,N=null,I=0,u=[],d=D=>{x??=D,S?.destroy(),y?.destroy()},h=()=>d(s.reason??new DOMException("HTTPS cancelled","AbortError")),v=setTimeout(()=>d(new DOMException("HTTPS deadline exceeded","TimeoutError")),l),E=()=>{clearTimeout(v),s?.removeEventListener("abort",h)};try{y=e(c,{method:t,agent:!1,rejectUnauthorized:!0,maxHeaderSize:16384,headers:{...f,"Accept-Encoding":"identity","Content-Type":"text/xml; charset=utf-8","Content-Length":String(Buffer.byteLength(i))}},D=>{S=D;let b=S.statusCode??0;if(b>=300&&b<400){d(Error("HTTPS redirects forbidden"));return}if(b<200||b>=300){d(Error("HTTPS status "+b));return}if(S.headers["content-encoding"]&&S.headers["content-encoding"]!=="identity"){d(Error("HTTPS compressed responses forbidden"));return}let A=S.headers["content-length"];if(A!==void 0&&(!/^[0-9]+$/.test(A)||Number(A)>p)){d(Error("HTTPS response exceeds byte limit"));return}S.on("data",T=>{I+=T.length,I>p?d(Error("HTTPS response exceeds byte limit")):x||u.push(T)}),S.on("error",d),S.on("end",()=>{if(!S.complete){d(Error("HTTPS response incomplete"));return}N=Buffer.concat(u)})}),y.on("error",D=>{x??=D}),y.once("close",()=>{E(),x?g(x):N!==null?m(N):g(Error("HTTPS response closed before completion"))}),s?.addEventListener("abort",h,{once:!0}),s?.aborted?h():y.end(i)}catch(D){E(),y?.destroy(),g(D)}})}}var tr=e=>e.replaceAll("&","&").replaceAll("<","<").replaceAll(">",">").replaceAll('"',""").replaceAll("'","'");function Ve(e,o){let r=M(e,o),t=r.args;if(t[0]!=="log")throw Error("DAV log report requires readonly log plan");let i=t.includes("--verbose"),n=t[i?9:8].split(":"),a=t[i?7:6],s=r.path==="/"?"":r.path.slice(1),p='<?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>"+a+"</S:limit><S:strict-node-history/>"+(i?"<S:discover-changed-paths/>":"")+"<S:path>"+tr(s)+"</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:r.pegRevision,path:r.path,requiresVerifiedBaseline:!0}}function Ie(e,{root:o,uuid:r,target:t}={}){let i=O(e),n=(f,m)=>{if(!f||typeof f!="object"||Array.isArray(f)||Object.keys(f).some(g=>!m.includes(g)&&!(g==="#text"&&typeof f[g]=="string"&&f[g].trim()==="")))throw Error("Unsupported DAV discovery structure")};n(i,["?xml","D:multistatus"]);let a=i["D:multistatus"];if(n(a,["D:response"]),!Array.isArray(a["D:response"])||a["D:response"].length!==1)throw Error("DAV discovery requires one response");let s=a["D:response"][0];if(n(s,["D:href","D:propstat"]),R(s["D:href"],{root:o})!==t)throw Error("DAV response target mismatch");if(!Array.isArray(s["D:propstat"])||s["D:propstat"].length!==1)throw Error("DAV discovery requires one property status");let p=s["D:propstat"][0];if(n(p,["D:prop","D:status"]),!/^HTTP[/]1[.][01] 200(?: OK)?$/.test(p["D:status"]))throw Error("DAV discovery properties unavailable");let l=p["D:prop"];if(n(l,["S:repository-uuid","D:version-controlled-configuration"]),l["S:repository-uuid"]!==r||typeof r!="string"||!/^[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}$/i.test(r))throw Error("DAV UUID does not match local identity");let c=l["D:version-controlled-configuration"];return n(c,["D:href"]),{uuid:r,vcc:R(c["D:href"],{root:o}),target:t}}function Te(e,{root:o,revision:r}={}){w(r);let t=O(e),i=(f,m)=>{if(!f||typeof f!="object"||Array.isArray(f)||Object.keys(f).some(g=>!m.includes(g)))throw Error("Invalid DAV baseline structure")};i(t,["D:multistatus"]);let n=t["D:multistatus"];if(i(n,["D:response"]),n["D:response"]?.length!==1)throw Error("One DAV baseline response required");let a=n["D:response"][0];i(a,["D:href","D:propstat"]);let s=R(a["D:href"],{root:o});if(a["D:propstat"]?.length!==1)throw Error("One DAV baseline status required");let p=a["D:propstat"][0];if(i(p,["D:status","D:prop"]),p["D:status"]!=="HTTP/1.1 200 OK")throw Error("DAV baseline status unavailable");let l=p["D:prop"];if(i(l,["D:version-name","D:baseline-collection"]),l["D:version-name"]!==r)throw Error("DAV baseline revision mismatch");i(l["D:baseline-collection"],["D:href"]);let c=R(l["D:baseline-collection"]["D:href"],{root:o});if(!c.endsWith("/"))throw Error("DAV baseline collection must end in slash");return{revision:r,baseline:s,collection:c}}function Ne({request:e,assertAuthorized:o}={}){if(typeof e!="function"||typeof o!="function")throw Error("DAV chain requires transport and authorization");return async(r,{revision:t,signal:i}={})=>{w(t),L(r.root,r.scope,t);let n=r.root,a=new URL(n).origin,s=n.endsWith("/")?n:n+"/",p=async(m,g)=>{i?.throwIfAborted(),await o(r,{signal:i}),i?.throwIfAborted();let y=await e(m,{origin:a,signal:i,...g});if(i?.throwIfAborted(),await o(r,{signal:i}),i?.throwIfAborted(),!Buffer.isBuffer(y)||y.length>2097152)throw Error("Invalid DAV response bytes");return new TextDecoder("utf-8",{fatal:!0}).decode(y)},l=await p(s,{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:c}=Ie(l,{root:n,target:s,uuid:r.uuid}),f=await p(c,{method:"PROPFIND",depth:0,label:t,body:'<D:propfind xmlns:D="DAV:"><D:prop><D:baseline-collection/><D:version-name/></D:prop></D:propfind>'});return{...Te(f,{root:n,revision:t}),uuid:r.uuid}}}function Le(e,{root:o,target:r}={}){let t=O(e),i=(c,f)=>{if(!c||typeof c!="object"||Array.isArray(c)||Object.keys(c).some(m=>!f.includes(m)))throw Error("Invalid DAV property names structure")};i(t,["D:multistatus"]);let n=t["D:multistatus"];if(i(n,["D:response"]),n["D:response"]?.length!==1)throw Error("One DAV property response required");let a=n["D:response"][0];if(i(a,["D:href","D:propstat"]),R(a["D:href"],{root:o})!==r)throw Error("DAV property target mismatch");if(a["D:propstat"]?.length!==1)throw Error("One DAV property status required");let s=a["D:propstat"][0];if(i(s,["D:status","D:prop"]),s["D:status"]!=="HTTP/1.1 200 OK")throw Error("DAV property names unavailable");let p=s["D:prop"];if(p!==""&&(!p||typeof p!="object"||Array.isArray(p)||Object.keys(p).length>1e3||Object.entries(p).some(([c,f])=>!/^[DSPC]:[^:]+$/.test(c)||f!=="")))throw Error("DAV property names must be empty");let l=p===""?[]:Object.keys(p);return{names:l,special:l.includes("P:special")}}var K=e=>e.replaceAll("&","&").replaceAll("<","<").replaceAll(">",">").replaceAll('"',""");function ke({request:e}={}){if(typeof e!="function")throw Error("Bounded DAV request required");return async(o,{identity:r,assertAuthorized:t,signal:i}={})=>{let n=M(o,r),a=n.args[0],s=Ne({request:e,assertAuthorized:t}),p=await s(r,{revision:n.pegRevision,signal:i}),l=async(y,S)=>{i?.throwIfAborted(),await t(),i?.throwIfAborted();let x=await e(y,{origin:new URL(r.root).origin,...S,signal:i});if(i?.throwIfAborted(),await t(),i?.throwIfAborted(),!Buffer.isBuffer(x)||x.length>2097152)throw Error("Invalid DAV transport bytes");return x};if(a==="log"){let y=Ve(o,r);return l(p.collection,{method:"REPORT",body:y.body})}let c=n.path.slice(1).split("/").map(y=>encodeURIComponent(y).replace(/[!'()*]/g,S=>"%"+S.charCodeAt(0).toString(16).toUpperCase())).join("/"),f=R(p.collection+c,{root:r.root});if(a==="info"){let y=await l(f,{method:"PROPFIND",depth:0,body:'<D:propfind xmlns:D="DAV:"><D:prop><D:resourcetype/></D:prop></D:propfind>'}),S=he(new TextDecoder("utf-8",{fatal:!0}).decode(y),{root:r.root,target:f}),x=n.args.at(-1).slice(0,n.args.at(-1).lastIndexOf("@"));return'<info><entry kind="'+S+'" revision="'+n.pegRevision+'"><url>'+K(x)+"</url><repository><root>"+K(r.root)+"</root><uuid>"+K(r.uuid)+"</uuid></repository></entry></info>"}let m=await l(f,{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:r.root,target:f});if(a==="proplist")return'<properties><target path="'+K(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 l(f,{method:"GET"})}}function Jt(e,o){return xe(e,o,{transportFormat:"dav",transport:ke({request:Ae()})})}export{Jt as createSvnHttpsHost};
|
package/docs/RELEASING.md
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
## 发布内容
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
每次改动完成并验证通过后,按授权渠道完成正式发布,不停留在本地修改或只推标签。默认 `both` 联合 npm/GitHub;明确仅 GitHub 的版本使用 `github-only`,不接触 npm token、registry 或 latest。渠道例外写入该版本说明,不能据 GitHub 版本号推断 npm 可用版本。版本使用 `vX.Y.Z`:X 为大版本,Y 为小版本/新增功能,Z 为 BUG 修复;递增前一位时后面的位归零。失败门禁必须先修复,不能为了发布跳过验证。发布不自动安装或重启宿主。
|
|
8
8
|
|
|
9
9
|
每个新版本必须有独立的中文发布说明,放在 `docs/releases/版本号.md`。按“本版变化、安装、限制、验证”组织,不把自动提交列表、开发轮次或内部恢复记录当作用户说明。没有新增功能时明确写出,不夸大改动。
|
|
10
10
|
|
|
@@ -12,37 +12,69 @@
|
|
|
12
12
|
|
|
13
13
|
## 1. 本地检查
|
|
14
14
|
|
|
15
|
-
需要 Node.js 22+、Git、SVN、svnadmin。在干净检出中执行:
|
|
15
|
+
需要 Node.js 22+、Git、SVN、svnadmin、OpenSSL。在干净检出中执行:
|
|
16
16
|
|
|
17
17
|
npm ci --include=dev
|
|
18
|
-
npm run
|
|
18
|
+
npm run build
|
|
19
|
+
npm run test:release
|
|
19
20
|
npm run verify:package
|
|
20
21
|
npm run test:install
|
|
21
22
|
|
|
22
23
|
测试失败或跳过须调查;隔离安装只检查包和宿主注册,不是用户实际 DSH 验收。
|
|
23
24
|
|
|
25
|
+
新增宿主兼容声明时,还需对精确宿主执行隔离检查:
|
|
26
|
+
|
|
27
|
+
```sh
|
|
28
|
+
npm run test:host-compat -- "<installed @deepseek-ai/dsh/package.json>"
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
参数是已安装 DSH 包的清单文件,不是运行时目录。桌面 ASAR 构建使用对应 Electron 的 `ELECTRON_RUN_AS_NODE=1` 执行脚本;若 ui-slots 是 shell 内建种子、未提供 Node 包,可额外传入从同一精确版本包解出的 ui-slots 清单绝对路径。脚本核对种子版本,不静默替换为 mock。宿主测试不启动 profile、监听端口或安装插件;记录实际测试的宿主版本,不能用一个宿主的结果声称整个版本矩阵本次均通过。
|
|
32
|
+
|
|
24
33
|
## 2. 固定版本并验证
|
|
25
34
|
|
|
26
35
|
1. 提交并推送版本修改,创建新标签 `vX.Y.Z`;不移动已有标签。
|
|
27
36
|
2. 等待标签 CI 的 Windows/Ubuntu × Node 22/24 全部成功。
|
|
28
37
|
3. 手动在同一标签运行 **Browser layout checks**,等待成功。
|
|
29
|
-
4.
|
|
38
|
+
4. 在同一标签运行 **Controlled host compatibility**,提供受控离线宿主工件 run ID 和四个精确版本的 SHA-256。每个版本必须有自己的真实合同结果。
|
|
39
|
+
5. 发布预检自动下载四份候选包与浏览器/宿主证据,绑定完整 tag ref、提交 SHA、run ID 和 attempt;核对内嵌版本、零跳过测试摘要及四包字节一致。缺失或失败均阻断。
|
|
30
40
|
|
|
31
41
|
分支检查不能代替同标签检查。失败后先定位;需要修改源码或测试时使用新的版本和标签。
|
|
32
42
|
|
|
33
|
-
|
|
43
|
+
### 浏览器/宿主证据绑定实际候选包
|
|
44
|
+
|
|
45
|
+
浏览器与宿主合同任务不重新构建插件:先以 `npm ci --include=dev --ignore-scripts` 安装测试依赖,再由[发布预检脚本](<https://github.com/f-e-n-g-0531/dsh-vcs/blob/main/scripts/release-preflight.mjs>)的 `select-ci` 选定同标签、同提交的成功 CI run/attempt,下载全部四矩阵候选;`consume` 校验版本、测试零失败/取消/跳过/待办及四包字节一致后,将候选包里的发行文件作为本次测试输入。
|
|
46
|
+
|
|
47
|
+
消费过程生成 `consumed-candidate.json` 收据,记录候选归档 SHA-256、CI run/attempt 和归档位置。生成 Chrome 或宿主成功报告前,再核对原归档及每个已消费发行文件的实际字节;变化即失败,不能用重新构建的文件替代。报告中的 `candidateSha256`、`ciRun`、`ciAttempt` 必须与最终发布预检选中的四份 CI 候选一致,同时仍核对各自的完整 tag ref、提交 SHA 和证据任务 run/attempt。仅“同一提交测试通过”而没有这条字节关联,不足以放行;CI 重跑后也必须重新取得对应候选的浏览器与四版本宿主证据。
|
|
48
|
+
|
|
49
|
+
## 受控宿主准备与离线合同
|
|
50
|
+
|
|
51
|
+
[宿主工件工作流](<https://github.com/f-e-n-g-0531/dsh-vcs/blob/main/.github/workflows/host-fixtures.yml>)仅允许默认分支手动执行,在 GitHub 托管的 Ubuntu runner 上为四个精确版本分别准备真实宿主:`0.1.7-rc.2`、`0.2.0-rc.2`、`0.2.1-rc.2`、`0.2.1-rc.5`。这是明确授权的**在线准备阶段**,不是离线安装,也不使用日常 DSH 的配置或依赖目录。
|
|
52
|
+
|
|
53
|
+
工作流通过[准备脚本](<https://github.com/f-e-n-g-0531/dsh-vcs/blob/main/scripts/prepare-host-fixture.mjs>)固定同版本的官方 DSH 与 ui-slots 包,仅从 `https://registry.npmjs.org` 解析依赖。先生成锁文件并校验精确版本、下载地址及 SHA-512 integrity,再按锁文件安装;两次 npm 操作均禁用安装脚本。工件同时保存锁文件及准备记录,包括锁文件 SHA-256、准备源码 SHA、run ID 和 attempt。不启动宿主,不安装插件,不接触运行中的 profile。
|
|
54
|
+
|
|
55
|
+
准备完成后,在待发布标签运行[宿主兼容工作流](<https://github.com/f-e-n-g-0531/dsh-vcs/blob/main/.github/workflows/host-compat.yml>),提供成功准备任务的 `fixture_run` 和四个版本各自 `host.tar.gz` 的 SHA-256 映射 `fixture_hashes`。工作流只接受本仓库默认分支的受控准备工作流结果,拒绝其他工作流、fork/PR 以及不匹配的摘要;验证归档路径后提取真实依赖树。**宿主合同使用已准备的离线依赖树**,不再在线安装宿主、不启动 profile,也不能用 mock 或一个版本的结果冒充四版本通过。工作流下载证据及安装插件项目的测试依赖仍需要网络,不表示整项 CI 在断网环境运行。
|
|
56
|
+
|
|
57
|
+
若维护者已有完整可信依赖树,也可本地生成相同宿主归档:
|
|
58
|
+
|
|
59
|
+
```sh
|
|
60
|
+
node scripts/release-host-fixture.mjs pack <含node_modules和package-lock.json的根目录> <精确版本> <新的输出目录>
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
归档来源必须包含完整依赖与锁文件;根目录内合法依赖链接可物化为普通文件,指向根目录外的链接或外部依赖 store 会被拒绝,须先将依赖完整准备在根目录内。归档本身不得保留链接或越界项。本地生成归档不绕过发布工作流的受控来源检查;四版本工件或真实合同不完整时发布仍会阻断。
|
|
64
|
+
|
|
65
|
+
## 3. 按渠道发布
|
|
34
66
|
|
|
35
67
|
在 Actions 从 main 运行 **Publish GitHub and npm**,输入已验证的标签。
|
|
36
68
|
|
|
37
|
-
|
|
69
|
+
工作流检出该标签,验证同标签证据,直接使用已验证的候选 tgz,不重新生成另一份包。`both` 先上传 npm,再验证公开 npm 元数据与实际下载字节,最后公开 GitHub Release;`github-only` 跳过全部 npm 步骤,仅上传已验证候选及 SHA-256 校验文件。
|
|
38
70
|
|
|
39
|
-
|
|
71
|
+
仅 `both` 渠道需配置 `NPM_AUTOMATION_TOKEN`,具有该包发布权限并符合 npm 2FA 策略;`github-only` 无需该凭据。凭据只用于上传步骤;不要写进源码、发布说明或聊天。
|
|
40
72
|
|
|
41
73
|
## 4. 发布后核验
|
|
42
74
|
|
|
43
|
-
- npm 版本与 latest 正确,实际 tarball
|
|
75
|
+
- `both`:npm 版本与 latest 正确,实际 tarball 可下载;`github-only` 不修改或要求 npm。
|
|
44
76
|
- GitHub Release 非草稿、非预发布,附件可下载。
|
|
45
|
-
-
|
|
77
|
+
- 本次授权渠道的实际包与四候选的字节大小和 SHA-256 一致。下载 tgz 与 `SHA256SUMS.txt` 到同一目录;校验文件使用包文件名,不包含 runner 的 `verified/` 前缀,可在下载目录直接执行 `sha256sum --check SHA256SUMS.txt`。
|
|
46
78
|
- 补记最终结论,但不修改已发布标签或替换工件。
|
|
47
79
|
|
|
48
80
|
npm 与 GitHub 不是原子发布。若 npm 已接受上传但公开下载暂时 404,**不要再次上传**;先只读确认公开包。确认相同后恢复原失败任务,工作流会核对已有 npm integrity 并跳过上传。不匹配则停止;已有 GitHub Release 不覆盖。
|
package/index.mjs
CHANGED
|
@@ -3,6 +3,7 @@ import {readFileSync} from 'node:fs';
|
|
|
3
3
|
import {registerFrontendAssets} from './frontend-assets.mjs';
|
|
4
4
|
const runningFrontend=JSON.parse(readFileSync(new URL('./dist/frontend.json',import.meta.url),'utf8'));
|
|
5
5
|
import * as adapter from './vcs.mjs';
|
|
6
|
+
import {withOperationBudget} from './src/operation-budget.mjs';
|
|
6
7
|
import {runtimeInfo,checkUpdate} from './dist/updates.mjs';
|
|
7
8
|
import {createSvnHttpsHost} from './dist/svn-host.mjs';
|
|
8
9
|
import {historySearchArgs,validateDateBound,isObjectId,isChangeId} from './git-history.mjs';
|
|
@@ -47,6 +48,7 @@ const REQUEST_FIELDS = {
|
|
|
47
48
|
'vcs/revision-compare': ['sessionId', 'repositoryId', 'base', 'target', 'id', 'large'],
|
|
48
49
|
'vcs/revision-image': ['sessionId', 'repositoryId', 'base', 'target', 'id', 'side'],
|
|
49
50
|
'vcs/tree': ['sessionId', 'repositoryId', 'commit'],
|
|
51
|
+
'vcs/tree-page': ['sessionId', 'repositoryId', 'commit', 'path', 'offset', 'limit', 'query'],
|
|
50
52
|
'vcs/tree-file': ['sessionId', 'repositoryId', 'commit', 'path'],
|
|
51
53
|
'vcs/tree-segment': ['sessionId', 'repositoryId', 'commit', 'path', 'offset'],
|
|
52
54
|
'vcs/tree-history': ['sessionId', 'repositoryId', 'commit', 'path', 'offset', 'limit', 'follow', 'since', 'until'],
|
|
@@ -69,7 +71,7 @@ const HISTORICAL_PATH_ENDPOINTS = new Set(['vcs/tree-file', 'vcs/tree-segment',
|
|
|
69
71
|
const REVISION_PAIR_ENDPOINTS = new Set(['vcs/revision-changes', 'vcs/revision-compare', 'vcs/revision-image']);
|
|
70
72
|
const REVISION_CHANGE_ID_ENDPOINTS = new Set(['vcs/revision-compare', 'vcs/revision-image']);
|
|
71
73
|
const SELECTED_CHANGE_ENDPOINTS = new Set(['vcs/commit-compare', 'vcs/file-history', 'vcs/blame', 'vcs/commit-image']);
|
|
72
|
-
const COMMIT_ENDPOINTS = new Set(['vcs/commit', 'vcs/commit-compare', 'vcs/file-history', 'vcs/blame', 'vcs/tree', 'vcs/tree-file', 'vcs/tree-segment', 'vcs/tree-history', 'vcs/commit-image']);
|
|
74
|
+
const COMMIT_ENDPOINTS = new Set(['vcs/commit', 'vcs/commit-compare', 'vcs/file-history', 'vcs/blame', 'vcs/tree', 'vcs/tree-page', 'vcs/tree-file', 'vcs/tree-segment', 'vcs/tree-history', 'vcs/commit-image']);
|
|
73
75
|
const LARGE_COMPARISON_ENDPOINTS = new Set(['vcs/compare', 'vcs/commit-compare', 'vcs/revision-compare']);
|
|
74
76
|
const HISTORY_PATH_ENDPOINTS = new Set(['vcs/file-history', 'vcs/tree-history']);
|
|
75
77
|
const PAGINATED_HISTORY_ENDPOINTS = new Set(['vcs/history', 'vcs/file-history', 'vcs/tree-history']);
|
|
@@ -88,7 +90,8 @@ function validateRepositoriesPayload(payload) {
|
|
|
88
90
|
|
|
89
91
|
/** Historical paths are repository-relative and can never be absolute or escape. */
|
|
90
92
|
function validateHistoricalPath(endpoint, path) {
|
|
91
|
-
if (
|
|
93
|
+
if (endpoint === 'vcs/tree-page' && (path === undefined || path === '')) return;
|
|
94
|
+
if (endpoint !== 'vcs/tree-page' && !HISTORICAL_PATH_ENDPOINTS.has(endpoint)) return;
|
|
92
95
|
if (typeof path !== 'string' || !path || path.length > 32768 || path.includes('\0') || path.startsWith('/') || path.split('/').some(part => !part || part === '.' || part === '..')) throw invalid('Invalid historical file path.');
|
|
93
96
|
}
|
|
94
97
|
|
|
@@ -174,6 +177,11 @@ function validateRepositoryPayload(endpoint, payload) {
|
|
|
174
177
|
validateHistoryDates(endpoint, payload);
|
|
175
178
|
if (endpoint === 'vcs/history') { try { historySearchArgs(payload.search); } catch { throw invalid('Invalid history search.'); } }
|
|
176
179
|
validateHistoryPagination(endpoint, payload);
|
|
180
|
+
if (endpoint === 'vcs/tree-page') {
|
|
181
|
+
const {offset = 0, limit = 100} = payload;
|
|
182
|
+
if (payload.query !== undefined && (typeof payload.query !== 'string' || payload.query.length > 1024 || /[\0\r\n]/.test(payload.query))) throw invalid('Invalid tree query.');
|
|
183
|
+
if (!Number.isInteger(offset) || offset < 0 || offset > 100000 || !Number.isInteger(limit) || limit < 1 || limit > 100) throw invalid('Invalid tree pagination.');
|
|
184
|
+
}
|
|
177
185
|
validateWorkspaceChangeId(endpoint, payload);
|
|
178
186
|
validateModeAndComparisonId(endpoint, payload);
|
|
179
187
|
}
|
|
@@ -192,6 +200,8 @@ const optionalField = (key, value) => value === undefined ? {} : {[key]: value};
|
|
|
192
200
|
/** Dispatch only after grants and capacity have been checked by createHandler. */
|
|
193
201
|
async function readRepository(api, endpoint, repository, payload, {cwd, mode, signal}) {
|
|
194
202
|
switch (endpoint) {
|
|
203
|
+
case 'vcs/tree-page':
|
|
204
|
+
return api.getHistoricalTreePage({...repository}, {commit: payload.commit, path: payload.path ?? '', offset: payload.offset ?? 0, limit: payload.limit ?? 100, ...optionalField('query', payload.query), signal});
|
|
195
205
|
case 'vcs/tree-history':
|
|
196
206
|
return api.listPathHistory({...repository}, {
|
|
197
207
|
commit: payload.commit, path: payload.path, offset: payload.offset ?? 0, limit: payload.limit ?? 50, follow: payload.follow ?? false,
|
|
@@ -245,15 +255,20 @@ async function readRepository(api, endpoint, repository, payload, {cwd, mode, si
|
|
|
245
255
|
}
|
|
246
256
|
|
|
247
257
|
/** Limit active adapter operations and retain only bounded, short-lived discovery grants. */
|
|
248
|
-
export function createHandler(ctx, api = adapter, maxActive = 4, { now = Date.now, ttlMs = 5 * 60_000, maxSessions = 32, maxRepositories = 512, bindSvnIdentityResolver } = {}) {
|
|
258
|
+
export function createHandler(ctx, api = adapter, maxActive = 4, { now = Date.now, ttlMs = 5 * 60_000, maxSessions = 32, maxRepositories = 512, operationTimeoutMs = 15000, bindSvnIdentityResolver } = {}) {
|
|
249
259
|
let active = 0, activeImages = 0, activeSegments = 0;
|
|
250
260
|
const sessions = new Map();
|
|
251
261
|
|
|
262
|
+
const clearGrantCache = entry => {
|
|
263
|
+
for (const repository of entry?.repositories.values() ?? []) api.clearHistoricalCache?.(repository.root);
|
|
264
|
+
};
|
|
265
|
+
const revokeSession = id => { clearGrantCache(sessions.get(id)); sessions.delete(id); };
|
|
266
|
+
|
|
252
267
|
/** Resolve a live Session grant, evicting expired entries and cwd-mismatched grants. */
|
|
253
268
|
function current(sessionId, cwd) {
|
|
254
|
-
for (const [id, entry] of sessions) if (entry.expiresAt <= now())
|
|
269
|
+
for (const [id, entry] of sessions) if (entry.expiresAt <= now()) revokeSession(id);
|
|
255
270
|
const entry = sessions.get(sessionId);
|
|
256
|
-
if (entry && entry.cwd !== cwd) {
|
|
271
|
+
if (entry && entry.cwd !== cwd) { revokeSession(sessionId); return undefined; }
|
|
257
272
|
return entry;
|
|
258
273
|
}
|
|
259
274
|
|
|
@@ -303,7 +318,7 @@ export function createHandler(ctx, api = adapter, maxActive = 4, { now = Date.no
|
|
|
303
318
|
/** Discovery creates, replaces or merges grants; a stale scan can never resurrect one. */
|
|
304
319
|
async function discoverRepositories(payload, cwd, entry, signal) {
|
|
305
320
|
if (!entry) {
|
|
306
|
-
while (sessions.size >= maxSessions)
|
|
321
|
+
while (sessions.size >= maxSessions) revokeSession(sessions.keys().next().value);
|
|
307
322
|
entry = { cwd, repositories: new Map(), expiresAt: now() + ttlMs, scanSequence: 0, fullScanSequence: 0 };
|
|
308
323
|
sessions.set(payload.sessionId, entry);
|
|
309
324
|
}
|
|
@@ -327,6 +342,7 @@ export function createHandler(ctx, api = adapter, maxActive = 4, { now = Date.no
|
|
|
327
342
|
repositories.push({ ...repository });
|
|
328
343
|
}
|
|
329
344
|
if (repositories.length !== result.repositories.length) warnings.push('Repository authorization limit reached; use a new Session or narrow its working directory.');
|
|
345
|
+
clearGrantCache(entry);
|
|
330
346
|
entry.repositories = grants;
|
|
331
347
|
entry.expiresAt = now() + ttlMs;
|
|
332
348
|
return { cwd, repositories, truncated, warnings };
|
|
@@ -364,7 +380,7 @@ export function createHandler(ctx, api = adapter, maxActive = 4, { now = Date.no
|
|
|
364
380
|
return value;
|
|
365
381
|
}
|
|
366
382
|
|
|
367
|
-
|
|
383
|
+
const execute = async (endpoint, payload, signal) => {
|
|
368
384
|
if (UPDATE_ENDPOINTS.has(endpoint)) return handleUpdateRequest(endpoint, payload, signal);
|
|
369
385
|
if (!ACTION_ENDPOINTS.has(endpoint)) return failure('vcs/not-found', 'Unknown VCS endpoint.');
|
|
370
386
|
if (active >= maxActive) return failure('vcs/busy', 'Too many VCS requests. Please retry.');
|
|
@@ -392,7 +408,7 @@ export function createHandler(ctx, api = adapter, maxActive = 4, { now = Date.no
|
|
|
392
408
|
return { ok: true, value: await readGrantedRepository(endpoint, repository, payload, cwd, mode, entry, signal) };
|
|
393
409
|
} catch (error) {
|
|
394
410
|
if (signal?.aborted || error?.name === 'AbortError') return failure('vcs/cancelled', 'The request was cancelled.');
|
|
395
|
-
const code = typeof error?.code === 'string' && error.code.startsWith('vcs/') ? error.code : 'vcs/operation-failed';
|
|
411
|
+
const code = error?.code === 'TIMEOUT' ? 'vcs/timeout' : typeof error?.code === 'string' && error.code.startsWith('vcs/') ? error.code : 'vcs/operation-failed';
|
|
396
412
|
return failure(code, error instanceof Error ? error.message : String(error));
|
|
397
413
|
} finally {
|
|
398
414
|
active--;
|
|
@@ -400,6 +416,14 @@ export function createHandler(ctx, api = adapter, maxActive = 4, { now = Date.no
|
|
|
400
416
|
if (segmentSlot) activeSegments--;
|
|
401
417
|
}
|
|
402
418
|
};
|
|
419
|
+
return async (endpoint, payload, signal) => {
|
|
420
|
+
try {
|
|
421
|
+
return await withOperationBudget({signal, timeoutMs: operationTimeoutMs}, () => execute(endpoint, payload, signal));
|
|
422
|
+
} catch (error) {
|
|
423
|
+
if (signal?.aborted || error?.name === 'AbortError') return failure('vcs/cancelled', 'The request was cancelled.');
|
|
424
|
+
return failure(error?.code === 'TIMEOUT' ? 'vcs/timeout' : 'vcs/operation-failed', error.message);
|
|
425
|
+
}
|
|
426
|
+
};
|
|
403
427
|
}
|
|
404
428
|
|
|
405
429
|
/** Content type is fixed per build artifact; URL input never chooses it. */
|
|
@@ -450,7 +474,7 @@ export function apply(ctx) {
|
|
|
450
474
|
// Pass the plugin owner explicitly: the nested rpc getter can retain the
|
|
451
475
|
// connection service context, which does not inject webServer.
|
|
452
476
|
const host=createSvnHttpsHost(ctx,adapter);
|
|
453
|
-
ctx.effect(()=>()=>host.dispose());
|
|
477
|
+
ctx.effect(()=>()=>{ host.dispose(); adapter.clearHistoricalCache?.(); });
|
|
454
478
|
ctx.connection.register(ctx, RPC_CHANNEL, host.handle);
|
|
455
479
|
registerAssets(ctx);
|
|
456
480
|
registerDirectAssets(ctx);
|
package/local-command.mjs
CHANGED
|
@@ -1,46 +1,79 @@
|
|
|
1
1
|
import {spawn} from 'node:child_process';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import {currentOperationBudget} from './src/operation-budget.mjs';
|
|
2
4
|
|
|
3
5
|
const MAX_OUTPUT = 16 * 1024 * 1024;
|
|
4
6
|
export const TIMEOUT = 15000;
|
|
7
|
+
const KILL_GRACE_MS = 100;
|
|
5
8
|
|
|
6
9
|
// Local adapter primitive, not the remote SVN transport. Preserve its error contract.
|
|
7
|
-
export function run(command, args, cwd, max = MAX_OUTPUT, {
|
|
10
|
+
export function run(command, args, cwd, max = MAX_OUTPUT, {signal, timeoutMs = TIMEOUT} = {}) {
|
|
8
11
|
return new Promise((resolve, reject) => {
|
|
9
|
-
|
|
12
|
+
const budget = currentOperationBudget();
|
|
13
|
+
if (signal?.aborted) { reject(Object.assign(new Error('Discovery cancelled'), {code: 'ABORT_ERR'})); return; }
|
|
14
|
+
try {
|
|
15
|
+
budget?.throwIfExpired();
|
|
16
|
+
if (!Number.isInteger(max) || max < 1 || max > MAX_OUTPUT || !Number.isFinite(timeoutMs) || timeoutMs <= 0) throw new Error('Invalid local command bounds.');
|
|
17
|
+
timeoutMs = Math.min(timeoutMs, TIMEOUT, budget?.remaining() ?? TIMEOUT);
|
|
18
|
+
} catch (error) { reject(error); return; }
|
|
10
19
|
// Inherited Git overrides must not redirect reads to a different repository.
|
|
11
20
|
const env = Object.fromEntries(Object.entries(process.env).filter(([key]) => !key.toUpperCase().startsWith('GIT_')));
|
|
12
|
-
Object.assign(env, {
|
|
13
|
-
const child = spawn(command, args, {
|
|
21
|
+
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'});
|
|
22
|
+
const child = spawn(command, args, {cwd, shell: false, windowsHide: true, detached: process.platform !== 'win32', stdio: ['ignore', 'pipe', 'pipe'], env});
|
|
14
23
|
const out = [], err = [];
|
|
15
|
-
let size = 0, error, done = false;
|
|
24
|
+
let size = 0, error, done = false, closed = false, exitCode, stopping = false, cleanupDone = true, killTimer;
|
|
25
|
+
const signals = [...new Set([signal, budget?.signal].filter(Boolean))];
|
|
26
|
+
const finish = () => {
|
|
27
|
+
if (done || !closed || !cleanupDone) return;
|
|
28
|
+
done = true;
|
|
29
|
+
clearTimeout(timer);
|
|
30
|
+
clearTimeout(killTimer);
|
|
31
|
+
for (const source of signals) source.removeEventListener('abort', abort);
|
|
32
|
+
if (error) reject(error);
|
|
33
|
+
else if (exitCode !== 0) reject(Object.assign(new Error(command + ': ' + Buffer.concat(err).toString('utf8').trim()), {code: 'VCS_COMMAND', exitCode}));
|
|
34
|
+
else resolve(Buffer.concat(out));
|
|
35
|
+
};
|
|
36
|
+
const killGroup = strength => {
|
|
37
|
+
try { process.kill(-child.pid, strength); } catch (failure) {
|
|
38
|
+
if (failure.code !== 'ESRCH') { try { child.kill(strength); } catch {} }
|
|
39
|
+
}
|
|
40
|
+
};
|
|
16
41
|
const stop = failure => {
|
|
17
42
|
error ??= failure;
|
|
18
|
-
child.
|
|
43
|
+
if (stopping || !child.pid) return;
|
|
44
|
+
stopping = true; cleanupDone = false;
|
|
45
|
+
if (process.platform === 'win32') {
|
|
46
|
+
// Kill the tree before its parent can disappear and orphan pipe-owning descendants.
|
|
47
|
+
const executable = path.join(process.env.SystemRoot || 'C:/Windows', 'System32', 'taskkill.exe');
|
|
48
|
+
const killer = spawn(executable, ['/PID', String(child.pid), '/T', '/F'], {windowsHide: true, shell: false, stdio: 'ignore'});
|
|
49
|
+
killer.on('error', () => { try { child.kill('SIGKILL'); } catch {} });
|
|
50
|
+
const guard = setTimeout(() => { try { child.kill('SIGKILL'); killer.kill(); } catch {} }, 1000);
|
|
51
|
+
killer.once('close', () => { clearTimeout(guard); cleanupDone = true; finish(); });
|
|
52
|
+
} else {
|
|
53
|
+
killGroup('SIGTERM');
|
|
54
|
+
killTimer = setTimeout(() => { killGroup('SIGKILL'); cleanupDone = true; finish(); }, KILL_GRACE_MS);
|
|
55
|
+
}
|
|
19
56
|
};
|
|
20
|
-
const abort = () =>
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
57
|
+
const abort = () => {
|
|
58
|
+
const source = signals.find(value => value.aborted);
|
|
59
|
+
const reason = source?.reason?.code === 'TIMEOUT' ? source.reason : Object.assign(new Error('Discovery cancelled'), {code: 'ABORT_ERR'});
|
|
60
|
+
stop(reason);
|
|
61
|
+
};
|
|
62
|
+
const timer = setTimeout(() => stop(Object.assign(new Error(command + ' timed out'), {code: 'TIMEOUT'})), timeoutMs);
|
|
63
|
+
// Both streams consume the same budget; never retain unbounded or partial output.
|
|
24
64
|
const collect = chunks => bytes => {
|
|
65
|
+
if (error) return;
|
|
25
66
|
size += bytes.length;
|
|
26
|
-
if (size > max) {
|
|
27
|
-
|
|
28
|
-
} else {
|
|
29
|
-
chunks.push(bytes);
|
|
30
|
-
}
|
|
67
|
+
if (size > max) stop(Object.assign(new Error('Output exceeds size limit'), {code: 'TOO_LARGE'}));
|
|
68
|
+
else chunks.push(bytes);
|
|
31
69
|
};
|
|
32
70
|
child.stdout.on('data', collect(out));
|
|
33
71
|
child.stderr.on('data', collect(err));
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
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)));
|
|
72
|
+
child.on('error', failure => { error ??= failure; });
|
|
73
|
+
child.once('close', code => { closed = true; exitCode = code; finish(); });
|
|
74
|
+
for (const source of signals) source.addEventListener('abort', abort, {once: true});
|
|
75
|
+
// Abort can race spawn/listener registration.
|
|
76
|
+
if (signals.some(value => value.aborted)) abort();
|
|
44
77
|
});
|
|
45
78
|
}
|
|
46
79
|
export const git = (root, args, max, options) => run('git', ['--no-pager', '--literal-pathspecs', '-c', 'core.fsmonitor=false', ...args], root, max, options);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@feng0531/dsh-vcs",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.1",
|
|
4
4
|
"private": false,
|
|
5
5
|
"publishConfig": { "access": "public", "registry": "https://registry.npmjs.org/" },
|
|
6
6
|
"type": "module",
|
|
@@ -27,6 +27,9 @@
|
|
|
27
27
|
"src/svn-path.mjs",
|
|
28
28
|
"src/svn-revision.mjs",
|
|
29
29
|
"src/commit-signature.mjs",
|
|
30
|
+
"src/operation-budget.mjs",
|
|
31
|
+
"src/historical-access.mjs",
|
|
32
|
+
"src/preview-capabilities.mjs",
|
|
30
33
|
"dist",
|
|
31
34
|
"locale",
|
|
32
35
|
"icon.svg",
|
|
@@ -43,10 +46,15 @@
|
|
|
43
46
|
"prepare": "npm run build",
|
|
44
47
|
"prepack": "npm run check",
|
|
45
48
|
"verify:package": "node scripts/verify-package.mjs",
|
|
46
|
-
"test:install": "node scripts/test-install.mjs"
|
|
49
|
+
"test:install": "node scripts/test-install.mjs",
|
|
50
|
+
"test:host-compat": "node scripts/test-host-compat.mjs",
|
|
51
|
+
"test:release": "node scripts/run-release-tests.mjs",
|
|
52
|
+
"test:preflight": "node scripts/test-preflight.mjs",
|
|
53
|
+
"test:browser": "node scripts/monaco-fixture.mjs && node scripts/run-isolated-browser.mjs",
|
|
54
|
+
"test:performance": "node scripts/performance-baseline.mjs"
|
|
47
55
|
},
|
|
48
56
|
"peerDependencies": {
|
|
49
|
-
"@deepseek-ai/dsh": "0.1.7-rc.2 || 0.2.0-rc.2"
|
|
57
|
+
"@deepseek-ai/dsh": "0.1.7-rc.2 || 0.2.0-rc.2 || 0.2.1-rc.2 || 0.2.1-rc.5"
|
|
50
58
|
},
|
|
51
59
|
"peerDependenciesMeta": {
|
|
52
60
|
"@deepseek-ai/dsh": { "optional": true }
|