diff --git a/README.md b/README.md index feabc53..8fe3b06 100644 --- a/README.md +++ b/README.md @@ -32,5 +32,10 @@ python -m unittest discover -s tests -v python scripts/check_public_repo.py . ``` +`scripts/materialize_private_drive_files.ps1` is the bounded transport used by +the acquisition workflow. It accepts authenticated download references over +standard input, rejects path traversal, and verifies byte sizes before files +enter PrivateStash. It never prints download credentials. + No project license has been selected yet. Until that decision is recorded, the contents are not offered under an open-source license. diff --git a/docs/data-handling.md b/docs/data-handling.md index e8f15c8..0abaee7 100644 --- a/docs/data-handling.md +++ b/docs/data-handling.md @@ -31,3 +31,7 @@ project artifact is created, edited, renamed, or reorganized there. 4. Create a sanitized derivative in the public repository. 5. Run automated guards and manually review the staged diff. 6. Commit and push the private evidence before the public claim that cites it. + +During Drive acquisition, authenticated download references are ephemeral and +must be passed to the bounded materializer over standard input. They must not be +written to scripts, manifests, command history, or Git. diff --git a/scripts/materialize_private_drive_files.ps1 b/scripts/materialize_private_drive_files.ps1 new file mode 100644 index 0000000..5532772 --- /dev/null +++ b/scripts/materialize_private_drive_files.ps1 @@ -0,0 +1,47 @@ +[CmdletBinding()] +param( + [Parameter(Mandatory = $true)] + [string]$Root +) + +$ErrorActionPreference = 'Stop' +$rootPath = (Resolve-Path -LiteralPath $Root).Path +$rootPrefix = $rootPath.TrimEnd([IO.Path]::DirectorySeparatorChar) + [IO.Path]::DirectorySeparatorChar + +while (($line = [Console]::In.ReadLine()) -ne $null) { + if ($line -eq '__END__') { + break + } + if ([string]::IsNullOrWhiteSpace($line)) { + continue + } + + $item = $line | ConvertFrom-Json + if (-not $item.relative_path -or -not $item.download_url) { + throw 'Each input line must contain relative_path and download_url.' + } + + $relativePath = $item.relative_path.Replace('/', [IO.Path]::DirectorySeparatorChar) + $destination = [IO.Path]::GetFullPath((Join-Path $rootPath $relativePath)) + if (-not $destination.StartsWith($rootPrefix, [StringComparison]::OrdinalIgnoreCase)) { + throw "Refusing path outside acquisition root: $($item.relative_path)" + } + + $parent = Split-Path -Parent $destination + New-Item -ItemType Directory -Path $parent -Force | Out-Null + Invoke-WebRequest -UseBasicParsing -Uri $item.download_url -OutFile $destination + + $actualSize = (Get-Item -LiteralPath $destination).Length + if ($null -ne $item.expected_size -and $actualSize -ne [long]$item.expected_size) { + throw "Size mismatch for $($item.relative_path): expected $($item.expected_size), got $actualSize" + } + + if ($item.expected_sha256) { + $actualHash = (Get-FileHash -Algorithm SHA256 -LiteralPath $destination).Hash.ToLowerInvariant() + if ($actualHash -ne $item.expected_sha256.ToLowerInvariant()) { + throw "SHA-256 mismatch for $($item.relative_path)" + } + } + + Write-Output ("OK`t{0}`t{1}" -f $actualSize, $item.relative_path) +}