@gfdelarue/testkit 0.3.1 → 0.3.3
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 +1 -1
- package/bin/testkit-windows-owner-arm64.exe +0 -0
- package/bin/testkit-windows-owner-x64.exe +0 -0
- package/dist/app/cloud/control.js +2 -2
- package/dist/app/cloud/login.d.ts +4 -4
- package/dist/app/inspect/artifacts.d.ts +4 -4
- package/dist/app/run/engine-failure-stack.d.ts +1 -0
- package/dist/app/run/engine-failure-stack.js +2 -0
- package/dist/app/run/worker-loop.js +1 -1
- package/dist/app/setup/playwright.js +1 -1
- package/dist/app/typecheck/index.js +4 -4
- package/dist/cli/command-catalog.d.ts +24 -24
- package/dist/cli/command-flags.d.ts +3 -3
- package/dist/cli/commands/artifacts.d.ts +16 -16
- package/dist/cli/commands/cleanup.d.ts +1 -1
- package/dist/cli/commands/cloud/login.d.ts +1 -1
- package/dist/cli/commands/cloud/status.d.ts +3 -3
- package/dist/config/loader/discover-tests.d.ts +5 -0
- package/dist/config/loader/discover-tests.js +1 -1
- package/dist/config/loader/file-metadata.d.ts +1 -1
- package/dist/config/loader/file-metadata.js +1 -1
- package/dist/config/loader/index.d.ts +1 -1
- package/dist/config/loader/index.js +1 -1
- package/dist/database/docker-metadata.d.ts +3 -0
- package/dist/database/docker-metadata.js +3 -0
- package/dist/database/docker.js +5 -5
- package/dist/database/index.js +1 -1
- package/dist/database/local-postgres.js +1 -1
- package/dist/database/postgres/index.js +2 -2
- package/dist/database/runtime-config.d.ts +5 -5
- package/dist/database/schema-source.d.ts +4 -1
- package/dist/database/schema-source.js +3 -3
- package/dist/database/template-steps.d.ts +4 -0
- package/dist/database/template-steps.js +4 -4
- package/dist/engines/managed-module-resolution.js +5 -3
- package/dist/engines/node-vitest/adapter/consumer-typescript-paths.js +2 -2
- package/dist/engines/node-vitest/runtime/runtime-input.js +1 -1
- package/dist/engines/playwright/runtime/runtime-input.js +1 -1
- package/dist/environment/template.d.ts +20 -20
- package/dist/process/execa-bundle-licenses.txt +411 -0
- package/dist/process/execa-bundle.json +874 -0
- package/dist/process/files/json-file.js +1 -1
- package/dist/process/host-process.d.ts +6 -1
- package/dist/process/host-process.js +1 -1
- package/dist/process/index.d.ts +2 -2
- package/dist/process/locks.js +3 -3
- package/dist/process/process-contracts.d.ts +10 -2
- package/dist/process/process-identity.js +1 -1
- package/dist/process/process.d.ts +3 -2
- package/dist/process/process.js +46 -1
- package/dist/repo/metadata.js +1 -1
- package/dist/repo/state.d.ts +1 -0
- package/dist/repo/state.js +4 -4
- package/dist/steps/configured-step-testkit-modules.d.ts +1 -1
- package/dist/steps/run.js +2 -2
- package/dist/toolchains/archive.js +1 -1
- package/dist/toolchains/node-integrity.d.ts +2 -0
- package/dist/toolchains/node-integrity.js +1 -1
- package/dist/toolchains/node-provisioning.js +2 -2
- package/package.json +4 -3
|
@@ -1 +1 @@
|
|
|
1
|
-
import h from"node:fs";import{createRequire as L}from"node:module";import u from"node:path";import{pipeline as _}from"node:stream/promises";import{open as F}from"yauzl";import{isPathWithin as p}from"../process/files/paths.js";export const ARCHIVE_FORMATS=["tar","tar.gz","zip"],MAX_ARCHIVE_ENTRIES=1e5;export class UnsafeArchiveError extends Error{code="TESTKIT_ARCHIVE_UNSAFE";constructor(e,i){super(e,i),this.name="UnsafeArchiveError"}}const I=L(import.meta.url),D=I("tar"),C=new Set(["File","OldFile","ContiguousFile"]),M=new Set(["Directory","GNUDumpDir"]),S=4096,P=255,U=128,W=/[<>:"|?*]/u,Z=/^(?:con|prn|aux|nul|com[1-9]|lpt[1-9])(?:\..*)?$/iu,N=new TextDecoder("utf-8",{fatal:!0});export class TarArchiveInspector{formats=["tar","tar.gz"];async inspect(e,i,r={}){x(this.formats,i);const n=v(r),o=[],s=h.createReadStream(e),c=new D.Parser({onReadEntry(a){if(r.signal?.aborted){const d=g(r.signal);s.destroy(d),c.abort(d);return}try{n.countEntry();const d=q(a);o.push(d),n.addPayload(d)}catch(d){const f=B(d,"Tar archive processing failed");s.destroy(f),c.abort(f);return}a.resume()},strict:!0});return await j(s,c,r.signal),o}}export class ZipArchiveExtractor{formats=["zip"];async inspect(e,i,r={}){x(this.formats,i);const n=v(r),o=[];return await $(e,r.signal,async(s,c)=>{n.countEntry();const a=await T(c,s,r.signal);o.push(a),n.addPayload(a)}),o}async extract(e,i,r,n={}){x(this.formats,r);const o=v(n,"extraction"),s=u.resolve(i),c=await this.inspect(e,r,n);validateArchiveEntryTopology(c),await Q(s);const a=[];await $(e,n.signal,async(f,y)=>{const l=await T(y,f,n.signal);o.countEntry(),o.addPayload(l);const w=assertSafeArchiveEntry(l),m=u.resolve(s,w);if(!p(s,m))throw new UnsafeArchiveError(`Archive path escapes its destination: ${JSON.stringify(w)}`);if(l.kind==="directory"){await k(s,m),await h.promises.chmod(m,l.mode);return}if(await k(s,u.dirname(m)),l.kind==="symlink"){if(l.linkTarget===void 0)throw new UnsafeArchiveError(`Archive symbolic link ${JSON.stringify(w)} has no target`);a.push({destinationPath:m,entry:{...l,kind:"symlink",linkTarget:l.linkTarget},normalized:w});return}const R=await O(y,f);await _(R,h.createWriteStream(m,{flags:"wx",mode:l.mode}),{signal:n.signal}),await h.promises.chmod(m,l.mode)});for(const f of a)throwIfAborted(n.signal),await k(s,u.dirname(f.destinationPath)),await h.promises.symlink(f.entry.linkTarget,f.destinationPath,V(f.normalized,c));const d=await h.promises.realpath(s);for(const f of a)await assertInstalledSymlinkTarget(f.destinationPath,f.normalized,d);throwIfAborted(n.signal)}}function v(t,e="inspection"){const i=Math.min(t.maxEntries??MAX_ARCHIVE_ENTRIES,MAX_ARCHIVE_ENTRIES),r=t.maxUnpackedSizeBytes??Number.MAX_SAFE_INTEGER,n=e==="extraction"?" during extraction":"";let o=0,s=0;return{countEntry(){if(e==="inspection"?o>=i:o+1>i)throw new UnsafeArchiveError(`Archive exceeds the ${i.toString()} entry limit${n}`);o+=1},addPayload(c){if(!(c.kind!=="file"&&c.kind!=="symlink")&&(s+=c.sizeBytes,s>r))throw new UnsafeArchiveError(`Archive exceeds its configured unpacked byte limit${n}`)}}}async function j(t,e,i){await new Promise((r,n)=>{let o=!1;const s=()=>{const d=g(i);t.destroy(d),e.abort(d)},c=d=>{o||(o=!0,i?.removeEventListener("abort",s),n(d))},a=()=>{o||(o=!0,i?.removeEventListener("abort",s),r())};t.once("error",c),e.once("error",c),e.once("end",a),i?.addEventListener("abort",s,{once:!0}),t.pipe(e)}),throwIfAborted(i)}export function validateArchiveEntryTopology(t){const e=new Map,i=new Map;for(const n of t){const o=assertSafeArchiveEntry(n),s=portableCollisionKey(o),c=i.get(s);if(c!==void 0)throw new UnsafeArchiveError(`Archive contains colliding entries ${JSON.stringify(c)} and ${JSON.stringify(o)}`);i.set(s,o),e.set(o,n)}const r=E(e);for(const[n,o]of e){for(const s of J(n)){const c=e.get(s);if(c?.kind==="symlink")throw new UnsafeArchiveError(`Archive entry ${JSON.stringify(n)} would write through symbolic-link ancestor ${JSON.stringify(s)}`);if(c?.kind==="file")throw new UnsafeArchiveError(`Archive entry ${JSON.stringify(n)} has regular-file ancestor ${JSON.stringify(s)}`)}o.kind==="symlink"&&z(n,e,r)}}export function assertSafeArchiveEntry(t){const e=normalizeArchivePath(t.path);if(t.kind==="forbidden")throw new UnsafeArchiveError(`Archive entry ${JSON.stringify(e)} has forbidden type ${JSON.stringify(t.rawType)}`);if(requireSafeMode(t.mode,e),requireSafeSize(t.sizeBytes,e),t.kind==="directory"&&t.sizeBytes!==0)throw new UnsafeArchiveError(`Archive directory ${JSON.stringify(e)} has non-zero size`);if(t.kind==="symlink"){if(t.linkTarget===void 0)throw new UnsafeArchiveError(`Archive symbolic link ${JSON.stringify(e)} has no target`);const i=normalizeSymlinkTarget(t.linkTarget,e),r=Buffer.byteLength(i,"utf8");if(t.sizeBytes!==r)throw new UnsafeArchiveError(`Archive symbolic link ${JSON.stringify(e)} has inconsistent target size`)}else if(t.linkTarget!==void 0)throw new UnsafeArchiveError(`Archive entry ${JSON.stringify(e)} has unexpected link target data`);return e}export function normalizeArchivePath(t){b(t,"archive path");const e=t.replace(/\/+$/u,"");if(!e||e.includes("\\")||e.startsWith("/")||Buffer.byteLength(e,"utf8")>S||X(e))throw new UnsafeArchiveError(`Archive path is not a portable relative path: ${JSON.stringify(t)}`);const i=e.split("/");if(i.length>U)throw new UnsafeArchiveError(`Archive path has too many segments: ${JSON.stringify(t)}`);for(const r of i)G(r,t);return i.join("/")}function X(t){for(let e=0;e<t.length;e+=1){const i=t.charCodeAt(e);if(i<=31||i===127)return!0}return!1}function q(t){const e=Y(t.type),i=e==="symlink"?t.linkpath:void 0;return{kind:e,...i===void 0?{}:{linkTarget:i},mode:t.mode,path:t.path,rawType:t.type,sizeBytes:e==="symlink"&&i!==void 0?Buffer.byteLength(i,"utf8"):t.size}}function H(t){const e=t.externalFileAttributes>>>16&65535,i=e&61440,r=t.versionMadeBy>>>8,n=t.fileName.endsWith("/")||i===16384||r===0&&(t.externalFileAttributes&16)!==0,o=i===40960,s=i!==0&&i!==16384&&i!==32768&&i!==40960,c=t.isEncrypted()||s?"forbidden":o?"symlink":n?"directory":"file",a=e&511;return{kind:c,mode:a||(n?493:420),path:t.fileName,rawType:t.isEncrypted()?"encrypted":o?"SymbolicLink":s?`mode-${i.toString(8)}`:n?"Directory":"File",sizeBytes:t.uncompressedSize}}async function T(t,e,i){const r=H(e);if(r.kind!=="symlink")return r;if(r.sizeBytes>S)throw new UnsafeArchiveError(`Archive symbolic link ${JSON.stringify(r.path)} exceeds the portable target size limit`);const n=await O(t,e),o=await tt(n,S,i);if(o.byteLength!==r.sizeBytes)throw new UnsafeArchiveError(`Archive symbolic link ${JSON.stringify(r.path)} has inconsistent target size`);return{...r,linkTarget:decodeUtf8(o,`archive symbolic link ${r.path}`),sizeBytes:o.byteLength}}async function $(t,e,i){const r=await K(t);await new Promise((n,o)=>{let s=!1;const c=()=>{s||(s=!0,e?.removeEventListener("abort",d),n())},a=f=>{s||(s=!0,e?.removeEventListener("abort",d),r.close(),o(B(f,"ZIP archive processing failed")))},d=()=>{a(g(e))};if(r.once("error",a),r.once("end",c),r.on("entry",f=>{Promise.resolve(i(f,r)).then(()=>{r.readEntry()},a)}),e?.addEventListener("abort",d,{once:!0}),e?.aborted){d();return}r.readEntry()})}function K(t){return new Promise((e,i)=>{F(t,{autoClose:!0,decodeStrings:!0,lazyEntries:!0,strictFileNames:!0,validateEntrySizes:!0},(r,n)=>{if(r!==null){i(r);return}e(n)})})}function O(t,e){return new Promise((i,r)=>{t.openReadStream(e,(n,o)=>{if(n!==null){r(n);return}i(o)})})}function Y(t){return C.has(t)?"file":M.has(t)?"directory":t==="SymbolicLink"?"symlink":"forbidden"}function G(t,e){if(!t||t==="."||t===".."||t.trim()!==t||t.endsWith(".")||t.normalize("NFC")!==t||Buffer.byteLength(t,"utf8")>P||W.test(t)||Z.test(t))throw new UnsafeArchiveError(`Archive path contains a non-portable segment: ${JSON.stringify(e)}`)}export function portableCollisionKey(t){return t.normalize("NFC").toLowerCase()}export function normalizeSymlinkTarget(t,e){b(t,`symbolic link ${e}`);const i=normalizeArchivePath(t);if(i!==t)throw new UnsafeArchiveError(`Archive symbolic link ${JSON.stringify(e)} has a non-canonical relative target: ${JSON.stringify(t)}`);return A(e,i),i}function J(t){const e=t.split("/"),i=[];for(let r=1;r<e.length;r+=1)i.push(e.slice(0,r).join("/"));return i}function V(t,e){const i=new Map(e.map(r=>[normalizeArchivePath(r.path),r]));return z(t,i,E(i))==="directory"?"dir":"file"}function z(t,e,i=E(e)){const r=e.get(t);if(r?.kind!=="symlink"||r.linkTarget===void 0)throw new UnsafeArchiveError(`Archive symbolic link ${JSON.stringify(t)} has no target`);let n=A(t,normalizeSymlinkTarget(r.linkTarget,t));const o=new Set;for(let s=0;s<=e.size;s+=1){const c=n.split("/");let a=!1;for(let f=0;f<c.length;f+=1){const y=c.slice(0,f+1).join("/"),l=e.get(y);if(l?.kind==="symlink"){if(l.linkTarget===void 0||o.has(y))throw new UnsafeArchiveError(`Archive symbolic link ${JSON.stringify(t)} has a cyclic target`);o.add(y);const w=A(y,normalizeSymlinkTarget(l.linkTarget,y)),m=c.slice(f+1).join("/");n=m?normalizeArchivePath(`${w}/${m}`):w,a=!0;break}if(l?.kind==="file"&&f<c.length-1)throw new UnsafeArchiveError(`Archive symbolic link ${JSON.stringify(t)} traverses regular file ${JSON.stringify(y)}`);if(l===void 0&&!i.has(y))throw new UnsafeArchiveError(`Archive symbolic link ${JSON.stringify(t)} has dangling target ${JSON.stringify(r.linkTarget)}`)}if(a)continue;const d=e.get(n);if(d?.kind==="file")return"file";if(d?.kind==="directory"||i.has(n))return"directory";throw new UnsafeArchiveError(`Archive symbolic link ${JSON.stringify(t)} has dangling target ${JSON.stringify(r.linkTarget)}`)}throw new UnsafeArchiveError(`Archive symbolic link ${JSON.stringify(t)} has a cyclic target`)}function E(t){const e=new Set;for(const[i,r]of t){for(const n of J(i))e.add(n);r.kind==="directory"&&e.add(i)}return e}function A(t,e){if(u.posix.isAbsolute(e))throw new UnsafeArchiveError(`Archive symbolic link ${JSON.stringify(t)} has an absolute target`);const i=u.posix.join(u.posix.dirname(t),e);if(i===".."||i.startsWith("../")||u.posix.isAbsolute(i))throw new UnsafeArchiveError(`Archive symbolic link ${JSON.stringify(t)} escapes the archive root`);return normalizeArchivePath(i)}export async function assertInstalledSymlinkTarget(t,e,i){let r;try{r=await h.promises.realpath(t)}catch(o){throw new UnsafeArchiveError(`Installed archive symbolic link ${JSON.stringify(e)} is dangling or cyclic`,{cause:o})}if(!p(i,r))throw new UnsafeArchiveError(`Installed archive symbolic link ${JSON.stringify(e)} resolves outside its root`);const n=await h.promises.lstat(r);if(!n.isFile()&&!n.isDirectory())throw new UnsafeArchiveError(`Installed archive symbolic link ${JSON.stringify(e)} resolves to a special file`)}async function Q(t){const e=await h.promises.lstat(t);if(!e.isDirectory()||e.isSymbolicLink())throw new UnsafeArchiveError("Archive extraction root must be a real directory");if((await h.promises.readdir(t)).length!==0)throw new UnsafeArchiveError("Archive extraction root must be empty")}async function k(t,e){if(!p(t,e))throw new UnsafeArchiveError(`Archive extraction path escapes its destination: ${JSON.stringify(e)}`);const i=u.relative(t,e);let r=t;for(const n of i.split(u.sep).filter(Boolean)){r=u.join(r,n);try{await h.promises.mkdir(r,{mode:493})}catch(s){if(!et(s,"EEXIST"))throw s}const o=await h.promises.lstat(r);if(!o.isDirectory()||o.isSymbolicLink())throw new UnsafeArchiveError(`Archive refuses to write through non-directory path ${JSON.stringify(r)}`)}}async function tt(t,e,i){const r=[];let n=0;const o=()=>t.destroy(g(i));i?.addEventListener("abort",o,{once:!0});try{for await(const s of t){throwIfAborted(i);const c=Buffer.isBuffer(s)?s:Buffer.from(s);if(n+=c.byteLength,n>e){const a=new UnsafeArchiveError("Archive symbolic-link target exceeds its byte limit");throw t.destroy(a),a}r.push(c)}}finally{i?.removeEventListener("abort",o)}return Buffer.concat(r,n)}export function decodeUtf8(t,e){try{const i=N.decode(t);return b(i,e),i}catch(i){throw i instanceof UnsafeArchiveError?i:new UnsafeArchiveError(`Archive ${e} is not valid UTF-8`,{cause:i})}}function b(t,e){if(t.includes("\uFFFD")||N.decode(Buffer.from(t,"utf8"))!==t)throw new UnsafeArchiveError(`Archive ${e} is not valid canonical UTF-8`)}function et(t,e){return typeof t=="object"&&t!==null&&Reflect.get(t,"code")===e}export function requireSafeMode(t,e){if(!Number.isSafeInteger(t)||t<0||t>4095)throw new UnsafeArchiveError(`Archive entry ${JSON.stringify(e)} has an invalid mode`);return t&511}export function requireSafeSize(t,e){if(!Number.isSafeInteger(t)||t<0)throw new UnsafeArchiveError(`Archive entry ${JSON.stringify(e)} has an invalid size`);return t}function x(t,e){if(!t.includes(e))throw new Error(`Archive format ${e} requires another extractor`)}export function throwIfAborted(t){if(t?.aborted)throw g(t)}function g(t){return t?.reason instanceof Error?t.reason:new Error("Archive operation was aborted")}function B(t,e){return t instanceof Error?t:new Error(e)}
|
|
1
|
+
import u from"node:fs";import{createRequire as F}from"node:module";import y from"node:path";import{pipeline as I}from"node:stream/promises";import{isPathWithin as g}from"../process/files/paths.js";export const ARCHIVE_FORMATS=["tar","tar.gz","zip"],MAX_ARCHIVE_ENTRIES=1e5;export class UnsafeArchiveError extends Error{code="TESTKIT_ARCHIVE_UNSAFE";constructor(t,i){super(t,i),this.name="UnsafeArchiveError"}}const N=F(import.meta.url);let D,M;function C(){return D??=N("tar")}function P(){return M??=N("yauzl")}const U=new Set(["File","OldFile","ContiguousFile"]),Z=new Set(["Directory","GNUDumpDir"]),S=4096,W=255,j=128,X=128*1024*1024,Y=/[<>:"|?*]/u,q=/^(?:con|prn|aux|nul|com[1-9]|lpt[1-9])(?:\..*)?$/iu,T=new TextDecoder("utf-8",{fatal:!0});export class TarArchiveInspector{formats=["tar","tar.gz"];async inspect(t,i,n={}){x(this.formats,i);const r=C(),o=E(n),a=[],s=u.createReadStream(t),c=new r.Parser({onReadEntry(l){if(n.signal?.aborted){const f=p(n.signal);s.destroy(f),c.abort(f);return}try{o.countEntry();const f=G(l);a.push(f),o.addPayload(f)}catch(f){const h=R(f,"Tar archive processing failed");s.destroy(h),c.abort(h);return}l.resume()},strict:!0});return await H(s,c,n.signal),a}}export class ZipArchiveExtractor{formats=["zip"];async inspect(t,i,n={}){x(this.formats,i);const r=E(n),o=[];return await O(t,n.signal,async(a,s)=>{r.countEntry();const c=await $(s,a,n.signal);o.push(c),r.addPayload(c)}),o}async extract(t,i,n,r={}){x(this.formats,n);const o=E(r,"extraction"),a=y.resolve(i),s=await this.inspect(t,n,r);validateArchiveEntryTopology(s),await re(a);const c=[];await O(t,r.signal,async(f,h)=>{const d=await $(h,f,r.signal);o.countEntry(),o.addPayload(d);const w=assertSafeArchiveEntry(d),m=y.resolve(a,w);if(!g(a,m))throw new UnsafeArchiveError(`Archive path escapes its destination: ${JSON.stringify(w)}`);if(d.kind==="directory"){await A(a,m,r.signal),await u.promises.chmod(m,d.mode);return}if(await A(a,y.dirname(m),r.signal),d.kind==="symlink"){if(d.linkTarget===void 0)throw new UnsafeArchiveError(`Archive symbolic link ${JSON.stringify(w)} has no target`);c.push({destinationPath:m,entry:{...d,kind:"symlink",linkTarget:d.linkTarget},normalized:w});return}const _=await z(h,f);await I(_,u.createWriteStream(m,{flags:"wx",mode:d.mode}),{signal:r.signal}),await u.promises.chmod(m,d.mode)});for(const f of c)throwIfAborted(r.signal),await A(a,y.dirname(f.destinationPath),r.signal),await u.promises.symlink(f.entry.linkTarget,f.destinationPath,ne(f.normalized,s));const l=await u.promises.realpath(a);for(const f of c)await assertInstalledSymlinkTarget(f.destinationPath,f.normalized,l);throwIfAborted(r.signal)}}function E(e,t="inspection"){const i=Math.min(e.maxEntries??MAX_ARCHIVE_ENTRIES,MAX_ARCHIVE_ENTRIES),n=e.maxUnpackedSizeBytes??Number.MAX_SAFE_INTEGER,r=t==="extraction"?" during extraction":"";let o=0,a=0;return{countEntry(){if(t==="inspection"?o>=i:o+1>i)throw new UnsafeArchiveError(`Archive exceeds the ${i.toString()} entry limit${r}`);o+=1},addPayload(s){if(!(s.kind!=="file"&&s.kind!=="symlink")&&(a+=s.sizeBytes,a>n))throw new UnsafeArchiveError(`Archive exceeds its configured unpacked byte limit${r}`)}}}async function H(e,t,i){await new Promise((n,r)=>{let o=!1;const a=()=>{const l=p(i);e.destroy(l),t.abort(l)},s=l=>{o||(o=!0,i?.removeEventListener("abort",a),r(l))},c=()=>{o||(o=!0,i?.removeEventListener("abort",a),n())};e.once("error",s),t.once("error",s),t.once("end",c),i?.addEventListener("abort",a,{once:!0}),e.pipe(t)}),throwIfAborted(i)}export function validateArchiveEntryTopology(e){const t=new Map,i=new Map;for(const r of e){const o=assertSafeArchiveEntry(r),a=portableCollisionKey(o),s=i.get(a);if(s!==void 0)throw new UnsafeArchiveError(`Archive contains colliding entries ${JSON.stringify(s)} and ${JSON.stringify(o)}`);i.set(a,o),t.set(o,r)}const n=v(t);for(const[r,o]of t){for(const a of J(r)){const s=t.get(a);if(s?.kind==="symlink")throw new UnsafeArchiveError(`Archive entry ${JSON.stringify(r)} would write through symbolic-link ancestor ${JSON.stringify(a)}`);if(s?.kind==="file")throw new UnsafeArchiveError(`Archive entry ${JSON.stringify(r)} has regular-file ancestor ${JSON.stringify(a)}`)}o.kind==="symlink"&&B(r,t,n)}}export function assertSafeArchiveEntry(e){const t=normalizeArchivePath(e.path);if(e.kind==="forbidden")throw new UnsafeArchiveError(`Archive entry ${JSON.stringify(t)} has forbidden type ${JSON.stringify(e.rawType)}`);if(requireSafeMode(e.mode,t),requireSafeSize(e.sizeBytes,t),e.kind==="directory"&&e.sizeBytes!==0)throw new UnsafeArchiveError(`Archive directory ${JSON.stringify(t)} has non-zero size`);if(e.kind==="symlink"){if(e.linkTarget===void 0)throw new UnsafeArchiveError(`Archive symbolic link ${JSON.stringify(t)} has no target`);const i=normalizeSymlinkTarget(e.linkTarget,t),n=Buffer.byteLength(i,"utf8");if(e.sizeBytes!==n)throw new UnsafeArchiveError(`Archive symbolic link ${JSON.stringify(t)} has inconsistent target size`)}else if(e.linkTarget!==void 0)throw new UnsafeArchiveError(`Archive entry ${JSON.stringify(t)} has unexpected link target data`);return t}export function normalizeArchivePath(e){k(e,"archive path");const t=e.replace(/\/+$/u,"");if(!t||t.includes("\\")||t.startsWith("/")||Buffer.byteLength(t,"utf8")>S||K(t))throw new UnsafeArchiveError(`Archive path is not a portable relative path: ${JSON.stringify(e)}`);const i=t.split("/");if(i.length>j)throw new UnsafeArchiveError(`Archive path has too many segments: ${JSON.stringify(e)}`);for(const n of i)ie(n,e);return i.join("/")}function K(e){for(let t=0;t<e.length;t+=1){const i=e.charCodeAt(t);if(i<=31||i===127)return!0}return!1}function G(e){const t=te(e.type),i=t==="symlink"?e.linkpath:void 0;return{kind:t,...i===void 0?{}:{linkTarget:i},mode:e.mode,path:e.path,rawType:e.type,sizeBytes:t==="symlink"&&i!==void 0?Buffer.byteLength(i,"utf8"):e.size}}function V(e){const t=e.externalFileAttributes>>>16&65535,i=t&61440,n=e.versionMadeBy>>>8,r=e.fileName.endsWith("/")||i===16384||n===0&&(e.externalFileAttributes&16)!==0,o=i===40960,a=i!==0&&i!==16384&&i!==32768&&i!==40960,s=e.isEncrypted()||a?"forbidden":o?"symlink":r?"directory":"file",c=t&511;return{kind:s,mode:c||(r?493:420),path:e.fileName,rawType:e.isEncrypted()?"encrypted":o?"SymbolicLink":a?`mode-${i.toString(8)}`:r?"Directory":"File",sizeBytes:e.uncompressedSize}}async function $(e,t,i){const n=V(t);if(n.kind!=="symlink")return n;if(n.sizeBytes>S)throw new UnsafeArchiveError(`Archive symbolic link ${JSON.stringify(n.path)} exceeds the portable target size limit`);const r=await z(e,t),o=await oe(r,S,i);if(o.byteLength!==n.sizeBytes)throw new UnsafeArchiveError(`Archive symbolic link ${JSON.stringify(n.path)} has inconsistent target size`);return{...n,linkTarget:decodeUtf8(o,`archive symbolic link ${n.path}`),sizeBytes:o.byteLength}}async function O(e,t,i){const{zipFile:n,buffered:r}=await Q(e,t);let o=Promise.resolve();try{await new Promise((a,s)=>{let c=!1;const l=()=>{c||(c=!0,t?.removeEventListener("abort",h),a())},f=d=>{c||(c=!0,t?.removeEventListener("abort",h),s(R(d,"ZIP archive processing failed")))},h=()=>{f(p(t))};if(n.once("error",f),n.once("end",l),n.on("entry",d=>{o=Promise.resolve().then(()=>i(d,n)).then(()=>{c||n.readEntry()}).catch(f)}),t?.addEventListener("abort",h,{once:!0}),t?.aborted){h();return}n.readEntry()})}finally{await o,r?n.close():await new Promise((a,s)=>{const c=()=>{n.removeListener("error",l),a()},l=f=>{n.removeListener("close",c),s(f)};n.once("close",c),n.once("error",l),n.close()})}}async function Q(e,t){const i=await ee(e,t);throwIfAborted(t);const{fromBuffer:n,open:r}=P();return new Promise((o,a)=>{const s={autoClose:!1,decodeStrings:!0,lazyEntries:!0,strictFileNames:!0,validateEntrySizes:!0},c=(l,f)=>{if(l!==null){a(l);return}o({zipFile:f,buffered:i!==void 0})};i===void 0?r(e,s,c):n(i,s,c)})}async function ee(e,t){throwIfAborted(t);const i=await u.promises.open(e,"r");try{const n=await i.stat();if(throwIfAborted(t),n.size>X)return;const r=Buffer.allocUnsafe(n.size+1);let o=0;for(;o<r.length;){throwIfAborted(t);const a=Math.min(1024*1024,r.length-o),{bytesRead:s}=await i.read(r,o,a,null);if(throwIfAborted(t),s===0)return r.subarray(0,o);o+=s}return}finally{await i.close()}}function z(e,t){return new Promise((i,n)=>{e.openReadStream(t,(r,o)=>{if(r!==null){n(r);return}i(o)})})}function te(e){return U.has(e)?"file":Z.has(e)?"directory":e==="SymbolicLink"?"symlink":"forbidden"}function ie(e,t){if(!e||e==="."||e===".."||e.trim()!==e||e.endsWith(".")||e.normalize("NFC")!==e||Buffer.byteLength(e,"utf8")>W||Y.test(e)||q.test(e))throw new UnsafeArchiveError(`Archive path contains a non-portable segment: ${JSON.stringify(t)}`)}export function portableCollisionKey(e){return e.normalize("NFC").toLowerCase()}export function normalizeSymlinkTarget(e,t){k(e,`symbolic link ${t}`);const i=normalizeArchivePath(e);if(i!==e)throw new UnsafeArchiveError(`Archive symbolic link ${JSON.stringify(t)} has a non-canonical relative target: ${JSON.stringify(e)}`);return b(t,i),i}function J(e){const t=e.split("/"),i=[];for(let n=1;n<t.length;n+=1)i.push(t.slice(0,n).join("/"));return i}function ne(e,t){const i=new Map(t.map(n=>[normalizeArchivePath(n.path),n]));return B(e,i,v(i))==="directory"?"dir":"file"}function B(e,t,i=v(t)){const n=t.get(e);if(n?.kind!=="symlink"||n.linkTarget===void 0)throw new UnsafeArchiveError(`Archive symbolic link ${JSON.stringify(e)} has no target`);let r=b(e,normalizeSymlinkTarget(n.linkTarget,e));const o=new Set;for(let a=0;a<=t.size;a+=1){const s=r.split("/");let c=!1;for(let f=0;f<s.length;f+=1){const h=s.slice(0,f+1).join("/"),d=t.get(h);if(d?.kind==="symlink"){if(d.linkTarget===void 0||o.has(h))throw new UnsafeArchiveError(`Archive symbolic link ${JSON.stringify(e)} has a cyclic target`);o.add(h);const w=b(h,normalizeSymlinkTarget(d.linkTarget,h)),m=s.slice(f+1).join("/");r=m?normalizeArchivePath(`${w}/${m}`):w,c=!0;break}if(d?.kind==="file"&&f<s.length-1)throw new UnsafeArchiveError(`Archive symbolic link ${JSON.stringify(e)} traverses regular file ${JSON.stringify(h)}`);if(d===void 0&&!i.has(h))throw new UnsafeArchiveError(`Archive symbolic link ${JSON.stringify(e)} has dangling target ${JSON.stringify(n.linkTarget)}`)}if(c)continue;const l=t.get(r);if(l?.kind==="file")return"file";if(l?.kind==="directory"||i.has(r))return"directory";throw new UnsafeArchiveError(`Archive symbolic link ${JSON.stringify(e)} has dangling target ${JSON.stringify(n.linkTarget)}`)}throw new UnsafeArchiveError(`Archive symbolic link ${JSON.stringify(e)} has a cyclic target`)}function v(e){const t=new Set;for(const[i,n]of e){for(const r of J(i))t.add(r);n.kind==="directory"&&t.add(i)}return t}function b(e,t){if(y.posix.isAbsolute(t))throw new UnsafeArchiveError(`Archive symbolic link ${JSON.stringify(e)} has an absolute target`);const i=y.posix.join(y.posix.dirname(e),t);if(i===".."||i.startsWith("../")||y.posix.isAbsolute(i))throw new UnsafeArchiveError(`Archive symbolic link ${JSON.stringify(e)} escapes the archive root`);return normalizeArchivePath(i)}export async function assertInstalledSymlinkTarget(e,t,i){let n;try{n=await u.promises.realpath(e)}catch(o){throw new UnsafeArchiveError(`Installed archive symbolic link ${JSON.stringify(t)} is dangling or cyclic`,{cause:o})}if(!g(i,n))throw new UnsafeArchiveError(`Installed archive symbolic link ${JSON.stringify(t)} resolves outside its root`);const r=await u.promises.lstat(n);if(!r.isFile()&&!r.isDirectory())throw new UnsafeArchiveError(`Installed archive symbolic link ${JSON.stringify(t)} resolves to a special file`)}async function re(e){const t=await u.promises.lstat(e);if(!t.isDirectory()||t.isSymbolicLink())throw new UnsafeArchiveError("Archive extraction root must be a real directory");if((await u.promises.readdir(e)).length!==0)throw new UnsafeArchiveError("Archive extraction root must be empty")}async function A(e,t,i){if(!g(e,t))throw new UnsafeArchiveError(`Archive extraction path escapes its destination: ${JSON.stringify(t)}`);const n=y.relative(e,t);let r=e;for(const[o,a]of n.split(y.sep).filter(Boolean).entries()){o>0&&o%16===0&&await new Promise(c=>setImmediate(c)),throwIfAborted(i),r=y.join(r,a);let s;try{s=u.lstatSync(r)}catch(c){if(!L(c,"ENOENT"))throw c;try{u.mkdirSync(r,{mode:493})}catch(l){if(!L(l,"EEXIST"))throw l}s=u.lstatSync(r)}if(!s.isDirectory()||s.isSymbolicLink())throw new UnsafeArchiveError(`Archive refuses to write through non-directory path ${JSON.stringify(r)}`)}}async function oe(e,t,i){const n=[];let r=0;const o=()=>e.destroy(p(i));i?.addEventListener("abort",o,{once:!0});try{for await(const a of e){throwIfAborted(i);const s=Buffer.isBuffer(a)?a:Buffer.from(a);if(r+=s.byteLength,r>t){const c=new UnsafeArchiveError("Archive symbolic-link target exceeds its byte limit");throw e.destroy(c),c}n.push(s)}}finally{i?.removeEventListener("abort",o)}return Buffer.concat(n,r)}export function decodeUtf8(e,t){try{const i=T.decode(e);return k(i,t),i}catch(i){throw i instanceof UnsafeArchiveError?i:new UnsafeArchiveError(`Archive ${t} is not valid UTF-8`,{cause:i})}}function k(e,t){if(e.includes("\uFFFD")||T.decode(Buffer.from(e,"utf8"))!==e)throw new UnsafeArchiveError(`Archive ${t} is not valid canonical UTF-8`)}function L(e,t){return typeof e=="object"&&e!==null&&Reflect.get(e,"code")===t}export function requireSafeMode(e,t){if(!Number.isSafeInteger(e)||e<0||e>4095)throw new UnsafeArchiveError(`Archive entry ${JSON.stringify(t)} has an invalid mode`);return e&511}export function requireSafeSize(e,t){if(!Number.isSafeInteger(e)||e<0)throw new UnsafeArchiveError(`Archive entry ${JSON.stringify(t)} has an invalid size`);return e}function x(e,t){if(!e.includes(t))throw new Error(`Archive format ${t} requires another extractor`)}export function throwIfAborted(e){if(e?.aborted)throw p(e)}function p(e){return e?.reason instanceof Error?e.reason:new Error("Archive operation was aborted")}function R(e,t){return e instanceof Error?e:new Error(t)}
|
|
@@ -13,4 +13,6 @@ export interface InstalledNodeTreeInspection {
|
|
|
13
13
|
}
|
|
14
14
|
export declare function validateNodeArchiveEntries(entries: readonly ArchiveEntry[], release: Readonly<ManagedNodeRelease>): void;
|
|
15
15
|
export declare function inspectInstalledNodeTree(root: string, limits: Readonly<InstalledNodeTreeLimits>, signal?: AbortSignal): Promise<InstalledNodeTreeInspection>;
|
|
16
|
+
export declare class InstalledNodeTreeMismatchError extends Error {
|
|
17
|
+
}
|
|
16
18
|
export declare function verifyInstalledNodeTree(root: string, expected: Readonly<ManagedNodeInstalledTreeLock>, signal?: AbortSignal): Promise<void>;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import O from"node:crypto";import
|
|
1
|
+
import O from"node:crypto";import u from"node:fs";import b from"node:path";import{normalizeArchivePath as L,portableCollisionKey as _,requireSafeMode as W,requireSafeSize as z}from"./archive.js";import{isPathWithin as C}from"../process/files/paths.js";import{throwIfSignalAborted as p}from"./shared-operation.js";const D=128,q=4,F=64*1024,K=1024*1024,H=new Map([["bin/corepack","../lib/node_modules/corepack/dist/corepack.js"],["bin/npm","../lib/node_modules/npm/bin/npm-cli.js"],["bin/npx","../lib/node_modules/npm/bin/npx-cli.js"]]);export function validateNodeArchiveEntries(o,e){const t=e.archive;let n=0,s=0;const a=new Map,h=new Map;if(o.length!==t.entryCount)throw B(o.length,n,0,e);for(const i of o){const r=L(i.path);if(r!==t.rootDirectory&&!r.startsWith(`${t.rootDirectory}/`))throw new Error(`Node archive entry escapes its locked root: ${JSON.stringify(r)}`);W(i.mode,r),z(i.sizeBytes,r);const l=_(r),f=h.get(l);if(f!==void 0)throw new Error(`Node archive contains colliding paths ${JSON.stringify(f)} and ${JSON.stringify(r)}`);if(h.set(l,r),a.set(r,i),i.kind==="forbidden")throw new Error(`Node archive entry ${JSON.stringify(r)} has forbidden type ${JSON.stringify(i.rawType)}`);if(i.kind==="directory"){if(i.sizeBytes!==0||i.linkTarget!==void 0)throw new Error(`Node archive directory ${JSON.stringify(r)} has invalid metadata`);continue}if(i.kind==="file"){if(i.linkTarget!==void 0)throw new Error(`Node archive file ${JSON.stringify(r)} has link metadata`);n+=1,s+=i.sizeBytes,P(o.length,n,s,e);continue}const v=r.slice(t.rootDirectory.length+1),y=H.get(v);if(y===void 0||i.linkTarget!==y||i.sizeBytes!==Buffer.byteLength(y,"utf8"))throw new Error(`Node archive contains an unsupported symbolic link at ${JSON.stringify(r)}`);M(r,y,t.rootDirectory),s+=i.sizeBytes,P(o.length,n,s,e)}if(a.get(t.rootDirectory)?.kind!=="directory")throw new Error("Node archive is missing its locked root directory");for(const[i]of a)for(const r of G(i)){const l=a.get(r);if(l?.kind==="file")throw new Error(`Node archive entry ${JSON.stringify(i)} has a file ancestor`);if(l?.kind==="symlink")throw new Error(`Node archive entry ${JSON.stringify(i)} would write through a symbolic link`)}if(n!==t.fileCount||s!==t.unpackedSizeBytes)throw B(o.length,n,s,e)}export async function inspectInstalledNodeTree(o,e,t){X(e);const n=await u.promises.lstat(o);if(!n.isDirectory()||n.isSymbolicLink())throw new Error("Installed Node toolchain content must be a real directory");const s=await u.promises.realpath(o),a=[],h=new Map;let N=0,i=0;const r=new Set,l=new AbortController,f={occurred:!1},v=()=>{y(t?.reason)};t?.addEventListener("abort",v,{once:!0}),t?.aborted&&v();try{if(await x(o,""),await Promise.all(r),f.occurred)throw f.error;return a.sort((d,w)=>R(d.path,w.path)),{entryCount:a.length,fileCount:N,sha256:O.createHash("sha256").update(JSON.stringify(a)).digest("hex"),unpackedSizeBytes:i}}catch(d){throw y(d),await Promise.all(r),f.error}finally{t?.removeEventListener("abort",v)}function y(d){f.occurred||(f.occurred=!0,f.error=d,l.abort(d))}async function j(d,w,S){const m=a.length;for(a.push({kind:"file",path:w,sha256:"",sizeBytes:S}),J(a.length,N,i,e);r.size>=q;)await Promise.race(r);p(l.signal);const E=Y(d,S,l.signal).then(c=>{a[m]={kind:"file",path:w,sha256:c,sizeBytes:S}}).catch(y).then(()=>{r.delete(E)});r.add(E)}async function x(d,w){p(l.signal);const S=await u.promises.readdir(d,{withFileTypes:!0});S.sort((m,E)=>R(m.name,E.name));for(const m of S){p(l.signal);const E=w?`${w}/${m.name}`:m.name,c=L(E),I=_(c),T=h.get(I);if(T!==void 0)throw new Error(`Installed Node toolchain contains colliding paths ${JSON.stringify(T)} and ${JSON.stringify(c)}`);h.set(I,c);const g=b.join(d,m.name);if(!C(o,g))throw new Error(`Installed Node toolchain path escapes its root: ${JSON.stringify(c)}`);const k=await u.promises.lstat(g);if(k.isSymbolicLink()){const $=await u.promises.readlink(g),A=H.get(c);if(A===void 0||$!==A)throw new Error(`Installed Node toolchain contains an unsupported symbolic link at ${JSON.stringify(c)}`);M(c,$,".");const U=await u.promises.realpath(g);if(!C(s,U))throw new Error(`Installed Node toolchain symbolic link escapes its root: ${JSON.stringify(c)}`);if(!(await u.promises.stat(g)).isFile())throw new Error(`Installed Node toolchain symbolic link is not file-backed: ${JSON.stringify(c)}`);i+=Buffer.byteLength($,"utf8"),a.push({kind:"symlink",path:c,target:$})}else if(k.isDirectory()){a.push({kind:"directory",path:c}),J(a.length,N,i,e),await x(g,c);continue}else if(k.isFile()){if(k.nlink!==1)throw new Error(`Installed Node toolchain contains a multiply-linked file at ${JSON.stringify(c)}`);N+=1,i+=z(k.size,c),await j(g,c,k.size)}else throw new Error(`Installed Node toolchain contains a special file at ${JSON.stringify(c)}`);J(a.length,N,i,e)}}}export class InstalledNodeTreeMismatchError extends Error{}export async function verifyInstalledNodeTree(o,e,t){const n=await inspectInstalledNodeTree(o,{maxEntries:e.entryCount,maxFiles:e.fileCount,maxUnpackedSizeBytes:e.unpackedSizeBytes},t);if(n.entryCount!==e.entryCount||n.fileCount!==e.fileCount||n.unpackedSizeBytes!==e.unpackedSizeBytes||n.sha256!==e.sha256)throw new InstalledNodeTreeMismatchError(`Installed Node toolchain does not match its trusted tree digest and shape: expected ${JSON.stringify(e)}, received ${JSON.stringify(n)}`)}function M(o,e,t){if(b.posix.isAbsolute(e)||b.win32.isAbsolute(e)||e.includes("\0"))throw new Error(`Node toolchain symbolic link has an absolute target at ${JSON.stringify(o)}`);const n=b.posix.normalize(b.posix.join(b.posix.dirname(o),e));if(t==="."?n===".."||n.startsWith("../"):n!==t&&!n.startsWith(`${t}/`))throw new Error(`Node toolchain symbolic link escapes its root at ${JSON.stringify(o)}`)}function G(o){const e=o.split("/");if(e.length>D)throw new Error("Node archive path has too many segments");const t=[];for(let n=1;n<e.length;n+=1)t.push(e.slice(0,n).join("/"));return t}function P(o,e,t,n){const s=n.archive;if(o>s.entryCount||e>s.fileCount||t>s.unpackedSizeBytes)throw B(o,e,t,n)}function J(o,e,t,n){if(o>n.maxEntries||e>n.maxFiles||t>n.maxUnpackedSizeBytes)throw new Error("Installed Node toolchain exceeds its locked tree shape")}function B(o,e,t,n){return new Error(`Node ${n.version} archive shape mismatch: received ${o.toString()} entries, ${e.toString()} files, and ${t.toString()} unpacked bytes`)}function X(o){for(const[e,t]of Object.entries(o))if(!Number.isSafeInteger(t)||t<0)throw new Error(`${e} must be a non-negative safe integer`)}async function Y(o,e,t){if(p(t),e<=F)return Q(o,t);const n=O.createHash("sha256"),s=u.createReadStream(o,{highWaterMark:K}),a=()=>s.destroy(t?.reason);t?.addEventListener("abort",a,{once:!0});try{for await(const h of s)p(t),n.update(h);return p(t),n.digest("hex")}finally{t?.removeEventListener("abort",a),s.destroy(),s.closed||await new Promise(h=>s.once("close",h))}}async function Q(o,e){const t=await u.promises.open(o,"r");try{const n=O.createHash("sha256"),s=Buffer.allocUnsafe(F);for(;;){p(e);const{bytesRead:a}=await t.read(s,0,s.length,null);if(p(e),a===0)return n.digest("hex");n.update(s.subarray(0,a))}}finally{await t.close()}}function R(o,e){return o<e?-1:o>e?1:0}
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import
|
|
2
|
-
`,{encoding:"utf8",flag:"wx",mode:384})}function
|
|
1
|
+
import k from"node:crypto";import u from"node:fs";import d from"node:path";import{TarArchiveInspector as z,ZipArchiveExtractor as M}from"./archive.js";import{withProcessLock as B}from"../process/locks.js";import{createContainedTemporaryDirectory as L,existingRealInstallDirectory as $,NODE_TOOLCHAIN_CONTENT_DIRECTORY as P,NODE_TOOLCHAIN_MARKER_FILENAME as R,nodeToolchainCacheKey as j,prepareNodeToolchainCache as H,readRealManifestFile as p,removeContainedInstallDirectory as F,requireContainedExecutable as v,requireRealContentDirectory as U}from"./node-cache.js";import{InstalledNodeTreeMismatchError as Y,validateNodeArchiveEntries as q,verifyInstalledNodeTree as g}from"./node-integrity.js";import{createSharedToolchainOperation as J,throwIfSignalAborted as f,waitForPromise as m,waitForSharedToolchainOperation as K}from"./shared-operation.js";import{TOOLCHAIN_METADATA_TIMEOUT_MS as W}from"./timeouts.js";const w=2,E=300*1e3,G=Object.freeze([100,250,500,1e3,2e3,3e3,3e3]),y=new Map;export async function provisionNodeToolchainWithRelease({archiveTimeoutMs:e=E,commandExecutor:r,fetchImpl:t,productDir:n,release:o,signal:i,zipExtractor:a}){f(i);const s=H(n,o),l=j(s);let c=y.get(l);if(c===void 0){c=J(V=>Z({archiveTimeoutMs:e,commandExecutor:r,fetchImpl:t,paths:s,productDir:n,release:o,signal:V,zipExtractor:a})),y.set(l,c);const h=c;c.promise.then(()=>{A(l,h)},()=>{A(l,h)})}return K(c,i)}export async function extractTrustedNodeArchive({archivePath:e,commandExecutor:r,destination:t,release:n,signal:o,timeoutMs:i=E,zipExtractor:a=new M}){if(ce(i),u.existsSync(t))throw new Error(`Node archive destination already exists: ${t}`);u.mkdirSync(t,{mode:448});const s=AbortSignal.timeout(i),l=o===void 0?s:AbortSignal.any([o,s]),c=n.archive.format==="zip"?a:new z;try{const h=await c.inspect(e,n.archive.format,{maxEntries:n.archive.entryCount,maxUnpackedSizeBytes:n.archive.unpackedSizeBytes,signal:l});q(h,n),n.archive.format==="zip"?await a.extract(e,t,n.archive.format,{maxEntries:n.archive.entryCount,maxUnpackedSizeBytes:n.archive.unpackedSizeBytes,signal:l}):(await r("tar",["-xzf",e,"-C",t],{cwd:d.dirname(t),env:process.env,signal:l,timeoutMs:i}),f(l))}catch(h){throw u.rmSync(t,{recursive:!0,force:!0}),h}}async function Z(e){let r=null;const t=await b(e.paths,e.release,e.commandExecutor,e.signal,n=>{r=n});return t!==null?t:B(e.paths.lockPath,async()=>{if(f(e.signal),r===null||!T(e.paths,r)){const n=await b(e.paths,e.release,e.commandExecutor,e.signal);if(n!==null)return n}return F(e.paths),Q(e)},{signal:e.signal,timeoutMs:null})}async function Q(e){const r=L(e.paths.rootDirectory,`.tmp-${e.release.version}`),t=e.release.archive.format==="zip"?".zip":".tar.gz",n=d.join(r,`node${t}`),o=d.join(r,"extracted"),i=d.join(r,"publish"),a=d.join(i,P);try{const s=await m(e.fetchImpl(e.release.archive.url,{signal:e.signal}),e.signal);if(!s.ok)throw new Error(`Failed to download ${e.release.archive.url}: HTTP ${String(s.status)}`);await ee(s,n,e.release,e.signal),await extractTrustedNodeArchive({archivePath:n,commandExecutor:e.commandExecutor,destination:o,release:e.release,signal:e.signal,timeoutMs:e.archiveTimeoutMs,zipExtractor:e.zipExtractor});const l=d.join(o,e.release.archive.rootDirectory);ae(o,l),await g(l,e.release.installed,e.signal),u.mkdirSync(i,{mode:448}),u.renameSync(l,a),u.rmSync(o,{recursive:!0,force:!0}),u.rmSync(n,{force:!0});const c=x(a,e.release),h=await D(a,c,e.release,e.commandExecutor,e.signal);return ne(d.join(i,R),te(e.release,h.npmVersion)),await renameNodeToolchainPublishDirectory(i,e.paths.installDirectory,e.signal),{binDir:S(e.paths.contentDirectory,e.release),nodeExecutable:d.join(e.paths.contentDirectory,c.nodeRelativePath),npmVersion:h.npmVersion,toolchainDir:e.paths.installDirectory}}finally{u.rmSync(r,{recursive:!0,force:!0})}}export async function renameNodeToolchainPublishDirectory(e,r,t){for(let n=0;;n+=1){f(t);try{await u.promises.rename(e,r);return}catch(o){const i=nodeToolchainPublishRetryDelay(o,process.platform,n);if(i===null||X(r))throw o;await m(new Promise(a=>setTimeout(a,i)),t)}}}function X(e){try{return u.lstatSync(e).isDirectory()&&u.readdirSync(e).length>0}catch{return!1}}export function nodeToolchainPublishRetryDelay(e,r,t){if(r!=="win32"||!(e instanceof Error))return null;const n=e.code;return n!=="EACCES"&&n!=="EBUSY"&&n!=="EPERM"?null:G[t]??null}async function b(e,r,t,n,o){if(!$(e))return null;const i=p(e);if(i===null)return null;const a=ie(i);if(a===null||!oe(a,r))return null;U(e);const s=o?N(e,i):null;try{await g(e.contentDirectory,r.installed,n)}catch(h){return f(n),h instanceof Y&&s!==null&&T(e,s)&&o?.(s),null}const l=x(e.contentDirectory,r),c=await D(e.contentDirectory,l,r,t,n);return c.npmVersion!==a.npmVersion?null:{binDir:S(e.contentDirectory,r),nodeExecutable:l.nodeExecutable,npmVersion:c.npmVersion,toolchainDir:e.installDirectory}}function N(e,r){try{const t=u.lstatSync(e.installDirectory,{bigint:!0});return!t.isDirectory()||t.isSymbolicLink()||t.ino===0n||t.birthtimeNs===0n?null:{birthtimeNs:t.birthtimeNs,dev:t.dev,ino:t.ino,manifestSource:r}}catch{return null}}function T(e,r){try{const t=N(e,r.manifestSource);return t!==null&&t.birthtimeNs===r.birthtimeNs&&t.dev===r.dev&&t.ino===r.ino&&p(e)===r.manifestSource}catch{return!1}}async function ee(e,r,t,n){const o=[];let i=0;if(e.body!==void 0&&e.body!==null){const l=e.body[Symbol.asyncIterator]();for(;;){const c=await m(l.next(),n);if(c.done)break;if(!(c.value instanceof Uint8Array))throw new Error("Node toolchain download yielded a non-binary chunk");const h=Buffer.from(c.value.buffer,c.value.byteOffset,c.value.byteLength);if(i+=h.byteLength,i>t.archive.sizeBytes)throw await l.return?.(),new Error(`Node toolchain archive exceeds its trusted ${t.archive.sizeBytes.toString()} byte size`);o.push(h)}}else{const l=await m(e.arrayBuffer(),n),c=Buffer.from(l);i=c.byteLength,i<=t.archive.sizeBytes&&o.push(c)}if(i!==t.archive.sizeBytes)throw new Error(`Node toolchain archive size mismatch: expected ${t.archive.sizeBytes.toString()}, received ${i.toString()}`);const a=Buffer.concat(o,i),s=k.createHash("sha256").update(a).digest("hex");if(s!==t.archive.sha256)throw new Error(`Node toolchain archive digest mismatch: expected ${t.archive.sha256}, received ${s}`);f(n),u.writeFileSync(r,a,{flag:"wx",mode:384})}function x(e,r){const t=r.target.platform==="win32"?"node.exe":"bin/node",n=r.target.platform==="win32"?"npm.cmd":"bin/npm";return{nodeExecutable:v(e,t,{allowSymbolicLink:!1}),nodeRelativePath:t,npmCli:r.target.platform==="win32"?v(e,"node_modules/npm/bin/npm-cli.js",{allowSymbolicLink:!1}):null,npmExecutable:v(e,n,{allowSymbolicLink:r.target.platform!=="win32"})}}async function D(e,r,t,n,o){const i=S(e,t),a=await O(r.nodeExecutable,e,i,n,o);if(a!==t.version)throw new Error(`Downloaded Node toolchain version mismatch: expected ${t.version}, received ${a}`);return{npmVersion:await O(r.npmCli===null?r.npmExecutable:r.nodeExecutable,e,i,n,o,r.npmCli===null?[]:[r.npmCli])}}async function O(e,r,t,n,o,i=[]){const a=nodeToolchainVersionInvocation(r,t,process.env,i),s=await n(e,a.args,{...a.options,signal:o});return re(typeof s.stdout=="string"?s.stdout:Buffer.from(s.stdout??"").toString())}export function nodeToolchainVersionInvocation(e,r,t,n=[]){const o=(t.PATH??"").split(d.delimiter).filter(i=>i.length>0&&i!==r);return{args:[...n,"--version"],options:{cwd:e,env:{...t,PATH:[r,...o].join(d.delimiter)},stdio:"pipe",timeoutMs:W,windowsHide:!0}}}function re(e){const r=e.trim().replace(/^v/u,"");if(!/^(?:0|[1-9]\d*)\.(?:0|[1-9]\d*)\.(?:0|[1-9]\d*)$/u.test(r))throw new Error(`Toolchain command returned an invalid version: ${JSON.stringify(e)}`);return r}function S(e,r){return r.target.platform==="win32"?e:d.join(e,"bin")}function te(e,r){return{arch:e.target.arch,archiveSha256:e.archive.sha256,installedSha256:e.installed.sha256,npmVersion:r,platform:e.target.platform,schemaVersion:w,version:e.version}}function ne(e,r){u.writeFileSync(e,`${JSON.stringify(r,null,2)}
|
|
2
|
+
`,{encoding:"utf8",flag:"wx",mode:384})}function ie(e){let r;try{r=JSON.parse(e)}catch{return null}if(!se(r))return null;const t=["arch","archiveSha256","installedSha256","npmVersion","platform","schemaVersion","version"],n=Object.keys(r).filter(o=>o!=="kind").sort();return n.length!==t.length||n.some((o,i)=>o!==t[i])||r.schemaVersion!==w||!I(r.arch)||!C(r.archiveSha256)||!C(r.installedSha256)||!_(r.npmVersion)||!I(r.platform)||!_(r.version)?null:{arch:r.arch,archiveSha256:r.archiveSha256,installedSha256:r.installedSha256,npmVersion:r.npmVersion,platform:r.platform,schemaVersion:w,version:r.version}}function oe(e,r){return e.version===r.version&&e.platform===r.target.platform&&e.arch===r.target.arch&&e.archiveSha256===r.archive.sha256&&e.installedSha256===r.installed.sha256}function ae(e,r){const t=u.readdirSync(e);if(t.length!==1||t[0]!==d.basename(r)||!u.lstatSync(r).isDirectory()||u.lstatSync(r).isSymbolicLink())throw new Error(`Expected Node archive to contain only ${d.basename(r)}`)}function A(e,r){y.get(e)===r&&y.delete(e)}function ce(e){if(!Number.isSafeInteger(e)||e<=0)throw new Error("Node archive timeout must be a positive safe integer")}function se(e){return e!==null&&typeof e=="object"&&!Array.isArray(e)}function I(e){return typeof e=="string"&&e.length>0}function C(e){return typeof e=="string"&&/^[a-f0-9]{64}$/u.test(e)}function _(e){return typeof e=="string"&&/^(?:0|[1-9]\d*)\.(?:0|[1-9]\d*)\.(?:0|[1-9]\d*)$/u.test(e)}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@gfdelarue/testkit",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.3",
|
|
4
4
|
"description": "Test orchestration platform for product acceptance and QA workflows",
|
|
5
5
|
"license": "SEE LICENSE IN LICENSE",
|
|
6
6
|
"type": "module",
|
|
@@ -74,9 +74,9 @@
|
|
|
74
74
|
"scripts": {
|
|
75
75
|
"build": "tsx scripts/build/index.ts",
|
|
76
76
|
"lint": "tsx scripts/lint/index.ts",
|
|
77
|
-
"test": "npm run build && vitest run --config scripts/test/unit.config.ts &&
|
|
77
|
+
"test": "npm run build && vitest run --config scripts/test/unit.config.ts && tsx scripts/test/integration.ts && vitest run --config scripts/test/system.config.ts",
|
|
78
78
|
"test:unit": "npm run build && vitest run --config scripts/test/unit.config.ts",
|
|
79
|
-
"test:integration": "npm run build &&
|
|
79
|
+
"test:integration": "npm run build && tsx scripts/test/integration.ts",
|
|
80
80
|
"test:system": "npm run build && vitest run --config scripts/test/system.config.ts",
|
|
81
81
|
"test:consumer-smoke": "npm run build && tsx scripts/test/installed-consumer-smoke.ts",
|
|
82
82
|
"test:cloud-account": "npm run build && vitest run --config scripts/test/cloud-account.config.ts",
|
|
@@ -115,6 +115,7 @@
|
|
|
115
115
|
"tsx": "4.23.12",
|
|
116
116
|
"typescript-eslint": "^8.65.0",
|
|
117
117
|
"typescript6": "npm:typescript@6.0.2",
|
|
118
|
+
"typescript7": "npm:typescript@7.0.2",
|
|
118
119
|
"verdaccio": "6.10.3",
|
|
119
120
|
"xk6-sql": "https://github.com/grafana/xk6-sql/archive/refs/tags/v1.0.6.tar.gz"
|
|
120
121
|
},
|