feat(ssh): add verified relay artifact extraction

Co-authored-by: Orca <help@stably.ai>
This commit is contained in:
Jinwoo-H
2026-07-15 17:51:26 -04:00
co-authored by Orca
parent a93e67413a
commit 7580600e37
11 changed files with 1724 additions and 19 deletions
@@ -263,6 +263,7 @@ jobs:
src/main/ssh/ssh-relay-release-asset.test.ts \
src/main/ssh/ssh-relay-artifact-selector.test.ts \
src/main/ssh/ssh-relay-artifact-download.test.ts \
src/main/ssh/ssh-relay-artifact-extraction.test.ts \
src/main/ssh/ssh-relay-runtime-identity.test.ts \
config/scripts/ssh-relay-runtime-compatibility.test.mjs \
config/scripts/ssh-relay-node-release-verification.test.mjs \
@@ -501,6 +502,23 @@ jobs:
cp "$first_output"/*.tar.br "$first_output"/*.spdx.json "$first_output"/*.provenance.json "$first_output"/*.identity.json \
"$signing_report" "$GITHUB_WORKSPACE/runtime-evidence/${{ matrix.tuple }}/"
- name: Measure full-size desktop extraction boundary
shell: bash
run: |
set -euo pipefail
first_output="$RUNNER_TEMP/ssh-relay-runtime/${{ matrix.tuple }}/first"
identity="$first_output/orca-ssh-relay-runtime-${{ matrix.tuple }}.identity.json"
archive=$(find "$first_output" -maxdepth 1 -name '*.tar.br' -print -quit)
test -n "$archive"
extraction_output="$RUNNER_TEMP/ssh-relay-runtime/${{ matrix.tuple }}/desktop-extraction-measurement"
# Why: exact payload size can expose memory behavior that synthetic archive fixtures cannot.
ORCA_SSH_RELAY_FULL_SIZE_ARCHIVE="$archive" \
ORCA_SSH_RELAY_FULL_SIZE_IDENTITY="$identity" \
ORCA_SSH_RELAY_FULL_SIZE_OUTPUT="$extraction_output" \
pnpm exec vitest run --config config/vitest.config.ts --maxWorkers=1 --reporter=verbose \
src/main/ssh/ssh-relay-artifact-extraction-full-size.test.ts
test ! -e "$extraction_output"
- name: Upload unpublished artifact evidence
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
with:
@@ -769,6 +787,7 @@ jobs:
src/main/ssh/ssh-relay-release-asset.test.ts `
src/main/ssh/ssh-relay-artifact-selector.test.ts `
src/main/ssh/ssh-relay-artifact-download.test.ts `
src/main/ssh/ssh-relay-artifact-extraction.test.ts `
src/main/ssh/ssh-relay-runtime-identity.test.ts `
config/scripts/ssh-relay-runtime-compatibility.test.mjs `
config/scripts/ssh-relay-node-release-verification.test.mjs `
@@ -1010,6 +1029,26 @@ jobs:
Copy-Item -LiteralPath $signingReport -Destination $evidence
Copy-Item -LiteralPath $sourceSignatureReport -Destination $evidence
- name: Measure full-size desktop extraction boundary
shell: pwsh
run: |
$ErrorActionPreference = 'Stop'
$firstOutput = Join-Path $env:RUNNER_TEMP 'ssh-relay-runtime/${{ matrix.tuple }}/first'
$identity = Join-Path $firstOutput 'orca-ssh-relay-runtime-${{ matrix.tuple }}.identity.json'
$archive = Get-ChildItem -LiteralPath $firstOutput -Filter '*.zip' -File | Select-Object -First 1
if (-not $archive) { throw 'runtime ZIP is missing for desktop extraction measurement' }
$extractionOutput = Join-Path $env:RUNNER_TEMP 'ssh-relay-runtime/${{ matrix.tuple }}/desktop-extraction-measurement'
# Why: exact payload size can expose memory behavior that synthetic archive fixtures cannot.
$env:ORCA_SSH_RELAY_FULL_SIZE_ARCHIVE = $archive.FullName
$env:ORCA_SSH_RELAY_FULL_SIZE_IDENTITY = $identity
$env:ORCA_SSH_RELAY_FULL_SIZE_OUTPUT = $extractionOutput
pnpm exec vitest run --config config/vitest.config.ts --maxWorkers=1 --reporter=verbose `
src/main/ssh/ssh-relay-artifact-extraction-full-size.test.ts
if ($LASTEXITCODE -ne 0) { throw 'full-size desktop extraction measurement failed' }
if (Test-Path -LiteralPath $extractionOutput) {
throw 'full-size desktop extraction measurement cleanup failed'
}
- name: Upload unpublished artifact evidence
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
with:
@@ -94,9 +94,25 @@ describe('SSH relay runtime artifact workflow', () => {
const uploadIndex = steps.findIndex(
(step) => step.name === 'Upload unpublished artifact evidence'
)
const measurementIndex = steps.findIndex(
(step) => step.name === 'Measure full-size desktop extraction boundary'
)
const windowsBuildIndex = windowsSteps.findIndex(
(step) => step.name === 'Build twice, inspect, smoke, and compare exact runtime'
)
const windowsMeasurementIndex = windowsSteps.findIndex(
(step) => step.name === 'Measure full-size desktop extraction boundary'
)
const windowsUploadIndex = windowsSteps.findIndex(
(step) => step.name === 'Upload unpublished artifact evidence'
)
expect(buildIndex).toBeGreaterThan(-1)
expect(uploadIndex).toBeGreaterThan(buildIndex)
expect(windowsBuildIndex).toBeGreaterThan(-1)
expect(measurementIndex).toBeGreaterThan(buildIndex)
expect(uploadIndex).toBeGreaterThan(measurementIndex)
expect(windowsMeasurementIndex).toBeGreaterThan(windowsBuildIndex)
expect(windowsUploadIndex).toBeGreaterThan(windowsMeasurementIndex)
expect(source).toContain('verify-ssh-relay-runtime.mjs')
expect(source).toContain('ssh-relay-runtime-workflow.test.mjs')
for (const testName of [
@@ -105,12 +121,16 @@ describe('SSH relay runtime artifact workflow', () => {
'ssh-relay-release-asset.test.ts',
'ssh-relay-artifact-selector.test.ts',
'ssh-relay-artifact-download.test.ts',
'ssh-relay-artifact-extraction.test.ts',
'ssh-relay-runtime-identity.test.ts'
]) {
// Why: portable desktop selection contracts need proof on every native runner family, not
// only the local client architecture or the generic Linux PR job.
expect(source.split(`src/main/ssh/${testName}`)).toHaveLength(3)
}
expect(
source.split('src/main/ssh/ssh-relay-artifact-extraction-full-size.test.ts')
).toHaveLength(3)
expect(source).toContain('pnpm install --frozen-lockfile --ignore-scripts')
expect(source).toContain('--connect-timeout 20 --max-time 300 --retry 2')
expect(source).toContain('mkdir -p "$output_root"')
@@ -9,16 +9,21 @@ keeps commands, hashes, runner identities, timings, and failure details.
A checked box means the work has evidence in the detailed ledger. Design approval alone does not
complete a box.
Active checkpoint: **Milestone 5 / Work Package 4 desktop cache boundary — in-progress disconnected
extraction/cache contract audit, 2026-07-15, Codex implementation owner.** Exact-head run
Active checkpoint: **Milestone 5 / Work Package 4 desktop cache boundary — in-progress native-gate
preparation for disconnected extraction, 2026-07-15, Codex implementation owner.** Exact-head run
[29449869519](https://github.com/stablyai/orca/actions/runs/29449869519) closes deterministic bounded
`.tar.br` under `E-M5-PORTABLE-ARCHIVE-CI-001`: all six primary Node 24 jobs and both Linux
oldest-userland supplements pass. PR Checks and Golden E2E are green; Windows x64 floor passes and
Windows arm64 passes smoke before the retained hosted build-26200 versus required-26100 rejection.
The next safe slice is only the isolated desktop extraction/cache resource and failure-semantics
audit followed by a purpose-named RED. Desktop consumers, SSH transfer/install, mode wiring,
tuple enablement, publication, production keys/environment/seed, and merge to `main` remain
disconnected.
The isolated desktop extractor is locally green under `E-M5-ARTIFACT-EXTRACTION-LOCAL-001` and
`E-M5-ARTIFACT-EXTRACTION-BROAD-LOCAL-001`: 17 focused tests cover both archive families, hostile
inputs, exact tree/modes, mutation, cancellation, cleanup, and ownership; broad relay/release/static
gates pass. An exact downloaded 122,027,869-byte Darwin arm64 tree extracts in 878.73ms with
49,283,072-byte incremental RSS under `E-M5-ARTIFACT-EXTRACTION-FULL-SIZE-LOCAL-001`. Both native job
families now require synthetic cross-family and post-build full-size gates under
`E-M5-ARTIFACT-EXTRACTION-CI-WIRING-LOCAL-001`. Next is exact-head all-six Node 24 execution. Desktop
consumers, cache publication, SSH transfer/install, mode wiring, tuple enablement, publication,
production keys/environment/seed, and merge to `main` remain disconnected.
## Safety status
@@ -307,9 +312,15 @@ disconnected.
- [ ] Stream bounded downloads; verify signature, size, archive hash, and extracted tree.
The disconnected Electron downloader is locally green under
`E-M5-ARTIFACT-DOWNLOAD-LOCAL-RED-001` and `E-M5-ARTIFACT-DOWNLOAD-LOCAL-001`; exact-head
all-six native CI is closed under `E-M5-ARTIFACT-DOWNLOAD-CI-001`. Packaged signature loading
and extracted-tree verification remain open.
all-six native CI is closed under `E-M5-ARTIFACT-DOWNLOAD-CI-001`. Disconnected strict
TAR/Brotli and ZIP extraction plus complete-tree verification are locally green under
`E-M5-ARTIFACT-EXTRACTION-LOCAL-001`; an exact full-size Actions payload is locally within time
and memory budgets under `E-M5-ARTIFACT-EXTRACTION-FULL-SIZE-LOCAL-001`. Native all-six
execution and packaged signature loading remain open.
- [ ] Add exclusive staging, atomic publication, quarantine, locking, and the 2 GiB cache policy.
Exclusive owned staging and failure/cancellation cleanup are locally green under
`E-M5-ARTIFACT-EXTRACTION-LOCAL-001`; publication, quarantine, locking, and eviction are not
implemented yet.
- [ ] Prove verified cached bytes can be transferred while the client is offline.
- [ ] Preserve `ORCA_RELAY_PATH` behind the official-build trust boundary.
@@ -8,8 +8,8 @@ work; keep exact commands, runner identities, hashes, metrics, and residual gaps
Date created: 2026-07-14<br>
Last updated: 2026-07-15<br>
Current phase: Milestone 5 / Work Package 4 desktop cache boundary — **In progress — 2026-07-15, Codex implementation owner: disconnected desktop extraction/cache contract audit and RED**. Exact-head run 29449869519 closes the portable POSIX artifact prerequisite under E-M5-PORTABLE-ARCHIVE-CI-001: all six primary Node 24 jobs and both Linux oldest-userland supplements pass deterministic build, strict inspection/extraction, bundled Node/PTY/watcher smoke, equality, and read-back. Audit the existing desktop boundaries and resource/failure contracts, then add only a purpose-named missing extraction/cache-staging RED. Do not add a desktop caller, cache publication, SSH transfer/install, tuple enablement, mode wiring, or default behavior.<br>
Session checkpoint: **In progress — 2026-07-15, Codex implementation owner** — exact implementation/evidence head `3080eda77fd6a11ed87f536c1963e5b67abe7621` passes artifact run 29449869519 on all six primary native jobs and both Linux supplements. PR Checks 29449869451 and both Golden E2E jobs in run 29449869407 are green. Windows x64 oldest-floor execution passes; Windows arm64 reconstructs and smokes the exact runtime in 7,884.492ms with 49,733,632-byte RSS, then rejects hosted build 26200 against required build 26100. E-M5-PORTABLE-ARCHIVE-CI-001 records exact jobs, runner images, tests, artifact IDs/sizes/hashes, and smoke metrics. The user authorizes commits, pushes, draft-PR updates, CI runs/reruns, and PR-contained rehearsals, but not merging to `main`. There is no release write, publication caller, desktop consumer, cache publication/extraction, SSH transfer/install, tuple enablement, or production/default behavior change. Legacy remains the production default.<br>
Current phase: Milestone 5 / Work Package 4 desktop cache boundary — **In progress — 2026-07-15, Codex implementation owner: disconnected desktop extraction exact-head native gate**. Focused, broad, static, exact downloaded-payload, and native-workflow wiring gates pass locally under E-M5-ARTIFACT-EXTRACTION-LOCAL-001, E-M5-ARTIFACT-EXTRACTION-BROAD-LOCAL-001, E-M5-ARTIFACT-EXTRACTION-FULL-SIZE-LOCAL-001, and E-M5-ARTIFACT-EXTRACTION-CI-WIRING-LOCAL-001. Checkpoint the exact implementation SHA, push it, and require all-six Node 24 synthetic cross-family plus native full-size evidence before cache publication begins. Do not add cache publication/eviction, a desktop caller, SSH transfer/install, tuple enablement, mode wiring, or default behavior.<br>
Session checkpoint: **In progress — 2026-07-15, Codex implementation owner** — the 17-test focused suite, 81-test artifact suite, 249-pass relay suite, 279-test release suite, typecheck, lint, and diff gates pass. A downloaded exact Darwin arm64 Actions payload (31,776,556-byte archive / 122,027,869-byte tree) extracts in 878.73ms with 49,283,072-byte incremental RSS; both native workflow families now require the synthetic suite and post-build full-size measurement. Native Node 24 evidence remains open. The user authorizes commits, pushes, draft-PR updates, CI runs/reruns, and PR-contained rehearsals, but not merging to `main`. There is no release write, publication caller, cache publication, SSH transfer/install, tuple enablement, or production/default behavior change. Legacy remains the production default.<br>
Primary design: [SSH relay GitHub Release plan](./2026-07-14-ssh-relay-github-release-plan.html)<br>
Motivating issues: [#8450](https://github.com/stablyai/orca/issues/8450), [#1693](https://github.com/stablyai/orca/issues/1693)
@@ -138,10 +138,10 @@ same change as the work it records.
archive, correction, all-six primary jobs, and both Linux oldest-userland supplements. Windows ZIP
and upstream Node `.tar.xz` remain unchanged.
- Active package: **In progress — 2026-07-15, Codex implementation owner** — disconnected desktop
extraction/cache contract audit and purpose-named RED. Inventory existing cache/path/atomic-file
boundaries, formalize resource and integrity failure oracles, and prove the missing contract before
implementation. Keep cache publication, desktop call sites, SSH behavior, mode wiring, tuple
enablement, release writes, and defaults intact.
extraction capability after E-M5-ARTIFACT-EXTRACTION-AUDIT-001 and
E-M5-ARTIFACT-EXTRACTION-LOCAL-RED-001. Implement only strict TAR/Brotli and ZIP staging plus full
signed-tree verification and bounded cleanup. Keep cache publication/eviction, desktop call sites,
SSH behavior, mode wiring, tuple enablement, release writes, and defaults intact.
- Completed Work Package 2 gate: target-native Windows source-signature reports from exact-head
artifact jobs 87267322867 and 87267322870 were independently downloaded and matched to their
identities and signing-stage reports under E-M3-WINDOWS-SOURCE-SIGNATURE-CI-001. PR Checks
@@ -1084,8 +1084,11 @@ Suggested concrete modules:
- [x] Verify archive SHA-256 before extraction.
E-M5-ARTIFACT-DOWNLOAD-LOCAL-001 and E-M5-ARTIFACT-DOWNLOAD-CI-001.
- [ ] Extract into an exclusive temporary directory with all archive-safety checks from Milestone 2.
Begin only after the `.tar.br` prerequisite has exact-head all-six artifact evidence.
Implemented locally under E-M5-ARTIFACT-EXTRACTION-LOCAL-001 after the `.tar.br` prerequisite;
native cross-client and exact full-size evidence remain required before this item closes.
- [ ] Verify the exact expanded tree before atomically publishing the cache entry.
The disconnected full-tree verifier passes locally under E-M5-ARTIFACT-EXTRACTION-LOCAL-001;
atomic cache publication is deliberately absent and remains a later package.
- [ ] Coordinate cache ownership across multiple Orca processes and windows.
- [ ] Recover stale download/extraction locks without deleting an active writer.
- [ ] Never select partial files, partial directories, or entries with a failed verification record.
@@ -12547,6 +12550,222 @@ config/scripts/ssh-relay-runtime-portable-archive.test.mjs`; changed-file `oxfmt
- Follow-up: checkpoint this evidence and begin only the disconnected desktop extraction/cache
boundary audit and purpose-named RED.
### E-M5-ARTIFACT-EXTRACTION-AUDIT-001 — Desktop extraction boundary and package split
- Date: 2026-07-15
- Owner: Codex implementation owner
- Source: exact local/pushed head `a93e67413a2a1446e8e0f7348af1fdf72737287d` after portable archive
evidence; draft PR #8741.
- Exact audit commands: `rg --files src/main/ssh config/scripts | rg 'ssh-relay|relay-runtime' |
sort`; targeted `rg -n` over `cache|extract|atomic|quarantine`; and complete reads of the desktop
schema/consistency/path-policy/selector/downloader plus release TAR/Brotli, ZIP, extraction, and
full-tree verification modules.
- Findings:
- the desktop has a signature-verified immutable selected-artifact value and an exclusive,
streaming size/hash-verified downloader, but no extraction or cache module and no consumer;
- release-job extractors under `config/scripts` are not packaged desktop modules and use the
five-minute release boundary rather than the two-minute desktop extraction budget;
- the verified selected artifact already carries the exact content ID, archive name/size/hash,
entry paths/types/modes/sizes/hashes, file count, expanded size, and portable native-attestation
hashes required for cross-client verification;
- extraction must revalidate the regular archive file and its signed size/hash before staging,
strictly inspect the authenticated archive family, extract only into an exclusive leaf, stream a
complete no-extra-file tree verification, detect source mutation, and remove only staging it
created on every failure/cancellation;
- native execution/trust probes are deliberately not part of desktop cross-client extraction.
Their signed hashes are bound by tuple consistency; target-native execution remains a later
remote-install gate.
- Package split: `ssh-relay-artifact-extraction.ts` owns input state, timeout, exclusive staging, and
cleanup; `ssh-relay-artifact-tar-extraction.ts` owns strict bounded TAR/Brotli behavior;
`ssh-relay-artifact-zip-extraction.ts` owns strict bounded ZIP behavior; and
`ssh-relay-artifact-tree-verification.ts` owns streamed complete-tree verification. Cache locking,
publication records, quarantine, eviction, and lookup stay in the later
`ssh-relay-artifact-cache.ts` package.
- Budgets: two-minute extraction timeout; 65,536-byte stream chunks; 64 MiB maximum measured desktop
incremental RSS; no buffer over 1 MiB; archive/expanded/file/count/path limits remain signed-schema
constraints. This package must expose the constants for contract and measurement tests.
- Consumer-disconnection oracle: this audit and the next capability do not create a cache root,
publish an entry, load an embedded key/manifest, call the downloader, transfer over SSH, enable a
tuple/mode, or change the legacy default.
- Does not prove: implementation, hostile archive behavior, packaged Electron behavior, full-size
memory/latency, cache concurrency/publication/quarantine/eviction, live SSH, native trust, or any
enabled tuple.
- Follow-up: add the purpose-named cross-family RED, then implement only the disconnected extraction
capability across the concrete modules above.
### E-M5-ARTIFACT-EXTRACTION-LOCAL-RED-001 — Packaged desktop extraction capability is absent
- Date: 2026-07-15
- Owner: Codex implementation owner
- Source: uncommitted purpose-named
`src/main/ssh/ssh-relay-artifact-extraction.test.ts` atop audit head `a93e67413`.
- Exact command:
`/usr/bin/time -l pnpm exec vitest run --config config/vitest.config.ts --maxWorkers=1 src/main/ssh/ssh-relay-artifact-extraction.test.ts`
- Environment: macOS 26.2 arm64, Node v26.0.0, pnpm 10.24.0, Vitest 4.1.5.
- Result: required RED; one failed suite / zero collected tests in 1.75s Vitest / 7.67s wall with
132,104,192-byte maximum RSS and zero swaps. Import fails because
`./ssh-relay-artifact-extraction` does not exist.
- Contract encoded before implementation:
- synthesize real TAR/Brotli and ZIP archives on every client OS;
- expose exact 120,000ms / 65,536-byte / 64 MiB incremental-RSS / 1 MiB maximum-buffer budgets;
- revalidate signed archive size/hash, strictly reject mismatched and undeclared entries, verify
every extracted file byte, and accept no extra tree entry;
- create a fresh parent but an exclusive final staging leaf; settle pre-cancellation; remove only
owned partial output and preserve an existing owner.
- Does not prove: implementation, collected test behavior, Node 24/native clients, full-size memory,
packaged Electron extraction, cache publication/concurrency, SSH, or any enabled tuple.
- Follow-up: implement the smallest disconnected extraction capability without cache or consumer
wiring, then run focused/release/desktop/static gates and require native CI.
### E-M5-ARTIFACT-EXTRACTION-LOCAL-001 — Disconnected desktop extraction passes locally
- Date: 2026-07-15
- Owner: Codex implementation owner
- Source: uncommitted extraction capability and purpose suite atop pushed head
`a93e67413a2a1446e8e0f7348af1fdf72737287d`; the exact implementation SHA will be appended after
the local broad gates pass and this evidence is checkpointed.
- Environment: macOS 26.2 arm64, Node v26.0.0, pnpm 10.24.0, Vitest 4.1.5. Node 24/native client
execution remains the next gate.
- Exact focused command:
`/usr/bin/time -l pnpm exec vitest run --config config/vitest.config.ts --maxWorkers=1 src/main/ssh/ssh-relay-artifact-extraction.test.ts`
- Focused result: one passed file / 17 passed tests in 1.65s Vitest / 4.15s wall; 158,105,600-byte
maximum RSS, 96,179,552-byte peak memory footprint, zero swaps, and 353 page faults. The observed
process maximum is 26,001,408 bytes above the 132,104,192-byte missing-module RED process baseline,
below the 64 MiB incremental contract. This is a synthetic 8 MiB cancellation/mutation fixture,
not the separately required exact full-size artifact measurement.
- Exact static commands:
`pnpm exec oxlint src/main/ssh/ssh-relay-artifact-extraction.ts src/main/ssh/ssh-relay-artifact-tar-extraction.ts src/main/ssh/ssh-relay-artifact-tree-verification.ts src/main/ssh/ssh-relay-artifact-zip-reader.ts src/main/ssh/ssh-relay-artifact-zip-extraction.ts src/main/ssh/ssh-relay-artifact-extraction.test.ts`;
`pnpm typecheck`.
- Static result: both commands exit zero. The implementation is split by concrete responsibility;
no max-lines disable or vague helper/common module was added.
- Oracle proved:
- only the immutable selected tuple/archive identity crosses the boundary; the exact signed regular
archive size/hash and filesystem state are checked before staging and again after extraction;
- TAR/Brotli and ZIP are strictly inspected and streamed on every client OS into an exclusive leaf
beneath a resolved parent; POSIX modes and every signed path/type/size/hash are revalidated;
- extra/traversal/truncated/case-fold-colliding entries, ZIP symbolic links/encryption shapes,
wrong bytes, archive mutation, and existing-owner collisions fail closed;
- pre-cancellation and cancellation after staging begins settle, remove only owned partial output,
and leave no eligible partial runtime tree;
- extraction retains the declared 120,000ms timeout, 65,536-byte chunks, 64 MiB measured
incremental-RSS budget, and 1 MiB maximum-write-buffer contract.
- Consumer-disconnection oracle: there is no cache root or publication record, downloader/manifest
loader caller, SSH transfer/install, runtime execution, target setting, Beta option, tuple
enablement, release write, or artifact publication. Legacy remains the only production/default
path.
- Does not prove: Node 24 or native Windows/Linux/macOS clients, packaged Electron behavior, exact
full-size runtime memory/latency, cache locking/publication/quarantine/eviction, live SSH, native
trust, or any enabled tuple.
- Follow-up: run the broad relay/release/static gates, prove the focused source suite is invoked by
both POSIX and Windows native artifact job families, then collect all-six Node 24 and exact
full-size payload evidence before beginning cache publication.
### E-M5-ARTIFACT-EXTRACTION-BROAD-RED-001 — Broad run exposes late rejection observer
- Date: 2026-07-15
- Owner: Codex implementation owner
- Source: uncommitted E-M5-ARTIFACT-EXTRACTION-LOCAL-001 tree atop `a93e67413`.
- Exact command:
`pnpm exec vitest run --config config/vitest.config.ts --maxWorkers=1 src/main/ssh/ssh-relay-artifact-*.test.ts`
- Result: required RED; three passed files / one failed file, 80 passed tests / one failed test, and
one unhandled rejection in 2.76s. The authenticated mutation test correctly fails closed with
`unexpected EOF`, but can reject during its awaited `writeFile` before the later Vitest assertion
attaches, so the runner reports `PromiseRejectionHandledWarning` and an unhandled rejection.
- Classification: purpose-test timing defect, not a product extraction acceptance. The owned staging
tree is still removed and the mutation is rejected.
- Correction: attach the Vitest rejection observer when extraction starts, before waiting for the
staging path or mutating/aborting; accept the valid fail-closed `unexpected EOF` diagnostic.
- Follow-up: rerun the exact broad artifact command and require zero unhandled errors before any CI
checkpoint.
### E-M5-ARTIFACT-EXTRACTION-BROAD-LOCAL-001 — Broad local relay and release gates pass
- Date: 2026-07-15
- Owner: Codex implementation owner
- Source: corrected local extraction tree atop `a93e67413`; exact implementation SHA pending the
checkpoint commit.
- Exact commands and results:
- `pnpm exec vitest run --config config/vitest.config.ts --maxWorkers=1 src/main/ssh/ssh-relay-artifact-*.test.ts`:
four passed files / 81 passed tests in 10.40s after the mutation observer correction, with zero
unhandled errors;
- `/usr/bin/time -l pnpm exec vitest run --config config/vitest.config.ts --maxWorkers=1 src/main/ssh/ssh-relay-*.test.ts`:
17 passed files / one platform-skipped file, 249 passed tests / one platform-skipped test in
21.60s Vitest / 24.97s wall, 244,383,744-byte maximum process RSS and zero swaps;
- `/usr/bin/time -l pnpm exec vitest run --config config/vitest.config.ts --maxWorkers=1 config/scripts/ssh-relay-runtime-*.test.mjs`:
50 passed files / 279 passed tests in 45.04s Vitest / 47.58s wall, 190,840,832-byte maximum
process RSS and zero swaps;
- `pnpm typecheck`; `pnpm lint`; `git diff --check`: all exit zero. Full lint retains only existing
warnings outside this package; reliability gates, max-lines ratchet, bundled skills, localization
catalog, and localization coverage pass.
- Diff oracle: no diff exists in the preserved Milestone 0 resolver pair or
`config/scripts/verify-release-required-assets.mjs` and its test. Only the concrete extraction,
measurement, workflow-wiring, and living-plan files are in scope.
- Does not prove: Node 24/native clients, exact full-size payload behavior, cache, live SSH, native
trust, or any enabled tuple.
- Follow-up: measure an exact downloaded payload, close native workflow wiring, then checkpoint and
push the implementation for all-six execution.
### E-M5-ARTIFACT-EXTRACTION-FULL-SIZE-LOCAL-001 — Exact downloaded payload stays within budget
- Date: 2026-07-15
- Owner: Codex implementation owner
- Source artifact: Actions artifact ID 8357083684, `ssh-relay-runtime-darwin-arm64`, downloaded from
exact implementation run [29449869519](https://github.com/stablyai/orca/actions/runs/29449869519)
using `gh run download 29449869519 --repo stablyai/orca --name ssh-relay-runtime-darwin-arm64`.
- Payload: `darwin-arm64`, archive SHA-256
`2031bea0f820107f8c92ab62a446fe0ebbeb64e78b63b6707c0610216ac68e3d`, 31,776,556 compressed
bytes, 122,027,869 expanded bytes, and 35 files.
- Exact measurement command: set `ORCA_SSH_RELAY_FULL_SIZE_ARCHIVE`,
`ORCA_SSH_RELAY_FULL_SIZE_IDENTITY`, and an absent owned `ORCA_SSH_RELAY_FULL_SIZE_OUTPUT`, then
run `pnpm exec vitest run --config config/vitest.config.ts --maxWorkers=1 --reporter=verbose src/main/ssh/ssh-relay-artifact-extraction-full-size.test.ts`.
- Result: four consecutive post-harness runs pass locally. The retained verbose run extracts,
complete-tree verifies, and cleans the exact payload in 878.731667ms with a 72,679,424-byte
baseline RSS, 121,962,496-byte sampled peak RSS, and 49,283,072-byte incremental RSS. This is below
the 120,000ms and 67,108,864-byte limits.
- Harness boundary: the measurement uses the authenticated Actions identity only to construct the
top-level selected-artifact shape; its explicit why-comment prohibits product use. The production
extractor still rehashes the archive, inspects it, creates exclusive staging, verifies the complete
tree, rehashes the archive, and cleans output. Signature selection itself is separately proven by
E-M5-OFFLINE-SELECTION-LOCAL-001/CI-001.
- Does not prove: Node 24, other native clients or archive families at full size, packaged Electron,
cache, SSH, or an enabled tuple.
- Follow-up: require this measurement after native build and before upload on all six artifact jobs;
retain synthetic both-family execution on every client runner.
### E-M5-ARTIFACT-EXTRACTION-CI-WIRING-RED-001 — Native jobs omit extraction proof
- Date: 2026-07-15
- Owner: Codex implementation owner
- Exact command:
`pnpm exec vitest run --config config/vitest.config.ts --maxWorkers=1 config/scripts/ssh-relay-runtime-workflow.test.mjs`
- Result: required RED; one failed / six passed tests in 483ms. The new source contract expects the
purpose suite in both native job families, but the workflow contains zero occurrences, so the
split count is one rather than three.
- Follow-up: invoke the synthetic cross-family suite in both native contract steps and add a
post-build full-size budget measurement before artifact upload in both job families.
### E-M5-ARTIFACT-EXTRACTION-CI-WIRING-LOCAL-001 — Both native job families require extraction gates
- Date: 2026-07-15
- Owner: Codex implementation owner
- Source: local workflow/test/full-size harness atop `a93e67413`; exact implementation SHA pending
the checkpoint commit.
- Exact command:
`pnpm exec vitest run --config config/vitest.config.ts --maxWorkers=1 config/scripts/ssh-relay-runtime-workflow.test.mjs`
- Result: one passed file / seven passed tests in 404ms after formatting; typecheck also exits zero.
- Oracle proved: all four POSIX native clients and both Windows native clients run the 17-test
synthetic TAR/Brotli-plus-ZIP suite under Node 24. After each exact native build, a separate step
runs the one-test full-size measurement against that job's first verified archive/identity, checks
120,000ms / 64 MiB, cleans its owned output, and must pass before upload. The workflow contract
requires both measurement steps to occur after build and before upload.
- Consumer-disconnection oracle: workflow artifacts remain unpublished Actions evidence; no release
write, packaged desktop consumer, cache publication, SSH transfer/install, tuple, or mode is
connected.
- Does not prove: actual runner execution, cross-family full-size payloads on every client, packaged
Electron, cache, SSH, or any enabled tuple.
- Follow-up: checkpoint, push, and collect exact-head per-job synthetic/full-size metrics from all six
native jobs before beginning the cache-publication package.
## Accepted Gaps
No product gap is accepted merely because it appears in this list. Each entry requires explicit
@@ -12605,9 +12824,10 @@ The project is not complete until every applicable item below is checked with ev
## Next Required Action
Audit the disconnected desktop extraction/cache boundary and add only the purpose-named missing
resource/failure-semantics RED. Do not implement a desktop call site, cache publication, SSH
transfer/install, mode wiring, tuple enablement, or default behavior in that audit/test package. Keep
Run the broader release/desktop/static gates for the locally green disconnected extraction
capability, wire its purpose suite into both native artifact job families, and collect exact-head
all-six Node 24 plus full-size payload evidence. Do not add a desktop call site, cache
publication/eviction, SSH transfer/install, mode wiring, tuple enablement, or default behavior. Keep
Node upstream `.tar.xz` inputs, Windows ZIP,
`ORCA_RELAY_PATH`, existing desktop required-assets behavior, detached-signature byte encoding,
Windows arm64 build 26100, macOS 13.5, Linux kernel 4.18, release-cut, desktop builds, publication,
@@ -0,0 +1,114 @@
import { readFile, rm, stat } from 'node:fs/promises'
import { performance } from 'node:perf_hooks'
import { describe, expect, it } from 'vitest'
import {
extractSshRelayArtifact,
SSH_RELAY_ARTIFACT_EXTRACTION_LIMITS
} from './ssh-relay-artifact-extraction'
import type { SshRelaySelectedArtifact } from './ssh-relay-artifact-selector'
type MeasurementIdentity = {
tupleId: SshRelaySelectedArtifact['tupleId']
contentId: SshRelaySelectedArtifact['contentId']
os: SshRelaySelectedArtifact['tuple']['os']
archive: SshRelaySelectedArtifact['tuple']['archive']
entries: SshRelaySelectedArtifact['tuple']['entries']
}
const archivePath = process.env.ORCA_SSH_RELAY_FULL_SIZE_ARCHIVE
const identityPath = process.env.ORCA_SSH_RELAY_FULL_SIZE_IDENTITY
const outputDirectory = process.env.ORCA_SSH_RELAY_FULL_SIZE_OUTPUT
const hasMeasurementInput = Boolean(archivePath && identityPath && outputDirectory)
function measurementIdentity(input: unknown): MeasurementIdentity {
if (
typeof input !== 'object' ||
input === null ||
!('tupleId' in input) ||
!('contentId' in input) ||
!('os' in input) ||
!('archive' in input) ||
!('entries' in input) ||
!Array.isArray(input.entries)
) {
throw new Error('Full-size extraction measurement identity is incomplete')
}
return input as MeasurementIdentity
}
function measurementArtifact(identity: MeasurementIdentity): SshRelaySelectedArtifact {
const tuple = identity as unknown as SshRelaySelectedArtifact['tuple']
// Why: this runner measures exact Actions artifact resources; it is never a product trust bypass.
return Object.freeze({
kind: 'selected',
tupleId: identity.tupleId,
contentId: identity.contentId,
releaseTag: 'measurement-only',
archive: Object.freeze({
...identity.archive,
downloadUrl: 'https://invalid.example/measurement-only'
}),
tuple
})
}
describe.skipIf(!hasMeasurementInput)('SSH relay full-size artifact extraction', () => {
it(
'stays within the desktop time and incremental-memory budgets',
async () => {
const identity = measurementIdentity(
JSON.parse(await readFile(identityPath as string, 'utf8')) as unknown
)
await expect(stat(outputDirectory as string)).rejects.toMatchObject({ code: 'ENOENT' })
const baselineRss = process.memoryUsage().rss
let peakRss = baselineRss
const sample = (): void => {
peakRss = Math.max(peakRss, process.memoryUsage().rss)
}
const sampler = setInterval(sample, 1)
const startedAt = performance.now()
let result
try {
result = await extractSshRelayArtifact({
artifact: measurementArtifact(identity),
archivePath: archivePath as string,
outputDirectory: outputDirectory as string
})
} finally {
clearInterval(sampler)
sample()
}
const elapsedMs = performance.now() - startedAt
const incrementalRssBytes = Math.max(0, peakRss - baselineRss)
console.log(
`ssh_relay_full_size_extraction=${JSON.stringify({
tupleId: identity.tupleId,
archiveBytes: identity.archive.size,
expandedBytes: identity.archive.expandedSize,
files: identity.archive.fileCount,
elapsedMs,
baselineRss,
peakRss,
incrementalRssBytes
})}`
)
try {
expect(result).toMatchObject({
tupleId: identity.tupleId,
contentId: identity.contentId,
files: identity.archive.fileCount,
expandedBytes: identity.archive.expandedSize
})
expect(elapsedMs).toBeLessThanOrEqual(SSH_RELAY_ARTIFACT_EXTRACTION_LIMITS.timeoutMs)
expect(incrementalRssBytes).toBeLessThanOrEqual(
SSH_RELAY_ARTIFACT_EXTRACTION_LIMITS.maximumIncrementalMemoryBytes
)
} finally {
await rm(outputDirectory as string, { recursive: true })
}
},
SSH_RELAY_ARTIFACT_EXTRACTION_LIMITS.timeoutMs + 10_000
)
})
@@ -0,0 +1,503 @@
import { createHash } from 'node:crypto'
import { lstat, mkdtemp, readFile, realpath, rm, stat, writeFile } from 'node:fs/promises'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
import { brotliCompressSync, constants as zlibConstants } from 'node:zlib'
import nacl from 'tweetnacl'
import { afterEach, describe, expect, it } from 'vitest'
import yazl from 'yazl'
import {
extractSshRelayArtifact,
SSH_RELAY_ARTIFACT_EXTRACTION_LIMITS
} from './ssh-relay-artifact-extraction'
import type { SshRelayArtifactManifest, SshRelayRuntimeTuple } from './ssh-relay-artifact-schema'
import {
createSshRelayArtifactTestManifest,
createSshRelayWindowsArtifactTestManifest
} from './ssh-relay-artifact-test-manifest'
import {
selectSshRelayArtifact,
type SshRelayHostEvidence,
type SshRelaySelectedArtifact
} from './ssh-relay-artifact-selector'
import {
signSshRelayArtifactManifest,
sshRelayManifestKeyId,
verifySshRelayArtifactManifest
} from './ssh-relay-manifest-signature'
import { sshRelayRuntimeArchiveName } from './ssh-relay-release-asset'
import { computeSshRelayRuntimeContentId, type SshRelayDigest } from './ssh-relay-runtime-identity'
const keyPair = nacl.sign.keyPair.fromSeed(Uint8Array.from({ length: 32 }, (_, index) => index))
const temporaryDirectories: string[] = []
function sha256(bytes: Uint8Array): SshRelayDigest {
return `sha256:${createHash('sha256').update(bytes).digest('hex')}`
}
function tarString(header: Buffer, value: string, offset: number, length: number): void {
header.write(value, offset, length, 'utf8')
}
function tarOctal(header: Buffer, value: number, offset: number, length: number): void {
header.write(value.toString(8).padStart(length - 1, '0'), offset, length - 1, 'ascii')
header[offset + length - 1] = 0
}
function tarHeader(entry: SshRelayRuntimeTuple['entries'][number], size: number): Buffer {
const header = Buffer.alloc(512)
tarString(header, entry.type === 'directory' ? `${entry.path}/` : entry.path, 0, 100)
tarOctal(header, entry.mode, 100, 8)
tarOctal(header, 0, 108, 8)
tarOctal(header, 0, 116, 8)
tarOctal(header, size, 124, 12)
tarOctal(header, 0, 136, 12)
header.fill(0x20, 148, 156)
header[156] = entry.type === 'directory' ? 0x35 : 0x30
tarString(header, 'ustar\0', 257, 6)
tarString(header, '00', 263, 2)
const checksum = header.reduce((total, byte) => total + byte, 0)
header.write(`${checksum.toString(8).padStart(6, '0')}\0 `, 148, 8, 'ascii')
return header
}
function tarBrotli(
tuple: SshRelayRuntimeTuple,
fileBytes: ReadonlyMap<string, Buffer>,
extraPath?: string
): Buffer {
const blocks: Buffer[] = []
for (const entry of tuple.entries) {
const bytes = entry.type === 'file' ? fileBytes.get(entry.path) : undefined
if (entry.type === 'file' && !bytes) {
throw new Error(`Missing test bytes for ${entry.path}`)
}
blocks.push(tarHeader(entry, bytes?.length ?? 0))
if (bytes) {
blocks.push(bytes, Buffer.alloc((512 - (bytes.length % 512)) % 512))
}
}
if (extraPath) {
const bytes = Buffer.from('undeclared archive bytes')
blocks.push(
tarHeader(
{
path: extraPath,
type: 'file',
role: 'license',
mode: 0o644,
size: 0,
sha256: sha256(bytes)
},
bytes.length
),
bytes,
Buffer.alloc((512 - (bytes.length % 512)) % 512)
)
}
blocks.push(Buffer.alloc(1024))
return brotliCompressSync(Buffer.concat(blocks), {
params: {
[zlibConstants.BROTLI_PARAM_QUALITY]: 9,
[zlibConstants.BROTLI_PARAM_LGWIN]: 20
}
})
}
async function zipArchive(
tuple: SshRelayRuntimeTuple,
fileBytes: ReadonlyMap<string, Buffer>,
{
extraPath,
symlinkPath
}: {
extraPath?: string
symlinkPath?: string
}
): Promise<Buffer> {
const zip = new yazl.ZipFile()
const mtime = new Date('2026-07-14T00:00:00.000Z')
for (const entry of tuple.entries) {
const options = { mode: entry.mode, mtime, forceDosTimestamp: true }
if (entry.type === 'directory') {
zip.addEmptyDirectory(entry.path, options)
} else {
const bytes = fileBytes.get(entry.path)
if (!bytes) {
throw new Error(`Missing test bytes for ${entry.path}`)
}
zip.addBuffer(bytes, entry.path, {
...options,
mode: entry.path === symlinkPath ? 0o120777 : entry.mode,
compress: true,
compressionLevel: 9
})
}
}
if (extraPath) {
zip.addBuffer(Buffer.from('undeclared archive bytes'), extraPath, {
mode: 0o644,
mtime,
forceDosTimestamp: true
})
}
zip.end({ forceZip64Format: false })
const chunks: Buffer[] = []
for await (const chunk of zip.outputStream) {
chunks.push(Buffer.from(chunk))
}
return Buffer.concat(chunks)
}
function markZipEntriesEncrypted(archive: Buffer): Buffer {
const changed = Buffer.from(archive)
for (let offset = 0; offset <= changed.length - 10; offset += 1) {
const signature = changed.readUInt32LE(offset)
const flagOffset =
signature === 0x04034b50 ? offset + 6 : signature === 0x02014b50 ? offset + 8 : -1
if (flagOffset >= 0) {
changed.writeUInt16LE(changed.readUInt16LE(flagOffset) | 0x0001, flagOffset)
}
}
return changed
}
type Fixture = {
archivePath: string
artifact: SshRelaySelectedArtifact
fileBytes: ReadonlyMap<string, Buffer>
root: string
}
async function fixture({
os,
mismatchedPath,
extraPath,
largeFileBytes,
truncated,
encryptedZip,
symlinkPath
}: {
os: 'linux' | 'win32'
mismatchedPath?: string
extraPath?: string
largeFileBytes?: number
truncated?: boolean
encryptedZip?: boolean
symlinkPath?: string
}): Promise<Fixture> {
const root = await mkdtemp(join(tmpdir(), 'orca-relay-extraction-'))
temporaryDirectories.push(root)
const manifest: SshRelayArtifactManifest =
os === 'win32'
? createSshRelayWindowsArtifactTestManifest()
: createSshRelayArtifactTestManifest()
const tuple = manifest.tuples[0]
const fileBytes = new Map<string, Buffer>()
const firstFilePath = tuple.entries.find((entry) => entry.type === 'file')?.path
for (const entry of tuple.entries) {
if (entry.type !== 'file') {
continue
}
const bytes =
entry.path === firstFilePath && largeFileBytes
? Buffer.alloc(largeFileBytes, 0x61)
: Buffer.from(`desktop extraction fixture:${tuple.tupleId}:${entry.path}`)
fileBytes.set(entry.path, bytes)
entry.size = bytes.length
entry.sha256 = sha256(bytes)
}
for (const attestation of tuple.nativeVerification.files) {
const entry = tuple.entries.find((candidate) => candidate.path === attestation.path)
if (!entry || entry.type !== 'file') {
throw new Error(`Missing attested fixture entry: ${attestation.path}`)
}
attestation.sha256 = entry.sha256
}
tuple.contentId = computeSshRelayRuntimeContentId(tuple)
tuple.archive.name = sshRelayRuntimeArchiveName(tuple.tupleId, tuple.contentId)
const files = tuple.entries.filter((entry) => entry.type === 'file')
tuple.archive.fileCount = files.length
tuple.archive.expandedSize = files.reduce((total, entry) => total + entry.size, 0)
const archiveFileBytes = new Map(fileBytes)
if (mismatchedPath) {
archiveFileBytes.set(mismatchedPath, Buffer.from('authenticated archive has wrong tree bytes'))
}
let archive =
os === 'win32'
? await zipArchive(tuple, archiveFileBytes, { extraPath, symlinkPath })
: tarBrotli(tuple, archiveFileBytes, extraPath)
if (encryptedZip) {
archive = markZipEntriesEncrypted(archive)
}
if (truncated) {
archive = archive.subarray(0, Math.max(1, archive.length - 64))
}
tuple.archive.size = archive.length
tuple.archive.sha256 = sha256(archive)
manifest.signatures = [signSshRelayArtifactManifest(manifest, keyPair.secretKey)]
const verified = verifySshRelayArtifactManifest(manifest, [
{ keyId: sshRelayManifestKeyId(keyPair.publicKey), publicKey: keyPair.publicKey }
])
const host: SshRelayHostEvidence =
os === 'win32'
? {
os,
architecture: 'x64',
processTranslated: false,
build: 22631,
openSshVersion: '9.5p1',
powerShellVersion: '5.1',
dotNetFrameworkRelease: 528040
}
: {
os,
architecture: 'x64',
processTranslated: false,
kernelVersion: '6.8',
libc: { family: 'glibc', version: '2.39' },
libstdcxxVersion: '6.0.33',
glibcxxVersion: '3.4.33'
}
const selected = selectSshRelayArtifact(verified, host)
if (selected.kind !== 'selected') {
throw new Error(`Expected selected fixture, got ${selected.reason}`)
}
const archivePath = join(root, tuple.archive.name)
await writeFile(archivePath, archive, { mode: 0o600 })
return { archivePath, artifact: selected, fileBytes, root }
}
async function waitForPath(path: string): Promise<void> {
const deadline = Date.now() + 5_000
for (;;) {
try {
await stat(path)
return
} catch (error) {
if (!(error instanceof Error) || !('code' in error) || error.code !== 'ENOENT') {
throw error
}
if (Date.now() >= deadline) {
throw new Error(`Timed out waiting for test path: ${path}`)
}
await new Promise((resolve) => setTimeout(resolve, 1))
}
}
}
afterEach(async () => {
await Promise.all(
temporaryDirectories.splice(0).map((path) => rm(path, { recursive: true, force: true }))
)
})
describe('SSH relay artifact extraction', () => {
it('declares the desktop extraction resource contract', () => {
expect(SSH_RELAY_ARTIFACT_EXTRACTION_LIMITS).toEqual({
timeoutMs: 2 * 60_000,
chunkBytes: 64 * 1024,
maximumIncrementalMemoryBytes: 64 * 1024 * 1024,
maximumWriteBufferBytes: 1024 * 1024
})
})
it.each(['linux', 'win32'] as const)(
'extracts and verifies an authenticated %s tree into an exclusive staging directory',
async (os) => {
const value = await fixture({ os })
const outputDirectory = join(value.root, 'fresh-parent', 'runtime')
const result = await extractSshRelayArtifact({
artifact: value.artifact,
archivePath: value.archivePath,
outputDirectory
})
expect(result).toEqual({
tupleId: value.artifact.tupleId,
contentId: value.artifact.contentId,
runtimeRoot: await realpath(outputDirectory),
files: value.artifact.tuple.archive.fileCount,
expandedBytes: value.artifact.tuple.archive.expandedSize
})
for (const [path, bytes] of value.fileBytes) {
expect(await readFile(join(outputDirectory, ...path.split('/')))).toEqual(bytes)
}
if (process.platform !== 'win32') {
for (const entry of value.artifact.tuple.entries) {
const metadata = await lstat(join(outputDirectory, ...entry.path.split('/')))
expect(metadata.mode & 0o777, entry.path).toBe(entry.mode)
}
}
}
)
it('rejects changed archive bytes before creating staging output', async () => {
const value = await fixture({ os: 'linux' })
await writeFile(value.archivePath, 'changed after authenticated download')
const outputDirectory = join(value.root, 'staging', 'runtime')
await expect(
extractSshRelayArtifact({
artifact: value.artifact,
archivePath: value.archivePath,
outputDirectory
})
).rejects.toThrow(/size|sha-?256|archive/i)
await expect(stat(outputDirectory)).rejects.toMatchObject({ code: 'ENOENT' })
})
it.each(['linux', 'win32'] as const)(
'rejects an authenticated %s archive whose extracted tree disagrees with the manifest',
async (os) => {
const value = await fixture({ os, mismatchedPath: 'relay.js' })
const outputDirectory = join(value.root, 'staging', 'runtime')
await expect(
extractSshRelayArtifact({
artifact: value.artifact,
archivePath: value.archivePath,
outputDirectory
})
).rejects.toThrow(/integrity|sha-?256|size|tree/i)
await expect(stat(outputDirectory)).rejects.toMatchObject({ code: 'ENOENT' })
}
)
it.each(['linux', 'win32'] as const)(
'rejects an authenticated %s archive with an undeclared entry',
async (os) => {
const value = await fixture({ os, extraPath: 'unexpected.txt' })
const outputDirectory = join(value.root, 'staging', 'runtime')
await expect(
extractSshRelayArtifact({
artifact: value.artifact,
archivePath: value.archivePath,
outputDirectory
})
).rejects.toThrow(/entry-count|extra|undeclared/i)
await expect(stat(outputDirectory)).rejects.toMatchObject({ code: 'ENOENT' })
}
)
it('rejects an authenticated TAR traversal without writing outside owned staging', async () => {
const value = await fixture({ os: 'linux', extraPath: '../escaped.txt' })
const outputDirectory = join(value.root, 'staging', 'runtime')
const escapedPath = join(value.root, 'staging', 'escaped.txt')
await expect(
extractSshRelayArtifact({
artifact: value.artifact,
archivePath: value.archivePath,
outputDirectory
})
).rejects.toThrow(/extra|path|undeclared|traversal/i)
await expect(stat(outputDirectory)).rejects.toMatchObject({ code: 'ENOENT' })
await expect(stat(escapedPath)).rejects.toMatchObject({ code: 'ENOENT' })
})
it.each(['linux', 'win32'] as const)(
'rejects an authenticated truncated %s archive and removes owned staging',
async (os) => {
const value = await fixture({ os, truncated: true })
const outputDirectory = join(value.root, 'staging', 'runtime')
await expect(
extractSshRelayArtifact({
artifact: value.artifact,
archivePath: value.archivePath,
outputDirectory
})
).rejects.toThrow(/archive|brotli|end|invalid|tar|zip/i)
await expect(stat(outputDirectory)).rejects.toMatchObject({ code: 'ENOENT' })
}
)
it.each([
['case-fold collision', { extraPath: 'RELAY.JS' }, /case-fold|duplicate/i],
['symbolic link', { symlinkPath: 'relay.js' }, /symbolic link|type|mode/i],
// yauzl may reject the encrypted-size shape before exposing the flagged entry to our guard.
['encrypted entry', { encryptedZip: true }, /encrypted|size mismatch/i]
] as const)('rejects an authenticated ZIP %s', async (_name, options, expectedError) => {
const value = await fixture({ os: 'win32', ...options })
const outputDirectory = join(value.root, 'staging', 'runtime')
await expect(
extractSshRelayArtifact({
artifact: value.artifact,
archivePath: value.archivePath,
outputDirectory
})
).rejects.toThrow(expectedError)
await expect(stat(outputDirectory)).rejects.toMatchObject({ code: 'ENOENT' })
})
it('detects archive mutation after staging begins and removes its partial tree', async () => {
const value = await fixture({ os: 'win32', largeFileBytes: 8 * 1024 * 1024 })
const outputDirectory = join(value.root, 'staging', 'runtime')
const rejection = expect(
extractSshRelayArtifact({
artifact: value.artifact,
archivePath: value.archivePath,
outputDirectory
})
).rejects.toThrow(/archive|central directory|changed|invalid|unexpected EOF|zip/i)
await waitForPath(outputDirectory)
await writeFile(value.archivePath, Buffer.alloc(value.artifact.archive.size, 0x62))
await rejection
await expect(stat(outputDirectory)).rejects.toMatchObject({ code: 'ENOENT' })
}, 15_000)
it('settles in-flight cancellation and removes only its partial staging tree', async () => {
const value = await fixture({ os: 'win32', largeFileBytes: 8 * 1024 * 1024 })
const outputDirectory = join(value.root, 'staging', 'runtime')
const controller = new AbortController()
const rejection = expect(
extractSshRelayArtifact({
artifact: value.artifact,
archivePath: value.archivePath,
outputDirectory,
signal: controller.signal
})
).rejects.toThrow(/cancel|abort/i)
await waitForPath(outputDirectory)
controller.abort(new Error('cancel in-flight desktop extraction'))
await rejection
await expect(stat(outputDirectory)).rejects.toMatchObject({ code: 'ENOENT' })
}, 15_000)
it('settles pre-cancellation and preserves an existing output owner', async () => {
const value = await fixture({ os: 'linux' })
const controller = new AbortController()
controller.abort(new Error('cancel desktop extraction'))
const cancelledOutput = join(value.root, 'cancelled', 'runtime')
await expect(
extractSshRelayArtifact({
artifact: value.artifact,
archivePath: value.archivePath,
outputDirectory: cancelledOutput,
signal: controller.signal
})
).rejects.toThrow(/cancel desktop extraction/i)
await expect(stat(cancelledOutput)).rejects.toMatchObject({ code: 'ENOENT' })
const existingOutput = join(value.root, 'existing')
await writeFile(existingOutput, 'owner bytes')
await expect(
extractSshRelayArtifact({
artifact: value.artifact,
archivePath: value.archivePath,
outputDirectory: existingOutput
})
).rejects.toMatchObject({ code: 'EEXIST' })
expect(await readFile(existingOutput, 'utf8')).toBe('owner bytes')
})
})
@@ -0,0 +1,177 @@
import { createHash } from 'node:crypto'
import { createReadStream } from 'node:fs'
import { lstat, mkdir, realpath, rm } from 'node:fs/promises'
import { basename, dirname, resolve } from 'node:path'
import type { SshRelaySelectedArtifact } from './ssh-relay-artifact-selector'
import {
extractSshRelayTarBrotli,
inspectSshRelayTarBrotli
} from './ssh-relay-artifact-tar-extraction'
import { verifySshRelayArtifactTree } from './ssh-relay-artifact-tree-verification'
import { extractSshRelayZip, inspectSshRelayZip } from './ssh-relay-artifact-zip-extraction'
const EXTRACTION_TIMEOUT_MS = 2 * 60_000
const CHUNK_BYTES = 64 * 1024
const MAXIMUM_INCREMENTAL_MEMORY_BYTES = 64 * 1024 * 1024
const MAXIMUM_WRITE_BUFFER_BYTES = 1024 * 1024
type ArchiveState = {
dev: bigint
ino: bigint
size: bigint
mtimeNs: bigint
ctimeNs: bigint
}
function sameArchiveState(left: ArchiveState, right: ArchiveState): boolean {
return (
left.dev === right.dev &&
left.ino === right.ino &&
left.size === right.size &&
left.mtimeNs === right.mtimeNs &&
left.ctimeNs === right.ctimeNs
)
}
async function describeArchive(
archivePath: string,
expectedSize: number,
signal: AbortSignal
): Promise<{ sha256: string; state: ArchiveState }> {
signal.throwIfAborted()
const before = await lstat(archivePath, { bigint: true })
if (!before.isFile() || before.isSymbolicLink() || before.size !== BigInt(expectedSize)) {
throw new Error('SSH relay extraction input must be the exact signed regular archive file')
}
const digest = createHash('sha256')
let size = 0
for await (const chunk of createReadStream(archivePath, {
highWaterMark: CHUNK_BYTES,
signal
})) {
size += chunk.length
if (size > expectedSize) {
throw new Error('SSH relay extraction input exceeds its signed size')
}
digest.update(chunk)
}
const after = await lstat(archivePath, { bigint: true })
if (!sameArchiveState(before, after) || size !== expectedSize) {
throw new Error('SSH relay extraction input changed while hashing')
}
return { sha256: `sha256:${digest.digest('hex')}`, state: after }
}
function assertSelectedArtifact(artifact: SshRelaySelectedArtifact): void {
// Why: only the recursively frozen signature-verified selector result may cross this boundary.
if (
artifact.tupleId !== artifact.tuple.tupleId ||
artifact.contentId !== artifact.tuple.contentId ||
artifact.archive.name !== artifact.tuple.archive.name ||
artifact.archive.size !== artifact.tuple.archive.size ||
artifact.archive.sha256 !== artifact.tuple.archive.sha256
) {
throw new Error('SSH relay selected artifact identity is inconsistent')
}
}
export type SshRelayArtifactExtractionResult = {
tupleId: SshRelaySelectedArtifact['tupleId']
contentId: SshRelaySelectedArtifact['contentId']
runtimeRoot: string
files: number
expandedBytes: number
}
export async function extractSshRelayArtifact({
artifact,
archivePath,
outputDirectory,
signal
}: {
artifact: SshRelaySelectedArtifact
archivePath: string
outputDirectory: string
signal?: AbortSignal
}): Promise<SshRelayArtifactExtractionResult> {
const effectiveSignal = signal
? AbortSignal.any([signal, AbortSignal.timeout(EXTRACTION_TIMEOUT_MS)])
: AbortSignal.timeout(EXTRACTION_TIMEOUT_MS)
effectiveSignal.throwIfAborted()
assertSelectedArtifact(artifact)
const absoluteArchive = resolve(archivePath)
const absoluteOutput = resolve(outputDirectory)
const before = await describeArchive(absoluteArchive, artifact.archive.size, effectiveSignal)
if (before.sha256 !== artifact.archive.sha256) {
throw new Error('SSH relay extraction input SHA-256 disagrees with the signed manifest')
}
const outputParent = dirname(absoluteOutput)
await mkdir(outputParent, { recursive: true, mode: 0o700 })
const physicalParent = await realpath(outputParent)
const physicalOutput = resolve(physicalParent, basename(absoluteOutput))
let outputCreated = false
try {
// Why: no cache publisher or concurrent extractor may observe a shared partial runtime tree.
await mkdir(physicalOutput, { mode: 0o700 })
outputCreated = true
if (artifact.tuple.os === 'win32') {
await inspectSshRelayZip({
archivePath: absoluteArchive,
tuple: artifact.tuple,
signal: effectiveSignal
})
await extractSshRelayZip({
archivePath: absoluteArchive,
outputDirectory: physicalOutput,
tuple: artifact.tuple,
signal: effectiveSignal
})
} else {
await inspectSshRelayTarBrotli({
archivePath: absoluteArchive,
tuple: artifact.tuple,
signal: effectiveSignal,
chunkBytes: CHUNK_BYTES
})
await extractSshRelayTarBrotli({
archivePath: absoluteArchive,
outputDirectory: physicalOutput,
tuple: artifact.tuple,
signal: effectiveSignal,
chunkBytes: CHUNK_BYTES
})
}
const tree = await verifySshRelayArtifactTree({
runtimeRoot: physicalOutput,
tuple: artifact.tuple,
signal: effectiveSignal,
chunkBytes: CHUNK_BYTES
})
const after = await describeArchive(absoluteArchive, artifact.archive.size, effectiveSignal)
if (!sameArchiveState(before.state, after.state) || before.sha256 !== after.sha256) {
throw new Error('SSH relay extraction input changed during extraction')
}
effectiveSignal.throwIfAborted()
return {
tupleId: artifact.tupleId,
contentId: artifact.contentId,
runtimeRoot: physicalOutput,
...tree
}
} catch (error) {
if (outputCreated) {
// Why: only a fully verified tree may remain eligible for later atomic cache publication.
await rm(physicalOutput, { recursive: true, force: true })
}
throw error
}
}
export const SSH_RELAY_ARTIFACT_EXTRACTION_LIMITS = Object.freeze({
timeoutMs: EXTRACTION_TIMEOUT_MS,
chunkBytes: CHUNK_BYTES,
maximumIncrementalMemoryBytes: MAXIMUM_INCREMENTAL_MEMORY_BYTES,
maximumWriteBufferBytes: MAXIMUM_WRITE_BUFFER_BYTES
})
@@ -0,0 +1,130 @@
import { createHash } from 'node:crypto'
import { createReadStream } from 'node:fs'
import { pipeline } from 'node:stream/promises'
import { createBrotliDecompress } from 'node:zlib'
import { extract, Parser, ReadEntry } from 'tar'
import type { SshRelaySelectedArtifact } from './ssh-relay-artifact-selector'
type SelectedTuple = SshRelaySelectedArtifact['tuple']
function normalizedTarPath(entry: ReadEntry): string {
return entry.path.endsWith('/') ? entry.path.slice(0, -1) : entry.path
}
function assertTarEntry(
entry: ReadEntry,
expected: ReadonlyMap<string, SelectedTuple['entries'][number]>,
seen: Set<string>
): SelectedTuple['entries'][number] {
const path = normalizedTarPath(entry)
const declared = expected.get(path)
if (!declared || seen.has(path)) {
throw new Error(`SSH relay TAR has an extra or duplicate entry: ${path}`)
}
seen.add(path)
const expectedType = declared.type === 'file' ? 'File' : 'Directory'
if (
entry.type !== expectedType ||
entry.mode === undefined ||
(entry.mode & 0o777) !== declared.mode
) {
throw new Error(`SSH relay TAR type or mode mismatch: ${path}`)
}
if (declared.type === 'file' && entry.size !== declared.size) {
throw new Error(`SSH relay TAR size mismatch: ${path}`)
}
return declared
}
async function decompressTar(
archivePath: string,
destination: NodeJS.WritableStream,
signal: AbortSignal,
chunkBytes: number
): Promise<void> {
await pipeline(
createReadStream(archivePath, { highWaterMark: chunkBytes }),
createBrotliDecompress({ chunkSize: chunkBytes }),
destination,
{ signal }
)
}
export async function inspectSshRelayTarBrotli({
archivePath,
tuple,
signal,
chunkBytes
}: {
archivePath: string
tuple: SelectedTuple
signal: AbortSignal
chunkBytes: number
}): Promise<void> {
const expected = new Map(tuple.entries.map((entry) => [entry.path, entry]))
const seen = new Set<string>()
const parser = new Parser({ strict: true })
parser.on('entry', (entry: ReadEntry) => {
try {
const declared = assertTarEntry(entry, expected, seen)
if (declared.type === 'directory') {
entry.resume()
return
}
const digest = createHash('sha256')
entry.on('data', (chunk: Buffer) => digest.update(chunk))
entry.once('error', (error) => parser.abort(error))
entry.once('end', () => {
const actual = `sha256:${digest.digest('hex')}`
if (actual !== declared.sha256) {
parser.abort(new Error(`SSH relay TAR file integrity mismatch: ${entry.path}`))
}
})
} catch (error) {
parser.abort(error instanceof Error ? error : new Error(String(error)))
}
})
await decompressTar(archivePath, parser, signal, chunkBytes)
const missing = tuple.entries.find((entry) => !seen.has(entry.path))
if (missing) {
throw new Error(`SSH relay TAR is missing a declared entry: ${missing.path}`)
}
}
export async function extractSshRelayTarBrotli({
archivePath,
outputDirectory,
tuple,
signal,
chunkBytes
}: {
archivePath: string
outputDirectory: string
tuple: SelectedTuple
signal: AbortSignal
chunkBytes: number
}): Promise<void> {
const expected = new Map(tuple.entries.map((entry) => [entry.path, entry]))
const seen = new Set<string>()
const unpack = extract({
cwd: outputDirectory,
strict: true,
preserveOwner: false,
noChmod: false,
unlink: false,
filter: (_path, entry) => {
if (!(entry instanceof ReadEntry)) {
throw new Error('SSH relay TAR extraction requires a streamed archive entry')
}
assertTarEntry(entry, expected, seen)
return true
}
})
await decompressTar(archivePath, unpack, signal, chunkBytes)
const missing = tuple.entries.find((entry) => !seen.has(entry.path))
if (missing) {
throw new Error(`SSH relay TAR is missing a declared entry: ${missing.path}`)
}
}
@@ -0,0 +1,103 @@
import { createHash } from 'node:crypto'
import { createReadStream } from 'node:fs'
import { lstat, readdir } from 'node:fs/promises'
import { join, relative, sep } from 'node:path'
import type { SshRelaySelectedArtifact } from './ssh-relay-artifact-selector'
export type SshRelayArtifactTreeVerification = {
files: number
expandedBytes: number
}
async function hashFile(
path: string,
expectedSize: number,
signal: AbortSignal,
chunkBytes: number
): Promise<string> {
const digest = createHash('sha256')
let size = 0
for await (const chunk of createReadStream(path, { highWaterMark: chunkBytes, signal })) {
size += chunk.length
if (size > expectedSize) {
throw new Error('SSH relay extracted tree file exceeds its signed size')
}
digest.update(chunk)
}
if (size !== expectedSize) {
throw new Error('SSH relay extracted tree file size disagrees with its signed size')
}
return `sha256:${digest.digest('hex')}`
}
export async function verifySshRelayArtifactTree({
runtimeRoot,
tuple,
signal,
chunkBytes
}: {
runtimeRoot: string
tuple: SshRelaySelectedArtifact['tuple']
signal: AbortSignal
chunkBytes: number
}): Promise<SshRelayArtifactTreeVerification> {
const expected = new Map(tuple.entries.map((entry) => [entry.path, entry]))
const foldedPaths = new Set<string>()
let files = 0
let expandedBytes = 0
async function visit(directory: string): Promise<void> {
signal.throwIfAborted()
const children = await readdir(directory, { withFileTypes: true })
for (const child of children) {
signal.throwIfAborted()
const absolutePath = join(directory, child.name)
const path = relative(runtimeRoot, absolutePath).split(sep).join('/')
const folded = path.toLowerCase()
if (foldedPaths.has(folded)) {
throw new Error(`SSH relay extracted tree has a case-fold collision: ${path}`)
}
foldedPaths.add(folded)
const declared = expected.get(path)
if (!declared) {
throw new Error(`SSH relay extracted tree has an undeclared entry: ${path}`)
}
expected.delete(path)
const metadata = await lstat(absolutePath)
const actualType = metadata.isDirectory()
? 'directory'
: metadata.isFile()
? 'file'
: 'unsupported'
if (actualType !== declared.type) {
throw new Error(`SSH relay extracted tree type mismatch: ${path}`)
}
if (process.platform !== 'win32' && (metadata.mode & 0o777) !== declared.mode) {
throw new Error(`SSH relay extracted tree mode mismatch: ${path}`)
}
if (declared.type === 'directory') {
await visit(absolutePath)
continue
}
if (metadata.size !== declared.size) {
throw new Error(`SSH relay extracted tree size mismatch: ${path}`)
}
if ((await hashFile(absolutePath, declared.size, signal, chunkBytes)) !== declared.sha256) {
throw new Error(`SSH relay extracted tree integrity mismatch: ${path}`)
}
files += 1
expandedBytes += declared.size
}
}
await visit(runtimeRoot)
const missing = expected.keys().next().value as string | undefined
if (missing) {
throw new Error(`SSH relay extracted tree is missing a declared entry: ${missing}`)
}
if (files !== tuple.archive.fileCount || expandedBytes !== tuple.archive.expandedSize) {
throw new Error('SSH relay extracted tree aggregate size or file count is inconsistent')
}
return { files, expandedBytes }
}
@@ -0,0 +1,103 @@
import { chmod, mkdir } from 'node:fs/promises'
import { join } from 'node:path'
import type { SshRelaySelectedArtifact } from './ssh-relay-artifact-selector'
import { type SshRelayZipLimits, visitSshRelayZip } from './ssh-relay-artifact-zip-reader'
type SelectedTuple = SshRelaySelectedArtifact['tuple']
function zipLimits(tuple: SelectedTuple): SshRelayZipLimits {
return {
maximumArchiveBytes: tuple.archive.size,
maximumEntries: tuple.entries.length,
maximumExpandedBytes: tuple.archive.expandedSize,
maximumFileBytes: Math.max(
...tuple.entries.map((entry) => (entry.type === 'file' ? entry.size : 0))
),
maximumDepth: 32,
maximumPathBytes: 240
}
}
async function visitExpectedZip({
archivePath,
tuple,
outputDirectory,
signal
}: {
archivePath: string
tuple: SelectedTuple
outputDirectory?: string
signal: AbortSignal
}): Promise<void> {
const expected = new Map(tuple.entries.map((entry) => [entry.path, entry]))
const seen = new Set<string>()
const result = await visitSshRelayZip(
archivePath,
zipLimits(tuple),
async (actual, consume) => {
const declared = expected.get(actual.path)
if (!declared || seen.has(actual.path)) {
throw new Error(`SSH relay ZIP has an extra or duplicate entry: ${actual.path}`)
}
seen.add(actual.path)
if (
actual.type !== declared.type ||
actual.unixMode === null ||
(actual.unixMode & 0o777) !== declared.mode
) {
throw new Error(`SSH relay ZIP type or mode mismatch: ${actual.path}`)
}
if (declared.type === 'directory') {
if (outputDirectory) {
const outputPath = join(outputDirectory, ...actual.path.split('/'))
await mkdir(outputPath, { recursive: true, mode: declared.mode })
if (process.platform !== 'win32') {
await chmod(outputPath, declared.mode)
}
}
return
}
if (actual.size !== declared.size) {
throw new Error(`SSH relay ZIP size mismatch: ${actual.path}`)
}
const outputPath = outputDirectory
? join(outputDirectory, ...actual.path.split('/'))
: undefined
if ((await consume({ outputPath, mode: declared.mode })) !== declared.sha256) {
throw new Error(`SSH relay ZIP file integrity mismatch: ${actual.path}`)
}
if (outputPath && process.platform !== 'win32') {
await chmod(outputPath, declared.mode)
}
},
signal
)
const missing = tuple.entries.find((entry) => !seen.has(entry.path))
if (missing) {
throw new Error(`SSH relay ZIP is missing a declared entry: ${missing.path}`)
}
if (
result.files !== tuple.archive.fileCount ||
result.expandedBytes !== tuple.archive.expandedSize
) {
throw new Error('SSH relay ZIP aggregate size or file count is inconsistent')
}
}
export async function inspectSshRelayZip(options: {
archivePath: string
tuple: SelectedTuple
signal: AbortSignal
}): Promise<void> {
await visitExpectedZip(options)
}
export async function extractSshRelayZip(options: {
archivePath: string
outputDirectory: string
tuple: SelectedTuple
signal: AbortSignal
}): Promise<void> {
await visitExpectedZip(options)
}
@@ -0,0 +1,285 @@
import { createHash } from 'node:crypto'
import type { EventEmitter } from 'node:events'
import { createWriteStream } from 'node:fs'
import { mkdir, stat } from 'node:fs/promises'
import { createRequire } from 'node:module'
import { dirname } from 'node:path'
import { type Readable, Transform, Writable } from 'node:stream'
import { pipeline } from 'node:stream/promises'
import { crc32 } from 'node:zlib'
type ZipEntry = {
fileName: Buffer
versionMadeBy: number
externalFileAttributes: number
generalPurposeBitFlag: number
compressionMethod: number
uncompressedSize: number
compressedSize: number
crc32: number
}
type ZipFile = EventEmitter & {
readEntry(): void
close(): void
openReadStream(entry: ZipEntry, callback: (error: Error | null, stream?: Readable) => void): void
}
type YauzlModule = {
open(
path: string,
options: {
autoClose: boolean
lazyEntries: boolean
decodeStrings: boolean
strictFileNames: boolean
validateEntrySizes: boolean
},
callback: (error: Error | null, zip?: ZipFile) => void
): void
}
export type SshRelayZipLimits = {
maximumArchiveBytes: number
maximumEntries: number
maximumExpandedBytes: number
maximumFileBytes: number
maximumDepth: number
maximumPathBytes: number
}
export type SshRelayZipEntryInfo = {
path: string
type: 'directory' | 'file'
size: number
crc32: number
unixMode: number | null
}
const yauzl = createRequire(import.meta.url)('yauzl') as YauzlModule
const UTF8_DECODER = new TextDecoder('utf-8', { fatal: true })
const ENCRYPTION_FLAGS = 0x0001 | 0x0040 | 0x2000
const WINDOWS_DEVICE_NAME = /^(?:con|prn|aux|nul|com[1-9]|lpt[1-9])(?:\.|$)/i
function openZip(path: string): Promise<ZipFile> {
return new Promise((resolve, reject) => {
yauzl.open(
path,
{
autoClose: false,
lazyEntries: true,
decodeStrings: false,
strictFileNames: true,
validateEntrySizes: true
},
(error, zip) =>
error ? reject(error) : zip ? resolve(zip) : reject(new Error('ZIP missing'))
)
})
}
function nextEntry(zip: ZipFile): Promise<ZipEntry | null> {
return new Promise((resolve, reject) => {
const cleanup = (): void => {
zip.off('entry', onEntry)
zip.off('end', onEnd)
zip.off('error', onError)
}
const onEntry = (entry: ZipEntry): void => {
cleanup()
resolve(entry)
}
const onEnd = (): void => {
cleanup()
resolve(null)
}
const onError = (error: Error): void => {
cleanup()
reject(error)
}
zip.once('entry', onEntry)
zip.once('end', onEnd)
zip.once('error', onError)
zip.readEntry()
})
}
function openEntryStream(zip: ZipFile, entry: ZipEntry): Promise<Readable> {
return new Promise((resolve, reject) => {
zip.openReadStream(entry, (error, stream) =>
error ? reject(error) : stream ? resolve(stream) : reject(new Error('ZIP stream missing'))
)
})
}
function decodeEntryName(entry: ZipEntry): string {
try {
return UTF8_DECODER.decode(entry.fileName)
} catch {
throw new Error('SSH relay ZIP entry name must be valid UTF-8')
}
}
function portableEntryPath(name: string, limits: SshRelayZipLimits): string {
if (
!name ||
Buffer.byteLength(name) > limits.maximumPathBytes ||
name.startsWith('/') ||
name.startsWith('\\') ||
/^[A-Za-z]:/.test(name)
) {
throw new Error('SSH relay ZIP entry path is not a bounded portable relative path')
}
for (const character of name) {
const code = character.charCodeAt(0)
if (code <= 0x1f || code === 0x7f || character === '\\') {
throw new Error('SSH relay ZIP entry path contains a control character or backslash')
}
}
const path = name.endsWith('/') ? name.slice(0, -1) : name
const segments = path.split('/')
if (
segments.length > limits.maximumDepth ||
segments.some(
(segment) =>
!segment ||
segment === '.' ||
segment === '..' ||
segment.includes(':') ||
segment.endsWith('.') ||
segment.endsWith(' ') ||
WINDOWS_DEVICE_NAME.test(segment)
)
) {
throw new Error('SSH relay ZIP entry path contains an unsafe path segment')
}
return path
}
function entryInfo(entry: ZipEntry, path: string): SshRelayZipEntryInfo {
const hostSystem = entry.versionMadeBy >>> 8
const unixMode = hostSystem === 3 ? (entry.externalFileAttributes >>> 16) & 0xffff : null
if (unixMode !== null && (unixMode & 0o170000) === 0o120000) {
throw new Error(`SSH relay ZIP contains a prohibited symbolic link: ${path}`)
}
if ((entry.generalPurposeBitFlag & ENCRYPTION_FLAGS) !== 0) {
throw new Error(`SSH relay ZIP contains an encrypted entry: ${path}`)
}
if (entry.compressionMethod !== 0 && entry.compressionMethod !== 8) {
throw new Error(`SSH relay ZIP uses an unsupported compression method: ${path}`)
}
const directory = entry.fileName.at(-1) === 0x2f
if (directory && (entry.uncompressedSize !== 0 || entry.compressedSize !== 0)) {
throw new Error(`SSH relay ZIP directory contains file data: ${path}`)
}
return {
path,
type: directory ? 'directory' : 'file',
size: entry.uncompressedSize,
crc32: entry.crc32 >>> 0,
unixMode
}
}
async function consumeEntry({
zip,
entry,
info,
outputPath,
mode,
signal
}: {
zip: ZipFile
entry: ZipEntry
info: SshRelayZipEntryInfo
outputPath?: string
mode?: number
signal: AbortSignal
}): Promise<string> {
const input = await openEntryStream(zip, entry)
let bytes = 0
let checksum = 0
const digest = createHash('sha256')
const verifier = new Transform({
transform(chunk: Buffer, _encoding, callback) {
bytes += chunk.length
if (bytes > info.size) {
callback(new Error(`SSH relay ZIP entry expanded beyond its signed size: ${info.path}`))
return
}
checksum = crc32(chunk, checksum)
digest.update(chunk)
callback(null, chunk)
}
})
const sink = outputPath
? (await mkdir(dirname(outputPath), { recursive: true }),
createWriteStream(outputPath, { flags: 'wx', mode: mode ?? 0o600 }))
: new Writable({ write: (_chunk, _encoding, callback) => callback() })
await pipeline(input, verifier, sink, { signal })
if (bytes !== info.size || checksum >>> 0 !== info.crc32) {
throw new Error(`SSH relay ZIP entry size or CRC-32 mismatch: ${info.path}`)
}
return `sha256:${digest.digest('hex')}`
}
export async function visitSshRelayZip(
archivePath: string,
limits: SshRelayZipLimits,
visitor: (
actual: SshRelayZipEntryInfo,
consume: (options?: { outputPath?: string; mode?: number }) => Promise<string>
) => Promise<void>,
signal: AbortSignal
): Promise<{ files: number; expandedBytes: number }> {
const metadata = await stat(archivePath)
if (!metadata.isFile() || metadata.size === 0 || metadata.size > limits.maximumArchiveBytes) {
throw new Error('SSH relay ZIP exceeds its compressed-size limit')
}
const zip = await openZip(archivePath)
const foldedPaths = new Set<string>()
let entries = 0
let files = 0
let expandedBytes = 0
try {
for (let entry = await nextEntry(zip); entry; entry = await nextEntry(zip)) {
signal.throwIfAborted()
const path = portableEntryPath(decodeEntryName(entry), limits)
const folded = path.toLowerCase()
if (foldedPaths.has(folded)) {
throw new Error(`SSH relay ZIP has a duplicate or case-fold collision: ${path}`)
}
foldedPaths.add(folded)
const info = entryInfo(entry, path)
entries += 1
if (entries > limits.maximumEntries) {
throw new Error('SSH relay ZIP exceeds the entry-count limit')
}
if (info.type === 'file') {
files += 1
if (!Number.isSafeInteger(info.size) || info.size > limits.maximumFileBytes) {
throw new Error(`SSH relay ZIP file exceeds the per-file size limit: ${path}`)
}
expandedBytes += info.size
if (!Number.isSafeInteger(expandedBytes) || expandedBytes > limits.maximumExpandedBytes) {
throw new Error('SSH relay ZIP exceeds the expanded-size limit')
}
}
let consumed = info.type === 'directory'
const consume = async (options?: { outputPath?: string; mode?: number }): Promise<string> => {
if (consumed) {
throw new Error(`SSH relay ZIP entry was consumed more than once: ${path}`)
}
consumed = true
return consumeEntry({ zip, entry, info, ...options, signal })
}
await visitor(info, consume)
if (!consumed) {
throw new Error(`SSH relay ZIP visitor did not verify file bytes: ${path}`)
}
}
return { files, expandedBytes }
} finally {
zip.close()
}
}